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
AppHeader
section, with an input for new todos. - The
AppFooter
sections, which shows some information, and allows you to filter todos by state. - The
App
component itself, which is responsible for rendering theTodoItem
components, and connect with theAppHeader
andAppFooter
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>`;
}
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:
- We enter the
title
in the label. - We conditionally add the
completed
class. And we mark the checkbox aschecked
. - 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.
- The
data-component
doesn'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 aclick
binding, 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-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>`;
}
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
uncompletedCount
that 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 theselected
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>
`;
}
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:
appHeaderTemplate
renders the header, passing along thetitle
prop.- We map over our
todos
list, and for each item, we render thetodoItemTemplate
template, passing along the data. appFooterTemplate
renders the footer, passing theuncompletedCount
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,
},
],
});
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!