Open dmitriz opened 7 years ago
[The app.html
file from the TodoMVC example]
https://github.com/tastejs/todomvc/blob/master/examples/angular2/app/app.html
<input
<!-- two-way binding by name string to the model defined in the controller -->
[(ngModel)]="newTodoText"
<!-- Angular expression attached by name to evaluate on the `keyup.enter` event -->
(keyup.enter)="addTodo()"
>
addTodo() {
if (this.newTodoText.trim().length) {
// delegate to a `todoStore` method, need to know the API from another file
this.todoStore.add(this.newTodoText);
// directly reset the input value
this.newTodoText = '';
}
}
Delegates to the store.ts
file:
add(title: String) {
// directly update the store
this.todos.push(new Todo(title));
// write to the `localStorage`
this.updateStore();
}
...
private updateStore() {
localStorage.setItem('angular2-todos', JSON.stringify(this.todos));
}
The index.html
file from the TodoMVC example:
<input
<!-- two-way binding by name to the model defined in the controller -->
v-model="newTodo"
<!-- function defined in the controller is attached by name string
to the `keyup.enter` event -->
@keyup.enter="addTodo"
>
addTodo: function () {
var value = this.newTodo && this.newTodo.trim()
if (!value) {
return
}
// directly update the `todos` property on the component
this.todos.push({
id: todoStorage.uid++,
title: value,
completed: false
})
// reset component's property and the input field via the 2-way binding
this.newTodo = ''
},
// watch for changes to update the `localStorage`
watch: {
todos: {
handler: function (todos) {
todoStorage.save(todos)
},
deep: true
}
},
The todomvc.js
file from the TodoMVC example:
view: function(vnode) {
var ui = vnode.state
...
// attach function referenced by state on the vnode argument
m("input", {onkeypress: ui.add})
...
}
add: function(e) {
if (e.keyCode === 13) {
// call the `dispatch` function
state.dispatch("createTodo", [e.target.value])
// directly reset the input value referenced by the event target
e.target.value = ""
}
},
...
state.dispatch = function(action, args) {
// run the action function
// this is a driver type general purpose helper function,
// where imperative style is expected
state[action].apply(state, args || [])
requestAnimationFrame(function() {
localStorage["todos-mithril"] = JSON.stringify(state.todos)
})
},
createTodo: function(title) {
// directly update the `state`
state.todos.push({title: title.trim(), completed: false})
},
The view.js
file in the TodoMVC example:
// actions are present but not attached here
input('.new-todo', {
props: {
type: 'text',
name: 'newTodo'
},
// Cycle's specific API method?
hook: {
update: (oldVNode, {elm}) => {
// directly updating the value by referencing the underlying element
elm.value = '';
},
},
})
Actions attached are found in the intent.js
file:
/* Original comments:
// CLEAR INPUT STREAM
// A stream of ESC key strokes in the `.new-todo` field.
*/
// Declaratively reference the `DOMSource` provided by the driver,
// the keydown event stream
DOMSource.select('.new-todo').events('keydown')
// is filtered by the key code
.filter(ev => ev.keyCode === ESC_KEY)
// and transformed into the `clearInput` action stream
.map(payload => ({type: 'clearInput', payload})),
/* Original comments:
// ENTER KEY STREAM
// A stream of ENTER key strokes in the `.new-todo` field.
*/
DOMSource.select('.new-todo').events('keydown')
/* Original comments:
// Trim value and only let the data through when there
// is anything but whitespace in the field and the ENTER key was hit.
*/
.filter(ev => {
let trimmedVal = String(ev.target.value).trim();
return ev.keyCode === ENTER_KEY && trimmedVal;
})
/* Original comments:
// Return the trimmed value.
*/
.map(ev => String(ev.target.value).trim())
// transform into the `insertTodo` action stream
.map(payload => ({type: 'insertTodo', payload})),
Both clear input and enter key streams are merged and returned by the intent
function. Next, the model
function provides reducers in the model.js
file:
/* Original comments:
// THIS IS THE MODEL FUNCTION
// It expects the actions coming in from the sources
*/
function model(action$, sourceTodosData$) {
/* Original comments:
// THE BUSINESS LOGIC
// Actions are passed to the `makeReducer$` function
// which creates a stream of reducer functions that needs
// to be applied on the todoData when an action happens.
*/
let reducer$ = makeReducer$(action$);
/* Original comments:
// RETURN THE MODEL DATA
*/
return sourceTodosData$.map(sourceTodosData =>
reducer$.fold((todosData, reducer) => reducer(todosData), sourceTodosData)
).flatten()
/* Original comments:
// Make this remember its latest event, so late listeners
// will be updated with the latest state.
*/
.remember();
...
let clearInputReducer$ = action$
.filter(a => a.type === 'clearInput')
.mapTo(function clearInputReducer(todosData) {
return todosData;
});
The MostJS example here was one of the main inspiration sources for un
and resembles some of the code in
https://github.com/dmitriz/un/blob/master/index.js
The view.js
file from the TodoMVC example:
export const renderHeader = () =>
...
form('.add-todo', [
// actions are present but not attached here
input('.new-todo', { attrs: {
placeholder: 'What needs to be done?',
autofocus: true
} })
...
// the whole app runs by this single command
// returns the stream of states
todos(window, container, initialState)
// save into localStorage or read from it
// both reads and writes into stream
// similar to middleware
.loop(updateStore, store)
// update the dom via the snabbdom's `patch` method
// the `reduce` method is used due to how the `patch`
// takes vnode and returns the next one
.reduce(updateApp, root)
// elements are taken directly from the dom
const root = document.querySelector('.todoapp')
const container = root.parentNode
...
const todos = (window, container, initialState) => {
// all actions streams are merged together
const actions = merge(
listActions(container),
editActions(container),
filterActions(window)
)
// and passed to the `scan` method by `mostjs` over `applyAction` reducer
return scan(applyAction, initialState, actions)
}
...
// the root dom element
const listActions = el => {
...
// is passed to the `submit` event stream creator
// and filtered down to only match the `.add-todo` selector
// and subsequently mapped over by the `addTodoAction` function
const add = map(addTodoAction, preventDefault(match('.add-todo', submit(el))))
...
// all streams are merged and returned by the `listActions`
return merge(add, remove, complete, all, clear)
}
The actions are finally defined in the action.js
file :
// The action function is composed from the pure action creator functions
export const addTodoAction = pipe(withFirstValue, resetForm, addTodo, setTodos)
which are defined in the todos.js
file:
// addTodo :: string -> [Todo] -> [Todo]
export const addTodo = description => todos =>
description.trim().length === 0 ? todos
: todos.concat(newTodo(newId(), description, false))
The example here was also one of the main inspiration sources for un and resembles some of the code in https://github.com/dmitriz/un/blob/master/index.js
// action stream is created from merging with the router stream of the `url` values
const action$ = flyd.merge(MyRouter.stream, flyd.stream());
// the input virtual node (snabbdom) updates the action stream via dom events
h('input.new-todo', {
props: {
placeholder: 'What needs to be done?',
// input value is controlled by the `model` property
value: model.newTitle
},
on: {
// on input field update,
// value is passed to the composition of the transformer functions
// and gets subsequently written into the `action$` stream
input: R.compose(action$, Action.ChangeNewTitle, targetValue),
// the keydown event is passed through the action creator,
// and is piped into `action$` stream
// and sent though the `ifEnter` filter to only contain the ENTER events
keydown: ifEnter(action$, Action.Create())
},
}),
...
// the action stream is scanned over the `update` reducer from the action stream
const model$ = flyd.scan(R.flip(update), restoreState(), action$)
// and gets mapped over the `view` function to generate vnode stream
const vnode$ = flyd.map(view(action$), model$)
// the state (aka model) stream is also passed to the saving middleware
flyd.map(saveState, model$);
Finally the app is run by
window.addEventListener('DOMContentLoaded', function() {
const container = document.querySelector('.todoapp')
// vnode stream is scanned over the snabbdom's `patch` method
// updating the dom in reaction to the virtual dom changes
flyd.scan(patch, container, vnode$)
})
What is it about?
DISCLAIMER.
These are my notes, mostly for my own reference, to study and analyse the approaches by different frameworks to implement the popular TodoMVC example.
The comments record my own description of the approach for a more convenient future reference. No judgement and no neither positive nor negative connotation is neither implied nor intended.
Redux (with React),
The
TodoTextInput
component from the TodoMVC example:State updates:
-- The
AddTodo
component from the Todos exampleUn
- related:Basic submit example with
un
implemented with pure functionsThe same example with the reset upon submit functionality with pure functions