-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor TodoMVC to use component function
- Loading branch information
Showing
5 changed files
with
279 additions
and
410 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,186 +1,126 @@ | ||
import { combine } from "@funkia/jabz"; | ||
import { | ||
Behavior, | ||
changes, | ||
filter, | ||
keepWhen, | ||
performStream, | ||
sample, | ||
snapshot, | ||
stepper, | ||
Stream, | ||
lift, | ||
toggle | ||
} from "@funkia/hareactive"; | ||
|
||
import { modelView, elements, fgo, Component } from "../../../src"; | ||
import * as H from "@funkia/hareactive"; | ||
import { elements, fgo, component } from "../../../src"; | ||
const { div, li, input, label, button, checkbox } = elements; | ||
|
||
import { setItemIO, itemBehavior, removeItemIO } from "./localstorage"; | ||
|
||
const enter = 13; | ||
const esc = 27; | ||
const isKey = (keyCode: number) => (ev: { keyCode: number }) => | ||
ev.keyCode === keyCode; | ||
export const itemIdToPersistKey = (id: number) => `todoItem:${id}`; | ||
export const itemOutputToId = ({ id }: Output) => id; | ||
|
||
export type Item = { | ||
taskName: Behavior<string>; | ||
isComplete: Behavior<boolean>; | ||
}; | ||
|
||
export type PersistedItem = { | ||
taskName: string; | ||
isComplete: boolean; | ||
}; | ||
|
||
export type Input = { | ||
export type Props = { | ||
name: string; | ||
id: number; | ||
toggleAll: Stream<boolean>; | ||
currentFilter: Behavior<string>; | ||
toggleAll: H.Stream<boolean>; | ||
currentFilter: H.Behavior<string>; | ||
}; | ||
|
||
type FromView = { | ||
toggleTodo: Stream<boolean>; | ||
taskName: Behavior<string>; | ||
startEditing: Stream<any>; | ||
nameBlur: Stream<any>; | ||
deleteClicked: Stream<any>; | ||
nameKeyup: Stream<any>; | ||
newNameInput: Stream<any>; | ||
toggleTodo: H.Stream<boolean>; | ||
taskName: H.Behavior<string>; | ||
startEditing: H.Stream<any>; | ||
nameBlur: H.Stream<any>; | ||
deleteClicked: H.Stream<any>; | ||
cancel: H.Stream<any>; | ||
enter: H.Stream<any>; | ||
newNameInput: H.Stream<any>; | ||
}; | ||
|
||
export type Output = { | ||
taskName: Behavior<string>; | ||
isComplete: Behavior<boolean>; | ||
newName: Behavior<string>; | ||
isEditing: Behavior<boolean>; | ||
focusInput: Stream<any>; | ||
hidden: Behavior<boolean>; | ||
destroyItemId: Stream<number>; | ||
completed: Behavior<boolean>; | ||
destroyItemId: H.Stream<number>; | ||
completed: H.Behavior<boolean>; | ||
id: number; | ||
}; | ||
|
||
const itemModel = fgo(function*( | ||
{ | ||
toggleTodo, | ||
startEditing, | ||
nameBlur, | ||
deleteClicked, | ||
nameKeyup, | ||
newNameInput, | ||
taskName | ||
}: FromView, | ||
{ toggleAll, name: initialName, id, currentFilter }: Input | ||
): any { | ||
const enterPress = filter(isKey(enter), nameKeyup); | ||
const enterNotPressed = yield toggle(true, startEditing, enterPress); | ||
const cancel = filter(isKey(esc), nameKeyup); | ||
const notCancelled = yield toggle(true, startEditing, cancel); | ||
const stopEditing = combine( | ||
enterPress, | ||
keepWhen(nameBlur, enterNotPressed), | ||
cancel | ||
); | ||
const isEditing = yield toggle(false, startEditing, stopEditing); | ||
const newName = yield stepper( | ||
initialName, | ||
combine( | ||
newNameInput.map((ev) => ev.target.value), | ||
snapshot(taskName, cancel) | ||
) | ||
export default (props: Props) => | ||
component<FromView, Output>( | ||
fgo(function*(on) { | ||
const enterNotPressed = yield H.toggle(true, on.startEditing, on.enter); | ||
const notCancelled = yield H.toggle(true, on.startEditing, on.cancel); | ||
const stopEditing = combine( | ||
on.enter, | ||
H.keepWhen(on.nameBlur, enterNotPressed), | ||
on.cancel | ||
); | ||
const editing = yield H.toggle(false, on.startEditing, stopEditing); | ||
const newName = yield H.stepper( | ||
props.name, | ||
combine( | ||
on.newNameInput.map((ev) => ev.target.value), | ||
H.snapshot(on.taskName, on.cancel) | ||
) | ||
); | ||
const nameChange = H.snapshot( | ||
newName, | ||
H.keepWhen(stopEditing, notCancelled) | ||
); | ||
|
||
// Restore potentially persisted todo item | ||
const persistKey = "todoItem:" + props.id; | ||
const savedItem = yield H.sample(itemBehavior(persistKey)); | ||
const initial = | ||
savedItem === null | ||
? { taskName: props.name, completed: false } | ||
: savedItem; | ||
|
||
// Initialize task to restored values | ||
const taskName: H.Behavior<string> = yield H.stepper( | ||
initial.taskName, | ||
nameChange | ||
); | ||
const completed: H.Behavior<boolean> = yield H.stepper( | ||
initial.completed, | ||
combine(on.toggleTodo, props.toggleAll) | ||
); | ||
|
||
// Persist todo item | ||
const item = H.lift( | ||
(taskName, completed) => ({ taskName, completed }), | ||
taskName, | ||
completed | ||
); | ||
yield H.performStream( | ||
H.changes(item).map((i) => setItemIO(persistKey, i)) | ||
); | ||
|
||
const destroyItem = combine( | ||
on.deleteClicked, | ||
nameChange.filter((s) => s === "") | ||
); | ||
const destroyItemId = destroyItem.mapTo(props.id); | ||
|
||
// Remove persist todo item | ||
yield H.performStream(destroyItem.mapTo(removeItemIO(persistKey))); | ||
|
||
const hidden = H.lift( | ||
(complete, filter) => | ||
(filter === "completed" && !complete) || | ||
(filter === "active" && complete), | ||
completed, | ||
props.currentFilter | ||
); | ||
|
||
return li({ class: ["todo", { completed, editing, hidden }] }, [ | ||
div({ class: "view" }, [ | ||
checkbox({ | ||
class: "toggle", | ||
props: { checked: completed } | ||
}).output({ toggleTodo: "checkedChange" }), | ||
label(taskName).output({ startEditing: "dblclick" }), | ||
button({ class: "destroy" }).output({ deleteClicked: "click" }) | ||
]), | ||
input({ | ||
class: "edit", | ||
value: taskName, | ||
actions: { focus: on.startEditing } | ||
}).output((o) => ({ | ||
newNameInput: o.input, | ||
nameBlur: o.blur, | ||
enter: o.keyup.filter((ev) => ev.keyCode === enter), | ||
cancel: o.keyup.filter((ev) => ev.keyCode === esc) | ||
})) | ||
]) | ||
.output(() => ({ taskName })) | ||
.result({ destroyItemId, completed, id: props.id }); | ||
}) | ||
); | ||
const nameChange = snapshot(newName, keepWhen(stopEditing, notCancelled)); | ||
|
||
// Restore potentially persisted todo item | ||
const persistKey = itemIdToPersistKey(id); | ||
const savedItem = yield sample(itemBehavior(persistKey)); | ||
const initial = | ||
savedItem === null | ||
? { taskName: initialName, isComplete: false } | ||
: savedItem; | ||
|
||
// Initialize task to restored values | ||
const taskName_: Behavior<string> = yield stepper( | ||
initial.taskName, | ||
nameChange | ||
); | ||
const isComplete: Behavior<boolean> = yield stepper( | ||
initial.isComplete, | ||
combine(toggleTodo, toggleAll) | ||
); | ||
|
||
// Persist todo item | ||
const item = lift( | ||
(taskName, isComplete) => ({ taskName, isComplete }), | ||
taskName_, | ||
isComplete | ||
); | ||
yield performStream( | ||
changes(item).map((i: PersistedItem) => setItemIO(persistKey, i)) | ||
); | ||
|
||
const destroyItem = combine( | ||
deleteClicked, | ||
nameChange.filter((s) => s === "") | ||
); | ||
const destroyItemId = destroyItem.mapTo(id); | ||
|
||
// Remove persist todo item | ||
yield performStream(destroyItem.mapTo(removeItemIO(persistKey))); | ||
|
||
const hidden = lift( | ||
(complete, filter) => | ||
(filter === "completed" && !complete) || | ||
(filter === "active" && complete), | ||
isComplete, | ||
currentFilter | ||
); | ||
|
||
return { | ||
taskName: taskName_, | ||
isComplete, | ||
isEditing, | ||
newName, | ||
focusInput: startEditing, | ||
id, | ||
destroyItemId, | ||
completed: isComplete, | ||
hidden | ||
}; | ||
}); | ||
|
||
function itemView( | ||
{ taskName, isComplete, isEditing, focusInput, hidden }: Output, | ||
_: Input | ||
): Component<any, FromView> { | ||
return li( | ||
{ | ||
class: ["todo", { completed: isComplete, editing: isEditing, hidden }] | ||
}, | ||
[ | ||
div({ class: "view" }, [ | ||
checkbox({ | ||
class: "toggle", | ||
props: { checked: isComplete } | ||
}).output({ toggleTodo: "checkedChange" }), | ||
label(taskName).output({ startEditing: "dblclick" }), | ||
button({ class: "destroy" }).output({ deleteClicked: "click" }) | ||
]), | ||
input({ | ||
class: "edit", | ||
props: { value: taskName }, | ||
actions: { focus: focusInput } | ||
}).output({ | ||
newNameInput: "input", | ||
nameKeyup: "keyup", | ||
nameBlur: "blur" | ||
}) | ||
] | ||
).output((o) => ({ taskName, ...o })); | ||
} | ||
|
||
export default modelView(itemModel, itemView); |
Oops, something went wrong.