7

I know the textbook rules on that <div *ngFor="let foo of foobars">{{foo.stuff}}</div> turns into <template ngFor let-foo="$implicit" [ngForOf]="foobars"><div>...</div></template>. My question is two-fold:

  • HOW?
  • What do I need to do to leverage this mechanism ("microsyntax") myself?

Ie turn <div *myDirective="item">{{item.stuff}}</div> into <template myDirective let-item="$implicit"><div>{{item.stuff}}</div></template>?

Since I read ngFor's source code top to bottom, I can only assume this dark magic is in the compiler somewhere, I've been up and down the angular github, but I can't put my finger on it. Help!

TDaver
  • 7,164
  • 5
  • 47
  • 94

2 Answers2

14

Yes, all magic happens in the compiler.

Let's take this template:

<div *ngFor="let foo of foobars">{{foo}}</div>

First it will be transformed to the following:

<div template="ngFor let foo of foobars>{{foo}}</div>

And then:

<template ngFor let-foo [ngForOf]="foobars"><div>{{foo}}</div></template>

In Angular2 rc.4 it looks like this enter image description here

First is generated ast tree node (Abstract Syntax Tree node) and then all magic happens in the TemplateParseVisitor.visitElement(https://github.com/angular/angular/blob/2.0.0-rc.4/modules/%40angular/compiler/src/template_parser.ts#L284) specifically at the bottom (https://github.com/angular/angular/blob/2.0.0-rc.4/modules/%40angular/compiler/src/template_parser.ts#L394)

if (hasInlineTemplates) {
  var templateCssSelector = createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
  var templateDirectiveMetas = this._parseDirectives(this.selectorMatcher, templateCssSelector);
  var templateDirectiveAsts = this._createDirectiveAsts(
      true, element.name, templateDirectiveMetas, templateElementOrDirectiveProps, [],
      element.sourceSpan, []);
  var templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(
      element.name, templateElementOrDirectiveProps, templateDirectiveAsts);
  this._assertNoComponentsNorElementBindingsOnTemplate(
      templateDirectiveAsts, templateElementProps, element.sourceSpan);
  var templateProviderContext = new ProviderElementContext(
      this.providerViewContext, parent.providerContext, parent.isTemplateElement,
      templateDirectiveAsts, [], [], element.sourceSpan);
  templateProviderContext.afterElement();

  parsedElement = new EmbeddedTemplateAst(
      [], [], [], templateElementVars, templateProviderContext.transformedDirectiveAsts,
      templateProviderContext.transformProviders,
      templateProviderContext.transformedHasViewContainer, [parsedElement], ngContentIndex,
      element.sourceSpan);
}
return parsedElement;

This method returns EmbeddedTemplateAst. It's the same as:

<template ngFor let-foo [ngForOf]="foobars"><div>{{foo}}</div></template>

If you want to turn:

<div *myDirective="item">{{item.stuff}}</div>

into

<template myDirective let-item><div>{{item.stuff}}</div></template>

then you need to use the following syntax:

<div *myDirective="let item">{{item.stuff}}</div>

But in this case you won't pass context. Your custom structural directive might look like this:

@Directive({
  selector: '[myDirective]'
})
export class MyDirective {
  constructor(
    private _viewContainer: ViewContainerRef, 
    private _templateRef: TemplateRef<any>) {}

   @Input() set myDirective(prop: Object) {
    this._viewContainer.clear();
    this._viewContainer.createEmbeddedView(this._templateRef, prop); <== pass context
  }
} 

And you can use it like:

<div *myDirective="item">{{item.stuff}}</div>

               ||
               \/

<div template="myDirective:item">{{item.stuff}}</div>

               ||
               \/

<template [myDirective]="item">
   <div>{{item.stuff}}</div>
</template>

I hope this will help you understand how structural directives work.

Update:

Let's see how it works (plunker)

*dir="let foo v foobars" => [dirV]="foobars"

enter image description here

So you can write the following directive:

@Directive({
  selector: '[dir]'
})
export class MyDirective {
  @Input()
  dirV: any;

  @Input()
  dirK: any;

  ngAfterViewInit() {
    console.log(this.dirV, this.dirK);
  }
}

@Component({
  selector: 'my-app',
  template: `<h1>Angular 2 Systemjs start</h1>
  <div *dir="let foo v foobars k arr">{ foo }</div>
  `,
  directives: [MyDirective]
})
export class AppComponent {
  foobars = [1, 2, 3];
  arr = [3,4,5]
}

Here is the corresponding Plunker

See also

Live example you can find here https://alexzuza.github.io/enjoy-ng-parser/

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Yes!!! I figured out the passing context yesterday, and I found the TEMPLATE_PREFIX thing in the compiler, so I was kinda getting to it, but your confirmation assures me. – TDaver Aug 04 '16 at 08:37
  • I now understand how it turns `*dir="let foo"` into `template dir let-foo`, but how does it know to turn `of foobars` into `[ngForOf]="foobars"`? Where does the ngFor prefix comes from? how does it know which one to turn into a let, and which one to prefix with the directive's name for a second directive? – TDaver Aug 04 '16 at 08:39
  • i'm not sure but seems `*dir="let foo of foobars"` will be `[dirOf]="toolbars` – yurzui Aug 04 '16 at 08:41
  • 1
    `*dir="let foo v foobars"` => `[dirV]="foobars"` https://plnkr.co/edit/myE61PkX7UU4nAmGDQ1x?p=preview – yurzui Aug 04 '16 at 08:49
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/120087/discussion-between-yurzui-and-tdaver). – yurzui Aug 04 '16 at 08:56
  • @yurzui, do you know why it's `[myDirective]="item"` with square brackets instead of `myDirective="item"` in here `< template [myDirective]="item">` ? I expected it to be in the former form without square brackets – Max Koretskyi Jan 14 '17 at 17:42
  • 1
    @yurzui, also, how did you manage to debug `ts` files? I've created a [separate question](http://stackoverflow.com/questions/41651866/how-to-debug-angular2-typescript-files). It seems that in the latest npm versions they bundle everything without sourcemaps – Max Koretskyi Jan 14 '17 at 17:43
  • `myDirective` is a property binding See also http://stackoverflow.com/questions/39925916/whats-the-difference-between-value-todo-title-and-value-todo-title/39928705#39928705 and http://stackoverflow.com/questions/40200802/difference-between-2-type-of-displaying-data-in-angular-2/40200828#40200828 – yurzui Jan 15 '17 at 13:13
  • Yeah i still can debug `ts` source file but i usually use es-2015 files – yurzui Jan 15 '17 at 13:17
0

*ngFor, *ngIf, ... are structural directives.

Either apply it on a <template> element or prefix it with a *

https://angular.io/docs/ts/latest/guide/structural-directives.html#!#unless

import { Directive, Input } from '@angular/core';
import { TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
    ) { }
  @Input() set myUnless(condition: boolean) {
    if (!condition) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567