Created
August 12, 2019 16:38
-
-
Save zaydek-old/3ba64d6679e82a43a7f93e29785a8998 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import React, { useEffect, useRef } from "react" | |
import useMethods from "use-methods" | |
const setLocalStorage = (key: string, value: Object) => { | |
const json = JSON.stringify(value) | |
localStorage.setItem(key, json) | |
} | |
const getLocalStorage = (key: string) => { | |
const json = localStorage.getItem(key) | |
if (json === null) { | |
return null | |
} | |
return JSON.parse(json) | |
} | |
const shortID = () => { | |
// See gist.github.com/gordonbrander/2230317 for reference. | |
return Math.random().toString(36).substr(2, 4) | |
} | |
type State = { | |
focus: boolean, | |
input: string, | |
todos: { | |
id: string, | |
value: string, | |
complete: boolean | |
}[], | |
saved: boolean, | |
// The undo/redo state stack and index. | |
stack: [], | |
index: 0 | |
} | |
const initialState: State = { | |
focus: false, | |
input: "", | |
todos: [], | |
saved: true, | |
// The udno/redo state stack and index. | |
stack: [], | |
index: 0 | |
} | |
// selected | |
// undo | |
// redo | |
// if (todo.complete) { | |
// state.count.push(todo.id) | |
// } else { | |
// const countIndex = state.count.findIndex(cmpTodo => todo.id == cmpTodo.id) | |
// if (countIndex !== -1) { | |
// state.count.splice(countIndex, 1) | |
// } | |
// } | |
const methods = (state: State) => ({ | |
clearAll() { | |
return initialState | |
}, | |
focus() { | |
state.focus = true | |
}, | |
blur() { | |
state.focus = false | |
}, | |
input(value: string) { | |
state.input = value | |
}, | |
addTodo() { | |
if (state.focus && state.input) { | |
state.todos.unshift({id: shortID(), value: state.input, complete: false}) | |
state.input = "" | |
} | |
}, | |
toggleTodo(id: string) { | |
const todo = state.todos.find(todo => id === todo.id) | |
if (todo) { | |
todo.complete = !todo.complete | |
} | |
}, | |
updateTodo(id: string, value: string) { | |
const todo = state.todos.find(todo => id === todo.id) | |
if (todo) { | |
todo.value = value | |
} | |
}, | |
deleteTodo(id: string) { | |
const index = state.todos.findIndex(todo => id === todo.id) | |
if (index !== -1) { | |
state.todos.splice(index, 1) | |
} | |
}, | |
deleteAllTodos() { | |
// Use `splice` backwards to avoid destructive resizing. | |
// See djave.co.uk/blog/read/splice-doesnt-work-very-well-in-a-javascript-for-loop for reference. | |
for (let index = state.todos.length; index >= 0; index--) { | |
const todo = state.todos[index] | |
if (todo && todo.complete) { | |
state.todos.splice(index, 1) | |
} | |
} | |
}, | |
saving() { | |
state.saved = false | |
}, | |
saved() { | |
state.saved = true | |
}, | |
undo() { | |
}, | |
redo() { | |
} | |
}) | |
// See github.com/streamich/react-use/blob/master/src/useUpdateEffect.ts for reference. | |
const useUpdateEffect: typeof useEffect = (effect, deps) => { | |
const didEffect = useRef(false) | |
useEffect( | |
!didEffect.current | |
? () => { | |
didEffect.current = true | |
} | |
: effect, | |
deps | |
) | |
} | |
// See github.com/streamich/react-use/blob/master/src/useDebounce.ts for reference. | |
const useDebounce = (fn: () => any, ms: number = 0, deps: any[] = []) => { | |
useUpdateEffect(() => { | |
const debounce = setTimeout(fn.bind(null, deps), ms) | |
return () => { | |
clearTimeout(debounce) | |
} | |
}, deps) | |
} | |
const App = (props: { children?: any }) => { | |
const [state, dispatch] = useMethods(methods, getLocalStorage("todo-app") || initialState) | |
const stateRef = useRef(state) | |
stateRef.current = state | |
useUpdateEffect(() => { | |
dispatch.saving() | |
}, [state.input, state.todos]) | |
useDebounce(() => { | |
dispatch.saved() | |
setLocalStorage("todo-app", stateRef.current) | |
}, 1e3, [state.input, state.todos]) | |
const handleClear = (e: React.FormEvent<EventTarget>) => { | |
e.preventDefault() | |
const sure = window.confirm("Are you sure?") | |
if (sure) { | |
dispatch.clearAll() | |
} | |
} | |
const handleSubmit = (e: React.FormEvent<EventTarget>) => { | |
e.preventDefault() | |
dispatch.addTodo() | |
} | |
const completed = state.todos.reduce((arr, todo, index) => { | |
if (todo.complete) { | |
arr.push(index) | |
} | |
return arr | |
}, [] as number[]) // TS requires `as number[]`. | |
// const a = state.todos.reduce((arr, todo, index) => { | |
// if (todo.complete) { | |
// arr.push(index) | |
// } | |
// return arr | |
// }, ([]: number[])) | |
return ( | |
<div> | |
<form onSubmit={handleSubmit}> | |
{/* Use `<input type="button" />` instead of <button type="submit" />. */} | |
<input type="button" value="Clear all" onClick={handleClear} /> | |
<input type="text" value={state.input} onFocus={dispatch.focus} onChange={e => dispatch.input(e.target.value)} /> | |
<button type="submit">Add</button> | |
<input type="button" value={"Delete all" + (!completed.length ? "" : " " + completed.length)} onClick={dispatch.deleteAllTodos} /> | |
Saved status: {"" + state.saved} | |
{state.todos && ( | |
<ul> | |
{/* {all.length} */} | |
{state.todos.map(({id, value, complete}) => ( | |
<li key={id}> | |
<input type="checkbox" checked={complete} onChange={() => dispatch.toggleTodo(id)} /> | |
<input style={{textDecoration: !complete ? "none" : "line-through"}} type="text" value={value} onFocus={dispatch.blur} onChange={e => dispatch.updateTodo(id, e.target.value)} /> | |
<button onClick={() => dispatch.deleteTodo(id)}>Delete</button> | |
</li> | |
))} | |
</ul> | |
)} | |
</form> | |
</div> | |
) | |
} | |
export default App |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment