Making a TODO app in Isotope
Tags: tutorial, javascript, webdev
Published: 2020-04-09 09:26:12

So, I've just unveiled my new open-source UI library called  [**Isotope**](https://areknawo.com/isotope). It's fast, lightweight, modular and overall - I think it's pretty good.

Anyway, if you're interested in trying something new and fresh, maybe consider giving Isotope a try? You can go straight up to the  [docs](https://areknawo.com/isotope)  or bear with me, as we're going to make a simple  **TODO app**, allowing us to learn the basics of Isotope.

# Setup

Isotope is written in  [TypeScript](https://www.typescriptlang.org/)  that's transpiled down to pure JS, which requires no additional tooling to get you up & running.

To set up our project, we'll use  **npm**  (but  [yarn](https://yarnpkg.com/)  is also an option). We'll start by running run  `npm init`  to create our base  _package.json_  file. Then, install the Isotope and  [Bulma](http://bulma.io/)  - a CSS-only library that will make our app look slightly prettier!

```bash
npm install @isotope/core bulma
```

Now, you can use Isotope with any bundler you want (or go  [buildless](https://buildless.site/)), but here, we'll use the  [Parcel](https://parceljs.org/)  - a zero-config bundler that doesn't require any setup whatsoever, and thus it's great for any kind of playground-like scenario!

```bash
npm install --dev parcel-bundler
```

With the bundler installed, we can start writing some code, or more specifically, the HTML!

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Isotope Playground</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
  </head>

  <body>
    <script src="src/index.js"></script>
  </body>
</html>
```

Aside from the basic boilerplate, we also load the  [Font Awesome](https://fontawesome.com/)  icon library through its CDN and include our main JS file, which is where the whole magic will happen. And that's it for HTML! Parcel will take care of the rest. Just make sure you've got all the files in correct places and run  `npx parcel index.html`  to start the  **dev server**.

# Container

So, now that we're all set up, let's get right into making our app. First, we have to create the a  **container**  that will house all our TODOs, as well as a form to add them.

```javascript
import { createDOMView } from "@isotope/core";
import "bulma/css/bulma.min.css";

const view = createDOMView(document.body);
const container = view
  .main({
    classes: ["container", "fluid"],
  })
  .div({
    classes: ["columns", "is-centered", "is-vcentered", "is-mobile"],
  })
  .div({
    classes: ["column", "is-narrow"],
    styles: {
      width: "70%",
    },
  });
```

In the snippet above we create our main container. We start by importing the  `createDOMView()`  function from the Isotope library, which is responsible for creating a  [view](https://areknawo.com/isotope#docs%3Eviews)  - a top-level node, that attaches to the specified DOM element to render its content.

Here, we attach our view to the  `<body>`  element, making Isotope effectively take control of the entire website. It's a nice solution for our current situation, but keep in mind that Isotope's  **progressive**  nature, allows it to attach to any element to control even the smallest pieces of your UI.

So, we've got our top-level node attached to the  `<body>`  element. This is a great start for our application. In Isotope a  [node](https://areknawo.com/isotope/#docs%3Enodes)  is the most important entity and having access to even a single one, grants you the power to create more.

That's essentially what we do in the next line.

```javascript
// ...
const container = view.main({
  classes: ["container", "fluid"],
});
// ...
```

We use the  `view`  reference we've got to create a new node - a child node that will append a new element to the DOM. For that, we use the  `main()`  method - a method from the Isotope's HTML node pack.  
Isotope's  [node packs](https://areknawo.com/isotope/#docs%3Enode-packs)  are essentially bundles of  **shortcut methods**  that get applied directly to the node's prototype.  `main()`  is one of such methods. It simplifies the creation of the  `<main>`  element, which would otherwise require a bit longer syntax (`child("main")`).

To configure our new node, we have to use a configuration object. Here, we make use of the [`classes`](https://areknawo.com/isotope/#docs%3Eclasses) config property, to add some CSS classes to the element.

So, to summarize, we create a new node which represents a  `<main>`  element - child to  `<body>`  - that has  `"container"`  and  `"fluid"`  CSS classes applied to it. On a side note - all of the used class names come from Bulma, which we import at the top of our JS file thanks to Parcel CSS imports support.

The  `main()`  like all other methods from the HTML node pack, returns the newly-created node. In this way we get the ability to add new child nodes to this node, effectively building our UI.

```javascript
const container = view
  .main({
    classes: ["container", "fluid"],
  })
  .div({
    classes: ["columns", "is-centered", "is-vcentered", "is-mobile"],
  })
  .div({
    classes: ["column", "is-narrow"],
    styles: {
      width: "70%",
    },
  });
```

As you can see, when setting up our container, we put this  **chainability**  of Isotope to a good use. In the end, it's the last node in the chain that gets assigned to the  `container`  variable. Also, notice how we use another configuration property - [`styles`](https://areknawo.com/isotope/#docs%3Estyles) - to set CSS styles of the underlying element.

At the moment our HTML structure should look somewhat like this:

```html
<body>
  <main>
    <div>
      <div></div>
    </div>
  </main>
</body>
```

# Basic elements

Now that we've got the container, it's time to add some real elements to our app!

```javascript
// ...
container
  .h1({
    classes: ["has-text-centered", "title"],
  })
  .text("Isotope TODO");
container.form();
container.ul();
```

Here we're adding 3 new child nodes to the container: header, form, and list. Apart from the usual stuff, notice how we use a special [`text()`](https://areknawo.com/isotope/#docs%3Etext-rendering) method to set the text of the created  `<h1>`  element.

Now, after the header, we create two more elements -  `<form>`  and  `<ul>`. These 2 elements are where the rest of our app will be placed. With this in mind, it's easy to see how our code can become bloated over time pretty easily. To prevent that, we'll move both of these elements into separate  **components**, which themselves will be placed within separate modules.

## Creating components

In Isotope things are meant to be simple - and so are the  [components](https://areknawo.com/isotope/#docs%3Ecomponents), which themselves are nothing more than simple functions. Take a look:

```javascript
// src/form.js
const Form = (container) => {
  const form = container.form();

  return form;
};

export { Form };
```

Here, in a new file (_src/form.js_), we create a new Isotope component -  `Form`. As you can see, it's a function that accepts a parent node, and optionally returns a new node.

Such a component can then be used through the  `$()`  method:

```
// src/index.js
// ...
import { Form } from "./form";
// ...
container.$(Form);
```

If the component function returns a node, then the same node is returned from the  `$()`  method. Otherwise, the  `$()`  method returns the node it was called upon (in our case it would be the  `container`) for easier chaining.

As you can see, Isotope components are really easy to use. Let's now set up our  `List`  component as well.

```javascript
// src/list.js
const List = (container) => {
  const list = container.ul();

  return list;
};

export { List };
```

```javascript
// src/index.js
// ...
import { Form } from "./form";
import { List } from "./list";
// ...
container.$(Form);
container.$(List);
```

# Building form

With our components set up, it's time to build our form for accepting new TODOs!

```javascript
// src/index.js
const Form = (container) => {
  const form = container.form({
    classes: ["field", "has-addons"],
    styles: { justifyContent: "center" },
  });
  const input = form.div({ classes: ["control"] }).input({
    attribs: { type: "text", placeholder: "TODO" },
    classes: ["input"],
  });

  form
    .div({ classes: ["control"] })
    .button({ classes: ["button", "is-primary"] })
    .span({ classes: ["icon", "is-small"] })
    .i({ classes: ["fas", "fa-plus"] });

  return form;
};

export { Form };
```

So, above we create our form layout. As you can see, there's not much new when compared to what we already know. There's only the [`attribs`](https://areknawo.com/isotope/#docs%3Eattribs) configuration property that's used to set attributes of the node's DOM element.

Apart from that, you can also notice how helpful Isotope's method chaining capabilities can be when creating the submit button.

## Reactivity

With our form ready, we now need to make it  [reactive](https://areknawo.com/isotope#docs%3Ereactivity). Isotope is a statically-dynamic UI library, which (apart from sounding cool) means that it has a bit different approach to reactivity. Instead of making the entire UI reactive out-of-the-box, Isotope requires you to specifically mark certain nodes as dynamic by either creating their own  **state**  or by  **linking**  them to other dynamic nodes. For the purpose of our TODO app, we'll explore both of these ways.

First, we have to identify what kind of data should be made reactive. In our case - it's the list of TODOs that we'll operate on, and the current user input for creating new TODOs.

So, we've got 2 properties to create in our  **state**  -  `input`  and  `todos`. The state should be accessible by both the  `Form`  (to write to  `input`), as well as  `List`  (to display TODOs) component. Thus, I think it'll be best to initialize our state on the  `container`  node.

```javascript
// src/index.js
// ...
const container = view
  .main({
    classes: ["container", "fluid"],
  })
  .div({
    classes: ["columns", "is-centered", "is-vcentered", "is-mobile"],
  })
  .div({
    classes: ["column", "is-narrow"],
    state: {
      input: "",
      todos: [],
    },
    styles: {
      width: "70%",
    },
  });
// ...
```

So, we go back to our  `index.js`  file and set up our state on the last node (the one that's assigned to the  `container`  variable. To do this, we make use of the  `state`  property, supplying it with our state object, containing initial values. And that's it! - Now our container is reactive!

## Event handling

Let's get back to the  _src/form.js_  file and put this reactivity to good use. First, we'll handle the  `<form>`  element itself.

```javascript
// src/form.js
const Form = (container) => {
  // ...
  form.on("submit", (event) => {
    const input = container.getState("input");
    const todos = container.getState("todos");

    if (input) {
      container.setState({
        input: "",
        todos: [
          ...todos,
          {
            text: input,
            id: Math.random().toString(36).substr(2, 9),
          },
        ],
      });
    }

    event.preventDefault();
  });
  // ...
};
// ...
```

On the  `form`  node, we use the  `on()`  method to listen to the  `submit`  event of the  `<form>`  element. Isotope provides a set of  [event-related methods](https://areknawo.com/isotope/#docs%3Eevents)  (`on()`,  `off()`  and  `emit()`), which are universal and can be used to handle all kinds of events - DOM, custom, and Isotope-related ones.

In our handling function, we first access the  `input`  and  `todos`  properties from the container's state. Remember that Isotope doesn't handle data passing on its own - you need to do that by having a reference to a stateful node, through custom events or in any other way you find suitable. In our case, because the  `container`  that holds the data is also the direct parent of our component, we can use that reference to access its state.

Isotope provides 2 methods to work with the state -  `getState()`  and  `setState()`. To access one of state properties, you have to pass its key to the  `getState()`  method. That's what we do to access the  `input`  and  `todos`  properties.

After that, we check whether the user has entered anything in the form (i.e. if the  `input`  isn't empty) and if so, we transform it into a new TODO. In our case, a TODO is an object with  `text`  and  `id`  property, where  `text`  contains TODO's actual content, and  `id`  is a random string, to help us identify a given TODO later on.

We use the  `setState()`  method to update the  `container`'s state. The method accepts an object that should be applied on top of the previous state. It doesn't have to include all the properties the original state object had, but we assign both anyway.  `input`  gets assigned an empty string to clean the value of  `<input>`  element, while  `todos`  is assigned a new array. Know that because arrays are passed by reference in JavaScript, you can as well use the  `push()`  method on the  `todos`  variable that we've got from the  `getState()`  call. It's just a matter of personal preference as to which way you prefer. Just know that you'll eventually have to call the  `setState()`  method (even with an empty object), to let Isotope know that it should update the node.

## Lifecycle events

Now we'll move to our  `input`  node to get it set up as well.

```javascript
// src/form.js
const Form = (container) => {
  // ...
  const input = form
    .div({ classes: ["control"] })
    .input({
      attribs: { type: "text", placeholder: "TODO" },
      classes: ["input"],
    })
    .on("input", ({ target }) => {
      container.setState({ input: target.value });
    })
    .on("node-updated", ({ node }) => {
      node.element.value = container.getState("input");
    });
  // ...
};
// ...
```

Here, we once again use Isotope's chainability (`on()`  method returns the node it was called upon) to listen to 2 events one after another. First, we handle the  `input`  event, which is native to HTML  `<input>`  element. Inside the listener, we use the  `setState()`  method, to set the value of  `input`  property to the current input.

Next up, we listen to one of Isotope's node  [lifecycle events](https://areknawo.com/isotope/#docs%3Eevents)  -  `node-updated`. This event is emitted every time a node updates - either via a change in state or in the result of a link. The listener is passed an object with  `node`  property, giving it access to the node the listener is connected to. We use that reference to access the node's underlying HTML element through the  `element`  property and set it's value to the value of  `input`  property from the container's state.

Through the code above, we've gained complete control over the  `<input>`  element. It's value is completely reliant on the value of the  `container`'s state.

## Linking

With the event listeners in place, our form is almost done. The last issue we have to solve is related to the  `node-updated`  event our  `input`  node is listening to. The problem is that it'll never be triggered as the node neither has its own state, nor it's linked to any other nodes.

To fix that issue, we have to write one magic line:

```javascript
// src/form.js
// ...
container.link(input);
// ...
```

With the use of the  `link()`  method, we  **link**  the  `input`  node to the  `container`. Linking in Isotope allows us to let one node know that it should update when the other one does so. What we do with the line above is letting  `input`  know that it should update (thus triggering the  `node-updated`  event) every time the  `container`'s state is changed.

It's important to remember that linking can happen between  **any 2 nodes**  - no matter where they are in the hierarchy. A single node can have multiple nodes linked to itself, but it can be linked only to a single node.

# Displaying TODOs

Now that our form is ready and can accept new TODOs, we have to take care of displaying them.

Let's get back to our  `List`  component and start our work:

```javascript
// src/list.js
const List = (container) => {
  const list = container.ul({
    classes: () => ({
      list: container.getState("todos").length > 0,
    }),
  });
  container.link(list);

  return list;
};

export { List };
```

First, we make a few changes to our base  `list`  node. We use the  `classes`  configuration property, but in a bit different way than usual. Instead of passing an array of CSS class names, we pass a function, which returns an object. In this way, we let Isotope know that it should rerun the function and update CSS classes every time the node  **updates**. The value that the function returns gets later applied like usual.

An object that the function returns is an alternative way of applying CSS class names. The object's keys represent certain CSS class names and their values - booleans that indicate whether the given CSS class should be applied or removed. As a side note, other configuration properties (`attribs`  and  `styles`) also accept a similar function configuration.

So, we apply the  `"list"`  CSS class name only when our TODOs list contains at least one TODO. But, in order for our dynamic  `classes`  to work, we also have to link the  `list`  node to the  `container`, which we do in the next line.

## List rendering

Now that we've got our  `<ul>`  element set up, we only need to display our TODOs. In Isotope, this can be done with a special [`map()`](https://areknawo.com/isotope/#docs%3Elist-rendering) method.

```javascript
// src/list.js
// ...
list.map(
  () => container.getState("todos"),
  ({ id, text }, node) => {
    const item = node.li({ classes: ["list-item"] });
    const itemContainer = item.div({
      classes: ["is-flex"],
      styles: { alignItems: "center" },
    });

    itemContainer.span({ classes: ["is-pulled-left"] }).text(text);
    itemContainer.div({ styles: { flex: "1" } });
    itemContainer
      .button({
        classes: ["button", "is-text", "is-pulled-right", "is-small"],
      })
      .on("click", () => {
        const todos = container.getState("todos");
        const index = todos.findIndex((todo) => todo.id === id);

        container.setState("todos", todos.splice(index, 1));
      })
      .span({ classes: ["icon"] })
      .i({ classes: ["fas", "fa-check"] });

    return item;
  }
);
// ...
```

`map()`  takes 2 arguments - the list of items to map and a function used to map them. The items list can have multiple forms. For static lists it can be an array of unique strings, numbers or objects with an  `id`  key. For dynamic lists, where items get modified on the way, you can pass parent's state property key, or a function that determines the items, as we do above. Because  `todos`  is a property of  `container`'s state - not the  `list`'s, a function is the only solution we have.

Inside the mapping function, we get access to the current item (in our case items are objects with  `text`  and  `id`  properties), the parent node (`list`) and the index of the current item. We only use 2 of those values.

Overall, the rest of the code is nothing new - we create nodes, set their CSS classes, styles, attributes and text, and listen to the  `click`  event on the button, to remove a certain TODO when needed.

# What do you think?

So, with that, our TODO app is ready. You can check out the finished results through the CodeSandbox playground, right here:

{% codesandbox isotope-playground-7nbc2 %}

To summarize, through making this very simple app, we've learned pretty much most of the Isotope API. That's right - it's that simple. Remember that although the API and the library itself is small and simple, it can still be used to create really incredible and very performant apps and websites!

If you like what you see, definitely check out  [Isotope's documentation](https://areknawo.com/isotope), and drop a star on  [its GitHub repo](https://github.com/Isotope-js/core)!

For more content about Isotope and web development as a whole, follow me on  [Twitter](https://twitter.com/areknawo),  [Facebook](https://facebook.com/areknawoblog)  or right here on [Dev.to](https://dev.to/areknawo).