Build a To-do List with Hyperapp, the 1KB JS Micro-framework

Blog

Web Design Tips / Blog 42 Views

In this tutorial, we’ll be using Hyperapp to build a to-do list app. If you want to learn functional programming principles, but not get bogged down in details, read on.

Hyperapp is hot right now. It recently surpassed 11,000 stars on GitHub and made the 5th place in the Front-end Framework section of the 2017 JavaScript Rising Stars. It was also featured on SitePoint recently, when it hit version 1.0.

The reason for Hyperapp’s popularity can be attributed to its pragmatism and ultralight size (1.4 kB), while at the same time achieving results similar to React and Redux out of the box.

So, What Is HyperApp?

Hyperapp allows you to build dynamic, single-page web apps by taking advantage of a virtual DOM to update the elements on a web page quickly and efficiently in a similar way to React. It also uses a single object that’s responsible for keeping track of the application’s state, just like Redux. This makes it easier to manage the state of the app and make sure that different elements don’t get out of sync with each other. The main influence behind Hyperapp was the Elm architecture.

At its core, Hyperapp has three main parts:

  • State. This is a single object tree that stores all of the information about the application.
  • Actions. These are methods that are used to change and update the values in the state object.
  • View. This is a function that returns virtual node objects that compile to HTML code. It can use JSX or a similar templating language and has access to the state and actions objects.

These three parts interact with each other to produce a dynamic application. Actions are triggered by events on the page. The action then updates the state, which then triggers an update to the view. These changes are made to the Virtual DOM, which Hyperapp uses to update the actual DOM on the web page.

Getting Started

To get started as quickly as possible, we’re going to use CodePen to develop our app. You need to make sure that the JavaScript preprocessor is set to Babel and the Hyperapp package is loaded as an external resource using the following link:

https://unpkg.com/hyperapp

To use Hyperapp, we need to import the app function as well as the h method, which Hyperapp uses to create VDOM nodes. Add the following code to the JavaScript pane in CodePen:

const { h, app } = hyperapp;

We’ll be using JSX for the view code. To make sure Hyperapp knows this, we need to add the following comment to the code:

/** @jsx h */

The app() method is used to initialize the application:

const main = app(state, actions, view, document.body);

This takes the state and actions objects as its first two parameters, the view() function as its third parameter, and the last parameter is the HTML element where the application is to be inserted into your markup. By convention, this is usually the <body> tag, represented by document.body.

To make it easy to get started, I’ve created a boilerplate Hyperapp code template on CodePen that contains all the elements mentioned above. It can be forked by clicking on this link.

Hello Hyperapp!

Let’s have a play around with Hyperapp and see how it all works. The view() function accepts the state and actions objects as arguments and returns a Virtual DOM object. We’re going to use JSX, which means we can write code that looks a lot more like HTML. Here’s an example that will return a heading:

const view = (state, actions) => (
  <h1>Hello Hyperapp!</h1>
);

This will actually return the following VDOM object:

{
  name: "h1",
  props: {},
  children: "Hello Hyperapp!"
}

The view() function is called every time the state object changes. Hyperapp will then build a new Virtual DOM tree based on any changes that have occurred. Hyperapp will then take care of updating the actual web page in the most efficient way by comparing the differences in the new Virtual DOM with the old one stored in memory.

Components

Components are pure functions that return virtual nodes. They can be used to create reusable blocks of code that can then be inserted into the view. They can accept parameters in the usual way that any function can, but they don’t have access to the state and actions objects in the same way that the view does.

In the example below, we create a component called Hello() that accepts an object as a parameter. We extract the name value from this object using destructuring, before returning a heading containing this value:

const Hello = ({name}) => <h1>Hello {name}</h1>;

We can now refer to this component in the view as if it were an HTML element entitled <Hello />. We can pass data to this element in the same way that we can pass props to a React component:

const view = (state, actions) => (
  <Hello name="Hyperapp" />
);

Note that, as we’re using JSX, component names must start with capital letters or contain a period.

State

The state is a plain old JavaScript object that contains information about the application. It’s the “single source of truth” for the application and can only be changed using actions.

