Angular - Dynamically load html that includes angular markups - html

In Angular 9+ I can successfully convert a string to a html and then load that that html using innerHtml and bypassSecurityTrustHtml().
My question is it possible to also dynamically load/render the converted html to include and recognise angular/javascript markup language eg *ngIf, handle bars and click events.
Below is the code and stackblitz at the attempt so far but as you can see it doesn't recognise the markup.
https://stackblitz.com/edit/dynamic-angular?file=app/app.component.ts
export class AppComponent implements OnInit {
text: string = "Hello world";
content: any;
constructor(private domSantizer: DomSanitizer) {}
ngOnInit() {
let body: any =
'<div>{{text}}<div><br><button (click)="test()">Test</button>';
this.content = this.domSantizer.bypassSecurityTrustHtml(body);
}
test() {
alert("It works");
}
}
Html
<div [innerHTML]="content"></div>

I have researched and tried many solutions.
My research and trial results are below.
html
<div #container></div>
typescript side as below
export class AppComponent implements OnInit {
#ViewChild("container", { read: ViewContainerRef })
container: ViewContainerRef;
constructor(private compiler: Compiler) {}
text: string = "asdasd";
ngOnInit() {
this.addComponent(
`<div>{{text}}<div><br><button (click)="test()">Test</button>
`,
{
text: "Hello word",
test: function() {
alert("It's work");
}
}
);
}
private addComponent(template: string, properties?: any = {}) {
#Component({ template })
class TemplateComponent {}
#NgModule({ declarations: [TemplateComponent] })
class TemplateModule {}
const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
const factory = mod.componentFactories.find(
comp => comp.componentType === TemplateComponent
);
const component = this.container.createComponent(factory);
Object.assign(component.instance, properties);
// If properties are changed at a later stage, the change detection
// may need to be triggered manually:
// component.changeDetectorRef.detectChanges();
}
demo
some posts I have reviewed
compile dynamic Component
angular-html-binding
I think it makes the most sense :)

Related

change url of HttpClient in angular when the language is changed

im trying to make a portfolio where when if someone wants changes the language in the menu the components change what json load for getting all data. I try to use BehaviourSubject and subject but i cant understand them so it difficult to use them. sorry for any mistake in english im learning
this is my service
export class PortfolioService {
language: string = 'espaniol';
constructor(private http: HttpClient) {}
obtenerDatos(): Observable<any> {
if (this.language === 'espaniol') {
return this.http.get('./assets/i18n/espaniol.json');
} else {
return this.http.get('./assets/i18n/english.json');
}
}
changeLang(value: string) {
this.language = value;
}
}
this is my header
export class HeaderComponent {
#Input() currentSection = 'section1';
siteLanguage = 'english';
languageList = [
{ code: 'english', label: 'English' },
{ code: 'espaniol', label: 'EspaƱol' },
];
constructor(private portfolioService: PortfolioService) {}
changeLang(localeCode: string) {
this.portfolioService.changeLang(localeCode);
}
scrollTo(section: string) {
document.querySelector('#' + section)!.scrollIntoView();
}
}
my template
<ng-container *ngFor="let language of languageList">
<li role="menuitem">
<a class="dropdown-item" (click)="changeLang(language.code)">
{{ language.label }}
</a>
</li>
</ng-container>
and my component that load the data
export class HomeComponent {
constructor(private datosPortfolio: PortfolioService) {}
miPortfolio: any;
ngOnInit(): void {
this.datosPortfolio.obtenerDatos().subscribe((data) => {
this.miPortfolio = data;
});
}
}
i tried to make a portfolio where i could change the language with a service that picked up when a changed happened in the header. the changed get picked up but the language is not changed in other components.
you need to wrap your data source observable with observable that changes when language changes. for that, the best is to use the BehaviorSubject.
take a look at this:
export class PortfolioService {
language = new BehaviorSubject<string>('espaniol');
constructor(private http: HttpClient) {}
obtenerDatos(): Observable<any> {
return this.language.asObservable().pipe(
switchMap(lang => this.http.get(`./assets/i18n/${lang}.json`))
)
}
changeLang(value: string) {
// update the value of this BehaviorSubject, and all
// subscribers notify about it
this.language.next(value);
}
}
this way every time language changed, new source emit in this.language BehaviorSubject and subscribe function fires again because of the new value, and them switch to other observable that makes the HTTP request with the language as a parameter that goes into the URL of the request.
hope it helps :)

ViewEncapsulation.None not working with innertHTML

