Loading Method for Non-Component HTML Pages

123 views
Skip to first unread message

Scott L

unread,
Dec 21, 2018, 3:26:27 PM12/21/18
to Angular and AngularJS discussion
I have a number of HTML pages that are being stored locally and to be loaded as needed into the Angular app. These page are basic HTML, non-component, instructional/eLearning type content, that are not necessarily dynamically driven. These aren't component html pages as there could be anywhere from 5 to 60 pages and it seems like this would be extremely inefficient to create 60 separate components just to be able to load an html page and then communicate with the app. The app would be re-usable and the number of html pages will change on each use. These page names/urls are stored in an array in the Angular app. 

My intention was to load these pages, by using HttpClient.get(url,{responseType:text}) and have the returned data bound to the [innerHTML] propery of a div. This seems to work as expected with one caveat. Any JavaScript on the page does not seem to be getting executed. I am using a custom pipe to sanitize the HTML via bypassSecurityTrustHtml. A few questions to ask regarding my loading method and JavaScript issue as I am relatively new to Angular:

  1. Is there a better way for me to load these HTML pages than using the [innerHTML] property? It seems like using a router / router-outlet would be a better choice and would solve the JavaScript issue as I would be able to directly communicate with the Angular app, but I couldn't find a way to achieve this as I am using an array of page urls to load.

  2. If I can't use a router/router-outlet, to load the pages via [innerHTML], how can I have these loaded pages communicate to Services/Components in my Angular app? 

Any insight is certainly appreciated.

Thanks, in advance.


Sander Elias

unread,
Dec 27, 2018, 12:57:25 AM12/27/18
to Angular and AngularJS discussion
Hi Scott,

Your use-case is quite common. The easiest way is to create a component to show your loaded code.
I Added a small module to my samples repo. You can check that out.

the gist is this:
ngOnInit() {
/** I don't need to unsubscribe, because htmlLoad completes */
this.html.load('Some url provided').subscribe(html => {
/**
* setting innerHTML doesn't "run" scripts,
* instead create a document fragment
*/
const frag = document.createRange().createContextualFragment(html);
/** erase old content */
this.elm.innerHTML = '';
/** add the new 'html' to the page */
this.elm.appendChild(frag);
});
}

I hope this helps you.

Regards
Sander


Scott L

unread,
Dec 27, 2018, 10:12:34 AM12/27/18
to Angular and AngularJS discussion
Sander, 

Thanks for your reply and help. I did found another solution, which I will post below as I hadn't found a method similar to yours. I have tweaked the solution I found to work for my needs, however, I am interested in finding out more about your way. With your method, can I use Directives, structural and custom, as well as Angular EventListeners in the loaded HTML code? 

Thanks again!

import {
Compiler, Component, Injector, VERSION, ViewChild, NgModule, NgModuleRef,
ViewContainerRef
} from '@angular/core';


@Component({
selector: 'my-app',
template: `
<h1>Hello {{name}}</h1>
<ng-container #vc></ng-container>
`
})
export class AppComponent {
@ViewChild('vc', {read: ViewContainerRef}) vc;
name = `Angular! v${VERSION.full}`;

constructor(private _compiler: Compiler,
private _injector: Injector,
private _m: NgModuleRef<any>) {
}

ngAfterViewInit() {
const tmpCmp = Component({
moduleId: module.id, templateUrl: './e.component.html'})(class {
});
const tmpModule = NgModule({declarations: [tmpCmp]})(class {
});

this._compiler.compileModuleAndAllComponentsAsync(tmpModule)
.then((factories) => {
const f = factories.componentFactories[0];
const cmpRef = f.create(this._injector, [], null, this._m);
cmpRef.instance.name = 'dynamic';
this.vc.insert(cmpRef.hostView);
})
}
}

Sander Elias

unread,
Dec 27, 2018, 11:08:44 AM12/27/18
to Angular and AngularJS discussion
Hi Scott,

No, my method doesn't support anything angular, Only plain HTML and JS. You can use Angular Elements if you need a couple that you want to use inside those pages.
Also, my method will work in AOT and production mode. It is not dependant on the JIT compiler.
Your method does need JIT, and will not work in AOT mode. Also, if your subpages are somewhat larger, it will take substantial time to compile them on the fly. This might not be a problem for your situation, but it's good to be aware of this. 
(BTW, JIT mode might disappear from future version, at least in the form we have it now)

Regards
Sander

Scott

unread,
Dec 28, 2018, 1:09:14 AM12/28/18
to ang...@googlegroups.com
Hi Sander,

Thanks for the information. I have been reading up on Angular Elements and they seem interesting. From what I understand, they can only be components, i.e. not directives, and can communicate between other Angular Element components on the same page - I assume by injecting the same service? They can not, however, communicate back to the Angular App upon which they are loaded into - as in the example you provided?

