Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request: Example of passing complex content projection #30

Closed
ChazUK opened this issue May 12, 2020 · 19 comments
Closed

Request: Example of passing complex content projection #30

ChazUK opened this issue May 12, 2020 · 19 comments
Labels
need doc That issue should be shown in documentation resolved That issue was resolved

Comments

@ChazUK
Copy link

ChazUK commented May 12, 2020

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
Copy link
Contributor

thekiba commented May 12, 2020

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();

@thekiba
Copy link
Contributor

thekiba commented May 18, 2020

Hey @ChazUK, did this solve your problem?

@xeladotbe
Copy link

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
Copy link
Contributor

thekiba commented Jul 17, 2020

Hello, @xeladotbe.

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

@xeladotbe
Copy link

xeladotbe commented Jul 17, 2020

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
Copy link
Contributor

thekiba commented Jul 17, 2020

@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
Copy link

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

@thekiba
Copy link
Contributor

thekiba commented Jul 17, 2020

@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
Copy link

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
Copy link
Contributor

thekiba commented Jul 17, 2020

@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
Copy link

sounds good :) thank you!

@thekiba thekiba added need doc That issue should be shown in documentation resolved That issue was resolved labels Jul 17, 2020
@xeladotbe
Copy link

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
Copy link
Contributor

thekiba commented Oct 7, 2020

Hey @xeladotbe,

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

@xeladotbe
Copy link

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
Copy link
Contributor

alQlagin commented Oct 8, 2020

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
Copy link

Hey @alQlagin ,

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

@alQlagin
Copy link
Contributor

alQlagin commented Oct 8, 2020

@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
Copy link

@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
Copy link
Contributor

alQlagin commented Oct 8, 2020

@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

@ChazUK ChazUK closed this as completed Jan 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
need doc That issue should be shown in documentation resolved That issue was resolved
Projects
None yet
Development

No branches or pull requests

4 participants