# Change Detection when Declaration and Insertion parent views don't match Author: Miško Hevery # Prior Discussion - [TemplateRef passed into other component not checked under Ivy · Issue #33393 · angular/angular](https://github.com/angular/angular/issues/33393#issuecomment-546243506) - [Embedded views with different declaration and insertion points](https://docs.google.com/document/d/1E0HYC9Dl8UhNsteK5GKtqISfV4bMo8G1fR1AUVY_no8/edit#heading=h.dfrdwxsgfuxu) - [Ivy Minor Changes](https://docs.google.com/document/d/1Dije0AsJ0PxL3NaeNPxpYDeapj30b_QC0xfeIvIIzgg/preview) - [FW-842 DESIGN: View engine dirty-checks projected views when the declaration place is checked](https://gist.github.com/mhevery/d6db04ec7b14027acdd0457b546466a8) - [“Transplanted” views](https://docs.google.com/presentation/d/1AlaZN7znhprEUkhr760i8DKwDTVaerU7HGOgrszYfsc/edit#slide=id.p) # Definitions ```typescript= @Component({ selector: 'lib-comp', template: ` LibComp: {{greeting}}! <ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{$implicit: greeting}"> </ng-container> ` }) class LibComp { @Input() template: TemplateRef; greeting:string = "Hello"; } @Component({ template: ` AppComp: {{name}}! <ng-template #myTmpl let-greeting> {{greeting}} {{name}}! </ng-template> <lib-comp [template]="myTmpl"></lib-comp> ` }) class AppComp { name: string = "world"; } ``` - **Declaration View**: The `LView` where the `<ng-template>` was declared. In our case it would be the `LView` of `AppComp`. - **Insertion View**: The `LView` where the instance of `<ng-template>` has been inserted. In our example it would be the `LView` of `LibComp`. - **Transplant `LView`**: Transplant `LView` is the `LView` where the insertion point `LView[PARENT]` and declaration `LView[DECLARATION_VIEW]` do not match. In our example this would be `LView` created by the `LibComp` from the `TemplateRef` which was declared at `AppComp`. # Mental Model - The developer of application (`AppComp`) and the library author (`LibComp`) are two different developers, therefore the assumptions of one should not leak into the other. For example the `LibComp` should be free to decide if the `LibComp` is `OnPush` or `Always` without effecting the kinds of assumptions `AppComp` has. To put it differently `LibComp` should be able to change from `Always` to `OnPush` without breaking CD for the transplanted `LView`. - Implication of this is that the CD strategy is attached to the transplanted template and not to the insertion `LView`. (Author of the template controls the CD strategy.) | `AppComp` | `LibComp` | Outcome |-----------|-----------|--------- | `Always` | `Always` | Run CD through all `LView`s on each tick. | `Always` | `OnPush` | Changing `AppComp.name` should trigger the update of the binding `{{greeting}} {{name}}!` regardless if `LibComp` is dirty. | `OnPush` | `Always` | Changing `LibComp.greeting` should trigger the update of the `{{greeting}} {{name}}!` regardless if the `AppComp` is dirty. | `OnPush` | `OnPush` | Changing `LibComp.greeting` or `AppComp.name` should trigger the CD of the template. What the above table suggest is that the transplanted`LView` should be CDed if either the `AppComp.name` or the `LibComp.greeting` changes. In other words the transplant `LView` is subject to being marked dirty by either the `AppComp` or `LibComp` being marked dirty. NOTE: - If the transplant `LView` is detached it should not be CDed. (This behavior is different from VE) Suppose both `AppComp` and `LibComp` are `OnPush`. Which views should be CDed when either `AppComp` or `LibComp` change state and are marked dirty? <div style="font-size: 11px"> |`AppComp` `.name`|`LibComp` `.greeting`|Transplanted `{{greeting}} {{name}}!`| `AppComp: {{name}}!`|`LibComp: {{greeting}}!`|Outcome |---|---|---|---|---|--- |- |- |- |- |- | when neither component is marked dirty a CD run should not update any of the bindings. | mutate | - | Update | Update | - | When `AppComp` is marked dirty than `{{greeting}} {{name}}!`, `AppComp: {{name}}!` should be updated, but `LibComp: {{greeting}}!` should not be updated. | - | mutate | Update | - | Update | When `LibComp` is marked dirty than `{{greeting}} {{name}}!`, `LibComp: {{greeting}}!` should be updated, but `AppComp: {{name}}!` should not be updated. | mutate | mutate | Update | Update | Update | When `LibComp` and `AppComp` is marked dirty than `{{greeting}} {{name}}!`, `LibComp: {{greeting}}!`, and `AppComp: {{name}}!` should be updated. </div> The implications from the above table are that: - `AppComp` being marked dirty should CD its transplanted `LView` but it should not cause CD in `LibComp`. - This is a breaking change with respect to VE, which (I believe) causes the CD in `LibComp` as well. - `LibComp` being marked dirty should CD its transplanted `LView` but it should not cause CD in `AppComp`. - (I belive) this is consistent with VE behavior. - A detached transplanted `LView` should not be CD in either case. - This is a breaking change with respect to VE, which (I believe) causes the CD even if the transplanted `LView` is detached. # Proposed Fix The implication from the above discussion is that the `<ng-template>` needs to keep track of the transplanted `LView` instances so that it can invoke change detection on them from both declared as well as inserted view. - [ ] `LView` currently keeps track of all components in `LView[CHILD_HEAD|CHILD_TAIL|NEXT]` (in addition to `LView`). This is unnecessary because we already keep track of all child components in `TView.components`. Removing them would speed up creation and updates as tracking and iterating requires time. - [ ] All transplanted `LView`s should be added the the declared `LView` linked-list. - Only add if `LView[PARENT] !== LView[DECLARATION_VIEW` (transplanted) and `LView[PARENT] !== null` (attached). - [ ] Component's linked-list should also contain `LView[PREV]` to make it efficient to remove transplanted `LView`s. This is an additional cost to the `LView` as it will increase its header size by one slot. - Change Detection Processing: - [x] During CD of `LibComp` we should always visit any (including transplanted) view at insertion point (current behavior). - [ ] During CD of declaration `LView` we should also visit the transplanted views in the `LView[CHILD_HEAD|CHILD_TAIL|NEXT]`. - The implication of this is that in the case of transplanted `LView` we will run the CD on the transplanted `LView` twice. Once from the `AppComp` and once from the `LibComp`. We could guard against this, but that would require additional work and checks, and given that this scenario is rare, I don't think it is worth worrying about this double checking. # Open Issues - [ ] This does not solve the problem of having the transplanted View inserted in the `LibComp` and than have the `LibCom` be detached. In such a situation one would expect that the template does not get CD under any use case, but in our case it would get CDed in the case when the `AppCom` would get marked as dirty.