- Continue building our app
- DOM Events
- Update state through actions
- Know and understand the Observe Pattern
- HTML markup
- Javascript
- Language
- Function and arrow functions
- Spread, Rest operators
- Observer
- Language
- Keep Code Modular
- IIFE
- Observer Pattern
- Discuss Events
- Talk about the observer pattern
- Define the actions in our app
- Use the Observer to publish actions
- Use Observer to subscribe to actions ad handle how state changes
- Render the view after mutating the state
In this session we will talk about the left half of our app cycle
By the end of our last session we had created the interface of an app that is rendered from data.
We now want to add interactivity to our application.
The browser has a large set of predefined events that we can attach to DOM elements in our interface and call functions with you can check out a list here
We can attach events to our DOM directly
<body>
<button onclick="alert(`${this.textContent} says Hello There`)">Click Me Button</button>
</body>
or programmatically
<body>
<button>Click Me Button</button>
<script>
let element = document.querySelector('button');
element.addEventListner('click', function (event) {
alert(`${this.textContent} says Hello There`);
})
</script>
</body>
When a DOM event is triggered the function assigned is called with an argument event
and the function’s this
refers to the element’s DOM node that triggered the event.
The following code is equivalent
<body>
<input class="test" onchange="state.message=this.value" />
<script>
let state = {
message: "Hello"
}
</script>
</body>
<body>
<input class="test" />
<script>
let state = {
message: "Hello"
}
let selectedElement = document.querySelector('test')
selectedElement.addEventListner('change', function () {
state.message = this.value
})
</script>
</body>
To inspect whether this code works right click, select inspect, element, open the console and type
state
An input’s value attribute is the text written in the input
The change
event is triggered every time the input value changes.
In our model of application design we render the entire application programmatically like this
<body>
<div id='appContainer'></div>
<script>
let appContainer = document.querySelector('#appContainer')
let App = {
render(state) {
return `
<h1>${state.message}</h1>
<input onchange="globalState.message=this.value;" />
<button onclick="globalRender()">Re-render</button>
`
}
}
let globalState = {
message: 'Hello'
}
globalRender()
function globalRender() {
appContainer.innerHTML = App.render(globalState)
}
</script>
</body>
Since we are rendering strings by attaching them into the innerHTML attribute of our element’s DOM Node. We can not select the element and add events to it. We can however have the event call functions we define.
You can checkout a live version of this example here
It is bad practice to introduce global variables in our application.
Global variables are bad because they can be accidentally modified by another part of our code if we’re not careful.
One simple way to avoid this is to scope our variables through the use of an Immediately-Invoked Function Expression (IIFE)
// Immediately-Invoked Function Expression (IIFE)
(function main() {
let appContainer = document.querySelector('#appContainer')
...
...
...
// not actually global anymore
let globalState = {
message: 'Hello'
}
...
...
...
// now scopedRender function
function globalRender() {
appContainer.innerHTML = App.render(globalState)
}
})()
If you modify the example we used previously to use the IIFE the code will stop working. If you checkout the console by Running JS you will see that an error is shown whenever you click on the button or modify the input value.
Scoping our application like this prevents our HTML from accessing the previously global variables we declared.
In order to intentionally export global variables to our window object.
We pass the window
object to our main function as global
.
As a convention we will
- Import at the start of our script the variables we want to use.
- Attach at the end of our script the variables we want to export.
// main.js
(function main(global) {
let document = global.document
// scoped variable no one can access this
let appContainer = document.querySelector('#appContainer')
...
...
...
let scopedState = {
message: 'Hello'
}
...
...
...
// now scopedRender function
function globalRender() {
appContainer.innerHTML = App.render(scopedState)
}
global.globalState = scopedState
global.globalRender = globalRender
})(window)
Our example now looks like this
The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. — Observer pattern - Wikipedia
The Observer pattern is used when there is one-to-many relationship between objects such as if one object is modified, its dependent objects are to be notified automatically.
Think of how in This Spreadsheet cell B8
is set to the sum of (B2:B7
), if any of B2
to B7
is changed B8
will also automatically update.
In any system we can use a single Subject calling it the Observer, Mediator, or EventEmitter.
The Observer subject fundamentally as an API is an object that implements the following methods.
let Observer = {
// add a subscriber (a function)
// to the array of subscribers to the event
subscribe(event, subscriber) {},
// remove a subscriber (a function)
// from the array of subscribers of the event
unsubscribe(event, subscriber) {},
// loop over array of functions subscribed to this event
// and call them with arguments
publish(event, args) {}
}
<<<<<<< HEAD
Other names for the subscribe
function can be on
Other names for the unsubscribe
function can be of
Other names for the publish
function can be trigger
emit
You can find an implementation with an example of something similar to our spreadsheet example here
Other names for the subscribe
function can be on
Other names for the unsubscribe
function can be of
Other names for the publish
function can be trigger
emit
You can find an implementation with an example of something similar to our spreadsheet example here
master
You will notice that if you change B1 or B2, Sum is automatically updated.
In our application model the state should be read only.
This means that in order to update the state of the app it can only be done Through an action.
So now our app can be
<body>
<div id="appContainer">
<h1>Hello</h1>
<input onchange="App.updateMessage(this.value)" />
<button onclick="App.saveMessage()">Re-render</button>
</div>
<script src="observer.js"></script>
<script src="app.js"></script>
<script src="main.js"></script>
</body>
// main.js
(function main(global) {
let { document, Observer } = global
let appContainer = document.querySelector('#appContainer')
let state = {
message: 'Hello'
}
Observer.subscribe('action', (action)=>{
switch(action.type) {
case 'SAVE_MESSAGE':
state.message = action.message
break;
}
Observer.publish('state.change', state)
})
Observer.subscribe('state.change',
(newState) => render(newState)
)
function render(state) {
appContainer.innerHTML = App.render(state)
}
})(window)
// app.js
(function app(global) {
let Observer = global.Observer
let App = {
localState: {},
render(state) {
return `
<h1>${state.message}</h1>
<input onchange="App.updateMessage(this.value)" />
<button onclick="App.saveMessage()">Re-render</button>
`
},
updateMessage(message) {
this.localState.message = message
},
saveMessage() {
Observer.publish('action', {
type: 'SAVE_MESSAGE',
message: this.localState.message
})
}
}
global.App = App
})(window)
You can find an example of this code here
Implement all actions in our ToDo App
In our todo app you have the following events
-
ADD_LIST
-
REMOVE_LIST
-
SELECT_LIST
-
ADD_ITEM
-
REMOVE_ITEM
-
TOGGLE_DONE_ITEM