Skip to content

Commit

Permalink
feat: improve external components (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
prevwong authored Nov 22, 2023
1 parent 47a76a7 commit e2f00d4
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 50 deletions.
7 changes: 7 additions & 0 deletions .changeset/twenty-deers-swim.md
Original file line number Diff line number Diff line change
@@ -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
128 changes: 78 additions & 50 deletions packages/core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -61,23 +104,45 @@ 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 <input type="text" value={props.value} onChange={e => 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,
component,
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),
}),
];
}
Expand Down Expand Up @@ -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,
});
});
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/generated/types.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,10 +453,12 @@ type ExternalComponentParameters = {
meta?: Record<string, any>;
name: string;
render: Function;
props?: ComponentProp[];
};

export class ExternalComponent extends Component {
declare render: Function;
declare props: ComponentProp[];
constructor(value: ExternalComponentParameters) {
super('ExternalComponent', value);
}
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/types.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ Schema.define('ExternalComponent', {
extends: 'Component',
fields: (t) => ({
render: t.type('Function'),
props: t.defaultValue(t.array(t.node('ComponentProp')), []),
}),
});

Expand Down

1 comment on commit e2f00d4

@vercel
Copy link

@vercel vercel bot commented on e2f00d4 Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

reka – ./

reka-git-main-prevwong.vercel.app
rekajs.vercel.app
reka-prevwong.vercel.app
reka.js.org

Please sign in to comment.