Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Need a way for a component to add CSS classes to host non-destructively #7289

Closed
jelbourn opened this issue Feb 25, 2016 · 28 comments
Closed
Assignees
Labels
area: core Issues related to the framework runtime feature Issue that requests a new feature hotlist: components team Related to Angular CDK or Angular Material state: Needs Design
Milestone

Comments

@jelbourn
Copy link
Member

Say a user wants to add margin around a component:

<cool-thing class="extra-margin"> ... 

But the component itself has need to set a dynamic CSS class to the host element:

@Component({
  ...
  host: { '[class]': 'myCustomStyle' }
})
class CoolThing {
  myCustomStyle: string = figureOutWhichClassToUse();
}

Doing this in the component will completely overwrite the class property, wiping away the user's classes.

In other situations, NgClass serves this purpose, but a component cannot apply a directive to its host element. Using the [class.whatever] syntax also doesn't solve this, as the whatever is the variable part.

Proposal

Each component can, conceptually, have two distinct class lists: one that is from the user and one that comes from the component itself. These could be resolved independently with Angular rendering the union. Any time the user uses ngClass or the [class.whatever] syntax, it should override whatever is set to "class".

cc @mhevery

@wembernard
Copy link

👍

@JustasKuizinas
Copy link

Any updates on this?

@Swiftwork
Copy link

Isn't this quite easily solved by:

@HostBinding('class') @Input('class') classList: string = '';

This will capture any previous classes that have been manually assigned to the components dom. However does not facilitate for ones set dynamically by e.g. directives.

@chuckjaz chuckjaz added the area: core Issues related to the framework runtime label May 23, 2017
@pkozlowski-opensource pkozlowski-opensource marked this as a duplicate of #15618 Jul 26, 2017
@tsu1980
Copy link

tsu1980 commented Sep 20, 2017

My workaround.

constructor(private el: ElementRef, private renderer: Renderer) { }

ngOnInit() {
  this.renderer.setElementClass(this.el.nativeElement, 'my-item-' + this.itemType, true);
}

@xmeng1
Copy link

xmeng1 commented Oct 18, 2017

Currently, I use 3 methods to add the class name for a component. I don't know whether they are good way to do it, but it works according to my test.

  1. use host: https://stackoverflow.com/a/35996292/2000468

@component({
host: {
'[class]': 'm-grid__item' +
' m-grid__item--fluid''},
})

  1. use HostBinding

@HostBinding('class.m-grid__item') public bindStyle: boolean = true;
@HostBinding('class.m-grid__item--fluid') public bindStyle2: boolean = true;

  1. add class name to the component selector directly https://stackoverflow.com/a/38472185/2000468
    @component({
    selector: 'app-test .m-grid__item.m-grid__item--fluid',
    })

@MartinMa
Copy link
Contributor

MartinMa commented Nov 8, 2017

@xmeng1 Thanks for your summary. Just a small correction for approach 1 to avoid confusion (for anyone who might read this).

It has to be '[class]': (instead of ['class']:)! The quotes have to be on the outside.

@xmeng1
Copy link

xmeng1 commented Nov 8, 2017

@MartinMa you are right, thanks!

@trotyl
Copy link
Contributor

trotyl commented Dec 16, 2017

I used a manually provided NgClass to achieve this like:

const prefix = 'ant-btn'

@Directive({
  selector: 'button[antBtn]',
  providers: [ NgClass ],
})
export class HelloComponent implements OnChanges  {
  @Input() color: string = 'default'

  private hostClasses: { [name: string]: boolean }

  constructor(@Self() private ngClass: NgClass) { }

  ngOnChanges(changes: SimpleChanges): void {
    this.hostClasses = {
      [`${prefix}`]: true,
      [`${prefix}-${this.color}`]: true,
    }

    this.updateHostClasses()
  }

  private updateHostClasses(): void {
    this.ngClass.ngClass = this.hostClasses
    this.ngClass.ngDoCheck()
  }
}

More details can be found in the StackOverflow answer.

@ngbot ngbot bot added this to the Backlog milestone Jan 23, 2018
@arniebradfo
Copy link

Overriding the native class="" attribute with an @Input() class: string; works pretty well:

import { Component, Input, HostBinding } from '@angular/core';

@Component({
	selector: 'gist-keeps-class',
	template: 'this component keepes class="class" attributes'
})
export class KeepsClass {

	@Input() booleanInput: boolean = false;
	@Input() stringInput: 'some-thing' | 'another-thing' | 'a-third-thing' = 'another-thing';

	@Input() class: string = ''; // override the standard class attr with a new one.
	@HostBinding('class')
	get hostClasses(): string {
		return [
			this.class, // include our new one 
			this.booleanInput ? 'has-boolean' : '',
			this.stringInput
		].join(' ');
	}
       
}
<gist-keeps-class 
  class="some classes" 
  [booleanInput]="true" 
  [stringInput]="some-thing"
></gist-keeps-class>

will output this:

  <gist-keeps-class class="some classes has-boolean some-thing" >
    this component keepes class="class" attributes
  </gist-keeps-class>

It's not throughly tested, but should work?

@kylecordes
Copy link

We sometimes get this issue's question (teaching Angular Boot Camp) from people first learning Angular, before they are really committed to it. We manage to get them up and running with variants of the solutions written above, but is definitely an area where we are to some extent "apologizing" that this is currently quite a rough edge.

My request for whoever designs a full solution to this: aim for best in class, very easy and smooth.

@MattewEon
Copy link

@arniebradfo do you think that it is possible to wrap your solution in an abstract component or in a directive ?

@chaosmonster
Copy link

An observation I made. If you have two directives (or a component and a directive) that do the above way using Input and HostBinding it will not work. Only one wins. Will build a small example repo this week to illustrate what I mean

@faizalmy
Copy link

My solution:

@HostBinding('class') classList = 'fixed-class';
@Input() class: string;

constructor() {
}

ngOnInit() {
   if (this.class) {
      this.classList += ' ' + this.class;
   }
}

usage:

<my-component [class]="'custom-class'">
    my content
</my-component>

output:

<my-component class="fixed-class custom-class">
    my content
</my-component>

@kyubisation
Copy link

Based on the answer by @trotyl I implemented a simple solution: https://stackblitz.com/edit/angular-cvkrpe?file=src%2Fapp%2Fapp.component.html

This solution will not overwrite class attribute on host, [class.binding] or [ngClass] (as other solutions might do).

import { Directive } from '@angular/core';
import { NgClass } from '@angular/common';

@Directive({})
export class HostClass extends NgClass {
  apply(value: string | string[] | Set<string> | { [klass: string]: any }) {
    this.ngClass = value;
    this.ngDoCheck();
  }
}

Usage:

@Component({
  selector: 'hello',
  template: `<h1>Hello {{name}}!</h1>`,
  providers: [HostClass]
})
export class HelloComponent implement OnInit  {
  @Input() name: string;

  constructor(@Self() private hostClass: HostClass) { }

  ngOnInit() {
    if (this.name === 'special name') {
      this.hostClass.apply('special);
    } else {
      this.hostClass.apply({
        [this.name]: true,
        normal: true,
      });
    }
  }

@mslooten
Copy link

How about using an ElementRef to do this? We ran into the same issue and saw that Material uses this method to add classes.

This works for both class and ngClass classnames set on the host element.

  constructor(elementRef: ElementRef) {
    elementRef.nativeElement.classList.add('my-classname');
  }

@MaKCbIMKo
Copy link

@mslooten - this is what I had to use as well. But it looks bulkier than @HostBinding

@greggbjensen
Copy link

This worked well for me and was a simple solution:

@HostBinding('class.header-logo') public additionalClass: boolean = true;

@jpcmf
Copy link

jpcmf commented Aug 21, 2019

My solution:

@HostBinding('class') classList = 'fixed-class';
@Input() class: string;

constructor() {
}

ngOnInit() {
   if (this.class) {
      this.classList += ' ' + this.class;
   }
}

usage:

<my-component [class]="'custom-class'">
    my content
</my-component>

output:

<my-component class="fixed-class custom-class">
    my content
</my-component>

All examples

constructor() {
}

I've just add @HostBinding('class') classList = 'd-flex flex-column h-100'; to my app.component and it works. Thank you @faizalmy 😃

michael-vasyliv added a commit to RenetConsulting/angularcore.net that referenced this issue Sep 10, 2019
@etay2000
Copy link

etay2000 commented Oct 8, 2019

Wow another common use case issue lingering since 2016. Perhaps my open source software expectations are too high?

@trotyl
Copy link
Contributor

trotyl commented Oct 9, 2019

@etay2000 It has already been fixed in Ivy for some time, just wan't brought back to view engine.

@etay2000
Copy link

etay2000 commented Oct 9, 2019

Ok thanks, I appreciate the info.

@exequiel09
Copy link

exequiel09 commented Dec 20, 2019

I'm having an issue with HostBinding in Angular v9.0.0-rc.6 which outputs the string [MAP].

Screen Shot 2019-12-20 at 5 05 52 PM

I am currently using @faizalmy reply here in this issue

My solution:

@HostBinding('class') classList = 'fixed-class';
@Input() class: string;

constructor() {
}

ngOnInit() {
   if (this.class) {
      this.classList += ' ' + this.class;
   }
}

usage:

<my-component [class]="'custom-class'">
    my content
</my-component>

output:

<my-component class="fixed-class custom-class">
    my content
</my-component>

/cc @kara @jelbourn

@vthinkxie
Copy link
Contributor

vthinkxie commented Jan 8, 2020

this issue has been fixed in angular 9.0.0

anyone who meet this issue can try the below codes in angular 9

import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  template: '<button (click)="updateHostClassList()">host</button>',
  host: {
    "[class]": "hostClass"
  }
})
export class AppComponent {
  hostClass = {};
  updateHostClassList() {
    const random = Math.floor(Math.random() * 1000).toString(36);
    this.hostClass = {
      [random]: true
    };
  }
}

@PeterHewat
Copy link

PeterHewat commented Jan 27, 2020

I confirm this works in Angular 9.0.0
Here is a simple example:

import { Component, OnInit, HostBinding } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  @HostBinding('class') private hostClass: object = {};

  public ngOnInit(): void {
    this.addClass('myYellowBackground');
  }

  private addClass(className: string): void {
    this.hostClass[className] = true;
  }

  private removeClass(className: string): void {
    delete this.hostClass[className];
  }

}

