diff --git a/src/component.ts b/src/component.ts index edea2d1..69b98f6 100644 --- a/src/component.ts +++ b/src/component.ts @@ -71,14 +71,17 @@ export abstract class Component implements Monad { output(handler: any): Component { if (typeof handler === "function") { return new HandleOutput( - (a, o) => ({ available: a, output: mergeObj(o, handler(a)) }), + (a, o) => ({ + available: a, + output: mergeObj(mergeObj({}, handler(a)), o) + }), this ); } else { return new HandleOutput( (a, o) => ({ available: a, - output: mergeObj(o, copyRemaps(handler, a)) + output: mergeObj(mergeObj({}, o), copyRemaps(handler, a)) }), this ); @@ -323,15 +326,17 @@ class MergeComponent< O extends object, B, P extends object -> extends Component { +> extends Component<{}, O & P> { constructor(private c1: Component, private c2: Component) { super(); } - run(parent: DomApi, destroyed: Future): Out { - const { output: o1 } = this.c1.run(parent, destroyed); - const { output: o2 } = this.c2.run(parent, destroyed); - const output = Object.assign({}, o1, o2); - return { available: output, output }; + run(parent: DomApi, destroyed: Future): Out<{}, O & P> { + const res1 = this.c1.run(parent, destroyed); + const res2 = this.c2.run(parent, destroyed); + return { + available: {}, + output: mergeObj(mergeObj({}, res2.output), res1.output) + }; } } @@ -341,7 +346,7 @@ class MergeComponent< export function merge( c1: Component, c2: Component -): Component { +): Component<{}, O & P> { return new MergeComponent(c1, c2); } @@ -561,11 +566,11 @@ class ListComponent extends Component { } } run(parent: DomApi, destroyed: Future): Out { - const output: Record = {}; + let output: Record = {}; for (let i = 0; i < this.components.length; ++i) { const component = this.components[i]; const res = component.run(parent, destroyed); - Object.assign(output, res.output); + mergeObj(output, res.output); } return { available: output, output }; } diff --git a/src/utils.ts b/src/utils.ts index d1bc419..6e52945 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,15 +20,26 @@ function isObject(item: any): item is Object { ); } -export function mergeObj(a: A, b: B): A & B { - const c: { [key: string]: any } = {}; - for (const key of Object.keys(a) as (keyof A & string)[]) { - c[key] = a[key]; - } - for (const key of Object.keys(b) as (keyof B & string)[]) { - c[key] = b[key]; +export function mergeObj< + A extends Record, + B extends Record +>(a: A, b: B): A & B { + for (const key of Object.keys(b) as string[]) { + const valueA: any = a[key]; + const valueB: any = b[key]; + if (valueA !== undefined) { + if (isStream(valueA) && isStream(valueB)) { + (a as any)[key] = valueA.combine(valueB); + } else { + throw new Error( + `Components was merged with colliding output on key ${key}` + ); + } + } else { + (a as any)[key] = valueB; + } } - return c; + return a; } export type Merge = { [K in keyof T]: T[K] }; diff --git a/test/component.spec.ts b/test/component.spec.ts index 4123e90..00eec32 100644 --- a/test/component.spec.ts +++ b/test/component.spec.ts @@ -121,11 +121,34 @@ describe("component specs", () => { const b2 = button().output({ click2: "click" }); const m = merge(b1, b2); const { output, available } = testComponent(m); - expect(available).to.have.property("click1"); - expect(available).to.have.property("click2"); + assert.deepEqual(available, {}); expect(output).to.have.property("click1"); expect(output).to.have.property("click2"); }); + it("merges colliding streams", () => { + const sink1 = H.sinkStream(); + const sink2 = H.sinkStream(); + const m = merge( + Component.of({ click: sink1 }), + Component.of({ click: sink2 }) + ); + const { output } = testComponent(m); + expect(output).to.have.property("click"); + const result: number[] = []; + output.click.subscribe((n) => result.push(n)); + sink1.push(0); + sink2.push(1); + assert.deepEqual(result, [0, 1]); + }); + it("throws on all other collisions", () => { + assert.throws(() => { + const m = merge( + Component.of({ click: H.Behavior.of(0) }), + Component.of({ click: H.empty }) + ); + testComponent(m); + }, "colliding"); + }); }); describe("empty component", () => { it("creates no dom", () => {