2. Templates and Styles

To save some time (and for consistency) we're going to copy all the html and css from the Todo MVC template repo.

The CSS can be found hereopen in new window, and it can be copied over into your src/style.css file.

The HTML can be cut into 4 different templates:

  1. An individual TodoItem.
  2. The AppHeader section, with an input for new todos.
  3. The AppFooter sections, which shows some information, and allows you to filter todos by state.
  4. The App component itself, which is responsible for rendering the TodoItem components, and connect with the AppHeader and AppFooter components.

TodoItem

Create a src/components/todo-item/TodoItem.template.ts with the following content;

import type { ComponentTemplateResult } from '@muban/template';
import { html } from '@muban/template';

export type TodoItemTemplateProps = {
  title: string;
  isCompleted: boolean;
};

export function todoItemTemplate({
  title,
  isCompleted,
}: TodoItemTemplateProps): ComponentTemplateResult {
  // there is also an `editing` class, but that's only set when interacting with the element
  return html`<li data-component="todo-item" class="${isCompleted ? 'completed' : ''}">
    <div class="view">
      <input data-ref="completedInput" class="toggle" type="checkbox" checked=${isCompleted} />
      <label data-ref="title">${title}</label>
      <button data-ref="destroyButton" class="destroy"></button>
    </div>
    <input data-ref="editInput" class="edit" />
  </li>`;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

This template has two properties: title, the title of each todo, and isCompleted, an indicator for when an item has been completed. Data wise, this is all we know about the item, and what we are later going to save into LocalStorage.

It does have an editing state, but that is all handled by the Component if users interact with it. The "initial state" can never be "editing".

With the props we do the following:

  1. We enter the title in the label.
  2. We conditionally add the completed class. And we mark the checkbox as checked.
  3. We're not setting the value of the "edit" input field, this will be done by the Component when switching into "editing mode".

Besides the props, we have also added data-ref attributes on all elements that require updates from the Component after interacting with the application later.

  1. The data-component doesn't need a data-ref, it can be referenced using refs.self.
  2. The data-ref="completedInput" is for the checkbox, we need to listen when its state changes.
  3. The data-ref="title" is to read the initial title value, and to update it later when editing.
  4. The data-ref="destroyButton" needs a click binding, so we can remove the item.
  5. The data-ref="editInput" needs a value, so we can set and update its value.

Anything that doesn't have a data-ref attribute will stay as rendered.

The two properties that we passed to the template ({ title, isCompleted }) are not automatically available in JS when we create our Component. We might not even always need them. In this case we do, so we need to think about how the Component is going to get access to them later.

For the title, we can easily read the textContent of the <label> tag, so as long as that element has a data-ref, we're good. For the isCompleted we have two options to extract it, we can either use the class="completed" on the root element, or use the checked attribute on the input.

AppHeader

Create a src/components/app-header/AppHeader.template.ts with the following content;

import type { ComponentTemplateResult } from '@muban/template';
import { html } from '@muban/template';

export type AppHeaderTemplateProps = {
  title?: string;
};

export function appHeaderTemplate({
  title = 'Todos',
}: AppHeaderTemplateProps = {}): ComponentTemplateResult {
  return html`<div data-component="app-header" class="header">
    <h1>${title}</h1>
    <input
      data-ref="newTodoInput"
      class="new-todo"
      placeholder="What needs to be done?"
      autofocus
    />
  </div>`;
}










 
 

 






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

This template is similar to what we had before in the App template (the title). It also has an input for adding new todos.

What we have done here is;

  1. Add the data-component="app-header" attribute to the root element.
  2. Render the title.
  3. Add data-ref="newTodoInput" to interact with the input element to add new todos.

There is no initial data in this template that our Component needs access to later, so there is nothing else to do.

AppFooter

Create a src/components/app-footer/AppFooter.template.ts with the following content;

import type { ComponentTemplateResult } from '@muban/template';
import { html } from '@muban/template';

type AppFooterTemplateProps = {
  uncompletedCount?: number;
  selectedFilter?: 'active' | 'completed';
};

export function appFooterTemplate({
  uncompletedCount = 0,
  selectedFilter,
}: AppFooterTemplateProps = {}): ComponentTemplateResult {
  return html`
    <footer data-component="app-footer" class="footer">
      <span data-ref="remainingCount" class="todo-count">
        <strong>${uncompletedCount}</strong> ${uncompletedCount === 1 ? 'item' : 'items'} left
      </span>
      <ul class="filters">
        <li>
          <a data-ref="filterAll" class="${!selectedFilter ? 'selected' : ''}" href="#/">All</a>
        </li>
        <li>
          <a
            data-ref="filterActive"
            class="${selectedFilter === 'active' ? 'selected' : ''}"
            href="#/active"
          >
            Active
          </a>
        </li>
        <li>
          <a
            data-ref="filterCompleted"
            class="${selectedFilter === 'completed' ? 'selected' : ''}"
            href="#/completed"
          >
            Completed
          </a>
        </li>
      </ul>
      <!-- Hidden if no completed items are left ↓ -->
      <button data-ref="clearCompletedButton" class="clear-completed">Clear completed</button>
    </footer>
  `;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

This template has 3 purposes:

  1. It shows how many todo items are uncompleted.
  2. Some filters we can activate (we need to set up routing for this).
  3. A button to remove everything that we have completed.

We receive two props:

  1. An uncompletedCount that we render here, including the correct pluralisation of items. Note that this condition is just for the template rendering, the initial state from the server. Whenever this changes after interacting on the client, it will be updated from the Component using bindings.
  2. An optional selectedFilter, which is used to set the selected class on the right filter element.

To update the count (and the correct label) later, we have added the data-ref="todoCount" to this span. For this example we're going to update the complete value later, including the <strong> tag – which means we will need to render html. We could also have chosen to introduce another <span> tag around the label, and update the count and the label separately with normal text bindings.

Our filters also have data-ref attributes added, so we can add or remove the selected class based on interactions we do later.

And lastly we added data-ref="clearCompletedButton" to interact with the clear button.

App

Update our src/components/app/App.template.ts to the following;

import type { ComponentTemplateResult } from '@muban/template';
import { html } from '@muban/template';
import { appFooterTemplate } from '../app-footer/AppFooter.template';
import { appHeaderTemplate } from '../app-header/AppHeader.template';
import type { TodoItemTemplateProps } from '../todo-item/TodoItem.template';
import { todoItemTemplate } from '../todo-item/TodoItem.template';

export type AppTemplateProps = {
  title?: string;
  todos?: Array<TodoItemTemplateProps>;
};

export function appTemplate({ title, todos = [] }: AppTemplateProps = {}): ComponentTemplateResult {
  return html`
    <div data-component="app">
      <section class="todoapp">
        ${appHeaderTemplate({ title })}
        <section class="main">
          <input data-ref="toggleAllInput" id="toggle-all" class="toggle-all" type="checkbox" />
          <label for="toggle-all">Mark all as complete</label>
          <ul data-ref="todoList" class="todo-list">
            ${todos.map((todo) => todoItemTemplate(todo))}
          </ul>
        </section>
        ${appFooterTemplate({ uncompletedCount: todos.filter((todo) => !todo.isCompleted).length })}
      </section>
      <footer class="info">
        <p>Double-click to edit a todo</p>
        <p>Created by <a href="http://github.com/mubanjs">Muban</a></p>
      </footer>
    </div>
  `;
}







 






 



 
 
 

 










1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

This is where everything comes together. Keep in mind that we're still just rendering templates, the "initial state" of our application, that is normally rendered on the server.

From the main.ts (that renders our component when mounting) we expect to receive the title, and a list of todo items (that are known on the server). We re-use the TodoItemTemplateProps type here to specify which fields each todo item should have. We could also have created a TodoItem type, and use that in the TemplateProps types of both components.

As you can see, we rendered some child templates:

  1. appHeaderTemplate renders the header, passing along the title prop.
  2. We map over our todos list, and for each item, we render the todoItemTemplate template, passing along the data.
  3. appFooterTemplate renders the footer, passing the uncompletedCount using an inline filter.

And we have a data-ref="toggleAllInput" here as well, to toggle the state of all todos in the list.

main.ts Data

And finally, since we update our App template, we should also pass different data when mounting our app. Please update main.ts to the following:

import './style.css';

import { createApp } from '@muban/muban';
import { App } from './components/app/App';
import { appTemplate } from './components/app/App.template';

const appRoot = document.getElementById('app')!;
const app = createApp(App);

app.mount(appRoot, appTemplate, {
  title: 'Todos',
  todos: [
    {
      title: 'Taste JavaScript',
      isCompleted: true,
    },
    {
      title: 'Buy a unicorn',
      isCompleted: false,
    },
  ],
});











 
 
 
 
 
 
 
 
 
 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

In here we're passing the todos array with the title and isCompleted for each item. Everything in that object is typed, so your editor knows exactly what to specify here, and you will get an error if you make a typo, or forget a required property.

If you visit http://localhost:3000/, you should now see a nice static Todo app!