Guidance on how to make a theming mechanism that effects all components in Angular?
Asked Answered
A

1

11

Question:

I need guidance on how to write a mechanism in Angular to set the "Look and Feel" of components globally in my application. Please note, I'm trying to learn @ngrx/platform and I thought this would be an interesting design constraint; however, I'm willing to let it go if it just doesn't make sense.

Breakdown:

I have an application in progress with many components. Each component in my application has currently 3 possible "look and feels(L&F)":

  • Morning (sepia)
  • Afternoon (white)
  • Evening (dark)

Please note there could be a spectrum of colors based on more granular time.

These L&Fs are set by the time of day of the current user e.g if user current time is 7 AM, the calculated L&F would be set to "Morning". I'm tracking this state inside ngrx/store of an angular module called SundialModule and gnomon is the mechanism of reducers and actions for getting or setting state:

sundial/reducers/gnomon.ts:

import * as gnomon from '../actions';

export interface State {
  currentHour: number,
}

const initialState: State = {
  currentHour: new Date().getHours()
};

export function reducer(state = initialState,
                        action: gnomon.Actions) {
  console.log(action, 'action');
    switch(action.type) {
      case gnomon.MORNING:
        return  {
          ...state,
          currentHour: 6,
        };
      case gnomon.AFTERNOON:
        return  {
          ...state,
          currentHour: 12,
        };
      case gnomon.EVENING:
        return  {
          ...state,
          currentHour: 7,
        };
      default:
        return state;
    }
}

Now I have an Angular Attribute directive called [sundialTheme] that will set a HostBinding('class') theme = 'light' on the element it's placed upon.

sundial/directives/theme.ts

@Directive({
  selector: '[sundialTheme]'
})
export class SundialThemeDirective implements OnInit {
  @HostBinding('class') theme = 'light';

  private gnomon: Observable<any>;

  constructor(private store: Store<fromGnomon.State>) {
    this.gnomon = store.select<any>('gnomon');
  }

  ngOnInit() {
    this.gnomon.subscribe((theme) => {
      if(theme.currentHour >= 7 && theme.currentHour <= 11){
        this.theme = 'sepia';
      } else if( theme.currentHour >= 12 && theme.currentHour <= 18) {
        this.theme = 'light'
      } else {
        this.theme = 'dark'
      }
    });
  }
}

The problem: Every component in my application will need to have this attribute of sundialTheme; moreover, every component will have a subscription to the this.gnomon = store.select<any>('gnomon');, which feels expensive/heavyhanded. Lastly, and as an aside gripe, is that, every component will need to have my sundialModule injected at every feature module, and every component will need the set of themes for each Time of day:

An example of this, every template's component. note: sundialTheme attribute directive:

<mh-pagination sundialTheme></mh-pagination>
<canvas glBootstrap class="full-bleed" sundialTheme></canvas>
<running-head [state]="(state$ | async)" sundialTheme></running-head>
<menu-navigation sundialTheme></menu-navigation>
<folio sundialTheme></folio>
<mh-footer sundialTheme></mh-footer>

Every Feature Module with a SundialModule dependency:

@NgModule({
  imports: [
    SundialModule
  ],
})
export class MenuModule {
}

Every component styleUrls with sundial-links: note the ugly sundial-morning.scss

@Component({
  selector: 'running-head',
  templateUrl: 'running-head.component.html',
  styleUrls: [
    'running-head.component.scss',
    '../../../components/sundial/sundial-morning.scss', // This !
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RunningHeadComponent {
}

Finally I've had other ways offered to me:

1) Since I'm using Angular CLI, I could add my styles globally and have a directive set a class on the body. Which seems to break any kind of web component standards.

2) I could use a factory loader in each components styleUrls:[]; which I havent gotten clarity on how to implement.

3) I could follow material design components architecture which adds the themes directly into the components which doesn't feel very dry? (however, I havent researched too deeply into)

4) I could make a custom decorator for each component (could be viable but I'm nto sure how to implement)

Any other suggestions? Any best practices with reasoning?

Aube answered 15/10, 2017 at 18:18 Comment(4)
I think you should just use themes (from material for example). You set a class at the top of your app and you just define all your components according to that.Bronchopneumonia
Is there something that prevents you from inheriting SundialThemeDirective in all themed components?Defecate
@estus I mean it just doesn't feel like is a "is a" relationships also, The community is saying use a factory loader or right your own decorators. Which I have no idea how to do eitherAube
I'm not sure what you mean. Class inheritance is exactly a relationship here. Because all descendant components should inherit common behaviour from a parent. I cannot confirm if this will surely work as expected because I don't do ngrx. I'm not sure which community you're referring to, but class decorator doesn't fit here because of its limitations.Defecate
N
6
@HostBinding('class') theme = 'light'

should better be

@HostBinding('class.light') isLight = theme === 'light';

to not overwriting all other classes on this element.

You can set the class only in AppComponent and use in component styles

:host-context(.light) {
  // my light component styles here
}
Nacelle answered 18/10, 2017 at 3:29 Comment(2)
1) thanks for the correction on Hostbinders. 2) alligator.io/angular/styles-between-components-angular well I'll be damned that missing piece of the puzzle with :host-context. QQ: I was also told that I could use a factory loader in the styleUrl of the component does that mean anything to you? Or another offering was a custom decorator*. Would love to hear alternatives ideas? Or how I'm doing it is how you would more or less do it? regardless, marking correct.Aube
I don't know about factory-loader. I'd just use host-context. It's CSS only. All other options seem too complicated to me.Michaelamichaele

© 2022 - 2024 — McMap. All rights reserved.