Angular 4 CLI, WebPack, switching bootstrap theme

2020-03-01 07:52发布

问题:

I want to provide users with a button to change the theme of the entire website. I am using bootstrap ".scss" files for everything. Here is what I have done:

I have "dark-styles.scss" and "light-styles.scss" in "src/styles" folder. Both of these files override the classes and methods that I need to override and also import the "bootstrap.scss" from "node-module" for the defaults. When I provide any of these file to the application through ".angular-cli.json" like below; it works perfectly.

"styles": [
        "../node_modules/font-awesome/css/font-awesome.css",
        "../node_modules/@swimlane/ngx-datatable/release/index.css",
        "../node_modules/@swimlane/ngx-datatable/release/assets/icons.css",
        "../src/styles/dark-styles.scss"
      ],

or,

"styles": [
        "../node_modules/font-awesome/css/font-awesome.css",
        "../node_modules/@swimlane/ngx-datatable/release/index.css",
        "../node_modules/@swimlane/ngx-datatable/release/assets/icons.css",
        "../src/styles/light-styles.scss"
      ],

If I provide the dark the theme is dark and if I provide light the theme is light.

But what I want to achieve is dynamically allow the users to change the theme. Hence, based on other answers in stackoverflow; I accessed the "document" in my app component which is also my root component. It gives me access to the entire html page where I have a link tag which I can set from the app-component.

HTML file

<head>
<link id="theme" type="text/scss" rel="stylesheet" src="">
</head>
<body>
content .....
</body>

Angular-cli.json

"styles": [
        "../node_modules/font-awesome/css/font-awesome.css",
        "../node_modules/@swimlane/ngx-datatable/release/index.css",
        "../node_modules/@swimlane/ngx-datatable/release/assets/icons.css"
      ],

App-Component:

import { Component, OnInit, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';

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

      constructor( @Inject(DOCUMENT) private document) {
      }

      ngOnInit() {
        this.currentTheme = 'dark';
        this.document.getElementById('theme').href = '../styles/dark-styles.scss';
      }

      handelChangeTheme(event) {
        if (this.currentTheme === 'dark') {
        this.document.getElementById('theme').href = '../styles/light-styles.scss';
          this.currentTheme = 'light';
          console.log('light');
        } else {
        this.document.getElementById('theme').href = '../styles/dark-styles.scss';
          this.currentTheme = 'dark';
          console.log('dark');
        }
      }
    }

While triggering the event "handleChangeTheme" the href for the #theme changes in the html file. But it does not update or apply the styles. I understand that it has something to do with WebPack and the way it compiles the scss files. Has anybody faced the similar situation or know the solution to this issue. Thanks

回答1:

I just done your example locally and seems to be issue with type="text/scss" after i changed it to text/css it starts working. I thought firstly that you need to recreate element but issue seems to be with type

Since its theme i would recommend you to use compiled css versions of those files put them into assets folder and angular-cli will copy them on serve and build

In addition When you refer to scss in your angular its compiled with webpack to css.



回答2:

I managed to get dynamic theme working in Angular 4 + Bootstrap by using the following approach:

Defined theme colors by using a map:

$themes: (
    alpha: (
        primary: #2b343e,
        info: #3589c7,
        top-color: #000000,
        top-font-color: #ffffff,
        ...
    ),
    beta: (
        primary: #d2a444,
        info: #d2a444,
        top-color: #ffffff,
        top-font-color: #000000,
        ...
    )
)

Then, created the following mixin to output colors for each theme name and given properties:

@mixin theme($property, $key, $themes: $themes) {
  @each $theme, $colors in $themes {
    &.theme-#{$theme},
    .theme-#{$theme} & {
      #{$property}: map-get($colors, $key);
    }
  }
}

I used mixins to control individual properties:

@mixin color($arguments...) {
  @include themify('color', $arguments...);
}

@mixin background-color($arguments...) {
  @include themify('background-color', $arguments...);
}

For each "themeable" component, I had to use the mixins above when declaring properties:

.page-top {
    @include background-color('top-color');
    @include color('top-font-color');    
    height: 66px;
    width: 100%;
    ...    
}

The CSS output would be something like:

.page-top {
    height: 66px;
    width: 100%;
    ...    
}

