2

I making a simple component to create tables:

@Component({
  selector: 'admin-table',
  template: `
    <table class='table table-bordered'>
      <thead>
      <th *ngFor='let column of columns'>
        {{ column.label }}
      </th>
      </thead>
      <tbody>
      <tr *ngFor="let record of records">
        <td *ngFor='let column of columns' [innerHTML]="fieldContent(column, record) | safeHtml">
        </td>
      </tr>
      </tbody>
    </table>
  `,
})
export class AdminTableComponent {

  @Input() columns: AdminTableColumn[];

  @Input() records: {}[];

  fieldContent(column: AdminTableColumn, record: {}) {
    if (column.template) {
      //TODO: parse the template and pass current record as argument
      return column.template;
    }

    return record[column.field];
  }
}

and other component to create a table of products using the above component

@Component({
  selector: 'product-admin',
  template: `
    <h1>Products</h1>
    <admin-table [columns]="columns" [records]="products"></admin-table>
  `,
  providers: [ProductService],
})
export class ProductAdminComponent implements OnInit {

  products: Product[];

  columns: AdminTableColumn[] = [
    {
      field: 'id',
      label: 'SKU',
    },
    {
      field: 'name',
      label: 'Name',
      template: '<strong>{{record.name}}</strong>',
    }
  ];
}

Like you can see the AdminTableColumn has a additional option called template to set the value of the cell using a template. But I can't do this when try to render the value I got {{record.name}} instead of the real product name.

I need parse the value entered in the template option to allow the use of angular declarations like: {{record.name}} or <some-component [title]="record.name"></some-component> in order to create a rich table.

in other words, exists something like render(template, { record: record })

rafrsr
  • 1,940
  • 3
  • 15
  • 31
  • You may be able to use `[innerHTML]` property binding. Worth a shot http://stackoverflow.com/questions/34936027/angular-2-how-do-you-render-html-from-a-json-response-without-displaying-the-ta – Alexander Staroselsky May 19 '17 at 15:42
  • currenly i'm using `[innerHTML]`, see the line with `[innerHTML]="fieldContent(column, record) | safeHtml"` but the content is rendered as is (raw), but I need pass some variables like `record`. – rafrsr May 19 '17 at 15:51
  • Try perhaps injecting `ChangeDetectorRef` then calling `detectChanges()`. Or the other approach seems to be injecting `NgZone` then executing `run()`. – Alexander Staroselsky May 19 '17 at 15:59
  • I'm not sure how and where use `ChangeDetectorRef` or `NgZone`. I'm beginner with angular and this is my first project, can you help with some example? thanks – rafrsr May 19 '17 at 16:23

2 Answers2

4

You can build special directive for this purpose:

@Directive({
  selector: '[compile]'
})
export class CompileDirective implements OnChanges {
  @Input() compile: string;
  @Input() compileContext: any;

  compRef: ComponentRef<any>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler) {}

  ngOnChanges() {
    if(!this.compile) {
      if(this.compRef) {
        this.updateProperties();
        return;
      }
      throw Error('You forgot to provide template');
    }

    this.vcRef.clear();
    this.compRef = null;

    const component = this.createDynamicComponent(this.compile);
    const module = this.createDynamicModule(component);
    this.compiler.compileModuleAndAllComponentsAsync(module)
      .then((moduleWithFactories: ModuleWithComponentFactories<any>) => {
        let compFactory = moduleWithFactories.componentFactories.find(x => x.componentType === component);

        this.compRef = this.vcRef.createComponent(compFactory);
        this.updateProperties();
      })
      .catch(error => {
        console.log(error);
      });
  }

  updateProperties() {
    for(var prop in this.compileContext) {
      this.compRef.instance[prop] = this.compileContext[prop];
    }
  }

  private createDynamicComponent (template:string) {
    @Component({
      selector: 'custom-dynamic-component',
      template: template,
    })
    class CustomDynamicComponent {}
    return CustomDynamicComponent;
  }

  private createDynamicModule (component: Type<any>) {
    @NgModule({
      // You might need other modules, providers, etc...
      // Note that whatever components you want to be able
      // to render dynamically must be known to this module
      imports: [CommonModule],
      declarations: [component]
    })
    class DynamicModule {}
    return DynamicModule;
  }
}

AdminComponent

@Component({
  selector: 'admin-table',
  template: `
    <table class='table table-bordered'>
      <thead>
      <th *ngFor='let column of columns'>
        {{ column.label }}
      </th>
      </thead>
      <tbody>
      <tr *ngFor="let record of records">
        <td *ngFor='let column of columns'>
          <ng-container *ngIf="column.template as tmpl; else staticTmpl">
            <ng-container *compile="tmpl; context: { record: record }"></ng-container>
          </ng-container>
          <ng-template #staticTmpl>{{record[column.field]}}</ng-template>
        </td>
      </tr>
      </tbody>
    </table>
  `,
})
export class AdminTableComponent {
  @Input() columns: any[];

  @Input() records: {}[];
}

Plunker Example

See also

Community
  • 1
  • 1
yurzui
  • 205,937
  • 32
  • 433
  • 399
-1

I'm pretty sure Angular sanitizes the html injected through innerHtml so your string interpolation will not work there.

Instead you can try parsing the template in the fieldContent function and adding the records's keys directly.

Here is an example that uses a regexp to replace all instances of {{record[key]}}, whatever the key may be, and returning the interpolated string to be injected into your Html.

@Component({
  selector: 'admin-table',
  template: `
    <table class='table table-bordered'>
      <thead>
      <th *ngFor='let column of columns'>
        {{ column.label }}
      </th>
      </thead>
      <tbody>
      <tr *ngFor="let record of records">
        <td *ngFor='let column of columns' [innerHTML]="fieldContent(column, record) | safeHtml">
        </td>
      </tr>
      </tbody>
    </table>
  `,
})
export class AdminTableComponent {

  @Input() columns: AdminTableColumn[];

  @Input() records: {}[];

  fieldContent(column: AdminTableColumn, record: {}) {
    if (column.template) {
      let template = column.template;

      // Go through the keys and replace all isntances in the field.
      // Note that this is strict and will not replace {{ record.name }}
      // You may want to add additional regexp to do that.

      Object.keys(record).forEach(key => {
        template = template.replace(new RegExp(`{{record.${key}}}`, 'g'), 'some name');
      });

      return template;
    }

    return record[column.field];
  }

}
mikias
  • 416
  • 3
  • 10
  • Thanks for your answer but that approach already discard it, because is not possible to use a `pipe` or any other component inside the template. It's most like a render what I need. – rafrsr May 19 '17 at 16:17