Search code examples
angularangular2-components

How to create a Component with a dynamic template? (Component transclude with inline template)


I'm trying to create a component that has a dynamic template string inside of it that can access the local variables on the template. Every approach I've tried ends up with the "dynamic template string" not being $compile'd (angular 1 terminology, please excuse me).

Here is the code for the component below. Where you see the comment I would like to insert a template string that can reference item in the ngFor.

@Component({
  selector: 'ion-alpha-scroll',
  template: `
    <ion-scroll [ngStyle]="calculateScrollHeight()" scrollX="false" scrollY="true">
      <ion-list class="ion-alpha-list-outer">
        <div *ngFor="let items of sortedItems | mapToIterable;">
          <ion-item-divider id="scroll-letter-{{items.key}}">{{items.key}}</ion-item-divider>
          <ion-item *ngFor="let item of items.value">
            <!-- how can I pass a dynamic template here that can reference item ? -->
          </ion-item>
        </div>
      </ion-list>
    </ion-scroll>
    <ul class="ion-alpha-sidebar" [ngStyle]="calculateDimensionsForSidebar()">
      <li (click)="alphaScrollGoToList(letter)" *ngFor="let letter of alphabet">
        <div class="letter">{{letter}}</div>
      </li>
    </ul>
  `,
  pipes: [MapToIterable]
})
export class IonAlphaScroll {
  @Input() listData: any;
  @Input() key: string;
  @Input() template: string;
  ....
}

Ideally I would like to have the transcluded content of the ion-alpha-scroll reference the item in the ngFor. I tried using ng-content in the necessary ngFor of the component and had no luck -

<ion-alpha-scroll *ngIf="breeds" [listData]="breeds" key="$t">
  {{item.$t}}
</ion-alpha-scroll>

One thing I tried was like this -

<ion-alpha-scroll *ngIf="breeds" [listData]="breeds" key="$t" [template]="alphaScrollTemplate">
</ion-alpha-scroll>

The alphaScrollTemplate is just a string containing {{item.$t}}. I then tried to reference it in the component where the comment is asking the question but it doesn't work -

...
<ion-item *ngFor="let item of items.value">
  {{template}}
  <!-- this just outputs {{item.$t}} literally -->
</ion-item>
...

I'm really curious if this is even possible with angular 2 yet. I just found this question which is very similar to mine. Any help or suggestions will be greatly appreciated, thanks.


Solution

  • Here is the solution I used for angular 2.0.0-rc.3

    This solution creates a dynamic component and loads it by using ViewContainerRef and ComponentFactory. Here is the ionic 2 component on GitHub.

    export function createComponentFactory(resolver: ComponentResolver, metadata: ComponentMetadata): Promise<ComponentFactory<any>> {
      const cmpClass = class DynamicComponent {};
      const decoratedCmp = Component(metadata)(cmpClass);
      return resolver.resolveComponent(decoratedCmp);
    }
    
    @Directive({
      selector: 'dynamic-html-outlet',
    })
    export class DynamicHTMLOutlet {
      @Input() src: string;
      @Input() ionAlphaScrollRef: any;
      @Input() currentPageClass: any;
    
      constructor(private vcRef: ViewContainerRef, private resolver: ComponentResolver) {
      }
    
      ngOnChanges() {
        if (!this.src) return;
    
        const metadata = new ComponentMetadata({
          selector: 'dynamic-html',
          template: this.src,
          pipes: [MapToIterable]
        });
        createComponentFactory(this.resolver, metadata).then(factory => {
          const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
          let component = this.vcRef.createComponent(factory, 0, injector, []);
          component.instance.ionAlphaScrollRef = this.ionAlphaScrollRef;
          component.instance.currentPageClass = this.currentPageClass;
        });
      }
    }
    
    @Component({
      selector: 'ion-alpha-scroll',
      template: `
        <dynamic-html-outlet
          [src]="alphaScrollTemplate"
          [ionAlphaScrollRef]="ionAlphaScrollRef"
          [currentPageClass]="currentPageClass">
        </dynamic-html-outlet>
      `,
      pipes: [MapToIterable],
      directives: [DynamicHTMLOutlet]
    })
    export class IonAlphaScroll {
      @Input() listData: any;
      @Input() key: string;
      @Input() itemTemplate: string;
      @Input() currentPageClass: any;
      @Input() triggerChange: any;
      private _scrollEle: HTMLElement;
      sortedItems: any = {};
      alphabet: any = [];
      alphaScrollTemplate: string;
      ionAlphaScrollRef = this;
    
      constructor(
        @Host() private _content: Content,
        private _elementRef: ElementRef,
        private vcRef: ViewContainerRef,
        private resolver: ComponentResolver
      ) {
    
      }
    
      ngOnInit() {
        this.alphaScrollTemplate = `
          <style>
            .ion-alpha-sidebar {
              position: fixed;
              right: 0;
              display: flex;
              flex-flow: column;
              z-index: 50000;
            }
    
            .ion-alpha-sidebar li {
              flex: 1 1 auto;
              list-style: none;
              width: 15px;
              text-align: center;
            }
          </style>
    
          <ion-scroll class="ion-alpha-scroll" [ngStyle]="ionAlphaScrollRef.calculateScrollDimensions()" scrollX="false" scrollY="true">
            <ion-list class="ion-alpha-list-outer">
              <div *ngFor="let items of ionAlphaScrollRef.sortedItems | mapToIterable; trackBy:ionAlphaScrollRef.trackBySortedItems">
                <ion-item-divider id="scroll-letter-{{items.key}}">{{items.key}}</ion-item-divider>
                <ion-item *ngFor="let item of items.value">
                  ${this.itemTemplate}
                </ion-item>
              </div>
            </ion-list>
          </ion-scroll>
          <ul class="ion-alpha-sidebar" [ngStyle]="ionAlphaScrollRef.calculateDimensionsForSidebar()">
            <li *ngFor="let letter of ionAlphaScrollRef.alphabet" tappable (click)="ionAlphaScrollRef.alphaScrollGoToList(letter)">
              <a>{{letter}}</a>
            </li>
          </ul>
        `;
    
        setTimeout(() => {
          this._scrollEle = this._elementRef.nativeElement.querySelector('scroll-content');
          this.setupHammerHandlers();
        });
      }
    
      ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
        let tmp = {};
        for (let i = 0; i < this.listData.length; i++) {
          let letter = this.listData[i][this.key].toUpperCase().charAt(0);
          if (typeof tmp[letter] === 'undefined') {
            tmp[letter] = [];
          }
          tmp[letter].push(this.listData[i]);
        }
    
        this.alphabet = this.iterateAlphabet(tmp);
        this.sortedItems = tmp;
      }
    
      // ....
    }