Merge branch 'dev'
using immer.js; can create new bins; works well
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
import Note from './Note.js';
|
import Note from './Note.js';
|
||||||
import {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes} from './handlers/App.js';
|
import {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes, new_bin_handler} from './handlers/App.js';
|
||||||
|
|
||||||
|
|
||||||
function App(vnode_init){
|
function App(vnode_init){
|
||||||
const {state, dispatch} = vnode_init.attrs;
|
const {state, dispatch} = vnode_init.attrs;
|
||||||
load_notes(state, dispatch);
|
//load_notes(state, dispatch);
|
||||||
return {
|
return {
|
||||||
view: function(vnode){
|
view: function(vnode){
|
||||||
const s = vnode.attrs.state;
|
const s = vnode.attrs.state;
|
||||||
@@ -20,12 +20,12 @@ function App(vnode_init){
|
|||||||
]),
|
]),
|
||||||
m('.top-right', {key: 'top-right'}, [
|
m('.top-right', {key: 'top-right'}, [
|
||||||
m('.bin-id', {key: 'bin-id'}, s.bin.id),
|
m('.bin-id', {key: 'bin-id'}, s.bin.id),
|
||||||
m('button', {key: 'new-bin-button'}, 'New Bin...')
|
m('button', {key: 'new-bin-button', onclick: new_bin_handler.bind(null, s, dispatch)}, 'New Bin...')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
m('.main', {key: 'main'}, [
|
m('.main', {key: 'main'}, [
|
||||||
m('.notes', s.notes.map(note_state =>
|
m('.notes', s.notes.map(note_state =>
|
||||||
m(Note, {note_state, dispatch, key: note_state.note.id})
|
m(Note, {state, note_state, dispatch, key: note_state.note.id})
|
||||||
))
|
))
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function Note(vnode_init){
|
|||||||
const {is_editing, note} = note_state;
|
const {is_editing, note} = note_state;
|
||||||
if(is_editing){
|
if(is_editing){
|
||||||
return m('.note', [
|
return m('.note', [
|
||||||
m('textarea', {key: 'textarea', onchange: text_change_handler.bind(null, note_state, dispatch) }, note.text),
|
m('textarea', {key: 'textarea', onchange: text_change_handler.bind(null, note_state, dispatch) }, note_state.temp_text),
|
||||||
m('.buttons', {key: 'editing-buttons'}, [
|
m('.buttons', {key: 'editing-buttons'}, [
|
||||||
m('button', {key: 'cancel-button', onclick: cancel_handler.bind(null, note_state, dispatch) }, 'Cancel'),
|
m('button', {key: 'cancel-button', onclick: cancel_handler.bind(null, note_state, dispatch) }, 'Cancel'),
|
||||||
m('button', {key: 'save-button', onclick: save_handler.bind(null, note_state, dispatch) }, 'Save')
|
m('button', {key: 'save-button', onclick: save_handler.bind(null, note_state, dispatch) }, 'Save')
|
||||||
|
|||||||
+19
-4
@@ -4,11 +4,16 @@ const api_stub = {};
|
|||||||
|
|
||||||
// for mocking persistent state, for use by the fake endpoints, just as a real server endpoint would have DB access for state:
|
// for mocking persistent state, for use by the fake endpoints, just as a real server endpoint would have DB access for state:
|
||||||
const state = {
|
const state = {
|
||||||
|
bins: [
|
||||||
|
{
|
||||||
|
id: nanoid(),
|
||||||
notes: [
|
notes: [
|
||||||
{id: nanoid(), text: 'Note one', modified: Date.now()},
|
{id: nanoid(), text: 'Note one', modified: Date.now()},
|
||||||
{id: nanoid(), text: 'Note two', modified: Date.now()},
|
{id: nanoid(), text: 'Note two', modified: Date.now()},
|
||||||
{id: nanoid(), text: 'Note three', modified: Date.now()}
|
{id: nanoid(), text: 'Note three', modified: Date.now()}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoints = {
|
const endpoints = {
|
||||||
@@ -16,11 +21,15 @@ const endpoints = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
endpoints.post['/load-notes'] = function(resolve, reject, body){
|
endpoints.post['/load-notes'] = function(resolve, reject, body){
|
||||||
resolve( {status: 'ok', notes: state.notes} );
|
let i = 0;
|
||||||
|
if(body.bin_id){
|
||||||
|
i = state.bins.findIndex(b=>b.id===body.bin_id);
|
||||||
|
}
|
||||||
|
resolve( {status: 'ok', notes: state.bins[i].notes} );
|
||||||
};
|
};
|
||||||
|
|
||||||
endpoints.post['/search'] = function(resolve, reject, body){
|
endpoints.post['/search'] = function(resolve, reject, body){
|
||||||
const notes = state.notes.filter(n => n.text.indexOf(body.search_term) !== -1 )
|
const notes = state.bins.find(b=>b.id===body.bin_id).notes.filter(n => n.text.indexOf(body.search_term) !== -1 )
|
||||||
if(body.sorting==='new->old'){
|
if(body.sorting==='new->old'){
|
||||||
notes.sort((a,b) => a.modified-b.modified);
|
notes.sort((a,b) => a.modified-b.modified);
|
||||||
}
|
}
|
||||||
@@ -32,12 +41,18 @@ endpoints.post['/search'] = function(resolve, reject, body){
|
|||||||
|
|
||||||
endpoints.post['/save'] = function(resolve, reject, body){
|
endpoints.post['/save'] = function(resolve, reject, body){
|
||||||
const {note_id, text} = body;
|
const {note_id, text} = body;
|
||||||
const note = state.notes.find( n => n.id===note_id );
|
let bin = state.bins.find(b=>b.id===body.bin_id);
|
||||||
|
if(!bin){
|
||||||
|
bin = {id: body.bin_id, notes: []};
|
||||||
|
state.bins.push(bin);
|
||||||
|
}
|
||||||
|
const note = bin.notes.find( n => n.id===note_id );
|
||||||
if(note){
|
if(note){
|
||||||
note.text = text;
|
note.text = text;
|
||||||
|
note.modified = Date.now();
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
state.notes.push({id: note_id, text, modified: Date.now()});
|
bin.notes.push({id: note_id, text, modified: Date.now()});
|
||||||
}
|
}
|
||||||
resolve( {status: 'ok'} );
|
resolve( {status: 'ok'} );
|
||||||
};
|
};
|
||||||
|
|||||||
+15
-9
@@ -1,33 +1,39 @@
|
|||||||
import nanoid from '../nanoid.min.js';
|
import nanoid from '../nanoid.min.js';
|
||||||
import api from '../api.js';
|
//import api from '../api.js';
|
||||||
//import api from '../api-stub.js';
|
import api from '../api-stub.js';
|
||||||
|
|
||||||
const load_notes = function(state, dispatch){
|
const load_notes = function(state, dispatch){
|
||||||
api.post('/load-notes', {bin_id: state.bin.id})
|
api.post('/load-notes', {bin_id: state.bin.id})
|
||||||
.then(res=>{
|
.then(res=>{
|
||||||
dispatch({type: 'notes-loaded', notes: res.notes});
|
dispatch('notes-loaded', res.notes);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const runSearch = function(state, dispatch){
|
const runSearch = function(state, dispatch){
|
||||||
api.post('/search', {search_term: state.search_term, sorting: state.sorting})
|
api.post('/search', {search_term: state.search_term, sorting: state.sorting, bin_id: state.bin.id})
|
||||||
.then(res=>{
|
.then(res=>{
|
||||||
dispatch({type:'update-search-results', notes: res.notes});
|
dispatch('update-search-results', res.notes);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const new_note_handler = function(state, dispatch){
|
const new_note_handler = function(state, dispatch){
|
||||||
dispatch({type:'add-note', id: nanoid()});
|
dispatch('add-note', {id: nanoid(), date: Date.now()});
|
||||||
};
|
};
|
||||||
const search_term_change_handler = function(state, dispatch, e){
|
const search_term_change_handler = function(state, dispatch, e){
|
||||||
if(e.code === 'Enter'){
|
if(e.code === 'Enter'){
|
||||||
runSearch(state, dispatch);
|
runSearch(state, dispatch);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
dispatch({type:'update-search-term', search_term: e.target.value});
|
dispatch('update-search-term', e.target.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const sorting_change_handler = function(state, dispatch, e){
|
const sorting_change_handler = function(state, dispatch, e){
|
||||||
runSearch(state, dispatch);
|
runSearch(state, dispatch);
|
||||||
dispatch({type:'update-sorting', sorting: e.target.value});
|
dispatch('update-sorting', e.target.value);
|
||||||
|
};
|
||||||
|
const new_bin_handler = function(state, dispatch){
|
||||||
|
const id = nanoid();
|
||||||
|
// TODO: consolidate: this will cause two redraws:
|
||||||
|
dispatch('new-bin', {id});
|
||||||
|
dispatch('notes-loaded', []);
|
||||||
};
|
};
|
||||||
|
|
||||||
export {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes};
|
export {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes, new_bin_handler};
|
||||||
+6
-9
@@ -1,21 +1,18 @@
|
|||||||
import api from '../api.js';
|
//import api from '../api.js';
|
||||||
//import api from '../api-stub.js';
|
import api from '../api-stub.js';
|
||||||
|
|
||||||
const edit_handler = function(note_state, dispatch){
|
const edit_handler = function(note_state, dispatch){
|
||||||
dispatch({type: 'update-note-editing', note_id: note_state.note.id, is_editing: true});
|
dispatch('update-note-editing', {id: note_state.note.id, is_editing: true});
|
||||||
};
|
};
|
||||||
const cancel_handler = function(note_state, dispatch){
|
const cancel_handler = function(note_state, dispatch){
|
||||||
dispatch({type: 'update-note-editing', note_id: note_state.note.id, is_editing: false});
|
dispatch('update-note-editing', {id: note_state.note.id, is_editing: false});
|
||||||
};
|
};
|
||||||
const text_change_handler = function(note_state, dispatch, e){
|
const text_change_handler = function(note_state, dispatch, e){
|
||||||
note_state.temp_text = e.target.value;
|
note_state.temp_text = e.target.value;
|
||||||
};
|
};
|
||||||
const save_handler = function(note_state, dispatch){
|
const save_handler = function(note_state, dispatch){
|
||||||
// TODO: consolidate: this will cause two redraws:
|
dispatch('save-note-edit', {id: note_state.note.id, text: note_state.temp_text});
|
||||||
dispatch({type: 'update-note-text', note_id: note_state.note.id, text: note_state.temp_text});
|
api.post('/save', {bin_id: note_state.bin_id, note_id: note_state.note.id, text: note_state.temp_text})
|
||||||
//note.text = temp_text;
|
|
||||||
dispatch({type: 'update-note-editing', note_id: note_state.note.id, is_editing: false});
|
|
||||||
api.post('/save', {note_id: note_state.note.id, text: note_state.temp_text})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { edit_handler, cancel_handler, text_change_handler, save_handler };
|
export { edit_handler, cancel_handler, text_change_handler, save_handler };
|
||||||
Vendored
+2
File diff suppressed because one or more lines are too long
+1
-1
@@ -8,9 +8,9 @@
|
|||||||
<!--<link rel="stylesheet" href="animate.min.css"></link>-->
|
<!--<link rel="stylesheet" href="animate.min.css"></link>-->
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<!--<script src="./mithril.min.js"></script>-->
|
|
||||||
<script src="./mithril.min.js"></script>
|
<script src="./mithril.min.js"></script>
|
||||||
<script src="./redux.min.js"></script>
|
<script src="./redux.min.js"></script>
|
||||||
|
<script src="./immer.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="module" src="./index.js"></script>
|
<script type="module" src="./index.js"></script>
|
||||||
|
|||||||
@@ -1,77 +1,42 @@
|
|||||||
import App from './App.js';
|
import App from './App.js';
|
||||||
import nanoid from './nanoid.min.js';
|
import nanoid from './nanoid.min.js';
|
||||||
|
const produce = immer.produce;
|
||||||
|
immer.setAutoFreeze(false); // needed for high-frequency updated values, like onkeyup->note.temp_text; only once 'save' is called will it produce a new immutable state tree
|
||||||
|
|
||||||
var root = document.body;
|
var root = document.body;
|
||||||
|
|
||||||
function bin_reducer(old_state, new_state, action){
|
/* Ruthlessly taken from [https://gist.github.com/kitze/fb65f527803a93fb2803ce79a792fff8]: */
|
||||||
new_state.bin = old_state.bin;
|
const handleActions = (actionsMap, defaultState) =>
|
||||||
}
|
(state=defaultState, {type, payload}) =>
|
||||||
|
produce(state, draft => {
|
||||||
|
const action = actionsMap[type];
|
||||||
|
action && action(draft, payload);
|
||||||
|
});
|
||||||
|
|
||||||
function search_reducer(old_state, new_state, action){
|
const reducer = handleActions({
|
||||||
if(action.type === 'update-search-term'){
|
'new-bin': (s, bin) => { s.bin=bin; },
|
||||||
new_state.search_term = action.search_term;
|
'update-search-term': (s, search_term) => { s.search_term=search_term; },
|
||||||
}
|
'update-search-results': (s, _notes) => { s.notes =_notes.map(n=>({is_editing: false, temp_text: '', bin_id: s.bin.id, note:n})); },
|
||||||
else if(action.type === 'update-search-results'){
|
'add-note': (s, {id, date}) => { s.notes.unshift({is_editing: true, temp_text: '', bin_id: s.bin.id, note: {id: id, text: '', modified: date}}); },
|
||||||
new_state.notes = action.notes.map(note=>({is_editing: false, note}));
|
'notes-loaded': (s, _notes) => { s.notes = _notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: s.bin_id, note:n})); },
|
||||||
new_state.search_term = old_state.search_term;
|
'update-note-text': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; }, // updates underlying note text (i.e. the "model", not the app note_state) "in the background" (e.g. from a server-pushed update), regardless of whether it's being edited; "save" is a separate action, below
|
||||||
}
|
'update-note-editing': (s, {id, is_editing}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.is_editing=is_editing; note_s.temp_text=note_s.note.text; },
|
||||||
else{
|
'save-note-edit': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; note_s.temp_text=text; note_s.is_editing=false; },
|
||||||
new_state.search_term = old_state.search_term;
|
'update-sorting': (s, sorting) => { s.sorting=sorting; }
|
||||||
}
|
}, {
|
||||||
}
|
|
||||||
|
|
||||||
function notes_reducer(old_state, new_state, action){
|
|
||||||
if(action.type === 'add-note'){
|
|
||||||
new_state.notes = old_state.notes.concat([{is_editing: true, note: {id: action.id, text: '', modified: Date.now()}}])
|
|
||||||
}
|
|
||||||
else if(action.type === 'notes-loaded'){
|
|
||||||
new_state.notes = action.notes.map(note=>({is_editing: false, note}));
|
|
||||||
}
|
|
||||||
else if(action.type === 'update-note-text'){
|
|
||||||
const i = old_state.notes.findIndex(note_state => note_state.note.id === action.note_id);
|
|
||||||
new_state.notes = old_state.notes.slice();
|
|
||||||
new_state.notes[i] = {...new_state.notes[i], note: {...new_state.notes[i].note, text: action.text, modified: Date.now()}};
|
|
||||||
}
|
|
||||||
else if(action.type === 'update-note-editing'){
|
|
||||||
const i = old_state.notes.findIndex(note_state => note_state.note.id === action.note_id);
|
|
||||||
new_state.notes = old_state.notes.slice();
|
|
||||||
new_state.notes[i] = {...new_state.notes[i], is_editing: action.is_editing};
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
new_state.notes = old_state.notes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sorting_reducer(old_state, new_state, action){
|
|
||||||
if(action.type === 'update-sorting'){
|
|
||||||
new_state.sorting = action.sorting;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
new_state.sorting = old_state.sorting;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reducer(old_state, action){
|
|
||||||
const new_state = {};
|
|
||||||
bin_reducer(old_state, new_state, action);
|
|
||||||
notes_reducer(old_state, new_state, action);
|
|
||||||
search_reducer(old_state, new_state, action);
|
|
||||||
sorting_reducer(old_state, new_state, action);
|
|
||||||
return new_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create Redux store, with Redux DevTools enabled:
|
|
||||||
const store = Redux.createStore(reducer, /* preloadedState, */ {
|
|
||||||
bin: {id: nanoid()},
|
bin: {id: nanoid()},
|
||||||
notes: [
|
notes: [
|
||||||
//{is_editing: false, note: {id: nanoid(), text: 'Note one', modified: 1}},
|
//{is_editing: false, temp_text: '', bin_id: '', note: {id: nanoid(), text: 'Note one', modified: 1}},
|
||||||
],
|
],
|
||||||
search_term: '',
|
search_term: '',
|
||||||
sorting: 'new->old'
|
sorting: 'new->old'
|
||||||
//search_result_notes: []
|
//search_result_notes: []
|
||||||
},
|
});
|
||||||
|
|
||||||
|
// create Redux store, with Redux DevTools enabled:
|
||||||
|
const store = Redux.createStore(reducer, /* preloadedState, */
|
||||||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
|
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
|
||||||
const dispatch = function(...args){ store.dispatch(...args); };
|
const dispatch = function(action_type, payload){ store.dispatch({type:action_type, payload}); };
|
||||||
|
|
||||||
store.subscribe(()=>{
|
store.subscribe(()=>{
|
||||||
m.render(root, m(App, {state: store.getState(), dispatch}));
|
m.render(root, m(App, {state: store.getState(), dispatch}));
|
||||||
|
|||||||
Reference in New Issue
Block a user