Skip to content

Commit

Permalink
feat: enable two way prop bindings (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
prevwong authored Nov 22, 2023
1 parent e76ffc3 commit b54562e
Show file tree
Hide file tree
Showing 20 changed files with 372 additions and 67 deletions.
7 changes: 7 additions & 0 deletions .changeset/seven-papayas-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rekajs/parser': patch
'@rekajs/types': patch
'@rekajs/core': patch
---

Enable component prop bindings
61 changes: 43 additions & 18 deletions packages/core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class ComponentViewEvaluator {

private declare rekaComponentRootComputation: DisposableComputation<t.View> | null;
private declare rekaComponentPropsComputation: IComputedValue<void> | null;
private declare rekaComponentPropsBindingComputation: IComputedValue<void> | null;
private declare rekaComponentStateComputation: IComputedValue<void> | null;

private readonly evaluator: Evaluator;
Expand Down Expand Up @@ -116,27 +117,29 @@ export class ComponentViewEvaluator {
});

component.props.forEach((prop) => {
const getPropValue = () => {
let propValue: any;

if (this.template.props[prop.name]) {
propValue = this.evaluator.computeExpr(
this.template.props[prop.name],
this.ctx.env
);
}
let propValue: any;

const tplPropValue = this.template.props[prop.name];

if (tplPropValue) {
let expr = tplPropValue;

if (!propValue && prop.init) {
propValue = this.evaluator.computeExpr(
prop.init,
this.ctx.env
);
if (t.is(tplPropValue, t.PropBinding)) {
expr = tplPropValue.identifier;
}

return propValue;
};
propValue = this.evaluator.computeExpr(
expr,
this.ctx.env
);
}

let propValue = getPropValue();
if (!propValue && prop.init) {
propValue = this.evaluator.computeExpr(
prop.init,
this.ctx.env
);
}

const classListBinding =
this.ctx.env.getByName(ClassListBindingKey);
Expand All @@ -153,7 +156,7 @@ export class ComponentViewEvaluator {

this.env.set(prop.name, {
value: propValue,
readonly: true,
readonly: false,
});
});
},
Expand All @@ -163,6 +166,27 @@ export class ComponentViewEvaluator {
);
}

if (!this.rekaComponentPropsBindingComputation) {
this.rekaComponentPropsBindingComputation = computed(() => {
for (const [prop, value] of Object.entries(
this.template.props
)) {
if (!t.is(value, t.PropBinding)) {
continue;
}

const currPropValue = this.env.getByName(prop, false, true);

this.evaluator.reka.change(() => {
this.ctx.env.reassign(
t.assert(value, t.PropBinding).identifier,
currPropValue
);
});
}
});
}

if (!this.rekaComponentStateComputation) {
this.rekaComponentStateComputation = computed(() => {
component.state.forEach((val) => {
Expand All @@ -173,6 +197,7 @@ export class ComponentViewEvaluator {

try {
this.rekaComponentPropsComputation.get();
this.rekaComponentPropsBindingComputation.get();
this.rekaComponentStateComputation.get();

render = this.evaluator.computeTemplate(component.template, {
Expand Down
20 changes: 12 additions & 8 deletions packages/core/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,7 @@ export class Environment {
return;
}

return env.bindings.set(identifier.name, {
...binding,
value,
});
binding.value = value;
}

set(name: BindingKey, binding: Binding) {
Expand All @@ -87,17 +84,24 @@ export class Environment {
this.bindings.delete(name);
}

getByName(name: BindingKey, external?: boolean) {
getByName(
name: BindingKey,
external?: boolean,
getInCurrentEnvironmentOnly?: boolean
) {
if (external) {
invariant(typeof name === 'string', 'Invalid external binding key');

return this.reka.externals.get(name);
}

const v = this.bindings.get(name)?.value;
const valueInCurrentEnvironment = this.bindings.get(name)?.value;

if (v !== undefined) {
return v;
if (
valueInCurrentEnvironment !== undefined ||
getInCurrentEnvironmentOnly
) {
return valueInCurrentEnvironment;
}

if (!this.parent) {
Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,14 +436,35 @@ export class Evaluator {
// TODO: currently props are re-evaluated any time a change occurs within the template tree
// We should maybe cache the props evaluation as well
const props = Object.keys(template.props).reduce((accum, key) => {
const value = this.computeExpr(template.props[key], ctx.env);
const prop = template.props[key];

let value: any;

if (t.is(prop, t.PropBinding)) {
value = this.computeExpr(prop.identifier, ctx.env);
} else {
value = this.computeExpr(prop, ctx.env);
}

return {
...accum,
[key]: value,
};
}, {});

const propBindings = Object.keys(template.props).reduce((accum, key) => {
const prop = template.props[key];

if (!t.is(prop, t.PropBinding)) {
return accum;
}

return {
...accum,
[key]: this.computeExpr(prop, ctx.env),
};
}, {});

const classListBinding = ctx.env.getByName(ClassListBindingKey);

if (classListBinding && Object.keys(classListBinding).length > 0) {
Expand All @@ -456,6 +477,7 @@ export class Evaluator {
tag: template.tag,
children,
props,
bindings: propBindings,
key: createKey(ctx.path),
template,
frame: this.frame.id,
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ export const computeExpression = (
});
}

if (expr instanceof t.PropBinding) {
return (value: any) => {
reka.change(() => {
updateLocalValue(expr.identifier, reka, env, ctx, () => {
return value;
});
});
};
}

if (expr instanceof t.Block) {
expr.statements.forEach((statement) => {
computeExpression(statement, reka, env);
Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/reka.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
makeObservable,
observable,
reaction,
runInAction,
} from 'mobx';
import { computedFn } from 'mobx-utils';

Expand Down Expand Up @@ -175,14 +176,16 @@ export class Reka {
* Perform a mutation to the State
*/
change(mutator: () => void) {
this.observer.change(mutator);
return runInAction(() => {
this.observer.change(mutator);

// Don't sync yet when we're still setting up (ie: creating the Extensions registry)
if (this.init) {
return;
}
// Don't sync yet when we're still setting up (ie: creating the Extensions registry)
if (this.init) {
return;
}

return this.sync();
return this.sync();
});
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ export class Resolver {
});
}

if (expr instanceof t.PropBinding) {
this.resolveExpr(expr.identifier, scope);
}

if (expr instanceof t.Func) {
const funcScope = scope.inherit(expr);

Expand Down
28 changes: 19 additions & 9 deletions packages/parser/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,19 +527,29 @@ class _Parser extends Lexer {
) {
if (this.check(TokenType.ELEMENT_PROPERTY)) {
const propName = this.consume(TokenType.ELEMENT_PROPERTY).value;
this.consume(TokenType.EQ);

let propValue;
if (this.check(TokenType.STRING)) {
const token = this.consume(TokenType.STRING);
propValue = t.literal({
value: token.value,
// Binding prop
if (this.match(TokenType.COLON)) {
this.consume(TokenType.EQ);

props[propName] = t.propBinding({
identifier: t.assert(this.parseElementExpr(), t.Identifier),
});
} else {
propValue = this.parseElementExpr();
}
this.consume(TokenType.EQ);

let propValue;
if (this.check(TokenType.STRING)) {
const token = this.consume(TokenType.STRING);
propValue = t.literal({
value: token.value,
});
} else {
propValue = this.parseElementExpr();
}

props[propName] = propValue;
props[propName] = propValue;
}
} else {
const directive = this.consume(TokenType.ELEMENT_DIRECTIVE).value;
this.consume(TokenType.EQ);
Expand Down
12 changes: 10 additions & 2 deletions packages/parser/src/stringifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ class _Stringifier {
});
this.writer.write(')');
},
PropBinding: (node) => {
this.stringify(node.identifier);
},
Template: (node) => {
const tag =
node instanceof t.ComponentTemplate
Expand Down Expand Up @@ -308,8 +311,13 @@ class _Stringifier {
props.push(
this.writer.withTemp(() => {
propKeys.forEach((prop, i, arr) => {
this.writer.write(`${prop}={`);
this.stringify(node.props[prop]);
const valueExpr = node.props[prop];
this.writer.write(`${prop}`);
if (t.is(valueExpr, t.PropBinding)) {
this.writer.write(':');
}
this.writer.write(`={`);
this.stringify(valueExpr);
this.writer.write('}');
if (i !== arr.length - 1) {
this.writer.write('\n');
Expand Down
12 changes: 12 additions & 0 deletions packages/parser/src/tests/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,4 +212,16 @@ describe('Parser', () => {
},
});
});
it('should be able to parse directives', () => {
const parsed = Parser.parseProgram(`component Test(value) => (
<input type="text" value:={value} />
)`);

expect(parsed.components[0].template.props['value']).toMatchObject({
identifier: {
type: 'Identifier',
name: 'value',
},
});
});
});
16 changes: 16 additions & 0 deletions packages/parser/src/tests/stringifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,20 @@ describe('Stringifier', () => {
)
).toEqual(`obj = {\n "foo": 1,\n "bar": 0\n}`);
});
it('should be able to stringifiy prop binding', () => {
expect(
Stringifier.toString(
t.tagTemplate({
tag: 'input',
props: {
value: t.propBinding({
identifier: t.identifier({
name: 'value',
}),
}),
},
})
)
).toEqual(`<input value:={value} />`);
});
});
3 changes: 3 additions & 0 deletions packages/types/src/generated/builder.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export const rekaComponent = (
export const externalComponent = (
...args: ConstructorParameters<typeof t.ExternalComponent>
) => new t.ExternalComponent(...args);
export const propBinding = (
...args: ConstructorParameters<typeof t.PropBinding>
) => new t.PropBinding(...args);
export const tagTemplate = (
...args: ConstructorParameters<typeof t.TagTemplate>
) => new t.TagTemplate(...args);
Expand Down
Loading

1 comment on commit b54562e

@vercel
Copy link

@vercel vercel bot commented on b54562e 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 – ./

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

Please sign in to comment.