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:
- An individual
TodoItem. - The
AppHeadersection, with an input for new todos. - The
AppFootersections, which shows some information, and allows you to filter todos by state. - The
Appcomponent itself, which is responsible for rendering theTodoItemcomponents, and connect with theAppHeaderandAppFootercomponents.
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>`;
}
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
editingstate, 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:
- We enter the
titlein the label. - We conditionally add the
completedclass. And we mark the checkbox aschecked. - We're not setting the value of the "edit"
inputfield, 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.
- The
data-componentdoesn't need adata-ref, it can be referenced usingrefs.self. - The
data-ref="completedInput"is for the checkbox, we need to listen when its state changes. - The
data-ref="title"is to read the initial title value, and to update it later when editing. - The
data-ref="destroyButton"needs aclickbinding, so we can remove the item. - The
data-ref="editInput"needs avalue, so we can set and update its value.
Anything that doesn't have a
data-refattribute 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>`;
}
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;
- Add the
data-component="app-header"attribute to the root element. - Render the
title. - 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>
`;
}
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:
- It shows how many todo items are uncompleted.
- Some filters we can activate (we need to set up routing for this).
- A button to remove everything that we have completed.
We receive two props:
- An
uncompletedCountthat we render here, including the correct pluralisation ofitems. 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 usingbindings. - An optional
selectedFilter, which is used to set theselectedclass 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>
`;
}
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:
appHeaderTemplaterenders the header, passing along thetitleprop.- We map over our
todoslist, and for each item, we render thetodoItemTemplatetemplate, passing along the data. appFooterTemplaterenders the footer, passing theuncompletedCountusing 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,
},
],
});
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!