faux-api calls, separate event handler modules
parent
f191cc998f
commit
6e7ab2ea67
@ -1,58 +1,36 @@
|
|||||||
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';
|
||||||
|
|
||||||
function App(){
|
|
||||||
const bin = {id: 'egf35'};
|
function App(vnode_init){
|
||||||
const notes = [
|
const {state, dispatch} = vnode_init.attrs;
|
||||||
{id: 'aaa', text: 'Note one', modified: 1},
|
load_notes(state, dispatch);
|
||||||
{id: 'bbb', text: 'Note two', modified: 1},
|
return {
|
||||||
{id: 'ccc', text: 'Note three', modified: 1},
|
view: function(vnode){
|
||||||
{id: 'ddd', text: 'Note four', modified: 1},
|
const s = vnode.attrs.state;
|
||||||
{id: 'eee', text: 'Note five', modified: 1},
|
return m('.app', {key: 'app'}, [
|
||||||
{id: 'fff', text: 'Note six', modified: 1},
|
m('.top', {key: 'top'}, [
|
||||||
{id: 'ggg', text: 'Note seven', modified: 1},
|
m('.top-left', {key: 'top-left'}, [
|
||||||
{id: 'hhh', text: 'Note eight', modified: 1}
|
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)}),
|
||||||
let search_term = '';
|
m('select.sorting', {key: 'sorting', value: s.sorting, onchange: sorting_change_handler.bind(null, s, dispatch)}, [
|
||||||
let count = 9;
|
m('option', {key: 'new->old', value: 'new->old'}, 'Newest -> Oldest'),
|
||||||
const new_note_handler = function(){
|
m('option', {key: 'old->new', value: 'old->new'}, 'Oldest -> Newest')
|
||||||
count++;
|
])
|
||||||
// TODO: make immutable-style:
|
]),
|
||||||
notes.push({id: ''+count, text: '', modified: Date.now()});
|
m('.top-right', {key: 'top-right'}, [
|
||||||
};
|
m('.bin-id', {key: 'bin-id'}, s.bin.id),
|
||||||
const search_term_change_handler = function(e){
|
m('button', {key: 'new-bin-button'}, 'New Bin...')
|
||||||
if(e.code === 'Enter'){
|
])
|
||||||
}
|
]),
|
||||||
else{
|
m('.main', {key: 'main'}, [
|
||||||
search_term = e.target.value;
|
m('.notes', s.notes.map(note_state =>
|
||||||
}
|
m(Note, {note_state, dispatch, key: note_state.note.id})
|
||||||
};
|
))
|
||||||
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})
|
|
||||||
))
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
@ -1,46 +1,30 @@
|
|||||||
|
import {edit_handler, cancel_handler, text_change_handler, save_handler} from './handlers/Note.js';
|
||||||
|
|
||||||
function Note(vnode_init){
|
function Note(vnode_init){
|
||||||
const {note} = vnode_init.attrs;
|
const {dispatch} = vnode_init.attrs;
|
||||||
let is_editing = false;
|
return {
|
||||||
let temp_text = note.text;
|
view: function(vnode){
|
||||||
const edit_handler = function(){
|
const {note_state} = vnode.attrs;
|
||||||
is_editing = true;
|
const {is_editing, note} = note_state;
|
||||||
};
|
if(is_editing){
|
||||||
const cancel_handler = function(){
|
return m('.note', [
|
||||||
is_editing = false;
|
m('textarea', {key: 'textarea', onchange: text_change_handler.bind(null, note_state, dispatch) }, note.text),
|
||||||
};
|
m('.buttons', {key: 'editing-buttons'}, [
|
||||||
const text_change_handler = function(e){
|
m('button', {key: 'cancel-button', onclick: cancel_handler.bind(null, note_state, dispatch) }, 'Cancel'),
|
||||||
temp_text = e.target.value;
|
m('button', {key: 'save-button', onclick: save_handler.bind(null, note_state, dispatch) }, 'Save')
|
||||||
// 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;
|
}
|
||||||
};
|
else{
|
||||||
const save_handler = function(){
|
return m('.note', [
|
||||||
note.text = temp_text;
|
m('.text', {key: 'text'}, note.text),
|
||||||
is_editing = false;
|
m('.buttons', {key: 'viewing-buttons'}, [
|
||||||
};
|
m('button', {key: 'edit-button', onclick: edit_handler.bind(null, note_state, dispatch) }, 'Edit')
|
||||||
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')
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Note;
|
export default Note;
|
@ -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;
|
@ -0,0 +1,11 @@
|
|||||||
|
const api = {};
|
||||||
|
|
||||||
|
api.post = function(url, body){
|
||||||
|
return m.request({
|
||||||
|
method: 'POST',
|
||||||
|
url,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
@ -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};
|
@ -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 };
|
@ -1,4 +1,83 @@
|
|||||||
import App from './App.js';
|
import App from './App.js';
|
||||||
|
import nanoid from './nanoid.min.js';
|
||||||
|
|
||||||
var root = document.body;
|
var root = document.body;
|
||||||
m.mount(root, App);
|
|
||||||
|
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}))
|
@ -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};
|
Loading…
Reference in New Issue