Skip to content

Commit 614e97d

Browse files
committed
solo-coder-phase4: 16887dade-8543-4b6c-a294-33d8ece545ae / alpha / round2 (.115908579437896:abc400e02567aa4acdbe0f194fffe5a9_69e41f8687d9eec1bd049559.69e41fa587d9eec1bd04955d.fe7b4e3c6a0e4962ba0122fb:Trae CN.T(2026/4/19 08:19:49))
1 parent 3919f37 commit 614e97d

File tree

2 files changed

+200
-23
lines changed

2 files changed

+200
-23
lines changed

lib/todo-app.js

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ function update(action, model, data) {
103103
new_model.hash = // (window && window.location && window.location.hash) ?
104104
window.location.hash // : '#/';
105105
break;
106+
case 'SEARCH':
107+
new_model.search = data;
108+
break;
106109
default: // if action unrecognised or undefined,
107110
return model; // return model unmodified
108111
} // see: https://softwareengineering.stackexchange.com/a/201786/211301
@@ -162,36 +165,49 @@ function render_item (item, model, signal) {
162165
* @return {Object} <section> DOM Tree which containing the todo list <ul>, etc.
163166
*/
164167
function render_main (model, signal) {
165-
// Requirement #1 - No Todos, should hide #footer and #main
166-
var display = "style=display:"
167-
+ (model.todos && model.todos.length > 0 ? "block" : "none");
168+
var has_todos = model.todos && model.todos.length > 0;
169+
var display = "style=display:" + (has_todos ? "block" : "none");
170+
171+
var filtered_by_route = has_todos ? model.todos.filter(function (item) {
172+
switch(model.hash) {
173+
case '#/active':
174+
return !item.done;
175+
case '#/completed':
176+
return item.done;
177+
default:
178+
return item;
179+
}
180+
}) : [];
181+
182+
var search_term = (model.search || '').trim();
183+
var filtered_items = filtered_by_route;
184+
185+
if (search_term.length > 0) {
186+
filtered_items = filtered_by_route.filter(function (item) {
187+
return item.title.toLowerCase().indexOf(search_term.toLowerCase()) !== -1;
188+
});
189+
}
190+
191+
var has_filtered_items = filtered_items.length > 0;
192+
var show_empty_message = has_todos && !has_filtered_items;
168193

169194
return (
170-
section(["class=main", "id=main", display], [ // hide if no todo items.
195+
section(["class=main", "id=main", display], [
171196
input(["id=toggle-all", "type=checkbox",
172197
typeof signal === 'function' ? signal('TOGGLE_ALL') : '',
173198
(model.all_done ? "checked=checked" : ""),
174199
"class=toggle-all"
175200
], []),
176201
label(["for=toggle-all"], [ text("Mark all as complete") ]),
177-
ul(["class=todo-list"],
178-
(model.todos && model.todos.length > 0) ?
179-
model.todos
180-
.filter(function (item) {
181-
switch(model.hash) {
182-
case '#/active':
183-
return !item.done;
184-
case '#/completed':
185-
return item.done;
186-
default: // if hash doesn't match Active/Completed render ALL todos:
187-
return item;
188-
}
189-
})
190-
.map(function (item) {
191-
return render_item(item, model, signal)
192-
}) : null
193-
) // </ul>
194-
]) // </section>
202+
show_empty_message ?
203+
div(["class=empty-message"], [text("No todos match your search.")]) :
204+
ul(["class=todo-list"],
205+
has_filtered_items ?
206+
filtered_items.map(function (item) {
207+
return render_item(item, model, signal)
208+
}) : null
209+
)
210+
])
195211
)
196212
}
197213

@@ -281,6 +297,7 @@ function render_footer (model, signal) {
281297
* var DOM = view(model);
282298
*/
283299
function view (model, signal) {
300+
var search_value = model.search || '';
284301

285302
return (
286303
section(["class=todoapp"], [ // array of "child" elements
@@ -293,7 +310,13 @@ function view (model, signal) {
293310
"class=new-todo",
294311
"placeholder=What needs to be done?",
295312
"autofocus"
296-
], []) // <input> is "self-closing"
313+
], []), // <input> is "self-closing"
314+
input([
315+
"id=search-todo",
316+
"class=search-todo",
317+
"placeholder=Search todos...",
318+
"value=" + search_value
319+
], []) // search input
297320
]), // </header>
298321
render_main(model, signal),
299322
render_footer(model, signal)
@@ -311,6 +334,8 @@ function subscriptions (signal) {
311334
var ENTER_KEY = 13; // add a new todo item when [Enter] key is pressed
312335
var ESCAPE_KEY = 27; // used for "escaping" when editing a Todo item
313336

337+
signal('SEARCH', '')();
338+
314339
document.addEventListener('keyup', function handler (e) {
315340
// console.log('e.keyCode:', e.keyCode, '| key:', e.key);
316341

@@ -337,6 +362,12 @@ function subscriptions (signal) {
337362
window.onhashchange = function route () {
338363
signal('ROUTE')();
339364
}
365+
366+
document.addEventListener('input', function (e) {
367+
if (e.target && e.target.id === 'search-todo') {
368+
signal('SEARCH', e.target.value)();
369+
}
370+
});
340371
}
341372

342373
/* module.exports is needed to run the functions using Node.js for testing! */

test/todo-app.test.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,149 @@ test('9. Routing > should allow me to display active/completed/all items',
678678
t.end();
679679
});
680680

681+
test('10. Search > should filter todos by search term', function (t) {
682+
localStorage.removeItem('todos-elmish_' + id);
683+
elmish.empty(document.getElementById(id));
684+
const model = {
685+
todos: [
686+
{ id: 0, title: "Buy milk", done: false },
687+
{ id: 1, title: "Buy eggs", done: false },
688+
{ id: 2, title: "Walk the dog", done: true }
689+
],
690+
hash: '#/'
691+
};
692+
elmish.mount(model, app.update, app.view, id, app.subscriptions);
693+
694+
t.equal(document.querySelectorAll('.view').length, 3, "three items total");
695+
696+
const model_with_search = app.update('SEARCH', model, 'milk');
697+
elmish.empty(document.getElementById(id));
698+
document.getElementById(id).appendChild(app.render_main(model_with_search, mock_signal));
699+
700+
t.equal(document.querySelectorAll('.view').length, 1, "one item matches 'milk'");
701+
t.equal(document.querySelectorAll('.view')[0].textContent, 'Buy milk', "matching item is 'Buy milk'");
702+
703+
elmish.empty(document.getElementById(id));
704+
localStorage.removeItem('todos-elmish_' + id);
705+
t.end();
706+
});
707+
708+
test('10.1 Search > should ignore leading and trailing whitespace in search term', function (t) {
709+
localStorage.removeItem('todos-elmish_' + id);
710+
elmish.empty(document.getElementById(id));
711+
const model = {
712+
todos: [
713+
{ id: 0, title: "Buy milk", done: false },
714+
{ id: 1, title: "Buy eggs", done: false }
715+
],
716+
hash: '#/'
717+
};
718+
719+
const model_with_whitespace = app.update('SEARCH', model, ' milk ');
720+
document.getElementById(id).appendChild(app.render_main(model_with_whitespace, mock_signal));
721+
722+
t.equal(document.querySelectorAll('.view').length, 1, "one item matches ' milk ' (trimmed)");
723+
t.equal(document.querySelectorAll('.view')[0].textContent, 'Buy milk', "matching item is 'Buy milk'");
724+
725+
elmish.empty(document.getElementById(id));
726+
727+
const model_with_only_spaces = app.update('SEARCH', model, ' ');
728+
document.getElementById(id).appendChild(app.render_main(model_with_only_spaces, mock_signal));
729+
730+
t.equal(document.querySelectorAll('.view').length, 2, "all items shown when search term is only spaces");
731+
732+
elmish.empty(document.getElementById(id));
733+
localStorage.removeItem('todos-elmish_' + id);
734+
t.end();
735+
});
736+
737+
test('10.2 Search > should show empty message when no items match search', function (t) {
738+
localStorage.removeItem('todos-elmish_' + id);
739+
elmish.empty(document.getElementById(id));
740+
const model = {
741+
todos: [
742+
{ id: 0, title: "Buy milk", done: false },
743+
{ id: 1, title: "Buy eggs", done: false }
744+
],
745+
hash: '#/'
746+
};
747+
748+
const model_with_search = app.update('SEARCH', model, 'nonexistent');
749+
document.getElementById(id).appendChild(app.render_main(model_with_search, mock_signal));
750+
751+
t.equal(document.querySelectorAll('.view').length, 0, "no items match 'nonexistent'");
752+
t.equal(document.querySelectorAll('.empty-message').length, 1, "empty message is shown");
753+
t.equal(document.querySelectorAll('.empty-message')[0].textContent, 'No todos match your search.', "empty message text is correct");
754+
755+
elmish.empty(document.getElementById(id));
756+
localStorage.removeItem('todos-elmish_' + id);
757+
t.end();
758+
});
759+
760+
test('10.3 Search > should not show empty message when there are no todos at all', function (t) {
761+
localStorage.removeItem('todos-elmish_' + id);
762+
elmish.empty(document.getElementById(id));
763+
const model = {
764+
todos: [],
765+
hash: '#/'
766+
};
767+
768+
const model_with_search = app.update('SEARCH', model, 'milk');
769+
document.getElementById(id).appendChild(app.render_main(model_with_search, mock_signal));
770+
771+
t.equal(document.querySelectorAll('.empty-message').length, 0, "no empty message when there are no todos");
772+
773+
elmish.empty(document.getElementById(id));
774+
localStorage.removeItem('todos-elmish_' + id);
775+
t.end();
776+
});
777+
778+
test('10.4 Search > should combine with route filter', function (t) {
779+
localStorage.removeItem('todos-elmish_' + id);
780+
elmish.empty(document.getElementById(id));
781+
const model = {
782+
todos: [
783+
{ id: 0, title: "Buy milk", done: false },
784+
{ id: 1, title: "Buy eggs", done: false },
785+
{ id: 2, title: "Walk the dog", done: true },
786+
{ id: 3, title: "Buy bread", done: true }
787+
],
788+
hash: '#/active'
789+
};
790+
791+
const model_with_search = app.update('SEARCH', model, 'Buy');
792+
document.getElementById(id).appendChild(app.render_main(model_with_search, mock_signal));
793+
794+
t.equal(document.querySelectorAll('.view').length, 2, "two active items match 'Buy'");
795+
796+
elmish.empty(document.getElementById(id));
797+
localStorage.removeItem('todos-elmish_' + id);
798+
t.end();
799+
});
800+
801+
test('10.5 Search > footer should still show original counts when searching', function (t) {
802+
localStorage.removeItem('todos-elmish_' + id);
803+
elmish.empty(document.getElementById(id));
804+
const model = {
805+
todos: [
806+
{ id: 0, title: "Buy milk", done: false },
807+
{ id: 1, title: "Buy eggs", done: false },
808+
{ id: 2, title: "Walk the dog", done: true }
809+
],
810+
hash: '#/',
811+
search: 'milk'
812+
};
813+
814+
document.getElementById(id).appendChild(app.render_footer(model));
815+
816+
const count = parseInt(document.getElementById('count').textContent, 10);
817+
t.equal(count, 2, "footer shows 2 items left (original count)");
818+
819+
const completed_count = parseInt(document.getElementById('completed-count').textContent, 10);
820+
t.equal(completed_count, 1, "footer shows 1 completed item (original count)");
821+
822+
elmish.empty(document.getElementById(id));
823+
localStorage.removeItem('todos-elmish_' + id);
824+
t.end();
825+
});
826+

0 commit comments

Comments
 (0)