Skip to content

Commit 1a89e35

Browse files
committed
add code to to make EDIT case pass for #60
1 parent c110c0c commit 1a89e35

File tree

3 files changed

+284
-66
lines changed

3 files changed

+284
-66
lines changed

Diff for: edit-todo.md

+225-58
Original file line numberDiff line numberDiff line change
@@ -110,65 +110,8 @@ which we will cover in **section 5.5** below, but for now we are
110110
only considering the "happy path" which results in a successful edit._
111111

112112

113-
#### 5.1 Double-Click to Edit
114-
115-
The TodoMVC ***spec*** for item
116-
https://github.com/tastejs/todomvc/blob/master/app-spec.md#item
117-
includes the line:
118-
119-
```sh
120-
Double-clicking the <label> activates editing mode, by toggling the .editing class on its <li>
121-
```
122-
123-
> _**Note**: the sample TodoMVC Browser Tests:
124-
https://github.com/tastejs/todomvc/tree/master/tests#example-output
125-
does **not** include a test-case for **double-clicking**.
126-
We are going to add one below for "extra credit"._
127-
128-
Since Double-clicking/tapping is the _only_ way to edit a todo item,
129-
we feel that it deserves a test.
130-
131-
When we don't know how to do something, a good place to start is to search
132-
for the keywords we want, e.g: "JavaScript detect double-click event"
133-
for which the top result is the following StackOverflow Q/A:
134-
https://stackoverflow.com/questions/5497073/how-to-differentiate-single-click-event-and-double-click-event
135-
Reading though all the answers, we determine that the most relevant (_to us_)
136-
is: https://stackoverflow.com/a/16033129/1148249 (_which uses "vanilla" JS_)
137-
138-
[![stackoverflow-double-click-example](https://user-images.githubusercontent.com/194400/45124122-14942f80-b161-11e8-94c0-f54f2352bdd5.png)](https://stackoverflow.com/a/16033129/1148249)
139-
140-
>_**Note**: when you find a StackOverflow question/answer **helpful, upvote**!_
141-
142-
```html
143-
<div onclick="doubleclick(this, function(){alert('single')}, function(){alert('double')})">click me</div>
144-
<script>
145-
function doubleclick(el, onsingle, ondouble) {
146-
if (el.getAttribute("data-dblclick") == null) {
147-
el.setAttribute("data-dblclick", 1);
148-
setTimeout(function () {
149-
if (el.getAttribute("data-dblclick") == 1) {
150-
onsingle();
151-
}
152-
el.removeAttribute("data-dblclick");
153-
}, 300);
154-
} else {
155-
el.removeAttribute("data-dblclick");
156-
ondouble();
157-
}
158-
}
159-
</script>
160-
```
161-
Given that we are using the Elm Architecture to manage the DOM,
162-
we don't want a function that _alters_ the DOM.
163-
So we are going to _borrow_ the _logic_ from this example but _simplify_ it.
164-
Since we are not mutating the DOM by setting `data-dblclick` attributes,
165-
we won't need to remove the attribute using a `setTimeout`,
166-
167-
168-
169113

170-
171-
#### 5.2 `render_item` view function with "Edit Mode" `<input class="edit">`
114+
#### 5.1 `render_item` view function with "Edit Mode" `<input class="edit">`
172115

173116
In order to edit an item the **`render_item`** function
174117
will require **3 modifications**:
@@ -393,3 +336,227 @@ node test/todo-app.test.js
393336
But we are building a _visual_ application and are not _seeing_ anything ...
394337

395338
#### Visualise Editing Mode?
339+
340+
Let's take a _brief_ detour to _visualise_ the progress we have made.
341+
342+
Open the `examples/todo-list/index.html` file
343+
and alter the contents of the `<script>` tag:
344+
```html
345+
<script>
346+
var model = {
347+
todos: [
348+
{ id: 0, title: "Make something people want.", done: false },
349+
{ id: 1, title: "Bootstrap for as long as you can", done: false },
350+
{ id: 2, title: "Let's solve our own problem", done: false }
351+
],
352+
hash: '#/', // the "route" to display
353+
editing: 2 // edit the 3rd todo list item (which has id == 2)
354+
};
355+
mount(model, update, view, 'app', subscriptions);
356+
</script>
357+
```
358+
359+
Then in your terminal, start the live-server:
360+
```sh
361+
npm start
362+
```
363+
In your browser, vist: http://127.0.0.1:8000/examples/todo-list/
364+
You should see that the _third_ todo list item is in "edit mode".
365+
![elm-todomvc-editing-item](https://user-images.githubusercontent.com/194400/45180706-0eab5680-b214-11e8-9dcf-a8c4476e4b11.png)
366+
367+
Nothing will happen (_yet_) if you attempt to "save" any changes.
368+
Let's work on the `case` (_handler_) for **`signal('EDIT', item.id)`**
369+
which will handle the "double-click" event and set `model.editing`.
370+
371+
372+
#### 5.2 Double-Click item `<label>` to Edit
373+
374+
The TodoMVC ***spec*** for item
375+
https://github.com/tastejs/todomvc/blob/master/app-spec.md#item
376+
includes the line:
377+
378+
```sh
379+
Double-clicking the <label> activates editing mode, by toggling the .editing class on its <li>
380+
```
381+
382+
> _**Note**: the sample TodoMVC Browser Tests:
383+
https://github.com/tastejs/todomvc/tree/master/tests#example-output
384+
does **not** include a test-case for **double-clicking**.
385+
We are going to add one below for "extra credit"._
386+
387+
Since Double-clicking/tapping is the _only_ way to edit a todo item,
388+
we feel that it deserves a test.
389+
390+
When we don't know how to do something, a good place to start is to search
391+
for the keywords we want, e.g: "JavaScript detect double-click event"
392+
for which the top result is the following StackOverflow Q/A:
393+
https://stackoverflow.com/questions/5497073/how-to-differentiate-single-click-event-and-double-click-event
394+
Reading though all the answers, we determine that the most relevant (_to us_)
395+
is: https://stackoverflow.com/a/16033129/1148249 (_which uses "vanilla" JS_)
396+
397+
[![stackoverflow-double-click-example](https://user-images.githubusercontent.com/194400/45124122-14942f80-b161-11e8-94c0-f54f2352bdd5.png)](https://stackoverflow.com/a/16033129/1148249)
398+
399+
>_**Note**: when you find a StackOverflow question/answer **helpful, upvote**!_
400+
401+
```html
402+
<div onclick="doubleclick(this, function(){alert('single')}, function(){alert('double')})">click me</div>
403+
<script>
404+
function doubleclick(el, onsingle, ondouble) {
405+
if (el.getAttribute("data-dblclick") == null) {
406+
el.setAttribute("data-dblclick", 1);
407+
setTimeout(function () {
408+
if (el.getAttribute("data-dblclick") == 1) {
409+
onsingle();
410+
}
411+
el.removeAttribute("data-dblclick");
412+
}, 300);
413+
} else {
414+
el.removeAttribute("data-dblclick");
415+
ondouble();
416+
}
417+
}
418+
</script>
419+
```
420+
Given that we are using the Elm Architecture to manage the DOM,
421+
we don't want a function that _alters_ the DOM.
422+
So we are going to _borrow_ the _logic_ from this example but _simplify_ it.
423+
Since we are not mutating the DOM by setting `data-dblclick` attributes,
424+
we won't need to remove the attribute using a `setTimeout`,
425+
426+
427+
### 5.2 `'EDIT' update case` _Test_
428+
429+
In keeping with our TDD approach,
430+
our first step when adding the `case` expression
431+
for `'EDIT'` in the `update` function is to write a _test_.
432+
433+
Append following test code to your `test/todo-app.test.js` file:
434+
435+
```js
436+
test.only('5.2 Double-click an item <label> to edit it', function (t) {
437+
elmish.empty(document.getElementById(id));
438+
localStorage.removeItem('todos-elmish_' + id);
439+
const model = {
440+
todos: [
441+
{ id: 0, title: "Make something people want.", done: false },
442+
{ id: 1, title: "Let's solve our own problem", done: false }
443+
],
444+
hash: '#/' // the "route" to display
445+
};
446+
// render the view and append it to the DOM inside the `test-app` node:
447+
elmish.mount(model, app.update, app.view, id, app.subscriptions);
448+
const label = document.querySelectorAll('.view > label')[1]
449+
// "double-click" i.e. click the <label> twice in quick succession:
450+
label.click();
451+
label.click();
452+
// confirm that we are now in editing mode:
453+
t.equal(document.querySelectorAll('.editing').length, 1,
454+
"<li class='editing'> element is visible");
455+
t.equal(document.querySelectorAll('.edit')[0].value, model.todos[1].title,
456+
"<input class='edit'> has value: " + model.todos[1].title);
457+
t.end();
458+
});
459+
```
460+
If you attempt to run this test: `node test/todo-app.test.js`
461+
you will see output similar to the following:
462+
463+
![edit-double-click-test-failing](https://user-images.githubusercontent.com/194400/45183202-54b7e880-b21b-11e8-84d8-7b3b50162113.png)
464+
465+
Let's write the code necessary to make the test assertions _pass_!
466+
If you want to try this yourself based on the StackOverflow answer (_above_),
467+
go for it! (_don't scroll down to the "answer" till you have tried..._)
468+
469+
### 5.2 `'EDIT' update case` _Implementation_
470+
471+
Given our "research" (_above_) of how to implement a "double-click" handler,
472+
we can write the `'EDIT'` case as the following:
473+
474+
```js
475+
case 'EDIT':
476+
// this code is inspired by: https://stackoverflow.com/a/16033129/1148249
477+
// simplified as we are not altering the DOM!
478+
if (new_model.clicked && new_model.clicked === data &&
479+
Date.now() - 300 < new_model.click_time ) { // DOUBLE-CLICK < 300ms
480+
new_model.editing = data;
481+
console.log('DOUBLE-CLICK', "item.id=", data,
482+
"| model.editing=", model.editing,
483+
"| diff Date.now() - new_model.click_time: ",
484+
Date.now(), "-", new_model.click_time, "=",
485+
Date.now() - new_model.click_time);
486+
}
487+
else { // first click
488+
new_model.clicked = data; // so we can check if same item clicked twice!
489+
new_model.click_time = Date.now(); // timer to detect double-click 300ms
490+
new_model.editing = false; // reset
491+
console.log('FIRST CLICK! data:', data);
492+
}
493+
break;
494+
```
495+
If you ignore/remove the `console.log` lines (_which we are using for now!_),
496+
the code is only a few lines long:
497+
```js
498+
case 'EDIT':
499+
// this code is inspired by: https://stackoverflow.com/a/16033129/1148249
500+
// simplified as we are not altering the DOM!
501+
if (new_model.clicked && new_model.clicked === data &&
502+
Date.now() - 300 < new_model.click_time ) { // DOUBLE-CLICK < 300ms
503+
new_model.editing = data;
504+
}
505+
else { // first click
506+
new_model.clicked = data; // so we can check if same item clicked twice!
507+
new_model.click_time = Date.now(); // timer to detect double-click 300ms
508+
new_model.editing = false; // reset
509+
}
510+
break;
511+
```
512+
The main "purpose" of this code is to _detect_ if a `<label>` was clicked
513+
twice in the space of 300 milliseconds and apply the `item.id` to
514+
the `model.editing` property so that we know which `<li>` to render in
515+
"editing mode".
516+
517+
Run the test and watch it _pass_: `node test/todo-app.test.js`
518+
![edit-double-click-test-pass](https://user-images.githubusercontent.com/194400/45183878-3bb03700-b21d-11e8-9842-be62113bfe0a.png)
519+
520+
In this case the time between the two clicks was 31 milliseconds,
521+
so they will count as a "double-click"!
522+
523+
524+
If a `<label>` is clicked slowly, the `model.editing` will _not_ be set,
525+
and we will _not_ enter "editing mode".
526+
Let's add a quick test for the scenario
527+
where two clicks are more than 300ms apart.
528+
529+
Append following test code to your `test/todo-app.test.js` file:
530+
531+
```js
532+
test.only('5.2.2 Slow clicks do not count as double-click > no edit!', function (t) {
533+
elmish.empty(document.getElementById(id));
534+
localStorage.removeItem('todos-elmish_' + id);
535+
const model = {
536+
todos: [
537+
{ id: 0, title: "Make something people want.", done: false },
538+
{ id: 1, title: "Let's solve our own problem", done: false }
539+
],
540+
hash: '#/' // the "route" to display
541+
};
542+
// render the view and append it to the DOM inside the `test-app` node:
543+
elmish.mount(model, app.update, app.view, id, app.subscriptions);
544+
const label = document.querySelectorAll('.view > label')[1]
545+
// "double-click" i.e. click the <label> twice in quick succession:
546+
label.click();
547+
setTimeout(function (){
548+
label.click();
549+
// confirm that we are now in editing mode:
550+
t.equal(document.querySelectorAll('.editing').length, 0,
551+
"<li class='editing'> element is NOT visible");
552+
t.end();
553+
}, 301)
554+
});
555+
```
556+
557+
There is no need to write any code to make this test pass,
558+
this is merely an additional test to _confirm_ that our check for the
559+
time between clicks works; clicks spaced more than 300ms will not count
560+
as "double-click".
561+
562+
![edit-item-not-double-click](https://user-images.githubusercontent.com/194400/45184155-ff310b00-b21d-11e8-8f6c-ef6d699861cf.png)

Diff for: examples/todo-list/todo-app.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,20 @@ function update(action, model, data) {
6262
case 'EDIT':
6363
// this code is inspired by: https://stackoverflow.com/a/16033129/1148249
6464
// simplified as we are not altering the DOM!
65-
console.log('EDIT', data, model.editing, new_model.click_time, Date.now());
66-
6765
if (new_model.clicked && new_model.clicked === data &&
68-
Date.now() - 300 < new_model.click_time ) { // DOUBLE CLICK is faster than 300ms
66+
Date.now() - 300 < new_model.click_time ) { // DOUBLE-CLICK < 300ms
6967
new_model.editing = data;
70-
console.log('DOUBLE CLICK', data);
68+
console.log('DOUBLE-CLICK', "item.id=", data,
69+
"| model.editing=", model.editing,
70+
"| diff Date.now() - new_model.click_time: ",
71+
Date.now(), "-", new_model.click_time, "=",
72+
Date.now() - new_model.click_time);
7173
}
7274
else { // first click
73-
new_model.clicked = data;
74-
new_model.click_time = Date.now()
75-
new_model.editing = false;
76-
console.log('FIRST CLICK', data);
75+
new_model.clicked = data; // so we can check if same item clicked twice!
76+
new_model.click_time = Date.now(); // timer to detect double-click 300ms
77+
new_model.editing = false; // reset
78+
console.log('FIRST CLICK! data:', data);
7779
}
7880
break;
7981
case 'SAVE':

Diff for: test/todo-app.test.js

+49
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,55 @@ test('5. Editing: > Render an item in "editing mode"', function (t) {
425425
t.end();
426426
});
427427

428+
test('5.2 Double-click an item <label> to edit it', function (t) {
429+
elmish.empty(document.getElementById(id));
430+
localStorage.removeItem('todos-elmish_' + id);
431+
const model = {
432+
todos: [
433+
{ id: 0, title: "Make something people want.", done: false },
434+
{ id: 1, title: "Let's solve our own problem", done: false }
435+
],
436+
hash: '#/' // the "route" to display
437+
};
438+
// render the view and append it to the DOM inside the `test-app` node:
439+
elmish.mount(model, app.update, app.view, id, app.subscriptions);
440+
const label = document.querySelectorAll('.view > label')[1]
441+
// "double-click" i.e. click the <label> twice in quick succession:
442+
label.click();
443+
label.click();
444+
// confirm that we are now in editing mode:
445+
t.equal(document.querySelectorAll('.editing').length, 1,
446+
"<li class='editing'> element is visible");
447+
t.equal(document.querySelectorAll('.edit')[0].value, model.todos[1].title,
448+
"<input class='edit'> has value: " + model.todos[1].title);
449+
t.end();
450+
});
451+
452+
test('5.2.2 Slow clicks do not count as double-click > no edit!', function (t) {
453+
elmish.empty(document.getElementById(id));
454+
localStorage.removeItem('todos-elmish_' + id);
455+
const model = {
456+
todos: [
457+
{ id: 0, title: "Make something people want.", done: false },
458+
{ id: 1, title: "Let's solve our own problem", done: false }
459+
],
460+
hash: '#/' // the "route" to display
461+
};
462+
// render the view and append it to the DOM inside the `test-app` node:
463+
elmish.mount(model, app.update, app.view, id, app.subscriptions);
464+
const label = document.querySelectorAll('.view > label')[1]
465+
// "double-click" i.e. click the <label> twice in quick succession:
466+
label.click();
467+
setTimeout(function (){
468+
label.click();
469+
// confirm that we are now in editing mode:
470+
t.equal(document.querySelectorAll('.editing').length, 0,
471+
"<li class='editing'> element is NOT visible");
472+
t.end();
473+
}, 301)
474+
});
475+
476+
428477
test.skip('5.1 Double-click an item <label> to edit it', function (t) {
429478
elmish.empty(document.getElementById(id));
430479
localStorage.removeItem('todos-elmish_' + id);

0 commit comments

Comments
 (0)