The method I have for loading dynamic html content is working, however, your points regarding AOT/JIT are greatly appreciated and now give me cause for concern. In fact, it makes me think that I might want to reconsider how I am loading these pages. Here's my dilemma. The Angular App I am creating will load instructional/learning content (pages) to be run on a Learning Management System (LMS). Some of the content will be interactive, i.e. multiple choice questions, drag and drop, videos, etc. There may even be a scored assessment at the end. There can be anywhere from 3 pages to 30+ or more, as it will vary depending on the course content. The course may even be resumed at a later time from where the user left off, based upon data returned from the LMS. Angular needs to handle the page loading based upon the data returned from the LMS.

With all that said, what I have now seems to work really well. I have all the page urls stored in a local JSON file and then depending on which page the user is on, the page dynamically gets loaded. The unknown is how well it will perform when it's 30+ pages and an assessment and I have to use JIT? Am I willing to take the chance to develop the App and find out in the end that it's bogged the browser down due to JIT compiling or as you stated the JIT compiler changes or goes away in future versions. I went this route of loading dynamic content as I didn't think it would be efficient to create 30+ components or more, 1 for each page of content and not including the other components in the App, as well as how to handle routing and what my router might look like? Your suggestion for loading would work great for static content. I am just not sure how to handle the dynamic content - i.e. interactions as I need to communicate back to the App.

Do you have any thoughts on my dilemma? I am new to Angular and certainly appreciate any insight you can provide.

Thanks again,

Scott

--
You received this message because you are subscribed to a topic in the Google Groups "Angular and AngularJS discussion" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/angular/PnHiXroP-VI/unsubscribe.
To unsubscribe from this group and all its topics, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at https://groups.google.com/group/angular.
For more options, visit https://groups.google.com/d/optout.

Sander Elias

unread,
Dec 28, 2018, 6:27:59 AM12/28/18
to Angular and AngularJS discussion
Hi Scott,

Angular elements is your answer there. As you noticed yourself, you can inject any service that is available to your app.
You can communicate with your app using those services.
In most of my somewhat bigger apps, I'm using a message-bus service. You can inject that where ever you need to send/retrieve state/data. As it's bi-directional, it solves most of those issues.

Regards
Sander

Scott

unread,
Dec 28, 2018, 10:38:00 AM12/28/18
to ang...@googlegroups.com
Sander,

Thanks for the information. Since, Elements is relatively new, I don't see a lot of information or examples out there. Can you shed some light on the following:

- Do I create the Elements in the same App as my Main App. Intuitively I would say yes, as the Elements would need access to the same Services as the Main App. However I see  examples that show I have to package Elements in a specific manner - see https://www.telerik.com/blogs/getting-started-with-angular-elements (Packaging Angular Elements). Has any of this changed with Angular 7?

- Do you have any basic examples of Elements you share that make use of shared injected services/

Thanks again for all of your help.

Scott

--

Sander Elias

unread,
Dec 28, 2018, 2:38:03 PM12/28/18
to Angular and AngularJS discussion
Hi Scott,

As you need them in the same app there is no problem building them. You just expose them as custom elements. This makes them available to be consumed as normal HTML in the dynamic pages you are loading. You only need special packaging if you want to use them outside your own app. Tooling to make this easier is coming along, but that might take a while to materialize. 
But your use-case is perfectly suited for Angular elements. As those are part of your application you can inject whatever you need.
There are some samples available, but I don't recall any right now. If I have some time, I will add a sample to my samples repo.

Regards
Sander

Sander Elias

unread,
Dec 28, 2018, 2:55:02 PM12/28/18
to Angular and AngularJS discussion
The documentation now contains a sample that suits your need. 

Scott

unread,
Dec 28, 2018, 4:20:50 PM12/28/18
to ang...@googlegroups.com
Thanks Sander. Seems to make sense. I have been messing around with them this morning, and I think you are right in that my use-case is perfect. The more I am working in it, the more I am able to get things working. I was even able to get a directive working within the element!  There is one caveat though. I am trying to pass in an array(responses) to my element using the following syntax:

                <radio-btn-element
                    elements=3
                    responses ='[This is default response #1,This is default response #2,This is default response #3]'>
                </radio-btn-element>