Then, if you add a class to that component as follows...

<app-root class="myRedBorder"></app-root>

... both myRedBorder and myYellowBackground classes will be applied to the instance of <app-root/>

@wnvko
Copy link

wnvko commented Feb 5, 2020

I think this is some easy way for adding class to component:

@Component({
  ...
})
export class CoolThingComponent {
  @HostBinding('class.myCustomStyle') public hostClass = true;
}

And then use component like this:

<cool-thing class="extra-margin">

@vladimiry you can add ass much classes as you need like this:

@Component({
  ...
})
export class CoolThingComponent {
  @HostBinding('class.myFirstHostedClass')
  @HostBinding('class.mySecondHostedClass')
  public hostClass = true;
}

and then several inline classes too:

<cool-thing class="firstInlineClass secondInlineClass">

@vladimiry
Copy link

@wnvko adding one class is easy, the issue is about host-bind a multiple classes.

@mhevery
Copy link
Contributor

mhevery commented Feb 6, 2020

This has been resolved as of #34938 Closing the issue.

@mhevery mhevery closed this as completed Feb 6, 2020
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Mar 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: core Issues related to the framework runtime feature Issue that requests a new feature hotlist: components team Related to Angular CDK or Angular Material state: Needs Design
Projects
None yet
Development

No branches or pull requests