Angular strips out data attribute from innerHTML - html

I have a string returned from an API endpoint as below:
If you died tomorrow, your wife <span class='person' data-relationship='Relationship.Wife' data-avatarid='1212'>Alice</span> will lose everything.
So to display this as HTML I'm using the innterHTML property:
<p *ngFor="let con1Died of consequences?.client1Died" [innerHTML]="con1Died"></p>
But this is outputted to the browser with the data attributes stripped out as:
<p _ngcontent-smy-c63="">If you died tomorrow, your wife <span class="person">Alice</span> will lose everything.</p>
How can I output this WITH the data attributes? Is there a way to this?
EDIT: So I tried the HTML sanitation technique from below but the CSS still isn't applied:
this.reportsService.getConsequences().subscribe(res => {
// Load the consequences to angular
this.consequences = res;
this.client1Died = new Array();
this.consequences.client1Died.forEach(element => {
const safehtml = this.sanitized.bypassSecurityTrustHtml(element);
this.client1Died.push(safehtml);
});
console.log(this.client1Died);
});

Create a pipe to sanitize the Html:
#Pipe({ name: 'safeHtml'})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}
transform(value) {
return this.sanitized.bypassSecurityTrustHtml(value);
}
}
#Component({
selector: 'my-app',
template: `<div [innerHTML]="content | safeHtml"></div>`,
})

Related

How to create HTML in angular?

I have been working on React for a year. Now, I am writing angular. How can I create a piece of html code in ts.file?
In react, I do it that way:
const example = (item: string): React.ReactNode => {
return <p> something.... {item} </p>
}
I want to do same thing in Angular8+
I know some way to do it. For example:
const example2= (name: string): string => {
return `
<div>
<p>heyyy ${name}</p>
</div>
`;
};
Are there any other ways to do it?
In Angular, there are a couple of ways to do this. If you need to generate HTML in the typescript and then interpolate it into the template, you can use a combination of the DomSanitizer and the innerHTML attribute into other elements (for example a span).
Below would be an example of what I suggested above:
hello-world.component.ts:
#Component({
selector: "hello-world",
templateUrl: "./hello-world.component.html",
styleUrls: ["./hello-world.component.scss"]
})
export class HelloWorld {
innerHTML: string = `<p>Hello, world!</p>`;
}
sanitze.pipe.ts:
#Pipe({
name='sanitize'
})
export class SanitizePipe {
constructor(private sanitizer: DomSanitizer) { }
transform(value: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(value);
}
}
hello-world.component.html:
<div [innerHTML]="innerHTML | sanitize"</div>

Angular - Dynamically load html that includes angular markups

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 :)

How can I using angular when I want to click?

I'm a beginner to learn this component. And I going to try to create a online book shop like this link https://www.fishpond.com.hk/Books , and I'm facing some problem now. Could you guys please help me? And first in my website, it have backend and frontend, and now I can show all book , insert new book, and now I want to know how can I do when I click the title of the book and what I have to do to transfer to get that book detail.
How can I click the title and I will see those book detail on the book-details page. And I hope get the isbn code to find that book.
My code here
HTML
<h1>All Books</h1>
<ul *ngIf="books" class="info">
<li *ngFor="let book of books">
<p><img [src]="book.image" class="bookimg"></p>
<a routerLink="/book-detail"><h3>{{ book.title }}</h3></a>
<p>{{ "By "+ book.author }}</p>
<span class="price-block" >{{ "HK$" + book.price}}</span>
</li>
</ul>
ts
import { Component, OnInit } from '#angular/core';
import { DataService } from '../data.service';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
h1Style: boolean = false;
books: Object;
constructor(private data: DataService) {}
ngOnInit() {
this.data.getBooks().subscribe(data=> {
console.log({data}); //show data
this.books = data
//console.log(this.books);
})
}
And I have created a component for book-detail
<h1>Book-detail</h1>
<div *ngIf="books" class="book-detail-block">
<div *ngFor="let bookrecord of books" class="book-record">
<h1>{{bookrecord.title}}</h1>
<p>{{bookrecord.image}}</p>
<p>{{bookrecord.author}}</p>
<p>{{bookrecord.price}}</p>
<p>{{bookrecord.isbn}}</p>
<p>{{bookrecord.description}}</p>
</div>
</div>
ts
import { Component, OnInit } from '#angular/core';
import { DataService } from '../data.service';
#Component({
selector: 'app-book-detail',
templateUrl: './book-detail.component.html',
styleUrls: ['./book-detail.component.scss']
})
export class BookDetailComponent implements OnInit {
h1Style: boolean = false;
books: Object;
constructor(private data: DataService) {}
ngOnInit() {
this.data.getOneBook().subscribe(data => {
this.books = data
console.log(this.books);
})
}
}
I can get the data in the service but how can I implement in show component
export class BookDetailComponent implements OnInit {
h1Style: boolean = false;
books: Object;
constructor(private data: DataService) {}
ngOnInit() {
console.log('-0-----' + this.books)
this.data.getBooks().subscribe(data=> {
console.log({data}); //show data
this.books = data
})
}
}
enter image description here
I may be late to the issue and you've already solved it but in the off-chance that you havent i'll hopefully provide some guidance.
What you want for accessing an individual item when clicking the title is to use a click-event on the tag representing the title, probably the h1-tag. It looks something like this:
<h1 (click)="getBookDetail(bookrecord)">{{bookrecord.title}}</h1>
The line above hooks up a clickevent to a function called getBookDetail and takes the individual object as a parameter, as of now this will render an error saying there's no function named getBookDetail(), so you'll need to create it in the component.ts file that corresponds to the view probably the homecomponent.ts and it looks like this:
getBookDetail(book: any) {
console.log(book);
}
If you now reload the application and click the title you'll see the individual book-object being logged in the console. What you'll need after is to set up routing if you havent already (you get the question to include app-routes module when creating the project) and to create a path for the bookDetailComponent. If you have routing in place add an array of routes as following:
const routes: Routes = [
{ path: '', redirectTo: '/books', pathMatch: 'full' },
{ path: 'books', component: HomeComponent},
{ path: 'book/:id', component: BookDetailComponent },
];
The first item in the routes array will match any route that is empty like localhost:4200 and redirect to the HomeComponent, and the other two arrays are routes for the individual component.
And if you dont have a routing-module i suggest you follow angulars tutorial for adding in-app navigation: https://angular.io/tutorial/toh-pt5.
And for making the click on the title actually navigate to the bookcomponent you first need to inject the Router class, so if you go back to the homecomponent you'll see an constructor (if not create one), add the router class like:
constructor(private router: Router) {}
And in the getBookDetail function you can remove the console.log and add:
getBookDetail(book: any) {
// Wrong path this.router.navigateByUrl('/book/' + book.isbn);
this.router.navigateByUrl('/bookdetail/' + book.isbn);
}
All that you need now is to get the isbn from the url and fetch one book with that identifier, but i'll leave those steps to you, everything you'll need is in the angular tutorial i linked previously. Good luck and if anything is confusing or you have any questions feel free to ask.
Added a stackblitz showing my idea:
https://stackblitz.com/edit/angular-ivy-c2znl2?file=src/app/books/books.component.ts

