Conditional duplicate templateref in ng-content with selector

Fartab picture Fartab · Apr 28, 2018 · Viewed 7.3k times · Source

I have a component which toggles the component's template based on client device size. Component code is:

import {Component} from '@angular/core';
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';

@Component({
    selector: 'ui-switcher',
    template: `
        <ng-content *ngIf="isSmall" select="mobile"></ng-content>
        <ng-content *ngIf="!isSmall" select="web"></ng-content>
`
})
export class UiSwitcherComponent {
    public isSmall: boolean;

    constructor(breakpointObserver: BreakpointObserver) {
        breakpointObserver.observe([Breakpoints.Small, Breakpoints.XSmall]).subscribe(result => {
            this.isSmall = result.matches;
        });
    }    
}

I use it like this:

<ui-switcher>
    <web>
        <!-- some commented details -->
        <input class="form-control mr-2" #searchInput 
        type="text" (keyup)="this.search(searchInput.value)">
    </web>

    <mobile>
        <!-- some commented details -->
        <input class="form-control" #searchInput 
        type="text" (keyup)="this.search(searchInput.value)">
    </mobile>
</ui-switcher>

In the mobile size, everything works correctly but in desktop size the value passed to search(value) function is always an empty string.

When I debug the app, it seems that #searchInput templateref is not working correctly (value of the element it refers to is always empty).

Why templateref doesn't work correctly?

Answer

yurzui picture yurzui · Apr 28, 2018

In angular template reference variables should be unique per view.

Views can be two types View and EmbeddedView. Templates, that we write within structural directives(inside ng-template tag or *ngFor), represent embedded views. This way we can have the same name of template reference variable in different ng-templates.

For an example see

Let's imagine we have AppComponent and wrote in template:

<ui-switcher>
    <web>
        <!-- some commented details -->
        <input class="form-control mr-2" #searchInput 
        type="text" (keyup)="this.search(searchInput.value)">
    </web>

    <mobile>
        <!-- some commented details -->
        <input class="form-control" #searchInput 
        type="text" (keyup)="this.search(searchInput.value)">
    </mobile>
</ui-switcher>

Angular treats it as one AppComponentView because there is no any structural directives in this template. Both inputs belong to the same view.

Now when Angular compiler parses this template it creates one ViewBuilder per view with refNodeIndices property:

private refNodeIndices: {[refName: string]: number} = Object.create(null);

that holds all references in current template.

Let's reproduce your case: enter image description here

We can see that second template reference variable overrides previous.

And as result Angular handles click event on the same element:

enter image description here