diff --git a/examples/todo/src/TodoFooter.ts b/examples/todo/src/TodoFooter.ts index 1864efa..c5615ce 100644 --- a/examples/todo/src/TodoFooter.ts +++ b/examples/todo/src/TodoFooter.ts @@ -1,5 +1,5 @@ import { Behavior, Stream, moment, combine } from "@funkia/hareactive"; -import { elements, modelView, fgo } from "../../../src"; +import { elements, modelView, view, fgo } from "../../../src"; const { span, button, ul, li, a, footer, strong } = elements; import { navigate, Router, routePath } from "@funkia/rudolph"; @@ -16,51 +16,45 @@ export type Out = { }; type FromView = { - filterBtnAll: Stream; - filterBtnActive: Stream; - filterBtnCompleted: Stream; - clearCompleted: Stream; + selectAll: Stream; + selectActive: Stream; + selectCompleted: Stream; + clearCompleted: Stream; }; const isEmpty = (list: any[]) => list.length === 0; const formatRemainer = (value: number) => ` item${value === 1 ? "" : "s"} left`; const filterItem = (name: string, selectedClass: Behavior) => - li( - a( - { - style: { - cursor: "pointer" + view( + li( + a( + { + style: { + cursor: "pointer" + }, + class: { + selected: selectedClass.map((s) => s === name) + } }, - class: { - selected: selectedClass.map((s) => s === name) - } - }, - name - ).output({ - [`filterBtn${name}`]: "click" - }) + name + ).output({ click: "click" }) + ) ); -const model = function*( - { - filterBtnActive, - filterBtnAll, - filterBtnCompleted, - clearCompleted - }: FromView, +function* todoFooterModel( + { selectAll, selectActive, selectCompleted, clearCompleted }: FromView, { router }: { router: Router } ) { - const navs = combine( - filterBtnAll.mapTo("all"), - filterBtnActive.mapTo("active"), - filterBtnCompleted.mapTo("completed") - ); + const navs = combine(selectAll, selectActive, selectCompleted); yield navigate(router, navs); return { clearCompleted }; -}; +} -const view = ({ }: Out, { router, todosB, areAnyCompleted }: Params) => { +const todoFooterView = ( + { }: Out, + { router, todosB, areAnyCompleted }: Params +) => { const hidden = todosB.map(isEmpty); const itemsLeft = moment( (at) => at(todosB).filter((t) => !at(t.completed)).length @@ -81,9 +75,15 @@ const view = ({ }: Out, { router, todosB, areAnyCompleted }: Params) => { itemsLeft.map(formatRemainer) ]), ul({ class: "filters" }, [ - filterItem("All", selectedClass), - filterItem("Active", selectedClass), - filterItem("Completed", selectedClass) + filterItem("All", selectedClass).output((o) => ({ + selectAll: o.click.mapTo("all") + })), + filterItem("Active", selectedClass).output((o) => ({ + selectActive: o.click.mapTo("active") + })), + filterItem("Completed", selectedClass).output((o) => ({ + selectCompleted: o.click.mapTo("completed") + })) ]), button( { @@ -97,6 +97,9 @@ const view = ({ }: Out, { router, todosB, areAnyCompleted }: Params) => { ]); }; -const todoFooter = modelView(fgo(model), view); +const todoFooter = modelView( + fgo(todoFooterModel), + todoFooterView +); export default todoFooter; diff --git a/src/component.ts b/src/component.ts index e7f479d..30b3d75 100644 --- a/src/component.ts +++ b/src/component.ts @@ -70,14 +70,17 @@ export abstract class Component implements Monad { ): Component, A>; output(handler: any): Component { if (typeof handler === "function") { - return new HandleOutput((e, o) => mergeObj(e, handler(o)), this); + return new HandleOutput((e, o) => [mergeObj(e, handler(o)), o], this); } else { return new HandleOutput( - (e, o) => mergeObj(e, copyRemaps(handler, o)), + (e, o) => [mergeObj(e, copyRemaps(handler, o)), o], this ); } } + view(): Component<{}, O> { + return view(this); + } static multi: boolean = false; multi: boolean = false; abstract run( @@ -122,17 +125,17 @@ export function liftNow(now: Now): Component<{}, A> { return performComponent(() => runNow(now)); } -class HandleOutput extends Component { +class HandleOutput extends Component { constructor( - private readonly handler: (explicit: O, output: A) => P, + private readonly handler: (explicit: O, output: A) => [P, B], private readonly c: Component ) { super(); } - run(parent: DomApi, destroyed: Future): Out { + run(parent: DomApi, destroyed: Future): Out { const { explicit, output } = this.c.run(parent, destroyed); - const newExplicit = this.handler(explicit, output); - return { explicit: newExplicit, output }; + const [newExplicit, newOutput] = this.handler(explicit, output); + return { explicit: newExplicit, output: newOutput }; } } @@ -318,13 +321,13 @@ class ModelViewComponent extends Component< constructor( private args: any[], private model: (...as: any[]) => Now, - private view: (...as: any[]) => Child, + private viewF: (...as: any[]) => Child, private placeholderNames?: string[] ) { super(); } run(parent: DomApi, destroyed: Future): Out<{}, M> { - const { view, model, args } = this; + const { viewF, model, args } = this; let placeholders: any; if (supportsProxy) { placeholders = new Proxy({}, placeholderProxyHandler); @@ -337,11 +340,11 @@ class ModelViewComponent extends Component< } } const { explicit: viewOutput } = toComponent( - view(placeholders, ...args) + viewF(placeholders, ...args) ).run(parent, destroyed); const helpfulViewOutput = addErrorHandler( model.name, - view.name, + viewF.name, Object.assign(viewOutput, { destroyed }) ); const behaviors = runNow(model(helpfulViewOutput, ...args)); @@ -379,6 +382,10 @@ export function modelView( new ModelViewComponent(args, m, view, toViewReactiveNames); } +export function view(c: Component): Component<{}, O> { + return new HandleOutput((explicit, _) => [{}, explicit], c); +} + // Child element export type CE = | Component diff --git a/test/component.spec.ts b/test/component.spec.ts index 56f7825..e725a77 100644 --- a/test/component.spec.ts +++ b/test/component.spec.ts @@ -12,6 +12,7 @@ import { toComponent, Component, modelView, + view, emptyComponent, elements, loop, @@ -344,6 +345,24 @@ describe("modelView", () => { }); }); +describe("view", () => { + const obj = { a: 0, b: 1 }; + it("turns selected output into available output", () => { + const c = view(Component.of(obj).output((o) => o)); + const { out, explicit } = testComponent(c); + expect(explicit).to.deep.equal({}); + expect(out).to.deep.equal(obj); + }); + it("is available as method", () => { + const c = Component.of(obj) + .output((o) => o) + .view(); + const { out, explicit } = testComponent(c); + expect(explicit).to.deep.equal({}); + expect(out).to.deep.equal(obj); + }); +}); + describe("list", () => { const createSpan = (content: string) => span(content); const initial = ["Hello ", "there", "!"];