Angular - String interpolation of undefined variable

I am trying to display some information which is fetched from an external API on a web page by using Angular's string interpolation.
When no information is fetched or hasn't "arrived" yet, I want to display 'N/A'.
I have tried the following approach, however I get an error saying:
'Can't read property 'name' of undefined' on line 2 of the following html code.
How can I show N/A while waiting for the response to be defined?
app.component.html:
<div id="api-info">
{{ !!this.apiInfo.name ? this.apiInfo.name : 'N/A' }}
</div>
app.component.ts:
import { ApiInfo } from 'src/app/apiInfo.model'
import { ApiService } from 'src/app/api.service'
(...)
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
apiInfo: ApiInfo;
apiInfoSub: Subscription;
constructor(apiService: ApiService) {}
ngOnInit() {
// apiService.apiInfo is a Subject<ApiInfo> where the API response is stored
this.apiInfoSub = this.apiService.apiInfo.subscribe(
(response) => {
this.apiInfo = response;
}
);
}
ngOnDestroy() {
this.apiInfoSub.unsubscribe();
}
}
apiInfo.model.ts:
export class ApiInfo {
public name: string,
public id: number,
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
I suggest to not subscribe within the component. Better use the async pipe instead and check as follows...
<div id="api-info" *ngIf="(apiInfo$ | async as apiInfo); else pending">
{{ apiInfo.name }}
</div>
<ng-template #pending>
n/a
</ng-template>
Which also allows you to style the n/a differently quite easy
It is better to use async pipe, instead of subscribing and unsubscribing to stream manually. That makes the code cleaner and in the html use expression:
{{(stream$|async)?.name || 'N/A'}}
Here is code sample: https://stackblitz.com/edit/angular-nknwuq
please change your app.component.html to below:
<div id="api-info">
{{ apiInfo?.name ? apiInfo.name : 'N/A' }}
</div>
this should resolve the issue.
apiInfo is undefined at start. The subscribe does not resolve immediately, so the apiInfo = response is set after some time. Maybe use <div id="api-info" *ngIf="apiInfo">
Or initialize on declaration: apiInfo: ApiInfo = <ApiInfo>{};

Angular 5 DomSanitizer with Hyperlinks

I am using a WYSIWYG editor (CKEditor) and trying to render the content with Angular 5.
What I am trying to figure out is the proper way to use DomSanitizer in Angular 5. The problem I am facing is that Hyperlinks are not working (are not "clickable") in the resulting sanitized HTML.
I am using the following Typescript code to return a safeHtml content:
public getSafeContent(): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(this.page.content);
}
and using it in my template this way:
<div [innerHTML]="getSafeContent()"></div>
This will render the HTML with all inline styles intact, but hyperlinks won't work.
I tried doing this instead:
public getSafeContent(): SafeHtml {
return this.sanitizer.sanitize(SecurityContext.HTML, this.page.content);
}
Which results in that Hyperlinks actually works, but inlines styles are not.
Is there a way to get both styles and hyperlinks to work with sanitized content?
Update
This is what the page looks like in Chrome dev tools:
<div _ngcontent-c22="" class="row">
<div _ngcontent-c22="" class="col-lg-12">
<div _ngcontent-c22="">
<p>google</p>
</div>
</div>
</div>
and the google link is not clickable.
bypassSecurityTrustHtml allows <script> tags in the content. For URLs you need bypassSecurityTrustUrl. See here: https://angular.io/api/platform-browser/DomSanitizer#bypassSecurityTrustUrl.
I've never tried stacking the bypassXXX methods, so I don't know if you can do something like this bypassSecurityTrustUrl(bypassSecurityTrustHtml(myContent)) but I would guess probably not since each method takes a string but returns an object (of type SafeHtml/SafeUrl), so it can't be used as the input to the stacked function call which expects a string.
So, you may need to parse the contents, pass each URL into the bypassSecurityTrustUrl and then combine everything back together again.
Update
I just looked at the sanitize method. I haven't tried this, but something like this might work:
this.sanitizer.sanitize(SecurityContext.HTML, this.sanitizer.bypassSecurityTrustUrl(myContent));
since sanitize can take a SafeValue as an input. The inner bypassSecurityTrustUrl sanitizes the URLs and returns a SafeUrl, which is unwrapped by the outer sanitize and used as input to make it HTML safe. Like I said, I haven't tried it, but it looks good in theory...
in .ts pipe for 'URL' sanitizer
import { Component, Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer } from '#angular/platform-browser';
#Pipe({ name: 'sanitizeUrl' })
export class SafeUrlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(url) {
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}
in .html
<div [innerHTML]="Content | sanitizeUrl| sanitizeHtml">
</div>
pipe for 'HTML' sanitizer
import { Component, Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer } from '#angular/platform-browser';
#Pipe({
name: 'sanitizeHtml'
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}
transform(value) {
return this.sanitized.bypassSecurityTrustHtml(value);
}
}
Please consider the above solution. this will apply both pipes without disturbing any style and link click event at same time
I used DOMPurify library to sanitize the DOM. DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG.
I created angular PureTextPipe pipe to sanitize the raw content
import { Pipe, PipeTransform } from '#angular/core';
import DOMPurify from 'dompurify';
#Pipe({
name: 'pureText',
pure: true
})
export class PureTextPipe implements PipeTransform {
transform(str: any): any {
let res = str;
if (str === null || str === undefined || str === '' || str === 0) {
res = '--';
}
return DOMPurify.sanitize(res);
}
}
Now to use this pipe you just need to add it to HTML like:
<div [innerHTML]="yourRawData | pureText"></div>
Documentation of DOMPurify at
https://www.npmjs.com/package/dompurify
import DOMPurify from 'dompurify';
var clean = DOMPurify.sanitize(dirty);
I have found a working solution to this. With this both the URL and the style would work. It is again with the use of "bypassSecurityTrustHtml". It's surprising how using this in html or TS does not make the link work, but using this in a seperate file as a pipe makes this work like a charm.
Here is what I did
Create a Custom Pipe
.safe.dom.pipe
import { Component, Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl } from '#angular/platform-browser';
#Pipe({
name: 'safeDom'
})
export class SafeDomPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
public transform(value: any, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
switch (type) {
case 'html': return this.sanitizer.bypassSecurityTrustHtml(value);
case 'style': return this.sanitizer.bypassSecurityTrustStyle(value);
case 'script': return this.sanitizer.bypassSecurityTrustScript(value);
case 'url': return this.sanitizer.bypassSecurityTrustUrl(value);
case 'resourceUrl': return this.sanitizer.bypassSecurityTrustResourceUrl(value);
default: throw new Error(`Invalid safe type specified: ${type}`);
}
}
}`
In Case you have a common module in your project that is included where you require to implement it, you can declare and export this package from there. Else just declare this pipe in the module you want to implement this.
import { SafeDomPipe } from './pipes/safe.dom.pipe';
#NgModule({
imports: [
.....
],
exports: [
....
SafeDomPipe
....
],
declarations: [
....
SafeDomPipe
....
])
Now in Html you can directly use this pipe.
<div [innerHtml]="data | safeDom: 'html' "> </div>
This would allow both link and style.
If you're data content is consisting of HTML with Hyperlinks. But we want to sanitize with hyperlink working. This worked for me:
HTML
<div [innerHTML]="getSafeContent()"></div>
TS:
public getSafeContent(): SafeHtml {
return this.sanitizer.sanitize(SecurityContext.HTML, this.page.content);
}
This works for me:
Component:
content = '<b>Hello World</b><p style=\'font-size:14pt\'>
<a href=\'http://www.google.com\'>Go to Google</a></p>Test123';
public getSafeContent(): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(this.content);
}
HTML:
<div [innerHTML]="getSafeContent()"></div>
Link works and inline styles are intact