I'm actually developing an angular application and I have to put an [innerHTML] element in a div.
My code
Like that :
something.component.html
<section class="mx-auto" *ngFor="let publication of publication">
<div [innerHTML]="publication.content"></div>
</section>
So in ts :
something.component.ts
import { Component, OnInit, ViewEncapsulation } from '#angular/core';
import { Subscription } from 'rxjs';
import { ActivatedRoute } from '#angular/router';
import { Title, Meta } from '#angular/platform-browser';
import { Publication } from '../publication.model';
import { PublicationsService } from '../publication.service';
#Component({
selector: 'app-free-publication',
templateUrl: './something.component.html',
styleUrls: ['./something.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FreePublicationComponent implements OnInit {
publication: Publication[] = [];
suggestions: Publication[] = [];
private routeSub: Subscription;
getId: any;
isLoading = false;
constructor(public publicationsService: PublicationsService, private route: ActivatedRoute, private titleService: Title, private meta: Meta) {
this.getId = this.route.url['_value'][1].path;
this.getId = + this.getId;
}
ngOnInit() {
this.isLoading = true;
// main publication
this.routeSub = this.route.params.subscribe(params => {
this.publicationsService.getPublication(params['publicationId']).then(dataPublication => {
for (let i = 0; (dataPublication.content.match(/wp-content/g) || []).length; i++) {
dataPublication.content = dataPublication.content.replace('https://aurelienbamde.com/wp-content/', 'assets/content/');
}
this.titleService.setTitle(dataPublication.title);
this.meta.addTag({ name: 'keywords', content: dataPublication.post_tag });
this.publication = [dataPublication];
});
});
}
}
And my innertHTML do not return the style of the html doc that I send.
My tests
With a console.log() at the end of ngOnInit, I can see my html with all of the styles attributs, but by inspecting the div of the innerHTML, there is no style inside.
My question
So I well implement ViewEncapsulation.None as you see, there is an action on other elements, so it works, but not on my innerHTML.
Do you have any idea, problem of version ? Or coworking with others elements ?
Thanks in advance for your time !
And I wish you success in your projects.
You must bypass the security imposed by angular for dangerous content (HTML content not generated by the app). There is a service, called DomSanitizer that enables you to declare a content as safe, preventing angular to filter potentially harm things to be used like styles, classes, tags etc. You basically need to pass your content through this sanitizer using a pipe:
<div [innerHTML]="dangerousContent | safeHtml"></div>
Your SafeHtmlPipe would be something like this:
#Pipe({name: 'safeHtml'})
export class SafeHtmlPipe implements PipeTransform {
constructor(protected sanitizer: DomSanitizer) {}
transform(value: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(value)
}
}
There are other bypassSecurityTrust* methods in DomSanitizer:
bypassSecurityTrustScript
bypassSecurityTrustStyle
bypassSecurityTrustUrl
bypassSecurityTrustResourceUrl
You can find more info in Angular docs.

Angular Inject custom tag in innerHTML

Text with custom tag to inject inside innerHtml:
const test = '<FREEZE> the image. Then try again.'
<td [innerHtml]="test | htmlEscape"></td>
I am using a custom pipe to disable Angular's built-in sanitization for the provided value.
custom-pipe.ts
#Pipe({
name: 'htmlEscape'
})
export class HtmlEscapePipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {
}
transform(content: string): SafeHtml {
if (!content) {
return null;
}
const t = this.sanitizer.bypassSecurityTrustHtml(content);
return t;
}
}
Unfortunately <FREEZE> tag inside the string is stripped out. Any suggestions would be appreciate it.

Angular2 include html from server into a div

