Todo demo annotated source

Back to index

        

The todo sample project shows how the view and model layers of Torus interact in a simple case.

3

Bootstrap the required globals from Torus, since we're not bundling

5for (const exportedName in Torus) {
6    window[exportedName] = Torus[exportedName];
7}
8

Task is our model for a single todo item. We just extend Record since we don't need it to have any special functionality.

11class Task extends Record {}
12

TaskStore represents a collection of todos, or our to-do list. Our todo list component will be bound to this store.

15class TaskStore extends StoreOf(Task) {

Let's sort this collection by the task description

17    get comparator() {
18        return task => task.get('description').toLowerCase();
19    }
20}
21

We create an instance of the task collection for our list, and initialize it with two items.

24const tasks = new TaskStore([
25    new Task(1, {description: 'Do this', completed: false}),
26    new Task(2, {description: 'Do that', completed: false}),
27]);
28

Component that represents a single todo item

30class TaskItem extends StyledComponent {
31
32    init(source, removeCallback) {
33        this.removeCallback = removeCallback;
34        this.onCheck = this.onCheck.bind(this);
35        this.deleteClick = this.deleteClick.bind(this);

We want the component to re-render when the todo item's properties change, so we bind the record's events to re-renders.

38        this.bind(source, data => this.render(data));
39    }
40
41    styles(data) {
42        return {

When the task is completed, we'll fade out that item.

44            'opacity': data.completed ? 0.4 : 1,
45            'height': '50px',
46            'width': '100%',
47            'background': '#eee',
48            'list-style': 'none',
49            'display': 'flex',
50            'flex-direction': 'row',
51            'align-items': 'center',
52            'justify-content': 'space-between',
53            'margin-bottom': '1px',
54            'cursor': 'pointer',
55            'padding': '0 12px',
56            '.description': {
57                'flex-grow': '1',
58                'height': '50px',
59                'line-height': '50px',
60                'padding-left': '6px',
61            },
62        }
63    }
64

When we check the item off the list from the UI, we toggle the 'completed' property on the record.

67    onCheck() {
68        this.record.update({
69            completed: !this.record.get('completed'),
70        });
71    }
72
73    deleteClick() {
74        this.removeCallback();
75    }
76
77    compose(data) {
78        return jdom`<li>
79            <input type="checkbox" checked="${data.completed}" onclick="${this.onCheck}"/>
80            <div class="description" onclick="${this.onCheck}">${data.description}</div>
81            <button class="removeButton" onclick="${this.deleteClick}">X</button>
82        </li>`;
83    }
84
85}
86

Subclass of the default list view that represents a list of tasks. We use the ListOf(TaskItem) syntax to tell TaskList to render new items that appear in the collection as TaskItem components.

90class TaskList extends ListOf(TaskItem) {
91
92    compose() {
93        return jdom`<ul style="padding:0">${this.nodes}</ul>`;
94    }
95
96}
97

Input field with a submit button that creates new items in the list of tasks.

100class TaskInput extends StyledComponent {
101
102    init() {
103        this.value = '';
104        this.boundOnKeyPress = this.onKeyPress.bind(this);
105        this.boundOnAddClick = this.onAddClick.bind(this);
106        this.boundSetValue = this.setValue.bind(this);
107    }
108

If an enter key is pressed, try to add a task

110    onKeyPress(evt) {
111        if (evt.key === 'Enter') {
112            this._addTask();
113        }
114    }
115

If the add button is clicked, try to add a task

117    onAddClick() {
118        this._addTask();
119    }
120

We want the input component to be a controlled component so every time there's an input, we update the component's value property accordingly.

124    setValue(evt) {
125        this.value = evt.target.value;
126        this.render();
127    }
128

What happens when we try to add a task? Create a new task in the task list with the current input value, in an uncompleted state.

132    _addTask() {
133        if (this.value) {
134            tasks.create({
135                description: this.value,
136                completed: false,
137            });
138
139            this.value = '';
140            this.render();
141        }
142    }
143
144    styles() {
145        return {
146            'width': '100%',
147            'display': 'flex',
148            'flex-direction': 'row',
149            'input': {
150                'flex-grow': 1,
151            },
152        }
153    }
154
155    compose() {
156        return jdom`<div>
157            <input
158                value="${this.value}"
159                oninput="${this.boundSetValue}"
160                onkeypress="${this.boundOnKeyPress}"/>
161            <button onclick="${this.boundOnAddClick}">Add</button>
162        </div>`;
163    }
164
165}
166

A component to represent the entire app, bringing together the input component and the todo list component.

169class App extends StyledComponent {
170
171    init() {

We create instances of both input and list views here, to reuse in repeated renders.

174        this.input = new TaskInput();
175        this.list = new TaskList(tasks);
176    }
177
178    styles() {
179        return {
180            'font-family': "system-ui, 'Helvetica', 'Ubuntu', sans-serif",
181            'width': '100%',
182            'max-width': '500px',
183            'margin': '0 auto',
184        }
185    }
186
187    compose() {

The app is really just both components' nodes wrapped in a single div.

190        return jdom`<div>
191            ${[this.input.node, this.list.node]}
192        </div>`;
193    }
194
195}
196

Create an instance of the app, and append to the DOM.

198const app = new App();
199document.body.appendChild(app.node);
200