67

I have this code which sets a class on the host:

@HostBinding('class.fixed') true;

What I would like to do is make this a variable class that I can modify. How can I do this?

B Hull
  • 3,153
  • 6
  • 22
  • 24
  • 8
    For future visitors of this question, the snippet in the question above only works due to a bug in Angular which will be fixed in future versions: https://github.com/angular/angular/issues/40220 – Ingo Bürk Jan 07 '21 at 13:26

7 Answers7

115

This can't be made variable.

What you can do instead is to bind to the class property directly

@HostBinding('class') classes = 'class1 class2 class3';
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 3
    What's the downvote for? Did it not work? There was some change since then that caused problems with binding to `[class]="..."` that might hit this approach as well. – Günter Zöchbauer Aug 24 '16 at 14:17
  • 1
    You sir, you are a life saviour! – Wagner Danda da Silva Filho Nov 27 '17 at 21:57
  • To build on @GünterZöchbauer's example, could have more dynamic classes via `@HostBind('class') hostClasses: string;` initially then in `ngOnInit` (and optionally in `ngOnChanges` for more dynamism): `this.hostClasses = this.getHostClasses();` and have whatever logic you'd want in the `getHostClasses` method, possibly pulling from some other `@Input()` values. – jmq Mar 23 '18 at 18:52
  • 33
    This works, but the issue is it overrides any classes added to the host element from the parent component – Ryan Silva May 02 '18 at 14:52
  • That's a known issue and I think unlikely to change. – Günter Zöchbauer May 02 '18 at 15:06
  • 1
    @GünterZöchbauer, is this issue raised somewhere on GitHub? Could you please share a link to the issue? – rpeshkov Oct 04 '18 at 14:06
  • @rpeshkov the answer is quite old already. I haven't followed Angular closely since quite a while and don't know if this is still the case or if it is planned or even possible to be changed. – Günter Zöchbauer Oct 04 '18 at 14:10
  • @GünterZöchbauer - I just have faced this case (`host object` appends classes, but `HostBinding` replaces all other classes – MaKCbIMKo Oct 04 '18 at 14:13
  • @MaKCbIMKo If you think it's a bug, please create an issue if you can't find an existing one. – Günter Zöchbauer Oct 04 '18 at 14:26
  • 2
    @GünterZöchbauer -I've created an issue (https://github.com/angular/angular/issues/26251) - will see how it goes. – MaKCbIMKo Oct 04 '18 at 14:52
  • 1
    If it helps this is a good example of using `@HostBinding('class.your-class')` - https://stackoverflow.com/a/58071653/1148107 – mtpultz Sep 24 '19 at 00:38
  • 1
    @RyanSilva this is not an issue anymore as with angular 13. I did not check the exact version when this was changed. The current behavior is, that classes set directly to the element in the parent template are merged with the `@HostBinding` definitions. However, multiple `@HostBinding('class')` in the component are still conflicting. – Mrvonwyl Oct 05 '22 at 14:23
49

If you have a limited number of classes you can conditionally add each one:

@HostBinding('class.c1') get c1 () { return this.useC1; } 
@HostBinding('class.c2') get c2 () { return this.useC2; }

Note that .c1 and .c2 need to be defined outside the component.

Plunker

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 3
    I needed to added `()` for it to work (e.g. `get c1() { return this.useC1; }`. – Lee Nov 10 '16 at 19:41
  • 6
    You shouldn't need to define `.c1` and `.c2` outside of the component if you use the `:host` selector. See this answer for an example: https://stackoverflow.com/questions/35168683/hostbinding-with-a-variable-class-in-angular2#46207423 – Steve Schrab Jan 08 '18 at 14:15
  • For me, it worked after adding 'get' keyword before method name. Thanks:) – Alfaz Jikani Jul 05 '18 at 10:08
40

I have to contradict the other answers, there is no reason why binding class.foo shouldn't work. Actually, the following format works properly:

@HostBinding('class.foo') variableName = true;

A common gotcha that foils many developers is the scope of the CSS class. You might need to change the selector scope in order to make the class visible to the selector itself, and not its children (you can see a discussion here).

To make the class visible to the Angular component itself you have two choices: you can either change the View Encapsulation to None (it will alter the behavior), or you can match the class with the :host pseudoselector.

In other words, the problem is that HostBinding only sees the host scope, it means that it only sees the classes and the id applied to the component itself, not to its children. So when you write your CSS, you need to specify that CSS belongs to the component itself (the host pseudoelement).

According to Angular documentation:

Use the :host pseudo-class selector to target styles in the element that hosts the component (as opposed to targeting elements inside the component's template).

You can easily specify the host scope just adding :host before your CSS rule:

:host.foo { // It only matches <component-name class="foo">
  /* Your CSS here */
}

In place of

.foo { // It matches any <element class="foo" /> inside <component-name>
  /* Your CSS here */
}

If you want, I created a working Plunker that you can see clicking here

  • 1
    I don't believe you need the parenthesis around `(.className)` when chaining it off `:host`. I think you make chain just like any other selector string. `:host.className` – Steve Schrab Jan 08 '18 at 14:14
  • 3
    This is for a fixed class name though, not a variable class name. – rooby Jun 04 '20 at 05:14
17

Günter's answer isn't really helpful in case of already having some class names bound to a variable.

A good way to combine variable string class names with boolean style predefined class names is to use the classnames npm package.

Use it together with the @HostBinding and a setter function to get amazing results:

import * as classNames from 'classnames';

(...)

@HostBinding('class') get classes(): string {
  return classNames(this.getDynamicClassName(), {
    'is-open': this.isOpen,
    'has-children': this.hasChildren
  });
}
matewka
  • 9,912
  • 2
  • 32
  • 43
11
@Input()
  class = '';

@HostBinding('attr.class')
get btnClasses() {
return [
    'btn',
    this.someClassAsString,
    this.enableLight ? 'btn-secondary-light' : '',
    this.class,
].filter(Boolean).join(' ');
};

You can hijack the class attribute with an input and add what you need to it in a hostbinding

Rico dos Santos
  • 113
  • 1
  • 4
  • This is a great answer, solves https://stackoverflow.com/questions/35168683/hostbinding-with-a-variable-class-in-angular IMHO in a better way – llmora May 03 '20 at 22:50
2

You can create some separated directives with class.

For example: I have a button in my page, and has may states: default, primary, danger and fluid. Button can have many different states, but I'll show you with theese three states because of huge amount of code. So let's get start it!

button.ts

//default button

@Directive({
    selector: '[appButtonDefault]'
})
export class ButtonDefaultDirective {

    // the name of the field is not important
    // if you put this directive to element, 
    // this element will have the class called 'button--default'

    @HostBinding("class.button--default")
    private defaultClass: boolean = true;
}


//primary button

@Directive({
    selector: '[appButtonPrimary]'
})
export class ButtonPrimaryDirective {

    // the name of the field is not important
    // if you put this directive to element, 
    // this element will have the class called 'button--primary'

    @HostBinding("class.button--primary")
    private primaryClass: boolean = true;
}


// danger button

@Directive({
    selector: '[appButtonDanger]'
})
export class ButtonDangerDirective {

    // the name of the field is not important
    // if you put this directive to element, 
    // this element will have the class called 'button--primary'

    @HostBinding("class.button--danger")
    private dangerClass: boolean = true;
}

@Directive({
    selector: '[appButtonFluid]'
})
export class ButtonFluidDirective {

    // the name of the field is not important
    // if you put this directive to element, 
    // this element will have the class called 'button--primary'

    @HostBinding("class.button--fluid")
    private fluidClass: boolean = true;
}


// you need to also create a component class,
// that import styles for this button
@Component({
    //just put created selectors in your directives
    selector: `[appButtonDefault], [appButtonPrimary], 
               [appButtonDanger], [appButtonFluid]`,
    styleUrls: ['<-- enter link to your button styles -->'],

    // it is required, because the content of <button> tag will disappear
    template: "<ng-content></ng-content>" 
})
export class ButtonComponent {}


// you don't have to do it, but I prefet to do it

@NgModule({
    declarations: [
        ButtonDefaultDirective,
        ButtonPrimaryDirective,
        ButtonDangerDirective,
        ButtonFluidDirective,
        ButtonComponent
    ],
    exports: [
        ButtonDefaultDirective,
        ButtonPrimaryDirective,
        ButtonDangerDirective,
        ButtonFluidDirective,
        ButtonComponent
    ]
})
export class ButtonModule {}

Don't forget to import ButtonModule to your app.module.ts file. It is very important.

app.component.html

<!-- This button will have the class 'button--default' -->
<button appButtonDefault>Default button</button>

<!-- But this button will have the class 'button--primary button--fluid' -->
<button appButtonPrimary appButtonFluid>Primary and fluid</button>

I hope it helps.

t.piwowarczyk
  • 365
  • 2
  • 5
  • 17
  • 1
    I hope this approach is not been used in your production code... Lot of code instead of one line – MarkosyanArtur May 24 '19 at 07:22
  • Excellent answer! Thanks – Ricardo Almeida Jul 06 '21 at 10:09
  • 1
    @MarkosyanArtur From this perspective it looks, that there are lot's of coding. Your solution is also good due to not required additional directives, but you know: if you create those directives, then you don't have to remember class names. Instead: you can use directive names. This causes better validation. If you provide wrong directive name, then app won't start. Different thing is for adding classes directly. – t.piwowarczyk Jul 07 '21 at 12:46
2

There are lots of answers already, but none have mentioned NgClass. In my opinion, the most reliable and consistent way to do this is extending NgClass because it provides everything we need out-of-the-box-ish:

@Directive({ selector: '[myDirective]'})
export class MyDirective extends NgClass {
  constructor(
    _iterableDiffers: IterableDiffers,
    _keyValueDiffers: KeyValueDiffers,
    _ngEl: ElementRef,
    _renderer: Renderer2
  ) {
    super(_iterableDiffers, _keyValueDiffers, _ngEl, _renderer);
  }

  setClass() {
    this.ngClass = {
      underline: true,
      bold: true,
      italic: true,
      pretty: false
    };

    // or
    this.ngClass = ['asd', 'abc', 'def'];

    // or 
    this.ngClass = 'foo';
  }
}
j2L4e
  • 6,914
  • 34
  • 40