3

Problem - I'm passing a ViewContainerRef into my constructor to instantiate a component named 'MemberCardComponent'. Then I'm trying to fill it with input data, but no data is being populated into the innerHTML.

Stackblitz - I've setup this SB demonstrate my issue: Stackblitz link

const component = this.viewContainerRef.createComponent(MemberCardComponent);
component.instance.MemberCard = memberData; // memberData fetched from Db
const memberCardHtml = component.location.nativeElement.innerHTML; // innterHTML doesn't have MemberCard data.

MemberCard is an input property that looks like this:

@Input() set MemberCard(member: IMemberCard) {
    this.member = member;
}

I see the inner html isn't filled with any of the data. All the fields are empty, where there should be member data. Ex. photoUrl, name, etc

QUESTION - Is there some other action I need to perform to get my input data into the innerHTML?

Here is what I see when I hover over the innerHtml. enter image description here

'<div _ngcontent-yft-c87="" class="card"><div _ngcontent-yft-c87="" class="row g-0 row-eq-height"><div _ngcontent-yft-c87="" class="col-md-4"><picture _ngcontent-yft-c87=""><source _ngcontent-yft-c87="" media="(min-width: 0px) and (max-width: 350px)"><source _ngcontent-yft-c87="" media="(min-width: 351px) and (max-width: 720px)"><source _ngcontent-yft-c87="" media="(min-width: 721px) and (max-width: 1001px)"><source _ngcontent-yft-c87="" media="(min-width: 1002px)"><img _ngcontent-yft-c87="" alt…"float-end"></span></div><div _ngcontent-yft-c87="" class="col-12"><span _ngcontent-yft-c87="">Experience</span><span _ngcontent-yft-c87="" class="float-end"></span></div><div _ngcontent-yft-c87="" class="col-12"><span _ngcontent-yft-c87=""></span><span _ngcontent-yft-c87="" class="float-end"></span></div><div _ngcontent-yft-c87="" class="col-12"><span _ngcontent-yft-c87="">Education</span><span _ngcontent-yft-c87="" class="float-end"></span></div></div></div></div></div><!--container--></div>'

FYI - I'm trying to create the string representation of the component to populate a swiper.js slide. Here is another SO post I created that explains why I need the string representation of my component.

chuckd
  • 13,460
  • 29
  • 152
  • 331
  • If the `memberData` is fetched from db, are you sure the `memberData` exists at the time you assign it to `component.instance.MemberCard`? Also what does `component.location.nativeElement.innerHTML` do? You aren't assigning anything to it? – Swiffy Jun 10 '22 at 14:54
  • @Swiffy yes I'm sure memberData has values. InnerHTML should, as far as I know, fetch the html of the component, with filled in property data, but it doesn't have the data being passed to it. – chuckd Jun 10 '22 at 17:31

2 Answers2

4
TL;DR: Add a template so the component can be evaluated and rendered.

Your approach of using innerHTML is not recommended as createComponent is meant to be used together with a template.

To fulfil the expected result I've fixed your Stackblitz example, and it shows the expected behaviour using both approaches, however the innerHTML way needs to be used with a wrapper so the TestComponent changeDetector has a chance to render variable values.

The main changes I did are the following:

  1. Added a custom template so the TestCompnent values can be evaluated
@ViewChild('customTemplate', { read: ViewContainerRef, static: true })
customTemplate: ViewContainerRef;
  1. Added the ViewContainerRef content to the template so it gets rendered, otherwise there's no way to get its html
this.customTemplate.insert(this.viewContainerRef.get(0))
  1. Added a detectChanges which evaluates both detection changes from the parent and its child
detectChanges(): void {
  if (this.customComponent?.changeDetectorRef) {
    this.customComponent.changeDetectorRef.detectChanges();
  } else {
    if (this.customComponent) {
      console.log('Custom component not ready.');
    }
  }
  this.cdr.detectChanges();
}

There's more ways that your desired result can be done, I answered with this one because your code was more suitable to be adapted to it.


Also

For the not recommended solution of using innerHTML the following changes were necessary:

  1. Everything that was mentioned in the previous solution, as there's no other way (known by me) to render and evaluate components.
  2. A wrapper DOM, so you can extract its evaluated innerHTML
  3. A domSatinizer so the DOM can be rendered, direct html code injection is "forbidden" in Angular, and you have to explicitly tell that you're willing to take the risks of using it.
  4. Changing textHtml value type from string to SafeHtml so its accepts sanitized values
  5. For this to work, you'll need to hide the wrapper setting style="display: none"
  6. You can then clean/destroy or do whatever you want with the wrapper as you already got its HTML.
