Background
I have a component that wraps two span elements. I would like parent components to be able to style the inner two spans by specifying CSS classes and styles as inputs (preferably as simple strings). Here's a simplified example of what I would like this to look like:
Code
app component (parent)
#Component({
selector: 'my-app',
template: `
<div>
<app-two-spans [classArg1]="'class1'" [classArg2]="'class2'"
[styleArg1]="'width: 100px'" [styleArg2]="'width:200px'"></app-two-spans>
</div>
`, style: `
::ng-deep .class1 {
border: 1px solid black;
}
::ng-deep .class2 {
border: 1px solid blue;
}
`
})
export class App {
}
two spans component (child)
import {Component, Input, Output, EventEmitter} from '#angular/core';
#Component({
selector: 'app-two-spans',
template: `
<span *ngIf="flag" [ngClass]="classArg1" [ngStyle]="styleArg1"
(click)="flag = !flag">click me</span>
<span *ngIf="!flag" [ngClass]="classArg2" [ngStyle]="styleArg2"
(blur)="flag = !flag" contenteditable="true">
click me to change my value</span>
`
})
export class TwoSpansComponent {
flag: boolean = true;
#Input() styleArg1: string;
#Input() styleArg2: string;
#Input() classArg1: string;
#Input() classArg2: string;
constructor() {}
}
Problem
The class styling seems to work in my local environment (though it doesn't seem to work on Plunker for some reason). However, the styles are not showing up. I've seen other posts about styles as inputs, but from what I've seen this is usually done by passing style-value pairs (see accepted answer here). However, I would really like to pass these styles as a simple string to make working with the component easier.
I notice the following error in the console: ERROR Error: Cannot find a differ supporting object 'width: 100px'. I'm not sure what this means at all.
Plunker here
Is there a way to do this? How can I give parent components the ability to stylize children?
ngStyle accepts an Object instead of plain string. You can pass your styles as:
[styleArg1]="{ width: '100px' }"
[styleArg2]="{ width: '200px' }"
Related
I have a component which is reusable. This component is called from parent component multiply times and sometimes the background page of the parent component is white and sometimes is black.
My child component generates form tags dynamycally - inputs,selects, textarea.
That means i can't have fixed styles in my css in my component for my content.
So when when the background page is white - i have one style for my inputs - for example black background. When the background page is black i have another style for my inputs - for example white bacgrkound.
To solve this is issue:
i tried
Adding input property in my child component ts file
#Input()
public cssTemplate;
in html
<div [ngClass]="{'form-group-white-bg': cssTemplate == 'white', 'form-group-dark-bg': cssTemplate == 'black'}">
<label for=""></label>
....
In the CHILD component i am sending value for input property depending on where the child component is called
if it is called on page with white background
<app-form-group cssTemplate="black" formControlName="address">
</app-form-group>
if it is called on black bacgrkound
<app-form-group cssTemplate="white" formControlName="address" [data]="{ field: 'address', label: 'Address' }">
</app-form-group>
but the problem here is that sometimes on my parent component this component is called multiply times
on one page can be called 12 times where i need 10 inputs and 2 selects
on other page can be called 15 times etc.
That means that i need to repat my self 15 times
<app-form-group cssTemplate="white" formControlName="address">
</app-form-group>
<app-form-group cssTemplate="white" formControlName="someItherControlName">
</app-form-group>
and everywhere to put cssTemplate="white".
ngFor is not an optin because this child component is called multiply times but not on same place in the HTML structure in the parent.
How can i solve this DRY?
you can add styles in your styles.css (the styles general for all the application). If e.g. you has
.white h1{
color:red;
}
.black h1{
color:green;
}
You can use [ngClass] in the "parent"
<div [ngClass]="toogle?'white':'black'">
<hello name="{{ name }}"></hello>
</div>
<button (click)="toogle=!toogle">toogle</button>
See [stackblitz][1]
NOTE: I used the way [ngClass]="expresion" (where expresion use conditional operator) better that [ngClass]="{'classname1':condition;'classname2':!condition}"
Update about your comment "how can i prevent repeating my self on child call", really I don't understand so much. I don't know if you want to make a directive like, e.g.
#Directive({
selector: 'hello', //<--the selector is the selector of the component
exportAs: 'helloDiv'
})
export class HelloDirective implements OnInit{
constructor(#Self() private component:HelloComponent,private dataService:DataService){
}
ngOnInit(){
console.log(this.component.name)
this.dataService.theme.subscribe(res=>{
this.component.theme=res;
})
}
}
This allow to "extends" the component -in the stackblitz the variable "theme" change-
[1]: https://stackblitz.com/edit/angular-ivy-sjwxyq?file=src%2Fapp%2Fapp.component.html
You can use an input property to create a css class map to pass on to ngClass. This object should be an object of string arrays.
It can be pretty much as complex and contain as many classes and rules as you need it too
#Input() color: 'white' | 'red' | 'hotpink' = 'white';
classMap: any;
ngOnInit() {
this.updateClassMap();
}
updateClassMap() {
this.classMap = {
[this.color]: !!this.color, // Add this class if not null
};
}
Then in the Html simply pass it to ngClass
<div [ngClass]="classMap">
Styling Child Components depending on Parent Component
There are two approaches I commonly take in this scenario
:ng-deep - create a style rule based on a class which is set in your parent component
utilize #ContentChildren() to set a property directly on your child components and call detectChanges() manually after the change.
To adopt the first solution you need to exercise greater care in your css naming rules, as using ng-deep obviously breaks the isolation of those style rules.
To adopt the second approach needs some considering due to it technically circumventing the standard input/output flow in Angular and thus can be a bit of a surprise "undocumented behavior" for any other maintainers of the application.
I'm a bit on the fence whether I prefer one approach over the other. The first approach seems more trivial to me, but it can also cause unintended style rule overwrites, while the second approach involves a lot more scripting and seems a bit of a hack.
Approach 1: ng-deep
Give your parent component an input and update a class on a block-element wrapping your <ng-content>.
create your desired style rules in your child component.
// parent component
#Component(...)
export class FooParent {
#Input() bgStyle: 'light' | 'dark' = 'light';
}
<!-- parent component template -->
<div class="parent" [ngClass]="{light: bgStyle == 'light', dark: bgStyle == 'dark'}">
<ng-content></ng-content>
</div>
// child.css
::ng-deep .light .child-container {
background-color: lightblue;
}
::ng-deep .dark .child-container {
background-color: royalblue;
}
My targeted element in the example is .child-container, you would write a similar style rule for each element you want to affect.
Approach 2: Using ContentChildren to pass along a value
Add a #ContentChildren() decorator to your parent component which selects for your child components.
inject a ChangeDetectorRef
implement ngAfterViewInit to loop through each child and set the value
call detectChanges() once done.
add the ngClass directive as normally in your child component.
Parent
#Component({
selector: 'parent',
templateUrl: 'parent.component.html',
styleUrls: ['parent.component.scss']
})
export class ParentComponent implements AfterViewInit, OnChanges {
#Input() bgStyle: 'light' | 'dark' = 'light';
#ContentChildren(ChildComponent) childComponents!: QueryList<ChildComponent>;
constructor(private change: ChangeDetectorRef) {
}
ngOnChanges(changes: SimpleChanges) {
if ('bgStyle' in changes) {
this.updateChildComponents();
}
}
updateChildComponents() {
this.childComponents.forEach(child => {
child.bgStyle = this.bgStyle;
});
this.change.detectChanges();
}
ngAfterViewInit() {
this.updateChildComponents();
}
}
<!-- parent.component.html -->
<ng-content></ng-content>
Child
#Component({
selector: 'child',
templateUrl: 'child.component.html',
styleUrls: ['child.component.scss']
})
export class ChildComponent implements OnInit {
bgStyle: 'light' | 'dark' = 'light';
constructor() {
}
ngOnInit(): void {
}
}
<!-- child.component.html -->
<div [ngClass]="{light: bgStyle == 'light', dark: bgStyle == 'dark'}" class="child-container"></div>
// child.component.css - you would apply styles as you needed obviously.
.child-container {
width: 40px;
height: 40px;
margin: .5rem;
}
.light.child-container {
background-color: lightblue;
}
.dark.child-container {
background-color: royalblue;
}
Usage
<!-- any other template -->
<parent>
<child></child>
<child></child>
<child></child>
</parent>
Note: If you are creating the ChildComponent directly in the ParentComponent's own template you need to use #ViewChildren instead of #ContentChildren
How to force CSS of child component from parent using ::ng-deep or something?
I have parent component where I put child component:
....parent.component...
<app-likes></app-likes>
.....parent.component......
Not inside that likes component there is he following HTML:
<div class="mainDiv">
<div class="secondDiv"><i class="far fa-heart fa-3x"></i></div></div>
Now I want to set color of fa-heart class to white from parent parent.component.css.
How can I do that?
You can do this way, in the css of the parent component:
parent.component.css:
:host ::ng-deep .fa-heart {
color: red;
}
or
:host ::ng-deep app-likes .fa-heart {
color: red;
}
Well I will go against the folks above and suggest that you don't do this.
If you consider the component an isolated building block in your app, you would consider it an advantage, that it looks the same in every place you use it. Using ::ng-deep to override this behaviour will cause you trouble in larger apps.
Angular promotes using #Inputs as the interface of passing data into the component. My suggestion is to use #Input to modify the view. Or, if in larger contexts you can use Dependency Injection to provide a token that specifies a theme for all children of a component.
<app-likes theme="white"></app-likes>
#Component({selector: 'app-likes'})
class AppLikesComponent {
#Input() theme: string;
#HostBinging("class") get themeBinding() {
return 'theme--' + this.theme;
}
}
You could set the ViewEncapsulation option in the parent component to remove the shadow DOM. This essentially allows the child component to use the selector definitions from the parent component.
Try the following
Parent component
import { Component, ViewEncapsulation } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ],
encapsulation: ViewEncapsulation.None // <-- no shadow DOM
})
export class AppComponent {
}
Parent CSS
.fa-heart {
color: white;
}
Working example: Stackblitz
I've been trying to style a BoxedComponent style from HeaderComponent without using ::ng-deep, but I'm not finding a way to so properly.
The BoxedComponent component class contains nothing different but a ViewEncapsulation.None.
#Component({
selector: 'labs-boxed',
templateUrl: './boxed.component.html',
styleUrls: ['./boxed.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class BoxedComponent {}
HTML
<div class="boxed">
<ng-content></ng-content>
</div>
I created two classes in its SCSS file to use everywhere, but I realized I couldn't still stylize the responsiveness of the component due to the same problem.
.boxed {
/* rules */
}
.tall .boxed {
height: 500px;
width: 360px;
}
.squared .boxed {
height: 340px;
width: 340px;
}
In HeaderComponent's HTML file, I am making use of labs-boxed as follows:
<div id="who-we-are" class="d-flex justify-content-around">
<labs-boxed [ngClass]="'tall'">
<p>
...
...
...
So far it works, but then entering in the SCSS file, I have attempted many ways to access lab-boxed's styles and changing it:
.boxed { ... }
.tall .boxed { ...}
and so on, but without success.
How can I do it, please?
Since BoxedComponent has ViewEncapsulation.None all rules defined in boxed.component.scss becomes global. When HeaderComponent has default encapsulation, all rules defined in header.component.scss are isolated from global scope and becomes specific to HeaderComponent. As a matter of fact ng-deep works by making single rules global. In other words it removes rules from isolated scope and puts into the global scope and it is the only way of making a single rule global within an encapsulated styles file. Without help of ng-deep you can override child component styles in following ways;
Apply another global style to child and add related styles to global; styles.scss file.
in header.component.html
<labs-boxed class="custom-box-style">
in styles.scss
.custom-box-style .boxed {
// new rules
}
Programatically set styles on specific child instance.
in header.component.html
<labs-boxed #childEl class="squared">squared and red</labs-boxed>
in header.component.ts
#ViewChild('childEl', {read: ElementRef, static: true}) childEl: ElementRef;
ngAfterViewInit() {
const box = this.childEl.nativeElement.getElementsByClassName("boxed");
box[0].style.backgroundColor = "red"
}
Here is a demonstration of both approaches.
Usually, I can style the very root of my component by using the :host pseudo style like this.
:host{ border: 1px solid gold; }
But how shold I handle if said style is supposed to be set dynamically, based on the parameters passed to #Input?
The only way I can think of at the moment is to add an auxilliary DIV and style it like so.
<div [ngClass]="styleMeDynamically"> ... </div>
Is there a way to apply a style dynamically directly on the host without the injected DIV?
I've found this suggestion but it requires explicitly stating the classes and connecting them to separate inputs. I'd like to get a config object as passed in parameter and bind the styling using [ngClass] to retail full flexibility.
Probably #HostBinding decorator can help you. It allows to bind any host attribute including class and style. For example:
#Component({ ... })
export class MyComponent {
// you can conditionally add a class to the host element
#Input()
#HostBinding('class.large')
large = false;
// it's possible to bind a style as well
#Input()
#HostBinding('style.border.px')
borderWidth = 1;
#Input()
green = false;
// and you can use a getter
#HostBinding('style.border-color')
get borderColorStyle() {
return this.green ? 'green' : 'black';
}
}
Since angular 9 it should be possible even to bind a CSS variable, see Improved CSS class and style binding section of the 9 version release article.
<div [style.--main-border-color]=" '#CCC' ">
<p style="border: 1px solid var(--main-border-color)">hi</p>
</div>
What you can do is,
Create a custom directive that will accept a style object. and inside that directive, you can get the reference of host element and modify its style.
Here is a Demo
And here is a quick explanation.
Create a directive as which will accept a style object.
import {Directive,TemplateRef,ElementRef,OnChanges,SimpleChanges,OnInit,Renderer2,DoCheck,Input} from "#angular/core";
#Directive({
selector: "[appSetStyle]"
})
export class SetStyleDirective implements OnInit, OnChanges {
#Input() appSetStyle: { [key: string]: any } = {};
constructor(private elementRef: ElementRef<HTMLElement>) {}
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges): void {
this.applyStyles();
}
applyStyles(): void {
if (this.appSetStyle) {
for (const key in this.appSetStyle) {
this.elementRef.nativeElement.style[key] = this.appSetStyle[key];
}
}
}
}
Use that style object with any html element or any other component in your project.
<app-header [appSetStyle]="dynamicStyles"></app-header>
If you don't want to make a directive then you can inject the ElementRef inside the component itself which you want to style.
ElementRef is the what you need to use to get the reference of host.
I hope this will help.
I am using angular (^8.2.14) and ag-grid-community (^20.1.0).
I try to achieve the following effect: when the user hovers a specific row, one column shows an additional button which may be clicked.
What does work?
The column with the wanted behaviour gets rendered by a custom cell renderer component which implements the ICellRendererAngularComponent interface. There I can inject the ElementRef and on the lifecycle hook AfterContentChecked I check the parents' parent for the css-class 'ag-row-hover' and if it's there, I will show this additional button in the renderer component.
custom-cell.component.ts
#Component({
selector: 'app-custom-cell',
template: `
<ng-container *ngIf="hovered; else notHovered">{{form.value * form.value}}
<button (click)="doStuff(form.value)">show root</button></ng-container>
<ng-template #notHovered>{{form.value + form.value}}</ng-template>
`,
})
export class CustomCellComponent implements ICellRendererAngularComp, AfterContentChecked {
form: FormControl;
params: ICellRendererParams;
hovered = false;
constructor(private elementRef: ElementRef) {
}
doStuff(val) {
alert(val);
}
ngAfterContentChecked() {
this.hovered = (this.elementRef.nativeElement as HTMLElement)
.parentElement
.parentElement
.classList.contains('ag-row-hover');
}
}
The source code for this is on github
What do I want to improve?
I want to improve the performance and not having explicit checks for this behaviour. I'd rather have a directive with the css-selector '.ag-row', which I can inject to the ICellRendererAngularComponent and then check for the css class '.ag-row-hover' on every check. Or have a directive with the css-selector '.ag-row-hover' which is there, or is not. Anyone has any ideas how to improve my existing solution?
You could actually achieve the desired behavior with CSS only, no need to inject ElementRef or do anything via Angular Lifecycle Callbacks.
That being said, your custom renderer could look something like this:
#Component({
selector: "app-custom-cell",
template: `
<span style="color: red;">
<div class="visible-on-hover">
{{ form.value * form.value }}
<button (click)="doStuff(form.value)">show root</button>
</div>
<div class="hidden-on-hover">{{ form.value + form.value }}</div>
</span>
`
})
export class CustomCellComponent
implements ICellRendererAngularComp {
form: FormControl;
params: ICellRendererParams;
constructor() {}
doStuff(val) {
alert(val);
}
//... other stuff
}
In your styles.scss (or somewhere it fits better, just make sure the styles apply), just add these rules:
div.visible-on-hover {
display: none;
}
.ag-row-hover {
div.visible-on-hover {
display: block;
}
div.hidden-on-hover {
display: none;
}
}
Conceptually, the browser rendering engine then does what you did in ngAfterContentChecked().