Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions renderers/web_core/src/v0_9/rendering/data-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ describe('DataContext', () => {
assert.deepStrictEqual(context.resolveDynamicValue(['literal', 'array']), ['literal', 'array']);
});

it('resolves arrays of DynamicValues element-wise', () => {
assert.deepStrictEqual(context.resolveDynamicValue([{path: 'name'}, 'literal']), [
'Alice',
'literal',
]);
});

it('subscribes literal arrays as static', () => {
let called = false;
const sub = context.subscribeDynamicValue(['literal', 'array'], () => {
Expand All @@ -118,6 +125,84 @@ describe('DataContext', () => {
assert.strictEqual(called, false);
});

it('subscribes arrays containing path bindings reactively', () => {
let latest: unknown;
const sub = context.subscribeDynamicValue([{path: 'name'}, 'x'], val => {
latest = val;
});
assert.deepStrictEqual(sub.value, ['Alice', 'x']);

context.set('name', 'Bob');
assert.deepStrictEqual(latest, ['Bob', 'x']);
sub.unsubscribe();
});

it('resolves nested DynamicValues inside and/or function args', () => {
const fnInvoker = (name: string, args: Record<string, any>) => {
if (name === 'and') return args.values.every((v: unknown) => !!v);
if (name === 'or') return args.values.some((v: unknown) => !!v);
if (name === 'required') {
const val = args.value;
if (val === null || val === undefined) return false;
if (typeof val === 'string' && val === '') return false;
return true;
}
return null;
};
const root = createTestDataContext(
new DataModel({
formData: {email: '', phone: '', zip: '', agree: false},
}),
'/',
fnInvoker,
);

const result = root.resolveDynamicValue({
call: 'and',
args: {
values: [
{path: '/formData/agree'},
{
call: 'or',
args: {
values: [
{call: 'required', args: {value: {path: '/formData/email'}}},
{call: 'required', args: {value: {path: '/formData/phone'}}},
],
},
},
{call: 'required', args: {value: {path: '/formData/zip'}}},
],
},
returnType: 'boolean',
});
assert.strictEqual(result, false);

root.set('/formData/agree', true);
root.set('/formData/email', 'a@b.com');
root.set('/formData/zip', '12345');
const valid = root.resolveDynamicValue({
call: 'and',
args: {
values: [
{path: '/formData/agree'},
{
call: 'or',
args: {
values: [
{call: 'required', args: {value: {path: '/formData/email'}}},
{call: 'required', args: {value: {path: '/formData/phone'}}},
],
},
},
{call: 'required', args: {value: {path: '/formData/zip'}}},
],
},
returnType: 'boolean',
});
assert.strictEqual(valid, true);
});

it('resolves function calls synchronously', () => {
const fnInvoker = (name: string, args: Record<string, any>) => {
if (name === 'add') return args.a + args.b;
Expand Down
25 changes: 21 additions & 4 deletions renderers/web_core/src/v0_9/rendering/data-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,16 @@ export class DataContext {
* @returns The synchronously resolved value.
*/
resolveDynamicValue<V>(value: DynamicValue): V {
// 1. Literal check (excluding arrays and objects)
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
// 1. Primitive literals (null, string, number, boolean)
if (value === null || typeof value !== 'object') {
return value as V;
}

// 1b. Arrays: each element may itself be a DynamicValue (e.g. `and`/`or` `values`)
if (Array.isArray(value)) {
return value.map(item => this.resolveDynamicValue(item)) as V;
}
Comment thread
kokoro-ele marked this conversation as resolved.

// 2. Path Check: { path: "..." }
if ('path' in value) {
const absolutePath = this.resolvePath((value as DataBinding).path);
Expand Down Expand Up @@ -173,11 +178,23 @@ export class DataContext {
* @returns A Preact Signal containing the reactive result of the evaluation.
*/
resolveSignal<V>(value: DynamicValue): Signal<V> {
// 1. Literal
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
// 1. Primitive literals
if (typeof value !== 'object' || value === null) {
return signal(value as V);
}

// 1b. Arrays: each element may itself be a DynamicValue (e.g. `and`/`or` `values`)
if (Array.isArray(value)) {
const itemSignals = value.map(item => this.resolveSignal(item));
const resultSig = computed(() => itemSignals.map(s => getValue(s))) as Signal<V>;
resultSig.unsubscribe = () => {
for (const s of itemSignals) {
s.unsubscribe?.();
}
};
return resultSig;
}
Comment thread
kokoro-ele marked this conversation as resolved.

// 2. Path Check
if ('path' in value) {
const absolutePath = this.resolvePath((value as DataBinding).path);
Expand Down
Loading