基于SCSS的angular项目的主题切换

 目前的主题切换大多要求动态主题切换,即用户点击切换主题button后不需要重新加载页面,页面不需要刷新即可切换主题。这就需要思考如下几点:

  • 如何让css监听用户切换主题了?
  • 类似:hover这一类的伪元素选择器,css会动态监听,当元素被hover时,css就会被添加上去

一、原理

 类似:hover这一类的伪元素选择器,css会动态监听,当元素被hover时,css就会被添加上去。

css也会监听元素上的attribute的值的变化,这样我们可以利用这个特性来实现css监听用户行为。

实现原理是这样的,我们在body元素上添加一个data-theme-style属性,让它的值根据用户的行为变化,比如dark或者light。

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6     <title>Document</title>
 7     <style>
 8         [data-theme-style=dark] .content1{
 9             background: black;
10         }
11         [data-theme-style=light] .content1{
12             background: gray;
13         }
14         [data-theme-style=dark] .content2{
15             background: red;
16         }
17         [data-theme-style=light] .content2{
18             background: blue;
19         }
20         .content {
21             width: 200px;
22             height: 200px;
23             border: 1px solid red;
24         }
25     </style>
26     
27 </head>
28 <body data-theme-style="dark">
29     <button>Change Theme</button>
30 
31     <div class="content content1"></div>
32     <div class="content content2"></div>
33     <script>
34         const button = document.getElementsByTagName(button)[0];
35         const body = document.body;
36         button.addEventListener(click, () => {
37             body.setAttribute(data-theme-style, body.getAttribute(data-theme-style) === dark ? light : dark)
38         })
39     </script>
40 </body>
41 </html>

 

 可以看到,点击button时修改body的data-theme-style的值,这样css就会监听到变化来切换css,也就是主题切换的最基本的功能。

实际上,这是主题切换的最基本的原理:用户触发html元素属性的变化,利用css监听属性的变化,再把需要变化的css添加到属性选择器下面,这样就能够让css跟着属性选择器的变化来变化了。

二、优化

 作为一个合格的单身的脱离了高级趣味的程序员来说,重复的代码是不可容忍的。

我们来解决一下这个问题,这里需要用到一些css的高级语法,所以我们使用scss完成,我们先把代码迁移到使用了scss的angular项目中。

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>CustomElementsDemo</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body data-theme-style="dark">
  <app-root></app-root>
</body>
</html>

 

app.component.html

<button>Change Theme</button>
<div class="content content1"></div>
<div class="content content2"></div>

 

app.component.scss

[data-theme-style=dark] .content1{
    background: black;
}
[data-theme-style=light] .content1{
    background: gray;
}
[data-theme-style=dark] .content2{
    border: 20px solid red;
}
[data-theme-style=light] .content2{
  border: 20px solid blue;
}
.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

 

app.component.ts

import { Component, AfterViewInit } from ‘@angular/core‘;

@Component({
  selector: ‘app-root‘,
  templateUrl: ‘./app.component.html‘,
  styleUrls: [‘./app.component.scss‘]
})
export class AppComponent implements AfterViewInit{
  ngAfterViewInit(): void {
    const button = document.getElementsByTagName(‘button‘)[0];
    const body = document.body;
    button.addEventListener(‘click‘, () => {
      body.setAttribute(‘data-theme-style‘, body.getAttribute(‘data-theme-style‘) === ‘dark‘ ? ‘light‘ : ‘dark‘)
    });
  }
}

 

 如果运行了代码之后,你会发现content1 和 content2并没有作用在元素上。

这是因为angular对于组件内的css是封闭的,[data-theme-style=dark]并不能找到body元素上,那怎么找到呢?

css中有一个选择器:host-context,它在当前组件宿主元素的祖先节点中查找 CSS 类, 直到文档的根节点为止。

 我们来修改scss

app.component.scss

:host-context([data-theme-style=dark]) .content1{
    background: black;
}
:host-context([data-theme-style=light]) .content1{
    background: gray;
}
:host-context([data-theme-style=dark]) .content2{
    border: 20px solid red;
}
:host-context([data-theme-style=light]) .content2{
    border: 20px solid blue;
}
.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

 

这样,就又可以使用了。

:host-context([data-theme-style=dark]) 就可以找到body元素了。

我们来使用scss的mixin来去掉重复的代码:

@mixin bg-context1() {
    :host-context([data-theme-style=dark]) .content1 {
        background: black;
    }
    :host-context([data-theme-style=light]) .content1{
        background: gray;
    }
}
@mixin border-context2() {
    :host-context([data-theme-style=dark]) .content2 {
        border: 20px solid red; 
    }
    :host-context([data-theme-style=light]) .content2 {
        border: 20px solid blue; 
    }
}
.content1 {
    // 使用@include来使用@mixin
    @include bg-context1();
}

