Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
97 changes: 97 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,25 @@ describe('DataContext', () => {
assert.deepStrictEqual(context.resolveDynamicValue(['literal', 'array']), ['literal', 'array']);
});

it('returns fully static arrays as-is without re-allocation', () => {
const staticArray = ['literal', 1, true, null, [2, 'nested']];
assert.strictEqual(context.resolveDynamicValue(staticArray), staticArray);
});

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

it('resolves DynamicValues nested inside inner arrays', () => {
assert.deepStrictEqual(context.resolveDynamicValue(['outer', [{path: 'name'}, 'inner']]), [
'outer',
['Alice', 'inner'],
]);
});

it('subscribes literal arrays as static', () => {
let called = false;
const sub = context.subscribeDynamicValue(['literal', 'array'], () => {
Expand All @@ -118,6 +137,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
47 changes: 43 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 @@ -75,6 +75,20 @@ export class DataContext {
this.dataModel.set(absolutePath, value);
}

/**
* Checks whether a value (typically an array element) contains any dynamic parts
* (path bindings or function calls) that require resolution.
*/
private static containsDynamicValue(value: unknown): boolean {
if (value === null || typeof value !== 'object') {
return false;
}
if (Array.isArray(value)) {
return value.some(item => DataContext.containsDynamicValue(item));
}
return 'path' in value || 'call' in value;
}

/**
* Synchronously evaluates a `DynamicValue` (a literal, a path binding, or a function call)
* into its concrete runtime value.
Expand All @@ -87,11 +101,20 @@ 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)) {
// Fast path: fully static arrays need no per-element resolution.
if (!DataContext.containsDynamicValue(value)) {
return value as V;
}
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 +196,27 @@ 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)) {
// Fast path: fully static arrays need no per-element signals.
if (!DataContext.containsDynamicValue(value)) {
return signal(value as V);
}
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