fix(ivy): correctly associate output bound events with directives (#31938)
Previously, bound events were incorrectly bound to directives with inputs matching the bound event attribute. This fixes that so bound events can only be bound to directives with matching outputs. Adds tests for all kinds of directive matching on bound attributes. PR Close #31938
This commit is contained in:
parent
7938ff34b1
commit
3e201181bb
|
@ -269,20 +269,23 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Associate attributes/bindings on the node with directives or with the node itself.
|
// Associate attributes/bindings on the node with directives or with the node itself.
|
||||||
const processAttribute = (attribute: BoundAttribute | BoundEvent | TextAttribute) => {
|
type BoundNode = BoundAttribute | BoundEvent | TextAttribute;
|
||||||
let dir = directives.find(dir => dir.inputs.hasOwnProperty(attribute.name));
|
const setAttributeBinding =
|
||||||
if (dir !== undefined) {
|
(attribute: BoundNode, ioType: keyof Pick<DirectiveMeta, 'inputs'|'outputs'>) => {
|
||||||
this.bindings.set(attribute, dir);
|
const dir = directives.find(dir => dir[ioType].hasOwnProperty(attribute.name));
|
||||||
} else {
|
const binding = dir !== undefined ? dir : node;
|
||||||
this.bindings.set(attribute, node);
|
this.bindings.set(attribute, binding);
|
||||||
}
|
};
|
||||||
};
|
|
||||||
node.attributes.forEach(processAttribute);
|
// Node inputs (bound attributes) and text attributes can be bound to an
|
||||||
node.inputs.forEach(processAttribute);
|
// input on a directive.
|
||||||
node.outputs.forEach(processAttribute);
|
node.inputs.forEach(input => setAttributeBinding(input, 'inputs'));
|
||||||
|
node.attributes.forEach(attr => setAttributeBinding(attr, 'inputs'));
|
||||||
if (node instanceof Template) {
|
if (node instanceof Template) {
|
||||||
node.templateAttrs.forEach(processAttribute);
|
node.templateAttrs.forEach(attr => setAttributeBinding(attr, 'inputs'));
|
||||||
}
|
}
|
||||||
|
// Node outputs (bound events) can be bound to an output on a directive.
|
||||||
|
node.outputs.forEach(output => setAttributeBinding(output, 'outputs'));
|
||||||
|
|
||||||
// Recurse into the node's children.
|
// Recurse into the node's children.
|
||||||
node.children.forEach(child => child.visit(this));
|
node.children.forEach(child => child.visit(this));
|
||||||
|
|
|
@ -31,6 +31,20 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
|
||||||
outputs: {},
|
outputs: {},
|
||||||
isComponent: false,
|
isComponent: false,
|
||||||
});
|
});
|
||||||
|
matcher.addSelectables(CssSelector.parse('[hasOutput]'), {
|
||||||
|
name: 'HasOutput',
|
||||||
|
exportAs: null,
|
||||||
|
inputs: {},
|
||||||
|
outputs: {'outputBinding': 'outputBinding'},
|
||||||
|
isComponent: false,
|
||||||
|
});
|
||||||
|
matcher.addSelectables(CssSelector.parse('[hasInput]'), {
|
||||||
|
name: 'HasInput',
|
||||||
|
exportAs: null,
|
||||||
|
inputs: {'inputBinding': 'inputBinding'},
|
||||||
|
outputs: {},
|
||||||
|
isComponent: false,
|
||||||
|
});
|
||||||
return matcher;
|
return matcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,4 +112,70 @@ describe('t2 binding', () => {
|
||||||
expect(elDirectives.length).toBe(1);
|
expect(elDirectives.length).toBe(1);
|
||||||
expect(elDirectives[0].name).toBe('Dir');
|
expect(elDirectives[0].name).toBe('Dir');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('matching inputs to consuming directives', () => {
|
||||||
|
it('should work for bound attributes', () => {
|
||||||
|
const template = parseTemplate('<div hasInput [inputBinding]="myValue"></div>', '', {});
|
||||||
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
||||||
|
const res = binder.bind({template: template.nodes});
|
||||||
|
const el = template.nodes[0] as a.Element;
|
||||||
|
const attr = el.inputs[0];
|
||||||
|
const consumer = res.getConsumerOfBinding(attr) as DirectiveMeta;
|
||||||
|
expect(consumer.name).toBe('HasInput');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for text attributes on elements', () => {
|
||||||
|
const template = parseTemplate('<div hasInput inputBinding="text"></div>', '', {});
|
||||||
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
||||||
|
const res = binder.bind({template: template.nodes});
|
||||||
|
const el = template.nodes[0] as a.Element;
|
||||||
|
const attr = el.attributes[1];
|
||||||
|
const consumer = res.getConsumerOfBinding(attr) as DirectiveMeta;
|
||||||
|
expect(consumer.name).toBe('HasInput');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for text attributes on templates', () => {
|
||||||
|
const template =
|
||||||
|
parseTemplate('<ng-template hasInput inputBinding="text"></ng-template>', '', {});
|
||||||
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
||||||
|
const res = binder.bind({template: template.nodes});
|
||||||
|
const el = template.nodes[0] as a.Element;
|
||||||
|
const attr = el.attributes[1];
|
||||||
|
const consumer = res.getConsumerOfBinding(attr) as DirectiveMeta;
|
||||||
|
expect(consumer.name).toBe('HasInput');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bind to the encompassing node when no directive input is matched', () => {
|
||||||
|
const template = parseTemplate('<span dir></span>', '', {});
|
||||||
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
||||||
|
const res = binder.bind({template: template.nodes});
|
||||||
|
const el = template.nodes[0] as a.Element;
|
||||||
|
const attr = el.attributes[0];
|
||||||
|
const consumer = res.getConsumerOfBinding(attr);
|
||||||
|
expect(consumer).toEqual(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('matching outputs to consuming directives', () => {
|
||||||
|
it('should work for bound events', () => {
|
||||||
|
const template =
|
||||||
|
parseTemplate('<div hasOutput (outputBinding)="myHandler($event)"></div>', '', {});
|
||||||
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
||||||
|
const res = binder.bind({template: template.nodes});
|
||||||
|
const el = template.nodes[0] as a.Element;
|
||||||
|
const attr = el.outputs[0];
|
||||||
|
const consumer = res.getConsumerOfBinding(attr) as DirectiveMeta;
|
||||||
|
expect(consumer.name).toBe('HasOutput');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bind to the encompassing node when no directive output is matched', () => {
|
||||||
|
const template = parseTemplate('<span dir (fakeOutput)="myHandler($event)"></span>', '', {});
|
||||||
|
const binder = new R3TargetBinder(makeSelectorMatcher());
|
||||||
|
const res = binder.bind({template: template.nodes});
|
||||||
|
const el = template.nodes[0] as a.Element;
|
||||||
|
const attr = el.outputs[0];
|
||||||
|
const consumer = res.getConsumerOfBinding(attr);
|
||||||
|
expect(consumer).toEqual(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue