Skip to content

Latest commit

 

History

History
344 lines (270 loc) · 9.17 KB

03.Pillars of JavaScript: Observed actions.md

File metadata and controls

344 lines (270 loc) · 9.17 KB

Pillars of JavaScript: Observed actions

Outline

Objective

  • Continue building our app
  • DOM Events
  • Update state through actions
  • Know and understand the Observe Pattern

Technologies used

  • HTML markup
  • Javascript
    • Language
      • Function and arrow functions
      • Spread, Rest operators
    • Observer

Design Patterns

  • Keep Code Modular
  • IIFE
  • Observer Pattern

What we will do

  • 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

Pre Session

Three Principles · Redux

Session

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.

Events trigger functions

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.

How we do it

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

Scoping our code

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

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.

Actions update the State

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