目前的主题切换大多要求动态主题切换,即用户点击切换主题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{
} [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来实现主题切换的好处是可以动态切换主题,不需要重新加载页面,而且代码实现比较简洁。