and the input as follows:
export class RadioBtnElementComponent implements OnInit {

@Input() responses:any[];
@Input() message:string = '';
@Input() elements:number = 1;
@Input() numbers:any[]

However, responses is strictly being sent in as a string. It's not being seen as an Array. Any thoughts on why?

Thanks again for all of your help. It's been a game changer.

Best regards,

Scott

--

Scott

unread,
Dec 28, 2018, 4:42:34 PM12/28/18
to ang...@googlegroups.com
Thanks Sander! That's the example I downloaded and was building on.

On Fri, Dec 28, 2018 at 12:55 PM Sander Elias <sande...@gmail.com> wrote:
The documentation now contains a sample that suits your need. 

--

Sander Elias

unread,
Dec 29, 2018, 12:16:16 AM12/29/18
to Angular and AngularJS discussion
Hi Scott,


However, responses is strictly being sent in as a string. It's not being seen as an Array. Any thoughts on why?
 
Yes, I do!. As Elements are not angular, but DOM elements, they follow DOM standards. So you are setting an attribute now, and attributes are strings by default. There is no way to set a property from within HTML, you need JS to do that. 
However, you can use a getter/setter in your element to make it an array. something like this:

private choice: string[] = [];
@Input('choice') set _choice(s: string) {
this.choice = s.split(',');
}

If your HTML pages already exist and have array's in them, you might want to create a special function that parses your syntax into their expected types. Also, if you really sure there can be no tampering (I wouldn't be, ever!) you might use eval to handle it. 

Regards
Sander

Scott

unread,
Dec 29, 2018, 4:54:20 PM12/29/18
to ang...@googlegroups.com
Thanks Sander. I actually ended up doing something very similar. So what I have learned is that anything within the HTML Angular Element will obviously have access to the Angular element’s class, properties, services, etc. Anything outside the element in the loaded html page has no access to  Angular.  One issue that I discovered today is that I have css classes in the Angular HTML Element dependent upon property values set and updated in the Angular Element’s class. The classes don’t seem to be changing upon the property values changing in Angular. So, I assume that I have to change the DOM element’s class from within the Angular Element’s class upon the property value changing?

Thanks!

Scott

Sander Elias

unread,
Dec 29, 2018, 11:01:31 PM12/29/18
to Angular and AngularJS discussion
Hi Scott,

Yes, anything that is not a template (as your inserted HTML) does not have access to anything angular. You can't set/get anything from your angular class from the DOM side of things. (that would break the whole elements part!)
To access attributes you can get them via inputs. However, you can't set them. Use Elemetref fro now. In ivy this will get easier.
for now, something like this will work:
addClass(cssClass: string) {
this.elm.classList.add(cssClass);
}
removeClass(cssClass: string) {
this.elm.classList.remove(cssClass);
}

This elm is the ElementRef.natveElement. Refer to my sample on how to get that into your class easily. Well, that's for the css classes, for attributes you have to use `this.elm.setAttribut(...)`

Regards
Sander

Scott

unread,
Dec 30, 2018, 2:38:10 PM12/30/18
to ang...@googlegroups.com
Thanks Sander.  I have made it work doing exactly your suggestion. I seem to stumble upon the answer after I email you, and then you respond with exactly what I found.

With that said, I am running into an issue with use of directives and event emitters in the Angular Element. I have a directive on input element of type='radio'. The input element is looped over a set number of times based upon the attribute set in the Angular Element HTML. In this particular case, it creates 4 input elements. In each of those elements, I subscribe to an event, "knowledgeCheckCompleted", executed when the question is completed or has previously been answered. If previously completed or upon completion, I change the label color and attach an image to the radio button indicating correct or incorrect. The first time through, everything works as expected. However, upon returning to the page, based upon console logs, it appears that ngOnInit() for each directive is getting called twice - almost liked the same directive is being loaded 2x.I checked the HTML and only one instance of the Angular Element is loaded. Additionally, the event that the directive is subscribed to "knowledgeCheckCompleted", is being run 2x as well and I cannot figure out why. I have attempted to use ngOnDestroy and unsubscribe to the event, however that just creates errors when return to the page - message: "object unsubscribed" name: "ObjectUnsubscribedError". If I go forward and then come back to the page, the event, along with ngOnInit() is still only run 2x - I was thinking that maybe it would be run a third time - but not the case. If I comment out the line that is emitting the event, then no duplication issues including ngOnInit() is only called 1x, but the subscribed function never gets called. I thought maybe I had a duplication of the emitter, but not the case.

Any thoughts or insight or where I might look or what I am doing wrong?

Thanks for the continued assistance. I greatly appreciate it.


--

Sander Elias

unread,
Dec 30, 2018, 11:41:50 PM12/30/18
to Angular and AngularJS discussion
Hi Scott,

That sounds like a bug in Angular Elements. Can you make a minimal reproduction? Preferably in a StackBlitz. Angular Elements makes a standard DOM event from @Outputs, My suspicion goes that something happens there.

Regards
Sander
Reply all
Reply to author
Forward
0 new messages