fix(compiler): evaluate safe navigation expressions in correct binding order (#37911)
When using the safe navigation operator in a binding expression, a temporary
variable may be used for storing the result of a side-effectful call.
For example, the following template uses a pipe and a safe property access:
```html
<app-person-view [enabled]="enabled" [firstName]="(person$ | async)?.name"></app-person-view>
```
The result of the pipe evaluation is stored in a temporary to be able to check
whether it is present. The temporary variable needs to be declared in a separate
statement and this would also cause the full expression itself to be pulled out
into a separate statement. This would compile into the following
pseudo-code instructions:
```js
var temp = null;
var firstName = (temp = pipe('async', ctx.person$)) == null ? null : temp.name;
property('enabled', ctx.enabled)('firstName', firstName);
```
Notice that the pipe evaluation happens before evaluating the `enabled` binding,
such that the runtime's internal binding index would correspond with `enabled`,
not `firstName`. This introduces a problem when the pipe uses `WrappedValue` to
force a change to be detected, as the runtime would then mark the binding slot
corresponding with `enabled` as dirty, instead of `firstName`. This results
in the `enabled` binding to be updated, triggering setters and affecting how
`OnChanges` is called.
In the pseudo-code above, the intermediate `firstName` variable is not strictly
necessary---it only improved readability a bit---and emitting it inline with
the binding itself avoids the out-of-order execution of the pipe:
```js
var temp = null;
property('enabled', ctx.enabled)
  ('firstName', (temp = pipe('async', ctx.person$)) == null ? null : temp.name);
```
This commit introduces a new `BindingForm` that results in the above code to be
generated and adds compiler and acceptance tests to verify the proper behavior.
Fixes #37194
PR Close #37911
			
			
This commit is contained in:
		
							parent
							
								
									2e9fdbde9e
								
							
						
					
					
						commit
						9514fd9080
					
				@ -180,6 +180,44 @@ describe('compiler compliance: bindings', () => {
 | 
				
			|||||||
         expectEmit(result.source, template, 'Incorrect template');
 | 
					         expectEmit(result.source, template, 'Incorrect template');
 | 
				
			||||||
       });
 | 
					       });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should emit temporary evaluation within the binding expression for in-order execution',
 | 
				
			||||||
 | 
					       () => {
 | 
				
			||||||
 | 
					         // https://github.com/angular/angular/issues/37194
 | 
				
			||||||
 | 
					         // Verifies that temporary expressions used for expressions with potential side-effects in
 | 
				
			||||||
 | 
					         // the LHS of a safe navigation access are emitted within the binding expression itself, to
 | 
				
			||||||
 | 
					         // ensure that these temporaries are evaluated during the evaluation of the binding. This
 | 
				
			||||||
 | 
					         // is important for when the LHS contains a pipe, as pipe evaluation depends on the current
 | 
				
			||||||
 | 
					         // binding index.
 | 
				
			||||||
 | 
					         const files = {
 | 
				
			||||||
 | 
					           app: {
 | 
				
			||||||
 | 
					             'example.ts': `
 | 
				
			||||||
 | 
					            import {Component} from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @Component({
 | 
				
			||||||
 | 
					              template: '<button [title]="myTitle" [id]="(auth()?.identity() | async)?.id" [tabindex]="1"></button>'
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            export class MyComponent {
 | 
				
			||||||
 | 
					              myTitle = 'hello';
 | 
				
			||||||
 | 
					              auth?: () => { identity(): any; };
 | 
				
			||||||
 | 
					            }`
 | 
				
			||||||
 | 
					           }
 | 
				
			||||||
 | 
					         };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					         const result = compile(files, angularFiles);
 | 
				
			||||||
 | 
					         const template = `
 | 
				
			||||||
 | 
					          …
 | 
				
			||||||
 | 
					          template: function MyComponent_Template(rf, ctx) {
 | 
				
			||||||
 | 
					            …
 | 
				
			||||||
 | 
					            if (rf & 2) {
 | 
				
			||||||
 | 
					              var $tmp0$ = null;
 | 
				
			||||||
 | 
					              $r3$.ɵɵproperty("title", ctx.myTitle)("id", ($tmp0$ = $r3$.ɵɵpipeBind1(1, 3, ($tmp0$ = ctx.auth()) == null ? null : $tmp0$.identity())) == null ? null : $tmp0$.id)("tabindex", 1);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					         expectEmit(result.source, template, 'Incorrect template');
 | 
				
			||||||
 | 
					       });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should chain multiple property bindings into a single instruction', () => {
 | 
					    it('should chain multiple property bindings into a single instruction', () => {
 | 
				
			||||||
      const files = {
 | 
					      const files = {
 | 
				
			||||||
        app: {
 | 
					        app: {
 | 
				
			||||||
@ -685,6 +723,46 @@ describe('compiler compliance: bindings', () => {
 | 
				
			|||||||
      expectEmit(source, HostBindingDirDeclaration, 'Invalid host binding code');
 | 
					      expectEmit(source, HostBindingDirDeclaration, 'Invalid host binding code');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should support host bindings with temporary expressions', () => {
 | 
				
			||||||
 | 
					      const files = {
 | 
				
			||||||
 | 
					        app: {
 | 
				
			||||||
 | 
					          'spec.ts': `
 | 
				
			||||||
 | 
					            import {Directive, NgModule} from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @Directive({
 | 
				
			||||||
 | 
					              selector: '[hostBindingDir]',
 | 
				
			||||||
 | 
					              host: {'[id]': 'getData()?.id'}
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            export class HostBindingDir {
 | 
				
			||||||
 | 
					              getData?: () => { id: number };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @NgModule({declarations: [HostBindingDir]})
 | 
				
			||||||
 | 
					            export class MyModule {}
 | 
				
			||||||
 | 
					          `
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const HostBindingDirDeclaration = `
 | 
				
			||||||
 | 
					      HostBindingDir.ɵdir = $r3$.ɵɵdefineDirective({
 | 
				
			||||||
 | 
					        type: HostBindingDir,
 | 
				
			||||||
 | 
					        selectors: [["", "hostBindingDir", ""]],
 | 
				
			||||||
 | 
					          hostVars: 1,
 | 
				
			||||||
 | 
					          hostBindings: function HostBindingDir_HostBindings(rf, ctx) {
 | 
				
			||||||
 | 
					            if (rf & 2) {
 | 
				
			||||||
 | 
					              var $tmp0$ = null;
 | 
				
			||||||
 | 
					              $r3$.ɵɵhostProperty("id", ($tmp0$ = ctx.getData()) == null ? null : $tmp0$.id);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const result = compile(files, angularFiles);
 | 
				
			||||||
 | 
					      const source = result.source;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expectEmit(source, HostBindingDirDeclaration, 'Invalid host binding code');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should support host bindings with pure functions', () => {
 | 
					    it('should support host bindings with pure functions', () => {
 | 
				
			||||||
      const files = {
 | 
					      const files = {
 | 
				
			||||||
        app: {
 | 
					        app: {
 | 
				
			||||||
 | 
				
			|||||||
@ -805,8 +805,7 @@ describe('i18n support in the template compiler', () => {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
          if (rf & 2) {
 | 
					          if (rf & 2) {
 | 
				
			||||||
              var $tmp_0_0$ = null;
 | 
					              var $tmp_0_0$ = null;
 | 
				
			||||||
              const $currVal_0$ = ($tmp_0_0$ = ctx.valueA.getRawValue()) == null ? null : $tmp_0_0$.getTitle();
 | 
					              $r3$.ɵɵi18nExp(($tmp_0_0$ = ctx.valueA.getRawValue()) == null ? null : $tmp_0_0$.getTitle());
 | 
				
			||||||
              $r3$.ɵɵi18nExp($currVal_0$);
 | 
					 | 
				
			||||||
              $r3$.ɵɵi18nApply(1);
 | 
					              $r3$.ɵɵi18nApply(1);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -1320,9 +1319,8 @@ describe('i18n support in the template compiler', () => {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
          if (rf & 2) {
 | 
					          if (rf & 2) {
 | 
				
			||||||
            var $tmp_2_0$ = null;
 | 
					            var $tmp_2_0$ = null;
 | 
				
			||||||
            const $currVal_2$ = ($tmp_2_0$ = ctx.valueA.getRawValue()) == null ? null : $tmp_2_0$.getTitle();
 | 
					 | 
				
			||||||
            $r3$.ɵɵadvance(2);
 | 
					            $r3$.ɵɵadvance(2);
 | 
				
			||||||
            $r3$.ɵɵi18nExp($r3$.ɵɵpipeBind1(2, 3, ctx.valueA))(ctx.valueA == null ? null : ctx.valueA.a == null ? null : ctx.valueA.a.b)($currVal_2$);
 | 
					            $r3$.ɵɵi18nExp($r3$.ɵɵpipeBind1(2, 3, ctx.valueA))(ctx.valueA == null ? null : ctx.valueA.a == null ? null : ctx.valueA.a.b)(($tmp_2_0$ = ctx.valueA.getRawValue()) == null ? null : $tmp_2_0$.getTitle());
 | 
				
			||||||
            $r3$.ɵɵi18nApply(1);
 | 
					            $r3$.ɵɵi18nApply(1);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -154,6 +154,11 @@ export enum BindingForm {
 | 
				
			|||||||
  // Try to generate a simple binding (no temporaries or statements)
 | 
					  // Try to generate a simple binding (no temporaries or statements)
 | 
				
			||||||
  // otherwise generate a general binding
 | 
					  // otherwise generate a general binding
 | 
				
			||||||
  TrySimple,
 | 
					  TrySimple,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Inlines assignment of temporaries into the generated expression. The result may still
 | 
				
			||||||
 | 
					  // have statements attached for declarations of temporary variables.
 | 
				
			||||||
 | 
					  // This is the only relevant form for Ivy, the other forms are only used in ViewEngine.
 | 
				
			||||||
 | 
					  Expression,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -168,7 +173,6 @@ export function convertPropertyBinding(
 | 
				
			|||||||
  if (!localResolver) {
 | 
					  if (!localResolver) {
 | 
				
			||||||
    localResolver = new DefaultLocalResolver();
 | 
					    localResolver = new DefaultLocalResolver();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const currValExpr = createCurrValueExpr(bindingId);
 | 
					 | 
				
			||||||
  const visitor =
 | 
					  const visitor =
 | 
				
			||||||
      new _AstToIrVisitor(localResolver, implicitReceiver, bindingId, interpolationFunction);
 | 
					      new _AstToIrVisitor(localResolver, implicitReceiver, bindingId, interpolationFunction);
 | 
				
			||||||
  const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression);
 | 
					  const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression);
 | 
				
			||||||
@ -180,8 +184,11 @@ export function convertPropertyBinding(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  if (visitor.temporaryCount === 0 && form == BindingForm.TrySimple) {
 | 
					  if (visitor.temporaryCount === 0 && form == BindingForm.TrySimple) {
 | 
				
			||||||
    return new ConvertPropertyBindingResult([], outputExpr);
 | 
					    return new ConvertPropertyBindingResult([], outputExpr);
 | 
				
			||||||
 | 
					  } else if (form === BindingForm.Expression) {
 | 
				
			||||||
 | 
					    return new ConvertPropertyBindingResult(stmts, outputExpr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const currValExpr = createCurrValueExpr(bindingId);
 | 
				
			||||||
  stmts.push(currValExpr.set(outputExpr).toDeclStmt(o.DYNAMIC_TYPE, [o.StmtModifier.Final]));
 | 
					  stmts.push(currValExpr.set(outputExpr).toDeclStmt(o.DYNAMIC_TYPE, [o.StmtModifier.Final]));
 | 
				
			||||||
  return new ConvertPropertyBindingResult(stmts, currValExpr);
 | 
					  return new ConvertPropertyBindingResult(stmts, currValExpr);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -714,7 +714,7 @@ function createHostBindingsFunction(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function bindingFn(implicit: any, value: AST) {
 | 
					function bindingFn(implicit: any, value: AST) {
 | 
				
			||||||
  return convertPropertyBinding(
 | 
					  return convertPropertyBinding(
 | 
				
			||||||
      null, implicit, value, 'b', BindingForm.TrySimple, () => error('Unexpected interpolation'));
 | 
					      null, implicit, value, 'b', BindingForm.Expression, () => error('Unexpected interpolation'));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function convertStylingCall(
 | 
					function convertStylingCall(
 | 
				
			||||||
 | 
				
			|||||||
@ -1213,7 +1213,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private convertPropertyBinding(value: AST): o.Expression {
 | 
					  private convertPropertyBinding(value: AST): o.Expression {
 | 
				
			||||||
    const convertedPropertyBinding = convertPropertyBinding(
 | 
					    const convertedPropertyBinding = convertPropertyBinding(
 | 
				
			||||||
        this, this.getImplicitReceiverExpr(), value, this.bindingContext(), BindingForm.TrySimple,
 | 
					        this, this.getImplicitReceiverExpr(), value, this.bindingContext(), BindingForm.Expression,
 | 
				
			||||||
        () => error('Unexpected interpolation'));
 | 
					        () => error('Unexpected interpolation'));
 | 
				
			||||||
    const valExpr = convertedPropertyBinding.currValExpr;
 | 
					    const valExpr = convertedPropertyBinding.currValExpr;
 | 
				
			||||||
    this._tempVariables.push(...convertedPropertyBinding.stmts);
 | 
					    this._tempVariables.push(...convertedPropertyBinding.stmts);
 | 
				
			||||||
 | 
				
			|||||||
@ -6,9 +6,10 @@
 | 
				
			|||||||
 * found in the LICENSE file at https://angular.io/license
 | 
					 * found in the LICENSE file at https://angular.io/license
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, Inject, Injectable, InjectionToken, Input, NgModule, OnDestroy, Pipe, PipeTransform, ViewChild} from '@angular/core';
 | 
					import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, Inject, Injectable, InjectionToken, Input, NgModule, OnChanges, OnDestroy, Pipe, PipeTransform, SimpleChanges, ViewChild, WrappedValue} from '@angular/core';
 | 
				
			||||||
import {TestBed} from '@angular/core/testing';
 | 
					import {TestBed} from '@angular/core/testing';
 | 
				
			||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
 | 
					import {expect} from '@angular/platform-browser/testing/src/matchers';
 | 
				
			||||||
 | 
					import {ivyEnabled} from '@angular/private/testing';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('pipe', () => {
 | 
					describe('pipe', () => {
 | 
				
			||||||
  @Pipe({name: 'countingPipe'})
 | 
					  @Pipe({name: 'countingPipe'})
 | 
				
			||||||
@ -285,6 +286,133 @@ describe('pipe', () => {
 | 
				
			|||||||
    expect(fixture.nativeElement).toHaveText('a');
 | 
					    expect(fixture.nativeElement).toHaveText('a');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('pipes within an optional chain', () => {
 | 
				
			||||||
 | 
					    it('should not dirty unrelated inputs', () => {
 | 
				
			||||||
 | 
					      // https://github.com/angular/angular/issues/37194
 | 
				
			||||||
 | 
					      // https://github.com/angular/angular/issues/37591
 | 
				
			||||||
 | 
					      // Using a pipe in the LHS of safe navigation operators would clobber unrelated bindings
 | 
				
			||||||
 | 
					      // iff the pipe returns WrappedValue, incorrectly causing the unrelated binding
 | 
				
			||||||
 | 
					      // to be considered changed.
 | 
				
			||||||
 | 
					      const log: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @Component({template: `<my-cmp [value1]="1" [value2]="(value2 | pipe)?.id"></my-cmp>`})
 | 
				
			||||||
 | 
					      class App {
 | 
				
			||||||
 | 
					        value2 = {id: 2};
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @Component({selector: 'my-cmp', template: ''})
 | 
				
			||||||
 | 
					      class MyCmp {
 | 
				
			||||||
 | 
					        @Input()
 | 
				
			||||||
 | 
					        set value1(value1: number) {
 | 
				
			||||||
 | 
					          log.push(`set value1=${value1}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Input()
 | 
				
			||||||
 | 
					        set value2(value2: number) {
 | 
				
			||||||
 | 
					          log.push(`set value2=${value2}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @Pipe({name: 'pipe'})
 | 
				
			||||||
 | 
					      class MyPipe implements PipeTransform {
 | 
				
			||||||
 | 
					        transform(value: any): any {
 | 
				
			||||||
 | 
					          log.push('pipe');
 | 
				
			||||||
 | 
					          return WrappedValue.wrap(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      TestBed.configureTestingModule({declarations: [App, MyCmp, MyPipe]});
 | 
				
			||||||
 | 
					      const fixture = TestBed.createComponent(App);
 | 
				
			||||||
 | 
					      fixture.detectChanges(/* checkNoChanges */ false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Both bindings should have been set. Note: ViewEngine evaluates the pipe out-of-order,
 | 
				
			||||||
 | 
					      // before setting inputs.
 | 
				
			||||||
 | 
					      expect(log).toEqual(
 | 
				
			||||||
 | 
					          ivyEnabled ?
 | 
				
			||||||
 | 
					              [
 | 
				
			||||||
 | 
					                'set value1=1',
 | 
				
			||||||
 | 
					                'pipe',
 | 
				
			||||||
 | 
					                'set value2=2',
 | 
				
			||||||
 | 
					              ] :
 | 
				
			||||||
 | 
					              [
 | 
				
			||||||
 | 
					                'pipe',
 | 
				
			||||||
 | 
					                'set value1=1',
 | 
				
			||||||
 | 
					                'set value2=2',
 | 
				
			||||||
 | 
					              ]);
 | 
				
			||||||
 | 
					      log.length = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fixture.componentInstance.value2 = {id: 3};
 | 
				
			||||||
 | 
					      fixture.detectChanges(/* checkNoChanges */ false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // value1 did not change, so it should not have been set.
 | 
				
			||||||
 | 
					      expect(log).toEqual([
 | 
				
			||||||
 | 
					        'pipe',
 | 
				
			||||||
 | 
					        'set value2=3',
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should not include unrelated inputs in ngOnChanges', () => {
 | 
				
			||||||
 | 
					      // https://github.com/angular/angular/issues/37194
 | 
				
			||||||
 | 
					      // https://github.com/angular/angular/issues/37591
 | 
				
			||||||
 | 
					      // Using a pipe in the LHS of safe navigation operators would clobber unrelated bindings
 | 
				
			||||||
 | 
					      // iff the pipe returns WrappedValue, incorrectly causing the unrelated binding
 | 
				
			||||||
 | 
					      // to be considered changed.
 | 
				
			||||||
 | 
					      const log: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @Component({template: `<my-cmp [value1]="1" [value2]="(value2 | pipe)?.id"></my-cmp>`})
 | 
				
			||||||
 | 
					      class App {
 | 
				
			||||||
 | 
					        value2 = {id: 2};
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @Component({selector: 'my-cmp', template: ''})
 | 
				
			||||||
 | 
					      class MyCmp implements OnChanges {
 | 
				
			||||||
 | 
					        @Input() value1!: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Input() value2!: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ngOnChanges(changes: SimpleChanges): void {
 | 
				
			||||||
 | 
					          if (changes.value1) {
 | 
				
			||||||
 | 
					            const {previousValue, currentValue, firstChange} = changes.value1;
 | 
				
			||||||
 | 
					            log.push(`change value1: ${previousValue} -> ${currentValue} (${firstChange})`);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (changes.value2) {
 | 
				
			||||||
 | 
					            const {previousValue, currentValue, firstChange} = changes.value2;
 | 
				
			||||||
 | 
					            log.push(`change value2: ${previousValue} -> ${currentValue} (${firstChange})`);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @Pipe({name: 'pipe'})
 | 
				
			||||||
 | 
					      class MyPipe implements PipeTransform {
 | 
				
			||||||
 | 
					        transform(value: any): any {
 | 
				
			||||||
 | 
					          log.push('pipe');
 | 
				
			||||||
 | 
					          return WrappedValue.wrap(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      TestBed.configureTestingModule({declarations: [App, MyCmp, MyPipe]});
 | 
				
			||||||
 | 
					      const fixture = TestBed.createComponent(App);
 | 
				
			||||||
 | 
					      fixture.detectChanges(/* checkNoChanges */ false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Both bindings should have been included in ngOnChanges.
 | 
				
			||||||
 | 
					      expect(log).toEqual([
 | 
				
			||||||
 | 
					        'pipe',
 | 
				
			||||||
 | 
					        'change value1: undefined -> 1 (true)',
 | 
				
			||||||
 | 
					        'change value2: undefined -> 2 (true)',
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					      log.length = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fixture.componentInstance.value2 = {id: 3};
 | 
				
			||||||
 | 
					      fixture.detectChanges(/* checkNoChanges */ false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // value1 did not change, so it should not have been included in ngOnChanges
 | 
				
			||||||
 | 
					      expect(log).toEqual([
 | 
				
			||||||
 | 
					        'pipe',
 | 
				
			||||||
 | 
					        'change value2: 2 -> 3 (false)',
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('pure', () => {
 | 
					  describe('pure', () => {
 | 
				
			||||||
    it('should call pure pipes only if the arguments change', () => {
 | 
					    it('should call pure pipes only if the arguments change', () => {
 | 
				
			||||||
      @Component({
 | 
					      @Component({
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user