Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Closed
wants to merge 9 commits into from

Conversation

crisbeto
Copy link
Member

@crisbeto crisbeto commented Jan 30, 2024

Includes some changes to update the instructions generated for two-way bindings from:

function template(rf, ctx) {
  if (rf & 1) {
    listener("valueChange", function($event) {
      return ctx.name = $event;
    });
  } 
  if (rf & 2) {
    property("value", ctx.name);
  }
}

To:

function template(rf, ctx) {
  if (rf & 1) {
    twoWayListener("valueChange", function($event) {
      twoWayBindingSet(ctx.name, $event) || (ctx.name = $event);
      return $event;
    });
  } 
  if (rf & 2) {
    twoWayProperty("value", ctx.name);
  }
}

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 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.

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 into listener, 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:

  1. It can interfere with other expressions (see fix(compiler): allow banana-in-a-box bindings to end with non-null assertion #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.

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" to a && (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.

@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer area: compiler Issues related to `ngc`, Angular's template compiler target: minor This PR is targeted for the next minor release labels Jan 30, 2024
@ngbot ngbot bot modified the milestone: Backlog Jan 30, 2024
@crisbeto crisbeto force-pushed the two-way-binding-instructions branch 3 times, most recently from dc22f4a to 5ab3289 Compare January 30, 2024 12:23
@crisbeto crisbeto requested a review from dylhunn January 30, 2024 13:25
@crisbeto crisbeto marked this pull request as ready for review January 30, 2024 13:25
@crisbeto crisbeto force-pushed the two-way-binding-instructions branch from 5ab3289 to cdbc421 Compare January 30, 2024 19:33
return wrapAssignmentReadExpression(ast);
}

// However, historically the expression parser was handling two-way events by appending `=$event`
Copy link
Member Author

@crisbeto crisbeto Jan 30, 2024

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.

Copy link
Contributor

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?

Copy link
Member Author

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.

@crisbeto crisbeto force-pushed the two-way-binding-instructions branch 2 times, most recently from d1fbe2a to 6191f5e Compare January 31, 2024 11:56
@crisbeto crisbeto force-pushed the two-way-binding-instructions branch from 6191f5e to ec1a647 Compare January 31, 2024 12:19
@crisbeto
Copy link
Member Author

Passing TGP

/**
* A logical operation representing the property binding side of a two-way binding in the update IR.
*/
export interface TwoWayPropertyOp extends Op<UpdateOp>, ConsumesVarsTrait,
Copy link
Contributor

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);
}
}
Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

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,

Copy link
Contributor

@dylhunn dylhunn Jan 31, 2024

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:

  1. 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 either Property or TwoWayProperty 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:

  1. 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?
  2. naming: Two-way bindings can never be an animation trigger or have @, right?
  3. resolveSanitizers: can two-way bindings ever have consts requiring sanitization?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Done.
  2. My understanding is that we don't support i18n in two-way bindings.
  3. Nope, they can't be animation triggers.
  4. I don't think so since they're always binding into inputs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks.

@dylhunn
Copy link
Contributor

dylhunn commented Jan 31, 2024

(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.
@crisbeto crisbeto force-pushed the two-way-binding-instructions branch from ec1a647 to 264cdf7 Compare January 31, 2024 20:38
Copy link
Contributor

@dylhunn dylhunn left a 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.
@crisbeto crisbeto force-pushed the two-way-binding-instructions branch from 264cdf7 to 4bbbbc1 Compare February 1, 2024 04:49
@crisbeto
Copy link
Member Author

crisbeto commented Feb 1, 2024

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.

@crisbeto crisbeto added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Feb 1, 2024
jessicajaniuk pushed a commit that referenced this pull request Feb 1, 2024
…4154)

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.

PR Close #54154
jessicajaniuk pushed a commit that referenced this pull request Feb 1, 2024
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
jessicajaniuk pushed a commit that referenced this pull request Feb 1, 2024
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
jessicajaniuk pushed a commit that referenced this pull request Feb 1, 2024
#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
@jessicajaniuk
Copy link
Contributor

This PR was merged into the repository by commit 3b892e9.

jessicajaniuk pushed a commit that referenced this pull request Feb 1, 2024
…54154)

Implements the new shape of two-way listener instructions in the template pipeline.

PR Close #54154
jessicajaniuk pushed a commit that referenced this pull request Feb 1, 2024
…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
jessicajaniuk pushed a commit that referenced this pull request Feb 1, 2024
…ties (#54154)

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.

PR Close #54154
jessicajaniuk pushed a commit that referenced this pull request Feb 1, 2024
Updates the instruction ordering logic in the template pipeline.

PR Close #54154
crisbeto added a commit to crisbeto/angular that referenced this pull request Feb 28, 2024
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 {}
```
pkozlowski-opensource pushed a commit that referenced this pull request Feb 28, 2024
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
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Mar 3, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: compiler Issues related to `ngc`, Angular's template compiler target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants