Indigosoft / ngxd

✨🦊 NgComponentOutlet + Data-Binding + Full Lifecycle = NgxComponentOutlet for Angular 7, 8, 9, 10, 11, 12, 13, 14, 15, 16+
MIT License
318 stars 29 forks source link

Request: Example of passing complex content projection #30

Closed ChazUK closed 2 years ago

ChazUK commented 4 years ago

Hi,

I'm looking to use NGXD to load dynamic components and content provided by a headless CMS like Contentful, but I've come into a bit of an issue where I'm struggling to figure out how to use content projection with the dynamically loaded components.

I have an X Column component that can host a number of predefined components, some of which are able to take complex content using ng-content. Is this something this package can handle? And is it possible to get an example?

Here's some demo code I've created to explain the situation https://stackblitz.com/edit/angular-simple-dynamic-8emvyb?file=src/app/app.module.ts

thekiba commented 4 years ago

Hello, @ChazUK

If you want to have using ng-content with dynamic components, you have to create projectableNodes by self. I'll explain to you what do you need to do.

  1. You have to create an <ng-template> and put an <ng-content> in it.

    @Component({
    selector: 'app-x-column',
    template: `
    <!-- 👇 place TemplateRef with NgContent here -->
    <ng-template #templateRef>
      <ng-content></ng-content>
    </ng-template>
    `
    })
    class XColumnComponent {}
  2. Then you have to get the TemplateRef using by @ViewChild() decorator.

    @Component({
    selector: 'app-x-column',
    template: `
    <ng-template #templateRef>
      <ng-content></ng-content>
    </ng-template>
    `
    })
    class XColumnComponent {
    // 👇 getting access to the TemplateRef
    @ViewChild('templateRef', { 
    static: true, 
    read: TemplateRef
    })
    templateRef: TemplateRef<{}>;
    }
  3. After that you need to create a projectableNodes. In order to do this you have to create a ViewRef of the TemplateRef and get rootNodes. Don't forget to place the projectableNodes in *ngxComponentOutlet="content: projectableNodes".

@Component({
  selector: 'app-x-column',
  template: `
    <ng-template #templateRef>
      <ng-content></ng-content>
    </ng-template>
    <ng-container *ngxComponentOutlet="
      <!-- putting 👇 projectableNodes into the content of NgxComponentOutlet -->
      component; content: projectableNodes"></ng-container>
  `
})
class XColumnComponent {
  projectableNodes: any[][];

  @ViewChild('templateRef', { 
    static: true, 
    read: TemplateRef
  })
  set templateRef(t: TemplateRef<{}>) {
    // 👇 creating projectableNodes
    this.projectableNodes = [
      t.createEmbeddedView({}).rootNodes
    ];
  };
}
  1. Don't forget to destroy the ViewRef, otherwise, this can lead to leaks.
    this.viewRef = t.createEmbeddedView({});
    // some later
    this.viewRef.destroy();
thekiba commented 4 years ago

Hey @ChazUK, did this solve your problem?

xeladotbe commented 4 years ago

Hey @thekiba,

that looks very interesting. Would it also be possible to combine it with https://stackblitz.com/edit/angular-simple-dynamic?file=src%2Fapp%2Fapp.module.ts ?

Instead of [{component: InfoCardComponent, title: 'Info Card 1', content: <p>Complex Content</p>}, {component: InfoCardComponent, title: 'Another Info Card', content: <p>Complex <a href="#">Content</a></p>}]

I would like to use [{component: 'info-card', title: 'Info Card 1', content: <p>Complex Content</p>}, {component: 'info-card', title: 'Another Info Card', content: <p>Complex <a href="#">Content</a></p>}].

and then lazy load the InfoCardComponent, for example using the resolve method.

something like:

const type: Type<any> = await import('../components/info-card/info-card.component').then(module => module.InfoCardComponent'); return this.componentFactoryResolver.resolveComponentFactory(type);

Regards, Alex

thekiba commented 4 years ago

Hello, @xeladotbe.

Could you please confirm that I understand you right: you want to use html string with dynamic components?

xeladotbe commented 4 years ago

Hey @thekiba ,

sorry, I was a bit hasty with the copying. Of course I don't want to insert pre-generated markup. I've a static json with data for example:

"headline": { "value": "My page headline" "tag": "h1" }

and I would like to get them later via auto binding:

@Input() headline: IHeadline;

This is what I've done so far:


  async loadComponents(data: any) {
    const components = await data.reduce(async (components: any, entry: any) => {
      const r = await components;

      const { component } = entry;
      const { alias } = component;

      const type: Type<any> = await import(/* webpackChunkName: "[request]", webpackMode: "lazy-once" */ `../${alias}/${alias}.component`).then(module => {
        const [ componentType ] = Object.keys(module);

        return module[componentType];
      }).catch(async error => {
        return await import('../noop/noop.component').then(module => module.NoopComponent);
      });

      r.push({ ...entry, instance: type });

      return Promise.resolve(r);
    }, Promise.resolve([]));

    this.components = components;

    this.cdr.detectChanges();
  }
thekiba commented 4 years ago

@xeladotbe I hope an example below is what you want to do https://stackblitz.com/edit/angular-ivy-ngxd-lazy-resolver-simple-demo?file=src%2Fapp%2Fapp.component.ts

Could you please check it and to say whether it's right for you?

xeladotbe commented 4 years ago

@thekiba that looks really helpful! did you just do that?

thekiba commented 4 years ago

@xeladotbe I did it some months ago 🦊

And I know that I have to give more documentation for the NGXD 😅 Hope that I'll todo it soon

xeladotbe commented 4 years ago

the documentation could really use some love. is there a way to set a default in the resolver for types that do not have a specific component? keep up the good work! and can I buy you a coffee? ;)