Let’s create the state object for our application and set a property called name:

const state = {
  name: "Hyperapp"
};

The view function now has access to this property. Update the code to the following:

const view = (state, actions) => (
  <Hello name={state.name} />
);

Since the view can access the state object, we can use its name property as an attribute of the <Hello /> component.

Actions

Actions are functions used to update the state object. They’re written in a particular form that returns another, curried function that accepts the current state and returns an updated, partial state object. This is partly stylistic, but also ensures that the state object remains immutable. A completely new state object is created by merging the results of an action with the previous state. This will then result in the view function being called and the HTML being updated.

The example below shows how to create an action called changeName(). This function accepts an argument called name and returns a curried function that’s used to update the name property in the state object with this new name.

const actions = {
  changeName: name => state => ({name: name})
};

To see this action, we can create a button in the view and use an onclick event handler to call the action, with an argument of “Batman”. To do this, update the view function to the following:

const view = (state, actions) => (
  <div>
    <Hello name={state.name} />
    <button onclick={() => actions.changeName('Batman')}>I'm Batman</button>
  </div>
);

Now try clicking on the button and watch the name change!

You can see a live example here.

Hyperlist

Now it’s time to build something more substantial. We’re going to build a simple to-do list app that allows you to create a list, add new items, mark them as complete and delete items.

First of all, we’ll need to start a new pen on CodePen. Add the following code, or simply fork my HyperBoiler pen:

const { h, app } = hyperapp;
/** @jsx h */

const state = {

};

const actions = {

};

const view = (state, actions) => (

);

const main = app(state, actions, view, document.body);

You should also add the following in the CSS section and set it to SCSS:

// fonts
@import url("https://fonts.googleapis.com/css?family=Racing+Sans+One");
$base-fonts: Helvetica Neue, sans-serif;
$heading-font: Racing Sans One, sans-serif;

// colors
$primary-color: #00caff;
$secondary-color: hotpink;
$bg-color: #222;

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  padding-top: 50px;
  background: $bg-color;
  color: $primary-color;
  display: flex;
  height: 100vh;
  justify-content: center;
  font-family: $base-fonts;
}

h1 {
  color: $secondary-color;
  & strong{ color: $primary-color; }
  font-family: $heading-font;
  font-weight: 100;
  font-size: 4.2em;
  text-align: center;
}

a{
  color: $primary-color;
}

.flex{

  display: flex;
  align-items: top;
  margin: 20px 0;


  input {
    border: 1px solid $primary-color;
    background-color: $primary-color;

    font-size: 1.5em;
    font-weight: 200;

    width: 50vw;
    height: 62px;

    padding: 15px 20px;
    margin: 0;
    outline: 0;

    &::-webkit-input-placeholder {
      color: $bg-color;
    }

    &::-moz-placeholder {
      color: $bg-color;
    }

    &::-ms-input-placeholder {
      color: $bg-color;
    }

    &:hover, &:focus, &:active {
      background: $primary-color;
    }
  }

  button {
    height: 62px;
    font-size: 1.8em;
    padding: 5px 15px;
    margin: 0 3px;
  }
}

ul#list {
  display: flex;
  flex-direction: column;
  padding: 0;
  margin: 1.2em;
  width: 50vw;
  li {
    font-size: 1.8em;
    vertical-align: bottom;
    &.completed{
      color: $secondary-color;
      text-decoration: line-through;
      button{
        color: $primary-color;
      }
    }
      button {
        visibility: hidden;
        background: none;
        border: none;
        color: $secondary-color;
        outline: none;
        font-size: 0.8em;
        font-weight: 50;
        padding-top: 0.3em;
        margin-left: 5px;
    }
    &:hover{
      button{
        visibility: visible;
      }
    }
  }
}

button {
    background: $bg-color;
    border-radius: 0px;
    border: 1px solid $primary-color;
    color: $primary-color;

    font-weight: 100;

    outline: none;

    padding: 5px;

    margin: 0;

    &:hover, &:disabled {
      background: $primary-color;
      color: #111;
    }
    &:active {
      outline: 2px solid $primary-color;
    }
    &:focus {
      border: 1px solid $primary-color;
    }
  }

