Skip to content

Commit

Permalink
fix(language-service): Support completions of two-way bindings (#40185)
Browse files Browse the repository at this point in the history
This commit adds special handling to the completion builder by detecting
a two way binding context and ensuring that we filter out any `Input`s
that do not support two way binding.

PR Close #40185
  • Loading branch information
atscott committed Jan 7, 2021
1 parent ebb7ac5 commit 7d74853
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 1 deletion.
8 changes: 7 additions & 1 deletion packages/language-service/ivy/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum CompletionNodeContext {
ElementAttributeKey,
ElementAttributeValue,
EventValue,
TwoWayBinding,
}

/**
Expand Down Expand Up @@ -415,7 +416,8 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
}

private isElementAttributeCompletion(): this is ElementAttributeCompletionBuilder {
return this.nodeContext === CompletionNodeContext.ElementAttributeKey &&
return (this.nodeContext === CompletionNodeContext.ElementAttributeKey ||
this.nodeContext === CompletionNodeContext.TwoWayBinding) &&
(this.node instanceof TmplAstElement || this.node instanceof TmplAstBoundAttribute ||
this.node instanceof TmplAstTextAttribute || this.node instanceof TmplAstBoundEvent);
}
Expand Down Expand Up @@ -460,6 +462,10 @@ export class CompletionBuilder<N extends TmplAstNode|AST> {
if (this.node instanceof TmplAstBoundEvent) {
continue;
}
if (!completion.twoWayBindingSupported &&
this.nodeContext === CompletionNodeContext.TwoWayBinding) {
continue;
}
break;
case AttributeCompletionKind.DirectiveOutput:
if (this.node instanceof TmplAstBoundAttribute) {
Expand Down
4 changes: 4 additions & 0 deletions packages/language-service/ivy/language_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export class LanguageService {
return null;
}

// For two-way bindings, we actually only need to be concerned with the bound attribute because
// the bindings in the template are written with the attribute name, not the event name.
const node = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
positionDetails.context.nodes[0] :
positionDetails.context.node;
Expand Down Expand Up @@ -272,6 +274,8 @@ function nodeContextFromTarget(target: TargetContext): CompletionNodeContext {
case TargetNodeKind.ElementInBodyContext:
// Completions in element bodies are for new attributes.
return CompletionNodeContext.ElementAttributeKey;
case TargetNodeKind.TwoWayBindingContext:
return CompletionNodeContext.TwoWayBinding;
case TargetNodeKind.AttributeInKeyContext:
return CompletionNodeContext.ElementAttributeKey;
case TargetNodeKind.AttributeInValueContext:
Expand Down
37 changes: 37 additions & 0 deletions packages/language-service/ivy/test/completions_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ const DIR_WITH_OUTPUT = {
`
};

const DIR_WITH_TWO_WAY_BINDING = {
'Dir': `
@Directive({
selector: '[dir]',
inputs: ['model', 'otherInput'],
outputs: ['modelChange', 'otherOutput'],
})
export class Dir {
model!: any;
modelChange!: any;
otherInput!: any;
otherOutput!: any;
}
`
};

const NG_FOR_DIR = {
'NgFor': `
@Directive({
Expand Down Expand Up @@ -519,6 +535,27 @@ describe('completions', () => {
['myOutput']);
expectReplacementText(completions, text, 'my');
});

it('should return completions inside an LHS of a partially complete two-way binding', () => {
const {ngLS, fileName, cursor, text} =
setup(`<h1 dir [(mod¦)]></h1>`, ``, DIR_WITH_TWO_WAY_BINDING);
const completions =
ngLS.getCompletionsAtPosition(fileName, cursor, /* options */ undefined);
expectReplacementText(completions, text, 'mod');

expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['model']);

// The completions should not include the events (because the 'Change' suffix is not used in
// the two way binding) or inputs that do not have a corresponding name+'Change' output.
expectDoesNotContain(
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
['modelChange']);
expectDoesNotContain(
completions, ts.ScriptElementKind.memberVariableElement, ['otherInput']);
expectDoesNotContain(
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.EVENT),
['otherOutput']);
});
});
});

Expand Down

0 comments on commit 7d74853

Please sign in to comment.