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 })

4
  • You may be able to use [innerHTML] property binding. Worth a shot stackoverflow.com/questions/34936027/… Commented May 19, 2017 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. Commented May 19, 2017 at 15:51
  • Try perhaps injecting ChangeDetectorRef then calling detectChanges(). Or the other approach seems to be injecting NgZone then executing run(). Commented May 19, 2017 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 Commented May 19, 2017 at 16:23

2 Answers 2

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

Sign up to request clarification or add additional context in comments.

Comments

-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];
  }

}

1 Comment

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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.