.content2 {
    @include border-context2();
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

 

@mixin是可以传递参数的,并且,&符号可以引用父级元素。

// @mixin指令是可以传递参数的
@mixin bg-context1($propname) {
    // & 符号可以引用父级元素
    :host-context([data-theme-style=dark]) & {
        // 当变量使用在属性名称时,需要使用插值表达式来使用变量
        #{$propname}: black;
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: gray;
    }
}
@mixin border-context2($propname) {
    :host-context([data-theme-style=dark]) & {
        #{$propname}: 20px solid red; 
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: 20px solid blue; 
    }
}
.content1 {
    // 使用@include来使用@mixin
    @include bg-context1(background);
}

.content2 {
    @include border-context2(border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

 

 目前为止,并没有去掉多少重复的代码,下面我们来定义2个map,一个是dark主题下的css值,一个是light主题下的css值。

$dark: (
    context1-bg-color: black,
    context2-border: 20px solid red,
);
$light: (
    context1-bg-color: gray,
    context2-border: 20px solid blue,
);
// @mixin指令是可以传递参数的
@mixin bg-context1($propname) {
    // & 符号可以引用父级元素
    :host-context([data-theme-style=dark]) & {
        // 当变量使用在属性名称时,需要使用插值表达式来使用变量
        // map-get是scss提供的获取map中的value的方法
        #{$propname}: map-get($dark, context1-bg-color);
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: map-get($light, context1-bg-color);
    }
}
@mixin border-context2($propname) {
    :host-context([data-theme-style=dark]) & {
        #{$propname}: map-get($dark, context2-border);
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: map-get($light, context2-border);
    }
}
.content1 {
    // 使用@include来使用@mixin
    @include bg-context1(background);
}

.content2 {
    @include border-context2(border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

 

 我们的目标是去掉重复的代码,需要想办法把2个mixin合并在一起。可以把变量的名称通过mixin的参数传递进去。

$dark: (
    context1-bg-color: black,
    context2-border: 20px solid red,
);
$light: (
    context1-bg-color: gray,
    context2-border: 20px solid blue,
);
// @mixin指令是可以传递参数的
@mixin theme($propname, $varname) {
    // & 符号可以引用父级元素
    :host-context([data-theme-style=dark]) & {
        // 当变量使用在属性名称时,需要使用插值表达式来使用变量
        // map-get是scss提供的获取map中的value的方法
        #{$propname}: map-get($dark, $varname);
    }
    :host-context([data-theme-style=light]) & {
        #{$propname}: map-get($light, $varname);
    }
}
.content1 {
    // 使用@include来使用@mixin
    @include theme(background, context1-bg-color);
}

.content2 {
    @include theme(border, context2-border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

 

来来来,看看,选择变成一个mixin了,我们再来简化一下,mixin中的css会根据主题的数量增加而增加,比如这个时候再加个default theme,mixin中就会有3个了。

观察一下,重复的地方很多,只有主题名称不同,我们再定义一个主题的map,然后在mixin中使用@each来循环这个集合:

$dark: (
    context1-bg-color: black,
    context2-border: 20px solid red,
);
$light: (
    context1-bg-color: gray,
    context2-border: 20px solid blue,
);
$themes: (
    dark: $dark,
    light: $light,
);
// @mixin指令是可以传递参数的
@mixin theme($propname, $varname) {
    // @each循环map时,第一个参数是map的key,第二个参数是map的value
    @each $themename, $theme in $themes {
        // & 符号可以引用父级元素
        // 不能直接使用变量,需要使用插值表达式包含变量
        :host-context([data-theme-style=#{$themename}]) & {
            // 当变量使用在属性名称时,需要使用插值表达式来使用变量
            // map-get是scss提供的获取map中的value的方法
            #{$propname}: map-get($theme, $varname);
        }
    }
}
.content1 {
    // 使用@include来使用@mixin
    @include theme(background, context1-bg-color);
}

.content2 {
    @include theme(border, context2-border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

 

 这样就把能简化的都简化了。

但是对于项目来说,需要把目录整理一下,我们给主题单独建立一个文件夹,把之前定义的主题$dark,$light独立成文件。把mixin指令独立成单独的文件。

app/theme/dark.scss

$dark: (
    context1-bg-color: black,
    context2-border: 20px solid red,
);

 

app/theme/light.scss

$light: (
    context1-bg-color: gray,
    context2-border: 20px solid blue,
);

 

app/theme/mixin.scss

@import ‘src/app/theme/dark.scss‘;
@import ‘src/app/theme/light.scss‘;

$themes: (
    dark: $dark,
    light: $light,
);
// @mixin指令是可以传递参数的
@mixin theme($propname, $varname) {
    // @each循环map时,第一个参数是map的key,第二个参数是map的value
    @each $themename, $theme in $themes {
        // & 符号可以引用父级元素
        // 不能直接使用变量,需要使用插值表达式包含变量
        :host-context([data-theme-style=#{$themename}]) & {
            // 当变量使用在属性名称时,需要使用插值表达式来使用变量
            // map-get是scss提供的获取map中的value的方法
            #{$propname}: map-get($theme, $varname);
        }
    }
}

 

 在app.component.scss中使用

app.component.scss

@import ‘src/app/theme/mixin.scss‘;

.content1 {
    // 使用@include来使用@mixin
    @include theme(background, context1-bg-color);
}

.content2 {
    @include theme(border, context2-border);
}

.content {
    width: 200px;
    height: 200px;
    border: 1px solid red;
}

 

 

 这样,我们基本完成了主题的切换了。

使用主题时需要注意的是,mixin theme的第一个参数是css的属性,比如border,比如background,第二个参数是定义在主题文件中的变量名称。

 

三、总结

 使用scss来实现主题切换的好处是可以动态切换主题,不需要重新加载页面,而且代码实现比较简洁。

 

基于SCSS的angular项目的主题切换

上一篇:eWebEditor实现word图片自动转存


下一篇:命令:curl