angularng-content

Get the original / raw template value of ng-content


Is it possible to get the pre-processed value of ng-content? I am building design-system docs and want to display the example code without repeating myself

html:

<tr>
  <td #contentEl>
    <ng-content #projected></ng-content>
  </td>
  <td>
    <mat-icon (click)="copyTagToClipboard()">content_paste</mat-icon>
  </td>
  <td>
    <pre>{{ contentEl.innerHTML }}</pre>
  </td>
</tr>

typescript

export class CodeExampleComponent implements OnInit, AfterContentInit {
  @ViewChild('contentEl') contentEl: HTMLTableCellElement;
  @ContentChild('template') template;
  @ViewChild('projected', { read: TemplateRef, static: true }) content;

  constructor() {}

  ngOnInit() {
    console.log(this.content);
  }

  ngAfterContentInit(): void {
    console.log(this.template);
  }
  
  copyTagToClipboard() {
    if (!this.contentEl) return;
    copyToClipboard(this.contentEl.innerHTML);
  }
}

Usage:

  <app-code-example>
    <template #template><app-footnote>Footnote</app-footnote></template>
  </app-code-example>

All I can ever get is the angular rendered contents of the ng-content/template, not the original raw template.


Solution

  • This doesn't necessarily answer your question about getting the pre-processed HTML from ng-content. However this approach can be used to solve your issue about displaying the HTML as an example. You can import the raw html as a string and use it as a variable. What I like to do is then create a component or multiple components that demonstrates usage of some component I'm documenting.

    This is kind of a lot, so I put it all in a StackBlitz to try and demonstrate.

    Create a file right in the src directory called typings.d.ts that allows us to import html files as text.

    declare module "*.html" {
      const content: string;
      export default content;
    }
    

    Create an abstract class that all our example component need to implement. We need it to be abstract because we're going to be using QueryList later.

    export abstract class Example {
      templateText: string; //the raw html to display 
    }
    

    Create a example wrapper component that displays the live example, the raw html and has your copy button

    import { Component, ContentChildren, Input, QueryList } from "@angular/core";
    import { Example } from "../example";
    
    @Component({
      selector: "app-example",
      template: `
        <div class="wrapper">
            <h1>{{label}}</h1>
            <ng-content></ng-content>
        
            <br />
            <button (click)="copyTagToClipboard()">Copy Content</button>
        
            <div>
                <pre>{{ example.templateText }}</pre>
            </div>
        </div>
      `
    })
    export class ExampleComponent{
      @Input() label: string; 
      
      @ContentChildren(Example) items: QueryList<Example>;
      example: Example;
    
      ngAfterContentInit(): void {
        //grab the first thing that implements the "Example" interface
        //hypothetically this component could be re-worked to display multiple examples!
        this.example = this.items.toArray()[0];    
      }
    
      copyTagToClipboard() {
        alert(this.example.templateText);
      }
    }
    

    Build a component that demonstrates how to use whatever component you are documenting. Note how we import the html file as a variable and use it as an attribute of the component, satisfying the interface. Don't forget to set up the providers so QueryList can pick up on the component. You can then build as many of these examples as you need, perhaps a different component per use case.

    import { Component, TemplateRef, ViewChild } from "@angular/core";
    import { Example } from "../example";
    
    // See typings.d.ts, which sets up the module that allows us to do this
    import * as template from "./my-component-example.component.html";
    // or const template = `<app-my-component [label]="myLabelValue"></app-my-component>`;
    
    @Component({
      selector: "app-my-component-example",
      template,
      styleUrls: ["./my-component-example.component.css"],
    
      //this is so the QueryList works in the example wrapper
      providers: [{ provide: Example, useExisting: MyComponentExampleComponent }]
    })
    export class MyComponentExampleComponent implements Example {
      myLabelValue = "Test Label!!";
      templateText = template.default;
    }
    

    Lastly, put it all together

    <app-example label="Basic Example">
      <app-my-component-example></app-my-component-example>
    </app-example>
    

    I feel like with this approach there's a lot of flexibility in you how create each example.