diff --git a/.changeset/twenty-deers-swim.md b/.changeset/twenty-deers-swim.md new file mode 100644 index 0000000..0609447 --- /dev/null +++ b/.changeset/twenty-deers-swim.md @@ -0,0 +1,7 @@ +--- +'@rekajs/types': patch +'@rekajs/core': patch +--- + +Require prop definition for External Components +Support 2 way bindings and classlist directives in External Components diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index b26a958..2484929 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -6,6 +6,8 @@ import { Environment } from './environment'; import { TemplateEvaluateContext, Evaluator } from './evaluator'; import { ClassListBindingKey, ComponentSlotBindingKey } from './symbols'; import { createKey } from './utils'; +import { invariant } from '@rekajs/utils'; +import capitalize from 'lodash/capitalize'; type ComponentViewTreeComputationCache = { component: t.Component; @@ -47,6 +49,47 @@ export class ComponentViewEvaluator { this.rekaComponentStateComputation = null; } + private computeProps(component: t.Component) { + invariant( + component instanceof t.RekaComponent || + component instanceof t.ExternalComponent + ); + + return component.props.reduce((accum, prop) => { + let propValue: any; + + const tplPropValue = this.template.props[prop.name]; + + if (tplPropValue) { + let expr = tplPropValue; + + if (t.is(tplPropValue, t.PropBinding)) { + expr = tplPropValue.identifier; + } + + propValue = this.evaluator.computeExpr(expr, this.ctx.env); + } + + if (!propValue && prop.init) { + propValue = this.evaluator.computeExpr(prop.init, this.ctx.env); + } + + const classListBinding = this.ctx.env.getByName(ClassListBindingKey); + + if ( + prop.name === 'className' && + classListBinding && + Object.keys(classListBinding).length > 0 + ) { + propValue = [propValue, ...classListBinding].filter(Boolean).join(' '); + } + + accum.push([prop.name, propValue]); + + return accum; + }, [] as Array<[string, any]>); + } + private computeViewTreeForComponent(component: t.Component) { if (component instanceof t.ExternalComponent) { this.rekaComponentPropsComputation = null; @@ -61,6 +104,37 @@ export class ComponentViewEvaluator { }) ); + const props = this.computeProps(component).reduce( + (accum, [prop, value]) => { + accum.push([prop, value]); + + const tplPropValue = this.template.props[prop]; + + /** + * External components should expose a `on{Prop}Change` prop in order to + * support 2 way bindings in Reka + * + * const Input = (props) => { + * return props.onValueChange(e.targetValue)} /> + * } + * + */ + if (t.is(tplPropValue, t.PropBinding)) { + accum.push([ + `on${capitalize(prop)}Change`, + (updatedValue: any) => { + this.evaluator.reka.change(() => { + this.ctx.env.reassign(tplPropValue.identifier, updatedValue); + }); + }, + ]); + } + + return accum; + }, + [] as [string, any][] + ); + return [ t.externalComponentView({ frame: this.evaluator.frame.id, @@ -68,16 +142,7 @@ export class ComponentViewEvaluator { key: this.key, template: this.template, children: children || [], - props: Object.keys(this.template.props).reduce( - (accum, key) => ({ - ...accum, - [key]: this.evaluator.computeExpr( - this.template.props[key], - this.ctx.env - ), - }), - {} - ), + props: Object.fromEntries(props), }), ]; } @@ -116,46 +181,9 @@ export class ComponentViewEvaluator { readonly: true, }); - component.props.forEach((prop) => { - let propValue: any; - - const tplPropValue = this.template.props[prop.name]; - - if (tplPropValue) { - let expr = tplPropValue; - - if (t.is(tplPropValue, t.PropBinding)) { - expr = tplPropValue.identifier; - } - - propValue = this.evaluator.computeExpr( - expr, - this.ctx.env - ); - } - - if (!propValue && prop.init) { - propValue = this.evaluator.computeExpr( - prop.init, - this.ctx.env - ); - } - - const classListBinding = - this.ctx.env.getByName(ClassListBindingKey); - - if ( - prop.name === 'className' && - classListBinding && - Object.keys(classListBinding).length > 0 - ) { - propValue = [propValue, ...classListBinding] - .filter(Boolean) - .join(' '); - } - - this.env.set(prop.name, { - value: propValue, + this.computeProps(component).forEach(([prop, value]) => { + this.env.set(prop, { + value, readonly: false, }); }); diff --git a/packages/types/src/generated/types.generated.ts b/packages/types/src/generated/types.generated.ts index eb0c834..0c1d413 100644 --- a/packages/types/src/generated/types.generated.ts +++ b/packages/types/src/generated/types.generated.ts @@ -453,10 +453,12 @@ type ExternalComponentParameters = { meta?: Record; name: string; render: Function; + props?: ComponentProp[]; }; export class ExternalComponent extends Component { declare render: Function; + declare props: ComponentProp[]; constructor(value: ExternalComponentParameters) { super('ExternalComponent', value); } diff --git a/packages/types/src/types.definition.ts b/packages/types/src/types.definition.ts index 1dfd353..90aaaa4 100644 --- a/packages/types/src/types.definition.ts +++ b/packages/types/src/types.definition.ts @@ -231,6 +231,7 @@ Schema.define('ExternalComponent', { extends: 'Component', fields: (t) => ({ render: t.type('Function'), + props: t.defaultValue(t.array(t.node('ComponentProp')), []), }), });