this.testHtml = this.sanitizer.sanitize(
  SecurityContext.HTML,
  this.sanitizer.bypassSecurityTrustHtml(
    document.getElementById('wrapper').innerHTML
  )
);
luiscla27
  • 4,956
  • 37
  • 49
  • thanks for the help! I looked at 'ComponentFactoryResolver', but it's been deprecated, so I don't know if I want to use it? Secondly, I'm never displaying the component in the view with my code, the HTML 'string' will be inserted into an array of strings for my swiper.js virtual slides. https://swiperjs.com/swiper-api#virtual-slides. Because I'm not that strong with typescript, if you can see a way to add new slides (a custom component would be ideal) using something other than a string as the documentation says the type is 'any'. But IDK... – chuckd Jun 11 '22 at 04:17
  • The _not recommended_ part of my answer should be enough to fulfill what you ask as you'll have the html sitting there waiting to be used. The type `SafeHtml` is a "must be" so you will not be able to use strings, Angular doesn't like to inject HTML unless you explicitly tell you want to, another approach might be to store dom nodes directly, and then just "append" them. Also, I removed the deprecated use `ComponentFactoryResolver`, now it uses `ViewContainerRef`. – luiscla27 Jun 12 '22 at 23:51
  • thanks for the clarification and fix. I'm trying to understand swiper.js better, and looking for a better solution. On page load I fetch up to but no more than the data for the first 3 slides. Then I take the html fora bootstrap card (image, data, etc), fill it with the data and insert the html into the virtual slides array. When the user slides to the next slide I fetch another members data, construct the string again and load it into the array and I do this until all slides are fetched. 10, 100, 1000+ Continued below... – chuckd Jun 13 '22 at 03:24
  • According to the docs for virtual slides only the slides in view will be loaded, but I still have to add all the html into the slide array each time I adda new slide, which seems cumbersome. Question 1 - Is this the best implementation for a large number of slides(to load a html string into the array)? According to the docs for virtual slides only the slides in view will be loaded, but I still have to add all the html into the slide array, which seems cumbersome. Continued below... – chuckd Jun 13 '22 at 03:25
  • Question 2- do you know if I can keep just one html tag in the swiper (ex. as a template) and have data go into and out of it when it's active? Thus elliminating me having to load the entire html string into the array every time I want to add a new slide. – chuckd Jun 13 '22 at 03:25
  • Q1: Is not cumbersome, the behavior you described is called "virtualization", and is the best approach for rendererization in a factor of really big arrays of data. When implementation is optimal, you can have millions of records being rendered with no impact in performance. The reason is that rendering HTML costs a lot of resources, but memory doesn't... Virtualization stores everything in memory and renders only the necessary. – luiscla27 Jun 13 '22 at 06:11
  • Q2: yes you can, however it'll work way better if just have all the needed HTML from the beginning, I would strongly suggest to take in account what I answered in the other question, but also take a look at the general concept of virtualization, like virtual scrolling: https://material.angular.io/cdk/scrolling/overview – luiscla27 Jun 13 '22 at 06:15
  • thanks for the help. I see your other answer where you suggest the best approach is to just store data in the array and I agree, but I don't believe you can do this with virtual slides? swiperjs.com/swiper-api#virtual-slides and I'm having trouble doing this with ngx-swiper-wrapper, maybe I should ditch the wrapper? I'm not exactly sure how to add a virtual slide, other than with a string of html that gets added to the array. Nothing seems to work or has problems displaying multiple slides per page. Maybe it's the damn wrapper, maybe I should just ditch it... – chuckd Jun 13 '22 at 07:42
  • Apparently you can store data in the array and then render it, there's a method in the documentation called [`renderExternal`](https://swiperjs.com/swiper-api#param-virtual-renderExternal) which does the trick. Take a look to [this commit](https://github.com/Jumble123/virtual-swiper-slides-angular8-renderExternal/commit/a5a99e6251245bf4556ec61dd8b2b92d88efffdc) from a guy who implemented it. – luiscla27 Jun 13 '22 at 14:50
1

You can inject ChangeDetectionRef to MemberCardComponent component and then call detectChanges() in your parent component.

You are trying to get the innerHtml before the change detection is triggered.

here is a working example: Stackblitz

izik
  • 1,058
  • 7
  • 16
  • thanks for the help. Do you know why, when I call const component = this.viewContainerRef.createComponent(TestComponent); in my parent component, I see the rendered component html on the screen at the bottom? I guess what I'm saying is, I don't want or need the component rendered on the screen, so is there a way to get the innterHTML without having it on the screen like in your example? – chuckd Jun 17 '22 at 03:54
  • @user1186050 you can use `detach()` after you create your component. or you can use `remove()` if you want to altogether remove the component. See the example here: https://stackblitz.com/edit/angular-ivy-cjxm6c?file=src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fapp.component.html – izik Jun 17 '22 at 13:19