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
"use strict" | |
const assign = Object.assign | |
function push(arr, item) { | |
let newArr = (arr && arr.slice()) || [] | |
newArr.push(item) | |
return newArr | |
} | |
function pop(arr) { | |
let newArr = arr.slice() | |
newArr.pop() | |
return newArr | |
} | |
function locateBy(predicate) { | |
return (key, arr) => { | |
for(let i = 0, len = arr.length; i < len; i++) { | |
if(predicate(key, arr[i], i, arr)) | |
return i | |
} | |
return -1 | |
} | |
} | |
const byId = locateBy((id, it) => it.id === id) | |
const ident = v => v | |
const head = arr => arr[arr.length - 1] | |
const error = (msg) => { throw new Error(msg) } | |
const unwrap = action => assign({}, action, { path: pop(action.path) }) | |
function combine(model) { | |
const fields = Object.keys(model) | |
function updateField(state, field, fieldModel, action) { | |
if(!fieldModel) | |
error(`model has no field ${field}`) | |
const fieldState = state !== undefined ? state[field] : undefined | |
return fieldModel.update(fieldState, action) | |
} | |
function combinedUpdate(state, action) { | |
if(action.path) { | |
const field = head(action.path) | |
let newState = assign({}, state) | |
newState[field] = updateField(state, field, model[field], unwrap(action)) | |
return newState | |
} | |
else { | |
let newState = assign({}, state) | |
fields.forEach((field) => { | |
newState[field] = updateField(state, field, model[field], action) | |
}) | |
return newState | |
} | |
} | |
combinedUpdate.fields = model | |
return combinedUpdate | |
} | |
function modelOf({init, update: _update, actions = {}, views = {}}) { | |
const actionNames = Object.keys(actions) | |
const viewNames = Object.keys(views) | |
let update | |
if(init) { | |
update = (state, action) => state === undefined ? init() : _update(state, action) | |
} else { | |
update = _update | |
} | |
function connect (select, dispatch) { | |
const dataset = { pick: select } | |
if(update.fields) { | |
Object.keys(update.fields).forEach(field => { | |
const getFieldState = () => select()[field] | |
const fieldModel = update.fields[field] | |
dataset[field] = fieldModel.connect( | |
getFieldState, | |
action => dispatch(assign({}, action, {path: push(action.path, field)})) | |
) | |
}) | |
} | |
actionNames.forEach(key => { | |
const action = actions[key] | |
dataset[key] = action.__thunk__ | |
? action(dataset) | |
: (...args) => dispatch(action(...args)) | |
}) | |
viewNames.forEach(key => { | |
const view = views[key] | |
dataset[key] = () => view(select()) | |
}) | |
return dataset | |
} | |
return assign({ init, update, connect }, actions, views) | |
} | |
const LIST_ADD = 'LIST_ADD' | |
const LIST_REMOVE = 'LIST_REMOVE' | |
function listOf(model, finder = ident, {update: _update, actions, views}={}) { | |
function update (state = [], action) { | |
if(action.type === LIST_ADD) { | |
let newState = state.slice() | |
newState.push( | |
model.init ? model.init(...action.args) : model.update(undefined, action) | |
) | |
return newState | |
} | |
else if(action.type === LIST_REMOVE) { | |
const idx = finder(action.key, state) | |
if(idx >= 0) { | |
let newState = state.slice() | |
newState.splice(idx, 1) | |
return newState | |
} else { | |
return state | |
} | |
} | |
else if(action.path) { | |
const key = head(action.path) | |
const idx = finder(key, state) | |
if(idx >= 0) { | |
let newState = state.slice() | |
newState[idx] = model.update(state[idx], unwrap(action)) | |
return newState | |
} | |
return state | |
} else if(_update) { | |
return _update(state, action) | |
} | |
return state.map(item => model.update(item, action)) | |
} | |
function itemSelector(key, select) { | |
return () => { | |
const state = select() | |
const idx = finder(key, state) | |
if(idx >= 0) | |
return state[idx] | |
} | |
} | |
const listActions = assign({}, { | |
add: (...args) => ({ type: LIST_ADD, args }), | |
remove: key => ({ type: LIST_REMOVE, key }) | |
}, actions) | |
function connectAt(key, select, dispatch) { | |
return model.connect( | |
itemSelector(key, select), | |
action => dispatch(assign({}, action, { path: push(action.path, key) })) | |
) | |
} | |
const listModel = modelOf({ update, actions: listActions, views }) | |
const baseConnect = listModel.connect | |
listModel.connect = (select, dispatch) => { | |
const dataset = baseConnect(select, dispatch) | |
dataset.get = key => connectAt(key, select, dispatch) | |
return dataset | |
} | |
return listModel | |
} | |
function thunk(th) { | |
th.__thunk__ = true | |
return th | |
} | |
function connect(model, store) { | |
return model.connect(store.getState, store.dispatch) | |
} |
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
const Field = (name, seed) => { | |
return modelOf({ | |
update(state = seed, action) { | |
if(action.type === 'SET_FIELD' && action.name === name) | |
return action.value | |
return state | |
}, | |
actions: { | |
set: (value) => ({type: 'SET_FIELD', name, value}) | |
} | |
}) | |
} | |
const Todo = modelOf({ | |
init(id=0, text='new Todo') { | |
return {id, text, completed: false} | |
}, | |
update(state = Todo.init(), action) { | |
switch(action.type) { | |
case 'TOGGLE_TODO': | |
return assign({}, state, {completed: !state.completed}) | |
default: | |
return state | |
} | |
}, | |
actions: { | |
toggle: () => ({type: 'TOGGLE_TODO'}) | |
} | |
}) | |
const TodoApp = modelOf({ | |
update: combine({ | |
todos: listOf(Todo, byId, { | |
update(state, action) { | |
if(action.type === 'ARCHIVE') | |
return state.filter(it => !it.completed) | |
return state | |
} | |
}), | |
filter: Field('filter', 'ALL') | |
}), | |
actions: { | |
toggleAll: Todo.toggle, | |
archive: () => ({type: 'ARCHIVE'}), | |
showAll: () => TodoApp.filter.set('ALL'), | |
showCompleted: () => TodoApp.filter.set('COMPLETED'), | |
showPedning: () => TodoApp.filter.set('PENDING') | |
}, | |
views: { | |
visibleTodos: state => ( | |
state.filter === 'ALL' ? state.todos | |
: state.filter === 'COMPLETED' ? state.todos.filter(it => it.completed) | |
: /* state.filter === 'PENDING'*/ state.todos.filter(it => !it.completed) | |
) | |
} | |
}) |
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
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Test</title> | |
</head> | |
<body> | |
<div id=root></div> | |
<script src="https://npmcdn.com/redux/dist/redux.min.js"></script> | |
<script src="model-helpers.js"></script> | |
<script src="model.js"></script> | |
<script> | |
const store = Redux.createStore(TodoApp.update) | |
const logState = () => console.log('state', store.getState()) | |
logState() | |
store.subscribe(logState) | |
const ds = connect(TodoApp, store) | |
ds.todos.add(1, 'learn') | |
ds.todos.add(2, 'eat') | |
ds.todos.add(3, 'sleep') | |
ds.toggleAll() | |
ds.todos.get(2).toggle() | |
ds.filter.set('COMPLETED') | |
console.log(ds.visibleTodos()) | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment