Host Binding rearranges applied classes - html

I am using #HostBinding('class') to inject classes into the host element. The classes to be injected are generated based on user-supplied parameters. The problem I ran into and I could not find anyone else experiencing is that the classes are applied in an order different from the way I expected them.
For example, having a component defined below:
import {Component, HostBinding, Input} from '#angular/core';
#Component({
selector: '[icon]',
template: `
<ng-content></ng-content>
`
})
export class SuiIconComponent {
#Input() iconType = '';
#HostBinding('class')
get classes(): string {
return [this.iconType, 'icon'].join((' '));
}
}
When I apply the component like shown below:
<div icon iconType="car"></div>
And inspect, I see <div class="icon car"></div> instead of the appropriately formatted <div class="car icon"></div>.
I have tried reversing the array before joining but that did not help either.
Is there any way I get the classes to get rendered in the proper order?
Edit: I realized the classes are being rearranged in alphabetic order.

I'm not sure why angular changes the order, but you can solve your problem with little bit of change in your template.
#Component({
selector: 'icon',
template: `
<div [ngClass]="iconType + ' icon'">
<ng-content></ng-content>
</div>
`
})
export class SuiIconComponent {
#Input() iconType = '';
}
and use it as follows
<icon iconType="car">
Some content here
</icon>

Related

Angular 10 Load SVG Dynamically Without IMG or OBJECT Tags

I'm working on a simplified way to load SVG files without using IMG or OBJECT tags as it impedes my ability to control fill colors through external CSS. Using inline SVG is ideal, but with so many components using repeated icons, it's a lot of maintenance and I'd prefer to centralize them in their .svg file format. I thought about just making each one their own component, but that means there's a component.ts file I don't need for each one, and it might be a little confusing or other developers.
So far, creating a custom element that pulls the svg location from a "src" attribute is working:
#Component({
selector: 'app-svg',
template: `
<ng-template>
{{ src }}
</ng-template>
<span [innerHTML]="svg"></span>
`
})
export class SvgComponent implements OnInit {
svg: SafeHtml = '';
#Input() public src = '';
constructor(private http: HttpClient, private sanitize: DomSanitizer) {
}
ngOnInit(): void {
this.http.get(this.src, {responseType: 'text'}).subscribe(svg => {
this.svg = this.sanitize.bypassSecurityTrustHtml(svg);
});
}
}
Then I use my custom element in another component.html:
<app-svg src="assets/test.svg"></app-svg>
The result of course is an inline SVG with an inline element as a wrapper:
<app-svg src="assets/test.svg" ng-reflect-source="assets/test.svg">
<span>
<svg>
<path d="...">
</svg>
</span>
</app-svg>
I suppose this is harmless enough, but it's a little annoying and there's unnecessary extra markup. Ideally, I'd want to have the innerHTML applied to APP-SVG, but that means the svg in the binding would need to exist outside of the TS for for the custom element due to scoping issues. It's also messy having to remember to include [innerHTML] on every APP-SVG tag. I've tried using [outerHTML] on the SPAN tag in the template, but I get a runtime error saying there is no parent container element.
So, my question is can this work?:
Replace the in the template with the loaded SafeHtml? Or,
Apply the loaded SafeHtml as the innerHTML of the selector in the SvgComponent TS? Or,
Use <svg [innerHTML]="svg"> as part of the template instead of SPAN, but remove the parent SVG from the loaded SafeHtml before applying it to the innerHTML? Or,
Is there something in NPM that already does what I'm trying to create?
I wish they made this easier. Any advice or explanation as to why this won't work would be greatly appreciated. Thank you!
Naturally, as SOON as I post my question, I trip over the solution. The trick is to use ElementRef so that I can target the selector's innerHTML, and I don't have to use DomSanitizer to do it. The new code looks as follows (including imports this time):
import {Component, OnInit, Input, ElementRef} from '#angular/core';
import {HttpClient} from '#angular/common/http';
#Component({
selector: 'app-svg',
template: `
<ng-template>
{{ src }}
</ng-template>
`
})
export class SvgComponent implements OnInit {
#Input() public src = '';
constructor(
private el: ElementRef,
private http: HttpClient,
) {}
ngOnInit(): void {
this.http.get(this.src, {responseType: 'text'}).subscribe(svg => {
this.el.nativeElement.innerHTML = svg;
});
}
}
If you don't want to have app-svg as a container, you can use instead:
this.el.nativeElement.outerHTML = svg;
And it will replace app-svg with he loaded svg. Hope this helps anyone else trying to accomplish the same thing. Cheers!

Angular interpolate inside a component like mat-checkbox

So I want to have a mat-checkbox component with a HTML string inside the label.
I tried the following:
<mat-checkbox class="check">
{{ someHtml }}
</mat-checkbox>
But it prints the HTML string as a string and doesn't render it.
Using the following doesn't work either:
<mat-checkbox class="check" [innerHtml]="someHtml">
</mat-checkbox>
This just replaces the whole content, including the checkbox that gets generated at runtime. Is there any way to inject the html into the label?
You could use Angular Directives
The idea here is to fetch the element from the HTML, then append some raw HTML dynamically.
Supose this scenario
app.component.html
<mat-checkbox class="check" [appendHtml]="innerHtml"></mat-checkbox>
app.component.ts
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
innerHtml = `<div style="border: 1px solid red;"> Text inside </div>`;
constructor() {}
}
As you can see, I added a appendHtml attribute to the mat-checkbox element. This is a custom directive that expects a string as "raw" HTML.
append-html.directive.ts
#Directive({
selector: '[appendHtml]'
})
export class AppendHtmlDirective implements AfterViewInit {
#Input('appendHtml') html: string
constructor(private element: ElementRef) {
}
ngAfterViewInit() {
const d = this.element.nativeElement.querySelector('label');
d.insertAdjacentHTML('beforeend', this.html);
}
}
The AppendHtmlDirective expects an html property of type string and implements AfterViewInit interface (from Angular) to fetch the element once it is rendered. By injection, Angular provides us the element which is being applied; so, the ElementRef from the constructor is our MatCheckbox element, in that case.
We can use the insertAdjacentHTML function to append childs to the element. I just fetched the label element from the MatCheckbox to fit inside of it. In every case, you should see where to append the HTML.
I mean, label here works, bc MatCheckbox has a tag whitin matching that. If you want to reuse this Directive for other elements, you should be passing the literal to find inside.
i.e.:
append-hmtl.directive.ts
// ...
#Input() innerSelector: string
// ...
ngAfterViewInit() {
const d = this.element.nativeElement.querySelector(this.innerSelector);
d.insertAdjacentHTML('beforeend', this.html);
}
app.component.hmtl
<mat-checkbox class="check" [appendHtml]="innerHtml" innerSelector="label"></mat-checkbox>
Moreover, you can pass as many inputs as you need to customize the styling or behavior of your directive.
Cheers
I think you should just wrap everything in a div and put it on the outside.
<div>
<mat-checkbox class="check"> </mat-checkbox>
{{ someHtml }}
</div>

In Angular how do I dynamically wrap certain words in another html element?

I have this simple Angular Component:
#Component({
selector: 'my-component',
template: '<p>{{someString}}</p>',
})
export class MyComponent {
#Input() someString: string;
}
someString could be any string of any length. As an example, imagine that someString's value is :
"If you want my body and you think I'm sexy, come on, sugar, tell me so."
In that case the HTML generated by Angular would essentially be equivalent to:
<p>If you want my body and you think I'm sexy, come on, sugar, tell me so.</p>
How would I modify MyComponent so that it detects each occurrence of the word sexy in someString and have Angular wrap that word in another HTML element such as <b>. So in this example case it would generate something like:
<p>If you want my body and you think I'm <b>sexy</b>, come on, sugar, tell me so.</p>
What if I wanted to wrap every occurrence of the word sexy in an Angular Component instead of a native HTML Element? Would that necessitate a different approach?
You can try this :D
#Component({
selector: 'app-test',
template: `
<p [innerHTML]="stringFormatted()"></p>
`,
styles: []
})
export class TestComponent {
someString = "If you want my body and you think I'm sexy, come on, sugar, tell me so.";
stringFormatted() {
return this.someString.replace(/sexy/g, '<b>sexy</b>');
}
}
You can use the something like below - wherein after rendering the main sentence, you can replace the special word with a span element and apply a CSS class, say .special to that span tag.
import { Component, Input, ElementRef, AfterViewInit } from '#angular/core';
#Component({
selector: 'my-component',
template: '<p>{{sentence}}</p>'
})
export class MyComponent implements AfterViewInit {
#Input() sentence: string;
#Input() specialWord: string;
constructor(private el: ElementRef) {
}
ngAfterViewInit() {
this.el.nativeElement.innerHTML = this.el.nativeElement.
innerHTML.replace(new RegExp(`${this.specialWord}`, 'g'),
`<span class="special">${this.specialWord}</span>`);
}
}
To keep your code generic, you can use additional #Input() for special word.
In your application's styles.scss, you can define the CSS class .special.
.special {
font-weight: bold;
}
If you wonder why you can't use similar logic to replace the content of sentence by something like below:
this.sentence = this.sentence.replace(new RegExp(`${this.specialWord}`, 'g'),
`<span class="special">${this.specialWord}</span>`);
then, note that Angular will escape the HTML tags and they will appear as is in the output. So you will see something like this in the browser, instead of styled spans.
Hello, it's a <span class="special">beautiful</span> day and I am in a very <span class="special">beautiful</span> city
That's why, I had to resort to manipulating the innerHTML so that replacement is done after Angular has rendered the sentence to DOM.
This solution avoids using innerHTML and relies strictly on Angular constructs.
#Component({
selector: 'my-component',
template: `
<p>
<ng-container *ngFor="let segment of segments">
<span *ngIf="!segment.shouldBeBold()">segment.text</span>
<b *ngIf="segment.shouldBeBold()">segment.text</b>
</ng-container>
</p>
`,
})
export class MyComponent {
#Input() someString: string;
private wordsToBold = new Set(['sexy', 'body', 'sugar']);
get segments() {
const regex = this.makeRegex();
const segments = this.someString.split(regex);
return segments.map(segment => {
return { text: segment, shouldBeBold: wordsToBold.has(segment.toLowerCase()) };
});
}
private makeRegex() {
const expression = [...this.wordsToBold].join('\\b|\\b');
return new RegExp(`(\\b${expression}\\b)+`, 'gi');
}
}

how to change labeltext inside template in Angular 4

I am having a angular component here is my component
#Component({
selector: 'labelnumeric',
template: '<label>hello</label>'
})
here in template i am using hello as label text
and here component is defining in HTML control
here is my HTML
<labedate></labedate>
so on the basis of HTML control i want to change the label text how can i done this ?
is there is any possibility to set the name based on attributes ?
What you are looking for is #Input in your component
See the documentation here:
https://angular.io/guide/component-interaction
What you basically need todo is to import Input and then define an input property in your component
#Component({
selector: 'labelnumeric',
template: '<label>{{something}}</label>'
})
export class XYZ {
#Input() something: string;
}
and then you can use this like so in the html part
<labelnumeric [something]= "Text"></labelnumeric>
I think all you need is #input
#Component({
selector: 'labelnumeric',
template: `<label>{{numeric}}</label>`,
})
export class HelloComponent {
#Input() numeric: string;
}
Then use it like :
<labelnumeric numeric='10'></labelnumeric>
//OR
<labelnumeric [numeric]='your_varible'></labelnumeric>
WORKING DEMO (Basic Working demo of #input)

How to access html elements of component tag?

I want to access plain HTML declared in my component tag. Suppose I have component
#Component({
selector: 'app-demo'
template: '<some_template></some_template>'
})
export class AppDemoComponent {
}
if I am defining h1 inside the tag in another component
<app-demo>
<h1> demo text </h1>
</app-demo>
How can I access the h1 element inside the AppDemoComponent?
Edit:
This question is not about ViewChild as ViewChild gets information from the current template of the component. I'm asking if the component tag is called in the different file and the tag has HTML elements then how to access it.
Use ElementRef
You can use ElementRef to access the current component reference, allowing you to query it for nested elements.
getElementsByTagName, querySelectorAll, and getElementsByClassName will look down into the nested elements as they operate by inspecting what's rendered in the DOM, ignoring any encapsulation Angular does.
I am not sure if there is an Angular specific way to do it, but using vanilla JS lets you get there.
Child Component
import { Component, OnInit } from "#angular/core"
#Component({
selector: 'app-demo-child'
template: `
<h1> demo text </h1>
<h1 class="test"> demo text2 </h1>
<h1> demo text3 </h1>
`
})
export class AppChildComponent {
}
Parent Component
import { Component, OnInit, ElementRef } from "#angular/core"
#Component({
selector: 'app-demo-parent'
template: `
<app-demo-child></app-demo-child>
`
})
export class AppParentComponent {
constructor(
private elRef:ElementRef
) { }
doStuff() {
let HeaderElsTag = this.elRef.nativeElement.getElementsByTagName('h1') // Array with 3 h3 elements
let HeaderElsQuer = this.elRef.nativeElement.querySelectorAll('h1') // Array with 3 h3 elements
let HeaderElsClass = this.elRef.nativeElement.getElementsByClassName('test') // Array with 1 h3 element
}
}
Warning, this will look indiscriminately within your component, so be careful that you don't have nested elements with the same class name otherwise you'll have some hard to debug side effects
You can use content children. for your reference please follow the link below:
content children vs view children