thekiba commented 4 years ago

@xeladotbe If you want to return a default component you have to make it in the resolver, see an example below:

resolve(type: T): Type<R> {
  if (exists(type) {
    return resolve(type);
  } else {
    return getDefaultComponent();
  }
}

We can just to drink a coffee ☕️ when I'll visit Germany or you'll in Moscow 😉

xeladotbe commented 4 years ago

sounds good :) thank you!

xeladotbe commented 3 years ago

Hello, @ChazUK

If you want to have using ng-content with dynamic components, you have to create projectableNodes by self. I'll explain to you what do you need to do.

  1. You have to create an <ng-template> and put an <ng-content> in it.
@Component({
  selector: 'app-x-column',
  template: `
    <!-- 👇 place TemplateRef with NgContent here -->
    <ng-template #templateRef>
      <ng-content></ng-content>
    </ng-template>
  `
})
class XColumnComponent {}
  1. Then you have to get the TemplateRef using by @ViewChild() decorator.
@Component({
  selector: 'app-x-column',
  template: `
    <ng-template #templateRef>
      <ng-content></ng-content>
    </ng-template>
  `
})
class XColumnComponent {
  // 👇 getting access to the TemplateRef
  @ViewChild('templateRef', { 
    static: true, 
    read: TemplateRef
  })
  templateRef: TemplateRef<{}>;
}
  1. After that you need to create a projectableNodes. In order to do this you have to create a ViewRef of the TemplateRef and get rootNodes. Don't forget to place the projectableNodes in *ngxComponentOutlet="content: projectableNodes".
@Component({
  selector: 'app-x-column',
  template: `
    <ng-template #templateRef>
      <ng-content></ng-content>
    </ng-template>
    <ng-container *ngxComponentOutlet="
      <!-- putting 👇 projectableNodes into the content of NgxComponentOutlet -->
      component; content: projectableNodes"></ng-container>
  `
})
class XColumnComponent {
  projectableNodes: any[][];

  @ViewChild('templateRef', { 
    static: true, 
    read: TemplateRef
  })
  set templateRef(t: TemplateRef<{}>) {
    // 👇 creating projectableNodes
    this.projectableNodes = [
      t.createEmbeddedView({}).rootNodes
    ];
  };
}
  1. Don't forget to destroy the ViewRef, otherwise, this can lead to leaks.
this.viewRef = t.createEmbeddedView({});
// some later
this.viewRef.destroy();

Hey @thekiba ,

do you have a complete working example of this? I've tried to integrate your suggestions into the example of ChazUK but I can't get it to work.

What I want:

I've a headless CMS which provides me a JSON, I can render the component without problems, but partly it is allowed to use markdown in a text, now I want to convert the markdown to HTML and output it. The generated HTML can contain for example [ngModel]="..." or [routerLink]="...". directives, I thought with content projection I can display it without problems, but I don't know how.

Pseudo code:

<ng-template #paragraph>
  <ng-content></ng-content>
</ng-template>

<ng-container *ngFor="let paragraph of paragraphs">
  <ng-container #paragraph>
    {{paragraph.value | transformToHTML | safeHTML}}
  </ng-container>
</ng-container>

paragraph.value = Hi *there*! [How are you](how-are-you)

transformToHTML = Hi <strong>there</strong! <a [routerLink]="['how-are-you']">How are you</a> 

safeHTML = DomSanitizer.bypassSecurityTrustHtml

To render my components I use:

<ng-container *ngFor="let component of components; index as index">
  <ng-container *ngxComponentOutlet="component.instance; context: component.data"></ng-container>
</ng-container>

One component in my case would be "MediaTextComponent" with the following definition

export interface MediaTextComponent {
  headline: string | undefined;
  text: string | undefined;
  paragraphs: Array<{
    headline: string | undefined;
    value: string
  }>
}

Thanks in advance for your help!

Regards, Alex

thekiba commented 3 years ago

Hey @xeladotbe,

Could you please reproduce an example on the StackBlitz? This will help me to better understand the problem to help you.

xeladotbe commented 3 years ago

Hey @thekiba ,

that was the last thing I tried out of desperation https://stackblitz.com/edit/angular-simple-dynamic-h5rxqr?file=src/app/app.module.ts

alQlagin commented 3 years ago

Hey @xeladotbe!

You doing it wrong. You should pass component ngxComponentOutlet instead of templateRef. In your case you shold use

<p *ngFor="let paragraph of paragraphs" [innerHtml]="paragraph?.value | toHTML"></p>

But the [routerLink] from pipe won't work anyway

xeladotbe commented 3 years ago

Hey @alQlagin ,

thanks! Is there any way to make the routerLinks work?

alQlagin commented 3 years ago

@xeladotbe this question is out of scope for this issue. In short you can't apply any dicrectives to html content from CMS. But you can handle link click and use Router api. See this example https://stackblitz.com/edit/angular-inner-html-links?file=src/app/app.component.ts

xeladotbe commented 3 years ago

@alQlagin thanks for the example, is this a best practice in angular? are there no standard solutions for this? I thought I could solve the problem with the help of content projection, hence the question what I have to do to get my dynamic html running with the help of content projection and ngxComponentOutlet

alQlagin commented 3 years ago

@xeladotbe I'm not sure about best practice but it works. Maybe @thekiba could tell his solution.

By the way html from CMS is not dynamic content projection. Content projection works only for compiled code. Possibly you can use JIT, but it's a bad practice