Skip to content

Commit

Permalink
Merge pull request #382 from tmarmer/binding-tracker-multi-node
Browse files Browse the repository at this point in the history
Fix binding tracking for validations within nested multi-nodes
  • Loading branch information
KetanReddy authored Jun 18, 2024
2 parents 947d3bb + cf22ed1 commit 498d9be
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 47 deletions.
69 changes: 69 additions & 0 deletions core/player/src/__tests__/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,67 @@ const simpleExpressionFlow: Flow = {
},
};

const flowWithMultiNode: Flow = {
id: 'test-flow',
views: [
{
id: 'view-1',
type: 'view',
multiNode: [
{
nestedMultiNode: [
{
asset: {
type: 'asset-type',
id: 'nested-asset',
binding: 'data.foo',
},
},
],
},
],
},
],
data: {},
schema: {
ROOT: {
data: {
type: 'DataType',
},
},
DataType: {
foo: {
type: 'CatType',
validation: [
{
type: 'names',
names: ['frodo', 'sam'],
trigger: 'navigation',
severity: 'warning',
},
],
},
},
},
navigation: {
BEGIN: 'FLOW_1',
FLOW_1: {
startState: 'VIEW_1',
VIEW_1: {
state_type: 'VIEW',
ref: 'view-1',
transitions: {
'*': 'END_1',
},
},
END_1: {
state_type: 'END',
outcome: 'test',
},
},
},
};

const flowWithThings: Flow = {
id: 'test-flow',
views: [
Expand Down Expand Up @@ -785,6 +846,14 @@ describe('validation', () => {
expect(validationController?.getBindings().size).toStrictEqual(6)
);
});

it('track bindings in nested multi nodes', async () => {
player.start(flowWithMultiNode);

await waitFor(() =>
expect(validationController?.getBindings().size).toStrictEqual(1)
);
});
});

describe('schema', () => {
Expand Down
105 changes: 58 additions & 47 deletions core/player/src/controllers/validation/binding-tracker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Validation } from '@player-ui/types';
import type { ViewPlugin, Resolver, Node, ViewInstance } from '../../view';
import { NodeType } from '../../view';
import type {
BindingInstance,
BindingLike,
Expand Down Expand Up @@ -67,25 +68,15 @@ export class ValidationBindingTrackerViewPlugin

let lastViewUpdateChangeSet: Set<BindingInstance> | undefined;

const nodeTree = new Map<Node.Node, Set<Node.Node>>();

/** Map of node to all bindings in children */
let lastComputedBindingTree = new Map<Node.Node, Set<BindingInstance>>();
const lastComputedBindingTree = new Map<Node.Node, Set<BindingInstance>>();
let currentBindingTree = new Map<Node.Node, Set<BindingInstance>>();

/** Map of registered section nodes to bindings */
const lastSectionBindingTree = new Map<Node.Node, Set<BindingInstance>>();

/** Add the given child to the parent's tree. Create the parent entry if none exists */
function addToTree(child: Node.Node, parent: Node.Node) {
if (nodeTree.has(parent)) {
nodeTree.get(parent)?.add(child);

return;
}

nodeTree.set(parent, new Set([child]));
}
/** Map of resolved nodes to their original nodes. */
const resolvedNodeMap: Map<Node.Node, Node.Node> = new Map();

resolver.hooks.beforeUpdate.tap(CONTEXT, (changes) => {
lastViewUpdateChangeSet = changes;
Expand Down Expand Up @@ -214,46 +205,66 @@ export class ValidationBindingTrackerViewPlugin
};
});

resolver.hooks.afterNodeUpdate.tap(CONTEXT, (node, parent, update) => {
if (parent) {
addToTree(node, parent);
}
resolver.hooks.afterNodeUpdate.tap(
CONTEXT,
(originalNode, parent, update) => {
// Compute the new tree for this node
// If it's not-updated, use the last known value

const { updated, node: resolvedNode } = update;
resolvedNodeMap.set(resolvedNode, originalNode);

if (updated) {
const newlyComputed = new Set(tracked.get(originalNode));
if (resolvedNode.type === NodeType.MultiNode) {
resolvedNode.values.forEach((value) =>
currentBindingTree
.get(value)
?.forEach((b) => newlyComputed.add(b))
);
}

// Compute the new tree for this node
// If it's not-updated, use the last known value

if (update.updated) {
const newlyComputed = new Set(tracked.get(node));
nodeTree.get(node)?.forEach((child) => {
currentBindingTree.get(child)?.forEach((b) => newlyComputed.add(b));
});
currentBindingTree.set(node, newlyComputed);
} else {
currentBindingTree.set(
node,
lastComputedBindingTree.get(node) ?? new Set()
);
}
if ('children' in resolvedNode && resolvedNode.children) {
resolvedNode.children.forEach((child) => {
currentBindingTree
.get(child.value)
?.forEach((b) => newlyComputed.add(b));
});
}

if (node === resolver.root) {
this.trackedBindings = new Set(currentBindingTree.get(node));
lastComputedBindingTree = currentBindingTree;
currentBindingTree.set(resolvedNode, newlyComputed);
} else {
currentBindingTree.set(
resolvedNode,
lastComputedBindingTree.get(originalNode) ?? new Set()
);
}

lastSectionBindingTree.clear();
sections.forEach((nodeSet, sectionNode) => {
const temp = new Set<BindingInstance>();
nodeSet.forEach((n) => {
tracked.get(n)?.forEach(temp.add, temp);
if (originalNode === resolver.root) {
this.trackedBindings = new Set(currentBindingTree.get(resolvedNode));
lastComputedBindingTree.clear();
currentBindingTree.forEach((value, key) => {
const node = resolvedNodeMap.get(key);
if (node) {
lastComputedBindingTree.set(node, value);
}
});
lastSectionBindingTree.set(sectionNode, temp);
});

nodeTree.clear();
tracked.clear();
sections.clear();
currentBindingTree = new Map();
lastSectionBindingTree.clear();
sections.forEach((nodeSet, sectionNode) => {
const temp = new Set<BindingInstance>();
nodeSet.forEach((n) => {
tracked.get(n)?.forEach(temp.add, temp);
});
lastSectionBindingTree.set(sectionNode, temp);
});

tracked.clear();
sections.clear();
currentBindingTree = new Map();
}
}
});
);
}

apply(view: ViewInstance) {
Expand Down

0 comments on commit 498d9be

Please sign in to comment.