These just add a bit of style and Hyperapp branding to the application.

Now let’s get on and start building the actual application!

Initial State and View

To start with, we’re going to set up the initial state object and a simple view.

When creating the initial state object, it’s useful to think about what data and information your application will want to keep track of throughout its lifecycle. In the case of our list, we’ll need an array to store the to-dos, as well as a string that represents whatever is written in the input field where the actual to-dos are entered. This will look like the following:

const state = {
  items: [],
  input: '',
};

Next, we’ll create the view() function. To start with, we’ll focus on the code required to add an item. Add the following code:

const view = (state, actions) => (
  <div>
    <h1><strong>Hyper</strong>List</h1>
    <AddItem add={actions.add} input={actions.input} value={state.input} />
  </div>
);

This will display a title, as well as an element called <AddItem />. This isn’t a new HTML element, but a component that we’ll need to create. Let’s do that now:

const AddItem = ({ add, input, value }) => (
  <div class='flex'>
    <input type="text" value={value}
           onkeyup={e => (e.keyCode === 13 ? add() : null)}
           oninput={e => input({ value: e.target.value })}
    />
    <button onclick={add}>+</button>
  </div>
);

This returns an <input> element that will be used to enter our to-dos, as well as a <button> element that will be used to add them to the list. The component accepts an object as an argument, from which we extract three properties: add, input and value.

As you might expect, the add() function will be used to add an item to our list of to-dos. This function is called, either if the Enter key is pressed (it has a KeyCode of 13) or if the button is clicked. The input() function is used to update the value of the current item in state and is called whenever the text field receives user input. Finally, the value property is whatever the user has typed into the input field.

Note that the input() and add() functions are actions, passed as props to the <AddItem /> component:

<AddItem add={actions.add} input={actions.input} value={state.input} />

You can also see that the value prop is taken from the state’s input property. So the text that’s displayed in the input field is actually stored in the state and updated every time a key is pressed.

To glue everything together, we need to add the input action:

const actions = {
  input: ({ value }) => ({ input: value })
}

Now if you start typing inside the input field, you should see that it displays what you’re typing. This demonstrates the Hyperapp loop:

  1. the oninput event is triggered as the user types text into the input field
  2. the input() action is called
  3. the action updates the input property in the state
  4. a change in state causes the view() function to be called and the VDOM is updated
  5. the changes in the VDOM are then made to the actual DOM and the page is re-rendered to display the key that was pressed.

Have a go and you should see what’s typed appear in the input field. Unfortunately, pressing Enter or clicking on the “+” button doesn’t do anything at the moment. That’s because we need to create an action that adds items to our list.

Adding a Task

Before we look at creating a list item, we need to think how they’re going to be represented. JavaScript’s object notation is perfect, as it lets us store information as key-value pairs. We need to think about what properties a list item might have. For example, it needs a value that describes what needs to be done. It also needs a property that states if the item has been completed or not. An example might be:

{
  value: 'Buy milk',
  completed: false,
  id: 123456
}

Notice that the object also contains a property called id. This is because VDOM nodes in Hyperapp require a unique key to identify them. We’ll use a timestamp for this.

Now we can have a go at creating an action for adding items. Our first job is to reset the input field to be empty. This is done by resetting the input property to an empty string. We then need to add a new object to the items array. This is done using the Array.concat method. This acts in a similar way to the Array.push() method, but it returns a new array, rather than mutating the array it’s acting on. Remember, we want to create a new state object and then merge it with the current state rather than simply mutating the current state directly. The value property is set to the value contained in state.input, which represents what’s been entered in the input field:

add: () => state => ({
  input: '',
  items: state.items.concat({
    value: state.input,
    completed: false,
    id: Date.now()
  })
})

Note that this action contains two different states. There’s the current state that’s represented by the argument supplied to the second function. There’s also the new state that’s the return value of the second function.

To demonstrate this in action, let’s imagine the app has just started with an empty list of items and a user has entered the text “Buy milk” into the input field and pressed Enter, triggering the add() action.

