-
Notifications
You must be signed in to change notification settings - Fork 26.3k
Update shape of two-way binding instructions #54154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
dc22f4a
to
5ab3289
Compare
5ab3289
to
cdbc421
Compare
return wrapAssignmentReadExpression(ast); | ||
} | ||
|
||
// However, historically the expression parser was handling two-way events by appending `=$event` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not excited by having to introduce this logic, but I don't think there's a better way of handling it in a non-breaking way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will you consider adding diagnostics and maybe even deprecate these usages?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a good idea to have a diagnostic for this but I suspect that completely removing it will be difficult since fixing the expression can change the app's behavior.
d1fbe2a
to
6191f5e
Compare
6191f5e
to
ec1a647
Compare
/** | ||
* A logical operation representing the property binding side of a two-way binding in the update IR. | ||
*/ | ||
export interface TwoWayPropertyOp extends Op<UpdateOp>, ConsumesVarsTrait, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I am planning to switch these interfaces to classes, so you'd be able to extend PropertyOp
and not duplicate all this code. But it's not implemented yet.)
return ir.createExtractedAttributeOp( | ||
xref, ir.BindingKind.TwoWayProperty, null, name, null, null, i18nMessage, | ||
securityContext); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the two-way binding is on an explicit ng-template
? There's nothing stopping the user from writing that, right? I think you'd need to also update the code below for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, ng-template
can have two-way bindings. I'm not sure which code you're referring to though. Is it the one wrapped in the if (templateKind === ir.TemplateKind.NgTemplate) {
check right below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was talking about the createBindingOp
below that check. But I suppose bindingType
just gets threaded through, so it's fine.
@@ -1043,15 +1042,22 @@ function createTemplateBinding( | |||
// If this is a structural template, then several kinds of bindings should not result in an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't you also need to modify ingestElementBindings
above, to make sure two way bindings work on non-template elements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm misunderstanding it, but I think that it should be handled by adding e.BindingType.TwoWay
to the BINDING_KINDS
map. This lookup should've taken care of it: https://github.com/angular/angular/blob/main/packages/compiler/src/template/pipeline/src/ingest.ts#L911
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, yes, overlooked that detail -- we are creating a binding op, not an extracted attribute directly.
* An operation to bind an expression to the property side of a two-way binding. | ||
*/ | ||
TwoWayProperty, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding the new OpKind
required updates to a couple phases. I think you may have missed a few though:
orderOps
: We should probably have an ordering invariant in order to maximize chaining opportunities. I think that, to maintain the TDB order, you'd probably have to test for eitherProperty
orTwoWayProperty
kinds grouped together by the same ordering rule. (I think it's actually a bug not to have this: currently,TwoWayProperty
instructions will break up the ordering of existing lists of binding ops, potentially leading to wrong orders compared to TDB, since we "naturally" emit them in different orders.)
Example:<elem [a]="bar++" [(foo)]="baz" [class.b]="readBar()" />
And then I'm less sure that you'd need changes to these:
createI18nContexts
: Can this ever result in an extracted i18n attr? I guess that raises the question of whether you can write[(i18n-foo)]="..."
, which would be an insane thing to do, so maybe it doesn't matter?naming
: Two-way bindings can never be an animation trigger or have@
, right?resolveSanitizers
: can two-way bindings ever have consts requiring sanitization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Done.
- My understanding is that we don't support i18n in two-way bindings.
- Nope, they can't be animation triggers.
- I don't think so since they're always binding into inputs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks.
(As mentioned on Slack, this is for the first 4 commits. Will review the next 4 later today. I'd also love to run a TGP with Template Pipeline on.) |
Reworks the compiler so that it generates a `twoWayProperty` instruction, instead of `property`, for the property side of a two-way binding. Currently the new instruction passes through to `property`, but it'll have some two-way-binding-specific logic in subsequent PRs.
Currently all the members of `_ParseAST` are public, even though they're all used only within the class. This change marks them as private so that it's explicit which ones are intended to be used outside the class.
Adds the following new instructions: * `twoWayBindingSet` - used to assign values inside of the listener side of a two-way binding. Currently a noop, but will come into play later. * `twoWayListener` - used to bind a two-way listener. Currently calls directly into `listener`, but it may be useful in the future.
Currently the listener side two-way listeners are parsed by appending `=$event` to the raw expression. This is problematic, because: 1. It can interfere with other expressions (see angular#37809). 2. It can lead to confusing error messages because users will see code that they didn't write. 3. It doesn't allow us to further manipulate the expression. These changes remove the logic that appends `=$event` to resolve the issue. There's also some new logic that checks the expression after it has been parsed to ensure that the result is an assignable expression. Subsequent commits will update the code that emits the expression to add back the `$event` assignment where it's needed.
Updates the template definition builder to emit the new format for the listener side of two-way bindings. ```js // Before listener("ngModelChange", function($event) { return ctx.name = $event; }); // After ɵɵtwoWayListener("ngModelChange", function($event) { ɵɵtwoWayBindingSet(ctx.name, $event) || (ctx.name = $event); return $event; }); ```
Implements the new shape of two-way listener instructions in the template pipeline.
…s that previously worked by accident In one of the earlier commits, the logic that appends `=$event` before parsing two-way bindings was removed and some validation was added to prevent unassignable expressions from being used. This ended up being problematic, because previously the parser was incorrectly allowing some invalid expressions which users came to depend on. For example, it transformed `[(value)]="a && a.b"` to `a && (a.b = $event)`. These changes add some special cases for the common breakages that came up during the TGP.
…ties One of the earlier commits separated one-way and two-way bindings which ended up breaking some internal targets, because it changed the assignment order. These changes bring back the old order.
ec1a647
to
264cdf7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, remainder looks good to me. Let's also check a Template Pipeline TGP, but then should be good to go.
Updates the instruction ordering logic in the template pipeline.
264cdf7
to
4bbbbc1
Compare
Pushed a couple of tests for the property/listener ordering since it ended up being a bit of a gotcha. Also linking the pipeline-specific TGP courtesy of @dylhunn. It has a handful of flakes and one pre-existing failure that is accounted for. |
Adds the following new instructions: * `twoWayBindingSet` - used to assign values inside of the listener side of a two-way binding. Currently a noop, but will come into play later. * `twoWayListener` - used to bind a two-way listener. Currently calls directly into `listener`, but it may be useful in the future. PR Close #54154
Currently the listener side two-way listeners are parsed by appending `=$event` to the raw expression. This is problematic, because: 1. It can interfere with other expressions (see #37809). 2. It can lead to confusing error messages because users will see code that they didn't write. 3. It doesn't allow us to further manipulate the expression. These changes remove the logic that appends `=$event` to resolve the issue. There's also some new logic that checks the expression after it has been parsed to ensure that the result is an assignable expression. Subsequent commits will update the code that emits the expression to add back the `$event` assignment where it's needed. PR Close #54154
#54154) Updates the template definition builder to emit the new format for the listener side of two-way bindings. ```js // Before listener("ngModelChange", function($event) { return ctx.name = $event; }); // After ɵɵtwoWayListener("ngModelChange", function($event) { ɵɵtwoWayBindingSet(ctx.name, $event) || (ctx.name = $event); return $event; }); ``` PR Close #54154
This PR was merged into the repository by commit 3b892e9. |
…s that previously worked by accident (#54154) In one of the earlier commits, the logic that appends `=$event` before parsing two-way bindings was removed and some validation was added to prevent unassignable expressions from being used. This ended up being problematic, because previously the parser was incorrectly allowing some invalid expressions which users came to depend on. For example, it transformed `[(value)]="a && a.b"` to `a && (a.b = $event)`. These changes add some special cases for the common breakages that came up during the TGP. PR Close #54154
Updates the instruction ordering logic in the template pipeline. PR Close #54154
As a part of angular#54154, an old parser behavior came up where two-way bindings were parsed by appending `= $event` to the event side. This was problematic, because it allowed some non-writable expressions to be passed into two-way bindings. These changes introduce a migration that will change the two-way bindings into two separate input/output bindings that represent the old behavior so that in a future version we can throw a parser error for the invalid expressions. ```ts // Before @component({ template: `<input [(ngModel)]="a && b"/>` }) export class MyComp {} // After @component({ template: `<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>` }) export class MyComp {} ```
As a part of #54154, an old parser behavior came up where two-way bindings were parsed by appending `= $event` to the event side. This was problematic, because it allowed some non-writable expressions to be passed into two-way bindings. These changes introduce a migration that will change the two-way bindings into two separate input/output bindings that represent the old behavior so that in a future version we can throw a parser error for the invalid expressions. ```ts // Before @component({ template: `<input [(ngModel)]="a && b"/>` }) export class MyComp {} // After @component({ template: `<input [ngModel]="a && b" (ngModelChange)="a && (b = $event)"/>` }) export class MyComp {} ``` PR Close #54630
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Includes some changes to update the instructions generated for two-way bindings from:
To:
The new shape will be necessary for some future work and implementing it required cleaning up some old parsing code. There are more details in the individual commits.
refactor(compiler): implement two-way property instruction
Reworks the compiler so that it generates a
twoWayProperty
instruction, instead ofproperty
, for the property side of a two-way binding. Currently the new instruction passes through toproperty
, but it'll have some two-way-binding-specific logic in subsequent PRs.refactor(compiler): update access of members in expression parser
Currently all the members of
_ParseAST
are public, even though they're all used only within the class. This change marks them as private so that it's explicit which ones are intended to be used outside the class.refactor(core): introduce two-way listener instructions
Adds the following new instructions:
twoWayBindingSet
- used to assign values inside of the listener side of a two-way binding. Currently a noop, but will come into play later.twoWayListener
- used to bind a two-way listener. Currently calls directly intolistener
, but it may be useful in the future.refactor(compiler): preserve expression in two-way listeners
Currently the listener side two-way listeners are parsed by appending
=$event
to the raw expression. This is problematic, because:These changes remove the logic that appends
=$event
to resolve the issue. There's also some new logic that checks the expression after it has been parsed to ensure that the result is an assignable expression.Subsequent commits will update the code that emits the expression to add back the
$event
assignment where it's needed.refactor(compiler): update two-way listener emit in definition builder
Updates the template definition builder to emit the new format for the listener side of two-way bindings.
refactor(compiler): implement new two-way listener shape in pipeline
Implements the new shape of two-way listener instructions in the template pipeline.
refactor(compiler): allow some invalid expressions in two-way bindings that previously worked by accident
In one of the earlier commits, the logic that appends
=$event
before parsing two-way bindings was removed and some validation was added to prevent unassignable expressions from being used. This ended up being problematic, because previously the parser was incorrectly allowing some invalid expressions which users came to depend on. For example, it transformed[(value)]="a && a.b"
toa && (a.b = $event)
.These changes add some special cases for the common breakages that came up during the TGP.
refactor(compiler): maintain order between two-way and one-way properties
One of the earlier commits separated one-way and two-way bindings which ended up breaking some internal targets, because it changed the assignment order. These changes bring back the old order.