Building a Simple Web App Using the Observer Pattern

Adam Wattis
codeburst
Published in
5 min readNov 17, 2020

--

You’ve probably heard of the observer pattern; perhaps you’ve used it if you’ve ever developed with a Javascript frontend framework before. In its most basic form it can look like this:

class Observable {
constructor() {
this.observers = []
}
subscribe(observer) {
this.observers.push(observer)
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => {
return obs != observer
})
}
publish(data) {
this.observers.forEach(observer => {
observer.update(data)
})
}
}

Essentially, you get an object that other objects can subscribe to. When you get some new data, you use the publish(data) method to let each subscribing object know that something new has happened. It’s a pretty simple concept.

However, the mental leap from this simple concept to having a working web app that updates reactively on state change may seem like quite a stretch. So, how can we implement the observer pattern to build a web app from scratch, without any frameworks to help us? This article aims to explore just that.

Observable state

The Observable() class above can seem very abstract. Let’s change this by bringing some state into the picture:

class State extends Observable {
constructor(state = {}) {
super()
this.state = state
this.publish(state)
}
setState(propName, newData) {
Object.defineProperty(this.state, propName, {value: newData, configurable: true})
this.publish(this)
}
getState() {
return this.state
}
}

We extend out Observable() to create a new class, State() that can maintain a state. In the constructor, we set an initial state. We have a setState() method that allows us to set the state which then automatically publishes it to our observers. This way we can have objects observe our state, set it, and then get notified by the update() function so they may change accordingly and in realtime.

Let’s house this functionality in observable.js and export { Observable, State} to make them available for other modules to use.

Markup and styling

Let's create a container into which we will inject our web app:

<div>
<div class="header">
<h1 class="app_header">My App</h1>
</div>
<!-- App anchor -->
<div class="main" id="app">
</div>
<!-- Javascript file -->
<script type="module" src="index.js"></script>
</div>

The application code will be in index.js and there it will hook into the id="app" to render our HTML dynamically. After adding some CSS now have a starting point:

Nothing fancy

Rendering HTML with Javascript

Each time the setState() method is called on the state object with some new data it will run the update() callback on each subscriber. When this happens we need each subscriber to render some HTML and inject our data into it. To make this task a bit easier let’s use lit-html to help us interact with the DOM in an efficient way:

import {html, render} from 'https://unpkg.com/lit-html?module'const greetingComponent = (name) => {
return html`<h1>Hello ${name}</h1>`
}
render(greetingComponent('Friend'), document.getElementById('app'))

The greetingsComponent will return a template containing a <h1></h1> tag with my greeting in it. If the name changes, the template will re-render to the element with id="app" in our HTML document. This is great as all we now need to do is create components that return these HTML templates, then use these components to compose our layout which is finally passed to the rendering function.

Components

We can split up our application to make it more modular by creating components. Starting with the App() component, which will carry our observable state, we’ll set up some essential functionality:

const appState = new State({user: '', friends: []})class App {
constructor(el, state) {
this.el = el
this.state = state
this.state.subscribe(this)
this.update(this.state)
}

loginWithName = (name) => {
return this.state.setState('user', name)
}
prepareTemplate(data) {
return html`
<div>
${data.user ? homeComponent(data, this.state) : loginComponent(this.loginWithName)}
</div>
`
}
update(state) {
render(this.prepareTemplate(state.getState()), this.el)
}
}
const app = new App(document.getElementById('app'), appState)

This class component has a constructor that takes the HTML element anchor and a state object, subscribes the component instance to the state, and finally runs an initial update() on itself.

When the update method is fired it renders the prepared template to our anchor element. In our prepareTemplate() method we conditionally select to display one of two different components:

  • If there is a user in the data object passed to it then we will render the homeComponent().
  • If there is no user available (which is the case initially since we initiate our appState with name being an empty string) we will render the loginComponent() and pass it our loginWithName() callback.

Let’s create a functional style component to render our login screen. This will be a function that returns some HTML template, and takes a callback function that gets fired if the user logs in:

const loginComponent = (handleLogin) => {
let name = ''
const handleTextInput = (input) => {
return name = input
}
return html`
<div class="login">
<input class="input" placeholder="Login with name" @input=${(e) => handleTextInput(e.target.value)}/>
<button class="button" @click=${() => handleLogin(name)}>Login</button>
</div>
`
}

Let's do something similar to show our home screen:

const homeComponent = (data, state) => {
let newFriend = ''
const handleInput = (input) => {
return newFriend = input
}
const handleClick = () => {
if (newFriend.length < 1) return
const newFriends = [...data.friends, newFriend]
state.setState('friends', newFriends)
newFriend = ''
}
const handleLogout = () => {
state.setState('user', '')
}
return html`
<div>
${greetingComponent(data.user)}
${listComponent(data.friends)}
<input class="input" @input=${(e) => handleInput(e.target.value)} />
<button class="button" @click=${() => handleClick()}>Add new friend</button>
<button class="button logout-button" @click=${() => handleLogout()}>Log Out</button>
</div>
`
}

Here we’re also passing the component our state, so that the component may directly mutate the state by calling setState() in the handleClick() function. After writing a new friend’s name in the input box, handleClick() will add that name to our friends list in the state.

We want to render out our friends in a list, so the last thing to do here is to create the listComponent(), which takes an array of strings and renders each as a new list item. If no list exists (or if it is empty) we will render a message.

const listComponent = (list) => {
if (!list || list.length < 1) return html`<h1>You have no friends 😔</h1>`
return html`
<div class="friends-list">
${list.map(item => html`<div class="friend">My friend ${item}</div>`)}
</div>
`
}

We should now have a simple login screen, and a home screen where we can add our friends:

And as we can see, the app reacts to our input and updates accordingly:

Basic, but it works!

Conclusion

Pretty neat, right? I’ll leave the code on GitHub for anybody interested. I would love to hear your thoughts on this exercise, and what you would do to improve the design of this code! Please leave a comment below, and don’t forget to follow.

--

--