diff --git a/.changeset/khaki-kangaroos-repeat.md b/.changeset/khaki-kangaroos-repeat.md new file mode 100644 index 0000000..0df47b6 --- /dev/null +++ b/.changeset/khaki-kangaroos-repeat.md @@ -0,0 +1,6 @@ +--- +'@rekajs/types': patch +'@rekajs/core': patch +--- + +Show error view when a template references a component that has been removed diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 4dde998..820be3b 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -10,13 +10,11 @@ import { createKey } from './utils'; type ComponentViewTreeComputationCache = { component: t.Component; - computed: IComputedValue; + computed: IComputedValue; }; export class ComponentViewEvaluator { - private declare resolveComponentComputation: IComputedValue< - t.RekaComponentView[] | t.ErrorSystemView[] | t.ExternalComponentView[] - >; + private declare resolveComponentComputation: DisposableComputation; private declare componentViewTreeComputation: ComponentViewTreeComputationCache | null; private declare rekaComponentRootComputation: DisposableComputation | null; @@ -32,6 +30,8 @@ export class ComponentViewEvaluator { readonly key: string; + private fragment: t.FragmentView; + constructor( evaluator: Evaluator, ctx: TemplateEvaluateContext, @@ -46,6 +46,13 @@ export class ComponentViewEvaluator { this.env = env; this.rekaComponentStateComputation = null; + + this.fragment = t.fragmentView({ + children: [], + frame: this.evaluator.frame.id, + key: this.key, + template: this.template, + }); } private computeProps(component: t.Component) { @@ -134,33 +141,38 @@ export class ComponentViewEvaluator { [] as [string, any][] ); - return [ + this.fragment.children = [ t.externalComponentView({ frame: this.evaluator.frame.id, component, - key: this.key, + key: createKey([this.key, 'root']), template: this.template, children: children || [], props: Object.fromEntries(props), }), ]; + + return this.fragment; } if (component instanceof t.RekaComponent) { + if (this.rekaComponentRootComputation) { + this.rekaComponentRootComputation.get(); + return this.fragment; + } + const componentViewTree = t.rekaComponentView({ frame: this.evaluator.frame.id, - key: this.key, + key: createKey([this.key, 'root']), component, render: [], template: this.template, owner: this.ctx.owner, }); - untracked(() => { - if (this.rekaComponentRootComputation) { - return this.rekaComponentRootComputation.get(); - } + this.fragment.children = [componentViewTree]; + untracked(() => { this.rekaComponentRootComputation = new DisposableComputation( () => { let render: t.View[] = []; @@ -236,7 +248,7 @@ export class ComponentViewEvaluator { this.rekaComponentStateComputation.get(); render = this.evaluator.computeTemplate(component.template, { - path: [this.key, 'root'], + path: [this.key, 'root', 'render'], env: this.env, owner: componentViewTree, componentStack: [...this.ctx.componentStack, component], @@ -275,25 +287,23 @@ export class ComponentViewEvaluator { return this.rekaComponentRootComputation.get(); }); - return [componentViewTree]; + return this.fragment; } throw new Error('Invalid Component Template'); } - recompute() { - if (this.rekaComponentRootComputation) { - this.rekaComponentRootComputation.get(); - - return; - } - - this.compute(); + private reset() { + this.componentViewTreeComputation = null; + this.rekaComponentRootComputation = null; + this.rekaComponentPropsComputation = null; + this.rekaComponentPropsBindingComputation = null; + this.rekaComponentStateComputation = null; } compute() { if (!this.resolveComponentComputation) { - this.resolveComponentComputation = computed( + this.resolveComponentComputation = new DisposableComputation( () => { const component = this.env.getByName( this.template.component.name, @@ -302,28 +312,35 @@ export class ComponentViewEvaluator { if (!component) { this.componentViewTreeComputation = null; + this.reset(); - return [ + this.fragment.children = [ t.errorSystemView({ frame: this.evaluator.frame.id, error: `Component "${this.template.component.name}" not found`, - key: this.key, + key: createKey([this.key, 'root']), template: this.template, owner: this.ctx.owner, }), ]; + + return this.fragment; } if (this.ctx.componentStack.indexOf(component) > -1) { - return [ + this.reset(); + + this.fragment.children = [ t.errorSystemView({ frame: this.evaluator.frame.id, error: `Cycle detected when attempting to render "${component.name}"`, - key: this.key, + key: createKey([this.key, 'root']), template: this.template, owner: this.ctx.owner, }), ]; + + return this.fragment; } if ( @@ -333,6 +350,8 @@ export class ComponentViewEvaluator { return this.componentViewTreeComputation.computed.get(); } + this.reset(); + this.componentViewTreeComputation = { component, computed: computed( @@ -347,18 +366,31 @@ export class ComponentViewEvaluator { }, { name: `component-${this.template.component.name}<${this.template.id}>-resolve-computation`, + keepAlive: true, } ); } - return this.resolveComponentComputation.get(); + this.resolveComponentComputation.get(); + + if (this.rekaComponentRootComputation) { + this.rekaComponentRootComputation.get(); + } + + return [this.fragment]; + } + + get view() { + return this.fragment.children[0]; } dispose() { - if (!this.rekaComponentRootComputation) { - return; + if (this.resolveComponentComputation) { + this.resolveComponentComputation.dispose(); } - this.rekaComponentRootComputation.dispose(); + if (this.rekaComponentRootComputation) { + this.rekaComponentRootComputation.dispose(); + } } } diff --git a/packages/core/src/evaluator.ts b/packages/core/src/evaluator.ts index 68d6e5a..536f81d 100644 --- a/packages/core/src/evaluator.ts +++ b/packages/core/src/evaluator.ts @@ -37,7 +37,7 @@ export type TemplateEachComputationCache = { }; export class Evaluator { - private _view: IObservableValue; + private _view: IObservableValue; private viewObserver: Observer | undefined; private rootTemplate: t.ComponentTemplate; @@ -177,13 +177,13 @@ export class Evaluator { const index = parent.children.indexOf(existingView); - parent.children[index] = newView; + parent.children.splice(index, 0, newView); return newView; }); } - private setView(view: t.RekaComponentView) { + private setView(view: t.FragmentView) { if (this.viewObserver) { this.viewObserver.dispose(); } @@ -524,7 +524,7 @@ export class Evaluator { this.tplKeyToComponentEvaluator.set(key, componentEvaluator); } - return componentEvaluator.compute(); + return untracked(() => componentEvaluator!.compute()); } computeExpr(expr: t.Any, env: Environment) { @@ -560,19 +560,19 @@ export class Evaluator { const _compute = () => { const views = this.computeRootTemplate(); - return t.assert(views[0], t.RekaComponentView); + return t.assert(views[0], t.FragmentView); }; - if (!this.viewObserver) { + const viewObserver = this.viewObserver; + + if (!viewObserver) { this.setView(_compute()); return; } - this.viewObserver.change(() => { - _compute(); - - this.tplKeyToComponentEvaluator.forEach((componentEvaluator) => { - componentEvaluator.recompute(); + this.tplKeyToComponentEvaluator.forEach((componentEvaluator) => { + viewObserver.change(() => { + componentEvaluator.compute(); }); }); } diff --git a/packages/core/src/tests/evaluator.test.ts b/packages/core/src/tests/evaluator.test.ts index bb0979c..7d81f0a 100644 --- a/packages/core/src/tests/evaluator.test.ts +++ b/packages/core/src/tests/evaluator.test.ts @@ -2,7 +2,7 @@ import { Parser } from '@rekajs/parser'; import * as t from '@rekajs/types'; import { invariant } from '@rekajs/utils'; -import { FrameOpts } from '../frame'; +import { Frame, FrameOpts } from '../frame'; import { RekaOpts } from '../interfaces'; import { Reka } from '../reka'; @@ -43,12 +43,14 @@ describe('evaluator', () => { ) `); - invariant(frame.view instanceof t.RekaComponentView); - expect(frame.view.render.length).toEqual(1); - invariant(frame.view.render[0] instanceof t.TagView); - expect(frame.view.render[0].tag).toEqual('div'); - invariant(frame.view.render[0].children[0] instanceof t.TagView); - expect(frame.view.render[0].children[0]).toMatchObject({ + invariant(frame.view?.children[0] instanceof t.RekaComponentView); + expect(frame.view.children[0].render.length).toEqual(1); + invariant(frame.view.children[0].render[0] instanceof t.TagView); + expect(frame.view.children[0].render[0].tag).toEqual('div'); + invariant( + frame.view.children[0].render[0].children[0] instanceof t.TagView + ); + expect(frame.view.children[0].render[0].children[0]).toMatchObject({ type: 'TagView', tag: 'text', props: { value: 0 }, @@ -64,7 +66,7 @@ describe('evaluator', () => { `); expect( - t.assert(frame.view, t.RekaComponentView, (view) => { + t.assert(frame.view?.children[0], t.RekaComponentView, (view) => { return t.assert(view.render[0], t.TagView, (view) => { return view.children .slice(1) @@ -89,7 +91,7 @@ describe('evaluator', () => { `); expect( - t.assert(frame.view, t.RekaComponentView, (view) => { + t.assert(frame.view?.children[0], t.RekaComponentView, (view) => { return t.assert(view.render[0], t.TagView, (view) => { return view.children.map((child) => t.assert(child, t.TagView, (c) => c.props['value']) @@ -111,11 +113,13 @@ describe('evaluator', () => { ) `); - t.assert(frame.view, t.RekaComponentView, (view) => { - t.assert(view.render[0], t.RekaComponentView, (view) => { - t.assert(view.render[0], t.TagView, (view) => { - expect(view.tag).toEqual('button'); - expect(view.props['value']).toEqual('red'); + t.assert(frame.view?.children[0], t.RekaComponentView, (view) => { + t.assert(view.render[0], t.FragmentView, (view) => { + t.assert(view.children[0], t.RekaComponentView, (view) => { + t.assert(view.render[0], t.TagView, (view) => { + expect(view.tag).toEqual('button'); + expect(view.props['value']).toEqual('red'); + }); }); }); }); @@ -132,9 +136,13 @@ describe('evaluator', () => { ) `); - const view = t.assert(frame.view, t.RekaComponentView, (view) => { - return t.assert(view.render[0], t.TagView); - }); + const view = t.assert( + frame.view?.children[0], + t.RekaComponentView, + (view) => { + return t.assert(view.render[0], t.TagView); + } + ); expect(view.props['value']).toEqual('Hello: 0, red'); @@ -163,9 +171,13 @@ describe('evaluator', () => { ) `); - const view = t.assert(frame.view, t.RekaComponentView, (view) => { - return t.assert(view.render[0], t.TagView); - }); + const view = t.assert( + frame.view?.children[0], + t.RekaComponentView, + (view) => { + return t.assert(view.render[0], t.TagView); + } + ); expect(view.props['value']).toEqual(0); @@ -186,9 +198,13 @@ describe('evaluator', () => { ) `); - const view = t.assert(frame.view, t.RekaComponentView, (view) => { - return t.assert(view.render[0], t.TagView); - }); + const view = t.assert( + frame.view?.children[0], + t.RekaComponentView, + (view) => { + return t.assert(view.render[0], t.TagView); + } + ); expect(view.props['value']).toEqual(3); @@ -219,7 +235,7 @@ describe('evaluator', () => { }); }; - const div = t.assert(frame.view, t.RekaComponentView, (view) => + const div = t.assert(frame.view?.children[0], t.RekaComponentView, (view) => t.assert(view.render[0], t.TagView) ); @@ -237,9 +253,15 @@ describe('evaluator', () => { expect(div.children.length).toEqual(2); + const divChildCompViews = div.children.map((child) => + t.assert(child, t.FragmentView, (view) => + t.assert(view.children[0], t.RekaComponentView) + ) + ); + await frame.reka.change(() => { const btnComponent = t.assert( - div.children[0], + divChildCompViews[0], t.RekaComponentView, (view) => view.component ); @@ -249,7 +271,84 @@ describe('evaluator', () => { }); }); - expect(getBtnText(div.children[0])).toEqual('New'); - expect(getBtnText(div.children[1])).toEqual('New'); + expect(getBtnText(divChildCompViews[0])).toEqual('New'); + expect(getBtnText(divChildCompViews[1])).toEqual('New'); + }); + describe('when a component gets removed', () => { + let frame: Frame; + beforeEach(async () => { + frame = await createFrame(` + component Button() => () + component App() => (