Before the action, the state looks like this:

state = {
  input: 'Buy milk',
  items: []
}

This object is passed as an argument to the add() action, which will return the following state object:

state = {
  input: '',
  items: [{
    value: 'Buy milk',
    completed: false,
    id: 1521630421067
  }]
}

Now we can add items to the items array in the state, but we can’t see them! To fix this, we need to update our view. First of all we need to create a component for displaying the items in the list:

const ListItem = ({ value, id }) => <li id={id} key={id}>{value}</li>;

This uses a <li> element to display a value, which is provided as an argument. Note also that the id and key attributes both have the same value, which is the unique ID of the item. The key attribute is used internally by Hyperapp, so isn’t displayed in the rendered HTML, so it’s useful to also display the same information using the id attribute, especially since this attribute shares the same condition of uniqueness.

Now that we have a component for our list items, we need to actually display them. JSX makes this quite straightforward, as it will loop over an array of values and display each one in turn. The problem is that the state.items doesn’t include JSX code, so we need to use Array.map to change each item object in the array into JSX code, like so:

state.items.map(item => ( <ListItem id={item.id} value={item.value} /> ));

This will iterate over each object in the state.items array and create a new array that contains ListItem components instead. Now we just need to add this to the view. Update the view() function to the code below:

const view = (state, actions) => (
  <div>
    <h1><strong>Hyper</strong>List</h1>
    <AddItem add={actions.add} input={actions.input} value={state.input} />
    <ul id='list'>
      { state.items.map(item => ( <ListItem id={item.id} value={item.value} /> )) }
    </ul>
  </div>
);

This simply places the new array of ListItem components inside a pair of <ul> tags so they’re displayed as an unordered list.

Now if you try adding items, you should see them appear in a list below the input field!

Mark a Task as Completed

Our next job is to be able to toggle the completed property of a to-do. Clicking on an uncompleted task should update its completed property to true, and clicking on a completed task should toggle its completed property back to false.

This can be done by using the following action:

toggle: id => state => ({
  items: state.items.map(item => (
    id === item.id ? Object.assign({}, item, { completed: !item.completed }) : item
  ))
})

There’s quite a bit going on in this action. First of all, it accepts a parameter called id, which refers to the unique ID of each task. The action then iterates over all of the items in the array, checking if the ID value provided matches the id property of each list item object. If it does, it changes the completed property to the opposite of what it currently is using the negation operator !. This change is made using the Object.assign() method, which creates a new object and performs a shallow merge with the old item object and the updated properties. Remember, we never update objects in the state directly. Instead, we create a new version of the state that overwrites the current state.

Now we need to wire this action up to the view. We do this by updating the ListItem component so that it has an onclick event handler that will call the toggle action we just created. Update the ListItem component code so that it looks like the following:

const ListItem = ({ value, id, completed, toggle, destroy }) => (
  <li class={completed && "completed"} id={id} key={id} onclick={e => toggle(id)}>{value}</li>
);

The eagle-eyed among you will have spotted that the component has gained some extra parameters and there’s also some extra code in the <li> attributes list:

class={completed && "completed"}

This is a common pattern in Hyperapp that’s used to insert extra fragments of code when certain conditions are true. It uses short-circuit or lazy evaluation to set the class as completed if the completed argument is true. This is because when using &&, the return value of the operation will be the second operand if both operands are true. Since the string "completed" is always true, this will be returned if the first operand — the completed argument — is true. This means that, if the task has been completed, it will have class of “completed” and can be styled accordingly.

Our last job is to update the code in the view() function to add the extra argument to the <ListItem /> component:

const view = (state, actions) => (
  <div>
    <h1><strong>Hyper</strong>List</h1>
    <AddItem add={actions.add} input={actions.input} value={state.input} />
    <ul id='list'>
      {
        state.items.map(item => (
          <ListItem id={item.id} value={item.value} completed={item.completed} toggle={actions.toggle} />
        ))
      }
    </ul>
  </div>
);

Now if you add some items and try clicking on them, you should see that they get marked as complete, with a line appearing through them. Click again and they revert back to incomplete.

Delete a Task

Our list app is running quite nicely at the moment, but it would be good if we could delete any items that we no longer need in the list.

Our first job is to add a destroy() action that will remove an item from the items array in state. We can’t do this using the Array.slice() method, as this is a destructive method that acts on the original array. Instead, we use the filter() method, which returns a new array that contains all the item objects that pass a specified condition. This condition is that the id property doesn’t equal the ID that was passed as an argument to the destroy() action. In other words, it returns a new array that doesn’t include the item we want to get rid of. This new list will then replace the old one when the state is updated.

Add the following code to the actions object:

destroy: id => state => ({ items: state.items.filter(item => item.id !== id) })

Now we again have to update the ListItem component to add a mechanism for triggering this action. We’ll do this by adding a button with an onclick event handler:

const ListItem = ({ value, id, completed, toggle, destroy }) => (
  <li class={completed && "completed"} id={id} key={id} onclick={e => toggle(id)}>
    {value}
    <button onclick={ () => destroy(id) }>x</button>
  </li>
);

Note that we also need to add another parameter called destroy that represents the action we want to use when the button is clicked. This is because components don’t have direct access to the actions object in the same was as the view does, so the view needs to pass any actions explicitly.

Last of all, we need to update the view to pass actions.destroy as an argument to the <ListItem /> component:

const view = (state, actions) => (
  <div>
    <h1><strong>Hyper</strong>List</h1>
    <AddItem add={actions.add} input={actions.input} value={state.input} />
    <ul id='list'>
      {state.items.map(item => (
        <ListItem
          id={item.id}
          value={item.value}
          completed={item.completed}
          toggle={actions.toggle}
          destroy={actions.destroy}
        />
      ))}
    </ul>
  </div>
);

Now if you add some items to your list, you should notice the “x” button when you mouse over them. Click on this and they should disappear into the ether!

Delete All Completed Tasks

The last feature we’ll add to our list app is the ability to remove all completed tasks at once. This uses the same filter() method that we used earlier — returning an array that only contains item objects with a completed property value of false. Add the following code to the actions object:

clearAllCompleted: ({items}) => ({ items: items.filter(item => !item.completed) })

To implement this, we simply have to add a button with an onclick event handler to call this action to the bottom of the view:

const view = (state, actions) => (
  <div>
    <h1><strong>Hyper</strong>List</h1>
    <AddItem add={actions.add} input={actions.input} value={state.input} />
    <ul id='list'>
      {state.items.map(item => (
        <ListItem
          id={item.id}
          value={item.value}
          completed={item.completed}
          toggle={actions.toggle}
          destroy={actions.destroy}
        />
      ))}
    </ul>
    <button onclick={() => actions.clearAllCompleted({ items: state.items })}>
      Clear completed items
    </button>
  </div>
);

Now have a go at adding some items, mark a few of them as complete, then press the button to clear them all away. Awesome!

See the Pen Hyperlist by SitePoint (@SitePoint) on CodePen.

That’s All, Folks

That brings us to the end of this tutorial. We’ve put together a simple to-do list app that does most things you’d expect such an app to do. If you’re looking for inspiration and want to add to the functionality, you could look at adding priorities, changing the order of the items using drag and drop, or adding the ability to have more than one list.

I hope this tutorial has helped you to gain an understanding about how Hyperapp works. If you’d like to dig a bit deeper into Hyperapp, I’d recommend reading the docs and also having a peek at the source code. It’s not very long, and will give you a useful insight into how everything works in the background. You can also ask more questions on the Hyperapp Slack group. It’s one of the friendliest groups I’ve used, and I’ve been given a lot of help by the knowledgable members. You’ll also find that Jorge Bucaran, the creator of Hyperapp, frequently hangs out on there and offers help and advice.

Using CodePen makes developing Hyperapp applications really quick and easy, but you’ll eventually want to build your own applications locally and also deploy them online. For tips on how to do that, check out my next article on bundling a Hyperapp app and deploying it to GitHub Pages!

I hope you enjoy playing around with the code from this article, and please share any thoughts or questions you have in the comments below.

Comments