Skip to content

Instantly share code, notes, and snippets.

@yelouafi
Created March 24, 2016 18:36
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save yelouafi/a7e6abaae01e982e6062 to your computer and use it in GitHub Desktop.
"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)
}
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)
)
}
})
<!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