Search code examples
angularreduxthemesngrxglobal-state

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


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?


Solution

  • @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
    }