.page-top.theme-alpha,
.theme-alpha .page-top {
    background-color: #000000;
    color: #ffffff;
}

.page-top.theme-beta,
.theme-beta .page-top {
    background-color: #ffffff;
    color: #000000;
}

And finally, to make the theme available, you have to control the theme class in some parent component:

<!-- class="theme-alpha" -->
<main themeChange>
    <page-top></page-top>
    <sidebar></sidebar>
    <page-bottom></page-bottom>
</main>

The themeChange directive could be written like this:

@Directive({
  selector: '[themeChange]'
})
export class ThemeChange implements OnInit {
    @HostBinding('class') classes: string;
    private _classes: string[] = [];

    ngOnInit() {        
        // grab the theme name from some config, variable or user input
        this.classesString = myConfig.theme;
    }
}


回答3:

After couple of hours trying, I finally get bootstrap theme switcher to work. Here is my solution:

  1. Instead of using bootswatch.scss, download compiled version for each theme, put them in same asset folder and rename the file accordingly.

  1. Add this line to index.html. choose default theme:

    < link id="theme" type="text/css" rel="stylesheet" href="/assets/css/theme_slate_bootstrap.min.css">
    ...
    ...
    <body class="theme-slate">
    ...
    


3. Currently setting up shortcut to switch theme in app.component. So this is app.component.ts

    import { Component, OnInit, Renderer2, Inject } from '@angular/core';
    import { DOCUMENT } from '@angular/platform-browser';

    @Component({
    selector: 'apps',
    templateUrl: './app.component.html'
    })
    export class AppComponent implements OnInit, OnDestroy {
        private currentTheme: string = 'slate';

        constructor(private renderer: Renderer2, @Inject(DOCUMENT) private document) { }

        ngOnInit() {}

        theme(type) {
            this.renderer.removeClass(document.body, 'theme-'+this.currentTheme);
            this.currentTheme = type;
            this.renderer.addClass(document.body, 'theme-'+this.currentTheme);
            this.document.getElementById('theme').href = '/assets/css/theme_'+type+'_bootstrap.min.css';
        }
    }
  1. Buttons to switch the themes

    <div style="display:block; position: absolute; top:0; right:0; text-align:right; z-index:100000;">
        <button class="btn btn-primary btn-sm" (click)="theme('slate')">Slate</button>
        <button class="btn btn-secondary btn-sm" (click)="theme('yeti')">Yeti</button>
        <button class="btn btn-secondary btn-sm" (click)="theme('darkly')">Darkly</button>
    </div>
    
  2. Add this custom file in angular.json

    "styles": [
        "src/assets/scss/custom.scss"
    ],
    
  3. custom.scss - add your custom code to work here

    $themes: (
        slate: (
            name: 'slate',
            primary: #3A3F44,
            secondary: #7A8288,
            ...
        ),
        yeti: (
            name: 'yeti',
            primary: #008cba,
            secondary: #eee,
            ...
        ),
        darkly: (
            name: 'darkly',
            primary: #008cba,
            secondary: #eee,
            ...
        ),
    );
    
    /*
    * Implementation of themes
    */
    @mixin themify($themes) {
        @each $theme, $map in $themes {
            .theme-#{$theme} & {
            $theme-map: () !global;
            @each $key, $submap in $map {
                $value: map-get(map-get($themes, $theme), '#{$key}');
                $theme-map: map-merge($theme-map, ($key: $value)) !global;
            }
            @content;
            $theme-map: null !global;
            }
        }
    }
    
    @function themed($key) {
        @return map-get($theme-map, $key);
    }
    
    
    * {
        @include themify($themes) {
            // custom theme to your needs. Add here
            @if themed('name') == 'slate' {
            }
    
    
            @if themed('name') == 'yeti' {
            }
    
    
            @if themed('name') == 'darkly' {
            }
    
    
            // Below will effect all themes
            .btn, .form-control, .dropdown, .dropdown-menu {
                border-radius: 0;
            }
    
            // This is just example to code. When switching theme, primary button theme should changed. Below code is not required. 
            .btn-primary {
                background-color: themed('primary');
                border-color: themed('primary');
            }
    
        }
    }
    // ***********************************************************************
    
    @import "custom-additional-classes";
    

Hope this help everyone who seeking solution for bootstrap themes switcher