-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Dierk Koenig
committed
May 9, 2021
1 parent
68c0b4c
commit 3c88477
Showing
11 changed files
with
571 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title> All Tests </title> | ||
<script src="util/test.js"></script> | ||
</head> | ||
<body> | ||
|
||
<h1>All Tests in HtmlJs</h1> | ||
|
||
<pre> | ||
|
||
<script> | ||
|
||
const testNames = [ | ||
'observable', | ||
'todo' | ||
]; | ||
|
||
testNames.forEach( testName => { | ||
|
||
|
||
document.write(`<script src="${testName}/${testName}.js"></s`+'cript>'); // dirty trick of the day | ||
document.write(`<script src="${testName}/${testName}Test.js"></s`+'cript>'); | ||
|
||
|
||
}); | ||
|
||
document.writeln("\nCheck possible 'compile' errors in the console .") | ||
|
||
</script> | ||
|
||
</pre> | ||
|
||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>View</title> | ||
<script src="observable.js"></script> | ||
</head> | ||
<body> | ||
|
||
<input id="name" type="text"> | ||
<div id="label"></div> | ||
<div id="size"></div> | ||
|
||
<script> | ||
const nameInput = document.getElementById("name"); | ||
const label = document.getElementById("label"); | ||
const size = document.getElementById("size"); | ||
|
||
const inputAttr = Observable(""); | ||
inputAttr.onChange(val => label.innerText = val); | ||
inputAttr.onChange(val => size.innerText = val.length); | ||
|
||
nameInput.oninput = _ => inputAttr.setValue(nameInput.value); | ||
|
||
</script> | ||
|
||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
|
||
|
||
const Observable = value => { | ||
const listeners = []; | ||
return { | ||
onChange: callback => { | ||
listeners.push(callback); | ||
callback(value, value); | ||
}, | ||
getValue: () => value, | ||
setValue: newValue => { | ||
if (value === newValue) return; | ||
const oldValue = value; | ||
value = newValue; | ||
listeners.forEach(callback => callback(value, oldValue)); | ||
} | ||
} | ||
}; | ||
|
||
|
||
const ObservableList = list => { | ||
const addListeners = []; | ||
const delListeners = []; | ||
const removeAt = array => index => array.splice(index, 1); | ||
const removeItem = array => item => { const i = array.indexOf(item); if (i>=0) removeAt(array)(i); }; | ||
const listRemoveItem = removeItem(list); | ||
const delListenersRemove = removeAt(delListeners); | ||
return { | ||
onAdd: listener => addListeners.push(listener), | ||
onDel: listener => delListeners.push(listener), | ||
add: item => { | ||
list.push(item); | ||
addListeners.forEach( listener => listener(item)) | ||
}, | ||
del: item => { | ||
listRemoveItem(item); | ||
const safeIterate = [...delListeners]; // shallow copy as we might change listeners array while iterating | ||
safeIterate.forEach( (listener, index) => listener(item, () => delListenersRemove(index) )); | ||
}, | ||
removeDeleteListener: removeItem(delListeners), | ||
count: () => list.length, | ||
countIf: pred => list.reduce( (sum, item) => pred(item) ? sum + 1 : sum, 0) | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
|
||
test("observable-value", assert => { | ||
|
||
const obs = Observable(""); | ||
|
||
// initial state | ||
assert.equals(obs.getValue(), ""); | ||
|
||
// subscribers get notified | ||
let found; | ||
obs.onChange(val => found = val); | ||
obs.setValue("firstValue"); | ||
assert.equals(found, "firstValue"); | ||
|
||
// value is updated | ||
assert.equals(obs.getValue(), "firstValue"); | ||
|
||
// it still works when the receiver symbols changes | ||
const newRef = obs; | ||
newRef.setValue("secondValue"); | ||
// listener updates correctly | ||
assert.equals(found, "secondValue"); | ||
|
||
// Attributes are isolated, no "new" needed | ||
const secondAttribute = Observable(""); | ||
|
||
// initial state | ||
assert.equals(secondAttribute.getValue(), ""); | ||
|
||
// subscribers get notified | ||
let secondFound; | ||
secondAttribute.onChange(val => secondFound = val); | ||
secondAttribute.setValue("thirdValue"); | ||
assert.equals(found, "secondValue"); | ||
assert.equals(secondFound, "thirdValue"); | ||
|
||
// value is updated | ||
assert.equals(secondAttribute.getValue(), "thirdValue"); | ||
|
||
}); | ||
|
||
test("observable-list", assert => { | ||
const raw = []; | ||
const list = ObservableList( raw ); // decorator pattern | ||
|
||
assert.equals(list.count(), 0); | ||
let addCount = 0; | ||
let delCount = 0; | ||
list.onAdd( item => addCount += item); | ||
list.add(1); | ||
assert.equals(addCount, 1); | ||
assert.equals(list.count(), 1); | ||
assert.equals(raw.length, 1); | ||
|
||
list.onDel( item => delCount += item); | ||
list.del(1); | ||
assert.equals(delCount, 1); | ||
assert.equals(list.count(), 0); | ||
assert.equals(raw.length, 0); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<title>Todos</title> | ||
<link rel="stylesheet" href="../../todo.css"> | ||
</head> | ||
<body> | ||
|
||
<main> | ||
<h1>Todo List</h1> | ||
|
||
<div class="holder"> | ||
<button id="plus" autofocus onclick="todoController.addTodo();"> + </button> | ||
|
||
<div class="table" id="todoContainer"></div> | ||
|
||
<div>Tasks: <span id="numberOfTasks">0</span></div> | ||
<div>Open: <span id="openTasks" >0</span></div> | ||
</div> | ||
</main> | ||
|
||
|
||
<script src="../observable/observable.js"></script> | ||
<script src="todo.js"></script> | ||
<script> | ||
const todoController = TodoController(); | ||
|
||
TodoItemsView(todoController, document.getElementById('todoContainer')); | ||
TodoTotalView(todoController, document.getElementById('numberOfTasks')); | ||
TodoOpenView (todoController, document.getElementById('openTasks')); | ||
|
||
todoController.addTodo(); | ||
|
||
</script> | ||
|
||
</body> | ||
|
||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// requires ../observable/observable.js | ||
|
||
const TodoController = () => { | ||
|
||
const Todo = () => { // facade | ||
const textAttr = Observable("text"); // we current don't expose it as we don't use it elsewhere | ||
const doneAttr = Observable(false); | ||
return { | ||
getDone: doneAttr.getValue, | ||
setDone: doneAttr.setValue, | ||
onDoneChanged: doneAttr.onChange, | ||
} | ||
}; | ||
|
||
const todoModel = ObservableList([]); // observable array of Todos, this state is private | ||
|
||
const addTodo = () => { | ||
const newTodo = Todo(); | ||
todoModel.add(newTodo); | ||
return newTodo; | ||
}; | ||
|
||
return { | ||
numberOfTodos: todoModel.count, | ||
numberOfopenTasks: () => todoModel.countIf( todo => ! todo.getDone() ), | ||
addTodo: addTodo, | ||
removeTodo: todoModel.del, | ||
onTodoAdd: todoModel.onAdd, | ||
onTodoRemove: todoModel.onDel, | ||
removeTodoRemoveListener: todoModel.removeDeleteListener, // only for the test case, not used below | ||
} | ||
}; | ||
|
||
|
||
// View-specific parts | ||
|
||
const TodoItemsView = (todoController, rootElement) => { | ||
|
||
const render = todo => { | ||
|
||
function createElements() { | ||
const template = document.createElement('DIV'); // only for parsing | ||
template.innerHTML = ` | ||
<button class="delete">×</button> | ||
<input type="text" size="42"> | ||
<input type="checkbox"> | ||
`; | ||
return template.children; | ||
} | ||
const [deleteButton, inputElement, checkboxElement] = createElements(); | ||
|
||
checkboxElement.onclick = _ => todo.setDone(checkboxElement.checked); | ||
deleteButton.onclick = _ => todoController.removeTodo(todo); | ||
|
||
todoController.onTodoRemove( (removedTodo, removeMe) => { | ||
if (removedTodo !== todo) return; | ||
rootElement.removeChild(inputElement); | ||
rootElement.removeChild(deleteButton); | ||
rootElement.removeChild(checkboxElement); | ||
removeMe(); | ||
} ); | ||
|
||
rootElement.appendChild(deleteButton); | ||
rootElement.appendChild(inputElement); | ||
rootElement.appendChild(checkboxElement); | ||
}; | ||
|
||
// binding | ||
|
||
todoController.onTodoAdd(render); | ||
|
||
// we do not expose anything as the view is totally passive. | ||
}; | ||
|
||
const TodoTotalView = (todoController, numberOfTasksElement) => { | ||
|
||
const render = () => | ||
numberOfTasksElement.innerText = "" + todoController.numberOfTodos(); | ||
|
||
// binding | ||
|
||
todoController.onTodoAdd(render); | ||
todoController.onTodoRemove(render); | ||
}; | ||
|
||
const TodoOpenView = (todoController, numberOfOpenTasksElement) => { | ||
|
||
const render = () => | ||
numberOfOpenTasksElement.innerText = "" + todoController.numberOfopenTasks(); | ||
|
||
// binding | ||
|
||
todoController.onTodoAdd(todo => { | ||
render(); | ||
todo.onDoneChanged(render); | ||
}); | ||
todoController.onTodoRemove(render); | ||
}; | ||
|
||
|
Oops, something went wrong.