I got a serie of html in my server. For example:
http://docs.example.com/intro.html
http://docs.example.com/page1.html
http://docs.example.com/page2.html
And I trying to include those files into a<div> in my angular2 v4 app. For example:
component.ts
public changePage(name: string) {
switch (name) {
case 'intro': this.myHtmlTemplate = 'http://docs.example.com/intro.html'; break;
case 'page1': this.myHtmlTemplate = 'http://docs.example.com/page1.html'; break;
case 'page2': this.myHtmlTemplate = 'http://docs.example.com/page2.html'; break;
}
}
component.html
<div [innerHtml]="myHtmlTemplate"></div>
but it doesnt work. I tried the following solutions:
Angular4 Load external html page in a div
Dynamically load HTML template in angular2
but it doesn't work for me. Can somebody help me with this problem please ?
Angular security Blocks dynamic rendering of HTML and other scripts. You need to bypass them using DOM Sanitizer.
Read more here : Angular Security
DO below changes in your code :
// in your component.ts file
//import this
import { DomSanitizer } from '#angular/platform-browser';
// in constructor create object
constructor(
...
private sanitizer: DomSanitizer
...
){
}
someMethod(){
const headers = new HttpHeaders({
'Content-Type': 'text/plain',
});
const request = this.http.get<string>('https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Your_first_HTML_form', {
headers: headers,
responseType: 'text'
}).subscribe(res => this.htmlString = res);
this.htmlData = this.sanitizer.bypassSecurityTrustHtml(this.htmlString); // this line bypasses angular security
}
and in HTML file ;
<!-- In Your html file-->
<div [innerHtml]="htmlData">
</div>
Here is the working example of your requirement :
Working Stackblitz Demo
This should do it:
First in your component.ts get the html with a http request:
import { Component, OnInit } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { map } from 'rxjs/operators'
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
constructor(private http: HttpClient) { }
htmlString: string;
ngOnInit() {
const headers = new HttpHeaders({
'Content-Type': 'text/plain',
});
const request = this.http.get<string>('https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Your_first_HTML_form', {
headers: headers,
responseType: 'text'
}).subscribe(res => this.htmlString = res);
}
}
And in your component.html simply use a one way data binding:
<div [innerHTML]="htmlString"></div>
You actually want to display a page inside your angular app right?
For that you can add a iframe tag:
<iframe width="400" height="600" [src]="myHtmlTemplate"></iframe>
you have to get HTTP call to load HTML in plain text and load in div using innerHtml.
export class AppComponent implements OnInit {
name = 'Kissht';
KisshtHtml;
constructor(
private http:HttpClient,
private sanitizer:DomSanitizer){ }
ngOnInit(){
this.http.get('https://kissht.com/',
{responseType:'text'}).subscribe(res=>{
this.KisshtHtml = this.sanitizer.bypassSecurityTrustHtml(res);
})
}
}
Sometime you might get CORS issue in stackblitz whil loading external Html
https://stackblitz.com/edit/display-external-html-into-angular
In your component first request pages with HTTP request
this.http.get('http://docs.example.com/intro.html').map(response => response.text()).subscribe(html => Your_template = html);
use innerhtml with the safehtml pipe so your inline styling will be applied
more info on GitHub page(https://gist.github.com/klihelp/4dcac910124409fa7bd20f230818c8d1)
<div [innerHtml]="Your_template | safeHtml"></div>

Angular2 functions in template and change detection

Im trying to build a method inside a service that checks whether a navigation button should be showed to the current user based on his permissions or not (this is just cosmetic "security" I know). Therefore this is the button placed inside the template
<button [routerLink]="['/some/where']"
*ngIf="AuthService.isAuthorized(['some', 'where'])">
Personen
</button>
The method AuthService.isAuthorized uses the provided array to run through all available routes and get the required permissions from the particular route's data object:
{
path: 'some',
component: SomeComponent,
data: {
permissions: [
"read:some",
"edit:some"
]
},
children: [
{
path: 'where',
component: SomeComponent,
data: {
permissions: [
"read:where"
]
}
},
]
}
so in this case the permissions ["read:some","edit:some","read:where"] are needed by the current signed in user so that the button would be displayed to him. Working so far!
But since the function is called inside the template it is called multiple times because of angular change detection. How could I change my code so that the function is called only once? Even better if it would only be called once after the authentication finished writing all permissions assigned to the authenticated user into AuthService.permissions
You can make AuthService.isAuthorized() method returns a promise:
#injectable()
export class AuthService {
...
isAuthorized(arr: string[]): Promise<boolean> {
return new Promise(resolve =>{
// your logic here
resolve(yourResult);
});
}
...
}
You can call this method on your ngOnInit of a component (Therefore it will be called once). You pass the return value to a new variable (e.g. isAuthorized) in the component and use this variable in the template instead.
#Component({
selector: "your-component",
templateUrl: "yourTemplate.html"
})
export class YourComponent implements OnInit {
isAuthorized: boolean;
constructor(private authService: AuthService) {}
ngOnInit() {
this.authService.isAuthorized(['some', 'where']).then(result => {
this.isAuthorized = result;
});
}
}
In the template you can just use isAuthorized variable.
<button [routerLink]="['/some/where']"
*ngIf="isAuthorized">
Personen
</button>
Edit:
If AuthService.isAuthorized() needed to be called only once but for more than one element, code like these may suits your need:
#Component({
selector: "your-component",
templateUrl: "yourTemplate.html"
})
export class YourComponent {
isObjectAuthorized = {} as {
isFirstAuthorized: boolean;
isSecondAuthorized: boolean;
};
constructor(private authService: AuthService) {}
checkForAuthorization(isElementAuthorized, arr: string[]) {
if (isElementAuthorized !== undefined) {
return;
}
this.authService.isAuthorized(arr).then(result => {
isElementAuthorized = result;
});
}
}
And in your template:
<button [routerLink]="['/some/where']"
*ngIf="checkForAuthorization(isObjectAuthorized.isFirstAuthorized, ['some', 'where'])">
First
</button>
<button [routerLink]="['/some/where']"
*ngIf="checkForAuthorization(isObjectAuthorized.isSecondAuthorized, ['some', 'where', 'else'])">
Second
</button>