diff --git a/App.js b/App.js index fc8206b..5c5353f 100644 --- a/App.js +++ b/App.js @@ -1,58 +1,36 @@ import Note from './Note.js'; +import {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes} from './handlers/App.js'; -function App(){ -const bin = {id: 'egf35'}; -const notes = [ - {id: 'aaa', text: 'Note one', modified: 1}, - {id: 'bbb', text: 'Note two', modified: 1}, - {id: 'ccc', text: 'Note three', modified: 1}, - {id: 'ddd', text: 'Note four', modified: 1}, - {id: 'eee', text: 'Note five', modified: 1}, - {id: 'fff', text: 'Note six', modified: 1}, - {id: 'ggg', text: 'Note seven', modified: 1}, - {id: 'hhh', text: 'Note eight', modified: 1} - ]; -let search_term = ''; -let count = 9; -const new_note_handler = function(){ - count++; - // TODO: make immutable-style: - notes.push({id: ''+count, text: '', modified: Date.now()}); - }; -const search_term_change_handler = function(e){ - if(e.code === 'Enter'){ - } - else{ - search_term = e.target.value; - } - }; -const sorting_change_handler = function(){ - }; -return { -view: function(vnode){ -return m('.app', {key: 'app'}, [ - m('.top', {key: 'top'}, [ - m('.top-left', {key: 'top-left'}, [ - m('button', {key: 'button', onclick: new_note_handler}, 'New Note...'), - m('input.search', {key: 'search', value: search_term, onkeyup: search_term_change_handler}), - m('select.sorting', {key: 'sorting', onchange: sorting_change_handler}, [ - m('option', {key: 'new-old'}, 'Newest -> Oldest'), - m('option', {key: 'old-new'}, 'Oldest -> Newest') - ]) - ]), - m('.top-right', {key: 'top-right'}, [ - m('.bin-id', {key: 'bin-id'}, bin.id), - m('button', {key: 'new-bin-button'}, 'New Bin...') - ]) - ]), - m('.main', {key: 'main'}, [ - m('.notes', notes.map(note => - m(Note, {note, key: note.id}) - )) - ]) - ]); -} -}; + +function App(vnode_init){ + const {state, dispatch} = vnode_init.attrs; + load_notes(state, dispatch); + return { + view: function(vnode){ + const s = vnode.attrs.state; + return m('.app', {key: 'app'}, [ + m('.top', {key: 'top'}, [ + m('.top-left', {key: 'top-left'}, [ + m('button', {key: 'button', onclick: new_note_handler.bind(null, s, dispatch)}, 'New Note...'), + m('input.search', {key: 'search', value: s.search_term, onkeyup: search_term_change_handler.bind(null, s, dispatch)}), + m('select.sorting', {key: 'sorting', value: s.sorting, onchange: sorting_change_handler.bind(null, s, dispatch)}, [ + m('option', {key: 'new->old', value: 'new->old'}, 'Newest -> Oldest'), + m('option', {key: 'old->new', value: 'old->new'}, 'Oldest -> Newest') + ]) + ]), + m('.top-right', {key: 'top-right'}, [ + m('.bin-id', {key: 'bin-id'}, s.bin.id), + m('button', {key: 'new-bin-button'}, 'New Bin...') + ]) + ]), + m('.main', {key: 'main'}, [ + m('.notes', s.notes.map(note_state => + m(Note, {note_state, dispatch, key: note_state.note.id}) + )) + ]) + ]); + } + }; } export default App; \ No newline at end of file diff --git a/Note.js b/Note.js index 28ac22f..6b8323e 100644 --- a/Note.js +++ b/Note.js @@ -1,46 +1,30 @@ - +import {edit_handler, cancel_handler, text_change_handler, save_handler} from './handlers/Note.js'; function Note(vnode_init){ -const {note} = vnode_init.attrs; -let is_editing = false; -let temp_text = note.text; -const edit_handler = function(){ - is_editing = true; - }; -const cancel_handler = function(){ - is_editing = false; - }; -const text_change_handler = function(e){ - temp_text = e.target.value; - // prevent redraw, because the temp text is anyway contained in the textarea: - // also, the "Save" button is going to cause its own redraw; otherwise clicking it would cause two redraws (and it is noticeable, that's how I knew two redraws were happening) - e.redraw = false; - }; -const save_handler = function(){ - note.text = temp_text; - is_editing = false; - }; -return { -view: function(){ -if(is_editing){ - return m('.note', [ - m('textarea', {key: 'textarea', onchange: text_change_handler}, note.text), - m('.buttons', {key: 'editing-buttons'}, [ - m('button', {key: 'cancel-button', onclick: cancel_handler}, 'Cancel'), - m('button', {key: 'save-button', onclick: save_handler}, 'Save') - ]) - ]); - } -else{ - return m('.note', [ - m('.text', {key: 'text'}, note.text), - m('.buttons', {key: 'viewing-buttons'}, [ - m('button', {key: 'edit-button', onclick: edit_handler}, 'Edit') - ]) - ]); - } -} -}; + const {dispatch} = vnode_init.attrs; + return { + view: function(vnode){ + const {note_state} = vnode.attrs; + const {is_editing, note} = note_state; + if(is_editing){ + return m('.note', [ + m('textarea', {key: 'textarea', onchange: text_change_handler.bind(null, note_state, dispatch) }, note.text), + m('.buttons', {key: 'editing-buttons'}, [ + 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') + ]) + ]); + } + else{ + return m('.note', [ + m('.text', {key: 'text'}, note.text), + m('.buttons', {key: 'viewing-buttons'}, [ + m('button', {key: 'edit-button', onclick: edit_handler.bind(null, note_state, dispatch) }, 'Edit') + ]) + ]); + } + } + }; } export default Note; \ No newline at end of file diff --git a/api-stub.js b/api-stub.js new file mode 100644 index 0000000..e8024f8 --- /dev/null +++ b/api-stub.js @@ -0,0 +1,54 @@ +import nanoid from './nanoid.min.js'; + +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: +const state = { + notes: [ + {id: nanoid(), text: 'Note one', modified: Date.now()}, + {id: nanoid(), text: 'Note two', modified: Date.now()}, + {id: nanoid(), text: 'Note three', modified: Date.now()} + ] + }; + +const endpoints = { + post: {} + }; + +endpoints.post['/load-notes'] = function(resolve, reject, body){ + resolve( {status: 'ok', notes: state.notes} ); + }; + +endpoints.post['/search'] = function(resolve, reject, body){ + const notes = state.notes.filter(n => n.text.indexOf(body.search_term) !== -1 ); + resolve( {status: 'ok', notes} ); + }; + +endpoints.post['/save'] = function(resolve, reject, body){ + const {note_id, text} = body; + const note = state.notes.find( n => n.id===note_id ); + if(note){ + note.text = text; + } + else{ + state.notes.push({id: note_id, text, modified: Date.now()}); + } + resolve( {status: 'ok'} ); + }; + + + +api_stub.post = function(url, body){ + if(endpoints.post[url]){ + return new Promise((resolve, reject)=>{ + endpoints.post[url](resolve, reject, body); + }); + } + else{ + return new Promise((resolve, reject)=>{ + reject( {error: 'no such endpoint'} ); + }); + } + }; + +export default api_stub; \ No newline at end of file diff --git a/api.js b/api.js new file mode 100644 index 0000000..ddc9132 --- /dev/null +++ b/api.js @@ -0,0 +1,11 @@ +const api = {}; + +api.post = function(url, body){ + return m.request({ + method: 'POST', + url, + body + }); + }; + +export default api; \ No newline at end of file diff --git a/handlers/App.js b/handlers/App.js new file mode 100644 index 0000000..b68396c --- /dev/null +++ b/handlers/App.js @@ -0,0 +1,32 @@ +import nanoid from '../nanoid.min.js'; +//import api from '../api.js'; +import api from '../api-stub.js'; + +const load_notes = function(state, dispatch){ + api.post('/load-notes', {bin_id: state.bin.id}) + .then(res=>{ + dispatch({type: 'notes-loaded', notes: res.notes}); + }); + }; +const runSearch = function(state, dispatch){ + api.post('/search', {search_term: state.search_term}) + .then(res=>{ + dispatch({type:'update-search-results', notes: res.notes}); + }); + }; +const new_note_handler = function(state, dispatch){ + dispatch({type:'add-note', id: nanoid()}); + }; +const search_term_change_handler = function(state, dispatch, e){ + if(e.code === 'Enter'){ + runSearch(state, dispatch); + } + else{ + dispatch({type:'update-search-term', search_term: e.target.value}); + } + }; +const sorting_change_handler = function(state, dispatch, e){ + dispatch({type:'update-sorting', sorting: e.target.value}); + }; + +export {new_note_handler, search_term_change_handler, sorting_change_handler, load_notes}; \ No newline at end of file diff --git a/handlers/Note.js b/handlers/Note.js new file mode 100644 index 0000000..911be87 --- /dev/null +++ b/handlers/Note.js @@ -0,0 +1,21 @@ +//import api from '../api.js'; +import api from '../api-stub.js'; + +const edit_handler = function(note_state, dispatch){ + dispatch({type: 'update-note-editing', note_id: note_state.note.id, is_editing: true}); + }; +const cancel_handler = function(note_state, dispatch){ + dispatch({type: 'update-note-editing', note_id: note_state.note.id, is_editing: false}); + }; +const text_change_handler = function(note_state, dispatch, e){ + note_state.temp_text = e.target.value; + }; +const save_handler = function(note_state, dispatch){ + // TODO: consolidate: this will cause two redraws: + dispatch({type: 'update-note-text', 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 }; \ No newline at end of file diff --git a/index.js b/index.js index c3973e9..09fe7a7 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,83 @@ import App from './App.js'; +import nanoid from './nanoid.min.js'; var root = document.body; -m.mount(root, App); \ No newline at end of file + +function bin_reducer(old_state, new_state, action){ + new_state.bin = old_state.bin; + } + +function search_reducer(old_state, new_state, action){ + if(action.type === 'update-search-term'){ + new_state.search_term = action.search_term; + } + else if(action.type === 'update-search-results'){ + new_state.notes = action.notes.map(note=>({is_editing: false, note})); + new_state.search_term = old_state.search_term; + } + else{ + new_state.search_term = old_state.search_term; + } + } + +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()}, + notes: [ + //{is_editing: false, note: {id: nanoid(), text: 'Note one', modified: 1}}, + ], + search_term: '', + sorting: 'new->old' + //search_result_notes: [] + }, + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()); +const dispatch = function(...args){ store.dispatch(...args); }; + +store.subscribe(()=>{ + m.render(root, m(App, {state: store.getState(), dispatch})); + }); + + +// we don't want Mithril auto-redraw system in place, since Redux will manually re-render when necessary with store.subscribe(): +// m.mount(root, App); +m.render(root, m(App, {state: store.getState(), dispatch})) \ No newline at end of file diff --git a/nanoid.min.js b/nanoid.min.js new file mode 100644 index 0000000..839d66e --- /dev/null +++ b/nanoid.min.js @@ -0,0 +1 @@ +export default (t=21)=>{let e="",r=crypto.getRandomValues(new Uint8Array(t));for(;t--;){let n=63&r[t];e+=n<36?n.toString(36):n<62?(n-26).toString(36).toUpperCase():n<63?"_":"-"}return e}; \ No newline at end of file