Angular自定义组件-UI组件篇-Switch组件

前言

前端框架多吗?
多!
前端UI组件库多吗?
更多!
我们都知道,前端生态圈里提供了各色各样的组件库供我们选择使用,大多数都能满足开发者的需求,相信大家也都用过很多。但是实际上,据我了解到的,稍微大一些有自己产品的公司都会有一套自定义的UI组件库,满足自身复杂的需求与绚丽的效果。
博主目前所在的公司也有一套自己的产品,PC端所用的前端框架是Angular4.
Angular4其实也有它专门定制的前端组件库PrimeNg.就像Vue.jsElement一样.
那么按理在开发中我们已经有了前端组件库可以使用,为什么还要花那么多的精力和时间来重新设计UI组件呢?话不多说,先上几张官方提供的UI组件图:
offSwitch
onSwitch
确实不是我吐槽,官方提供的一些组件样式真的有点奇怪😭…虽然Angular官网组件样式这一文档中已经说明了可以用::ng-deep来进行对组件样式的修改,但修改起来还是比较麻烦。有那时间,自己都已经撸了一个了…
(项目中用的primengv4.2.2版本的,目前已经迭代到了v6.1.5,所以现在官网上看到的inputSwitch组件会比这个好看点)
emmm….为了追求用户体验(呸,熟悉angular4的使用)所以博主决定利用闲暇之余自定义一些UI组件,以满足我们产品”一些无礼的要求”。

一、确定组件存放的位置

一个项目中会有各种文件、文件夹,如何存放管理好这些文件真的很重要。不仅为自己提供了方便,也为后来的开发者提供方便。
所以我们在设计公用组件的时候也应该把它们都归结在一起。
我习惯在项目中新建一个common文件夹,里面存放一些共用的compoentservice等等。
app/common/component
如上图,可以看到common文件夹下导出的是一个名为shared的模块。
shared模块的创建过程:
(1)打开命令行(使用vscode编辑器的小伙可以直接使用Ctrl+` 快捷键打开终端,然后一路跳转到common文件夹:

1
cd src\app\common

(2) 使用创建模块的指令:

1
ng g m shared

其实很好理解:ngangular一贯的指令,ggenerate创建的缩写,mmodule模块的缩写,后面接着你的模块名。(后面创建组件也是这个原理)
创建的模块实际上导出的是一个带有@NgModule装饰器的类而已,其中提供了我们自定义的公有组件component,公有服务service,以及管道pipe等等。

二、创建组件

由于我们要创建的是一个switch公用组件,所以在component文件夹下在创建一个文件夹general-control,之前都是直接堆积在component文件夹下的,近期发现堆得有点多了,所以又单独创建了一个general-control文件夹来存放一些基础的公用组件。
此时你需要打开命令行(使用vscode编辑器的小伙可以直接使用Ctrl+` 快捷键打开终端,然后一路跳转到general-control文件夹:

1
cd src\app\common\component\general-control

在此目录下执行指令:

1
ng g c switch

上面指令的意思是创建一个名为switch的组件,原理和创建模块时一样。
可以看到现在的general-control文件夹下多出了一些东西:
switch文件夹
没错,就是我们使用指令创建的switch组件。
指令会自动帮你生成一个文件夹和4个文件。(基于TypeScript的语法,所以生成的js文件也就是ts)
很好理解,对应的html文件编写HTML代码css文件编写CSS代码ts文件编写js代码,至于spec.ts文件我们可以不用管它。
由于我在项目中使用的是sass,所以将switch.component.css这个文件的后缀名修改为scss(使用了less等其它扩展语言的小伙同理),并在ts中对css的引用进行修改:
scss
修改ts

使用上面的指令创建的组件是会被自动引用到shared这个模块中的。
shared.module.ts:

1
2
3
4
5
6
7
8
import { SwitchComponent } from './component/general-control/switch/switch.component';//模块中import引入组件

@NgModule({
declarations: [
SwitchComponent //模块中声明组件
...
]
})

上面俩步是你在使用ng g c switch指令时自动帮你完成的,但若是你想在其它的模块中使用这个switch组件,还得将其导出,导出的方式是将这个组件添加至shared.module.ts文件的exports中:

1
2
3
4
5
6
7
8
9
10
11
12
import { SwitchComponent } from './component/general-control/switch/switch.component';//模块中import引入组件

@NgModule({
declarations: [
SwitchComponent //模块中声明组件
...
],
exports: [
SwitchComponent //模块中导出组件
...
]
})

完成上面的步骤你就可以安心的来开发自己的组件了。

三、编写switch组件

一番查找,发现网上也有很多自定义switch组件的文章和源码,可能是大家都觉得原生的样式不好看吧…
有使用input然后来进行修改样式的,也有用其它标签来自定义的。
博主这里找了一个最简单方案,一个span标签搞定:

1
2
3
4
// switch.component.html
<span class="weui-switch" [ngClass]="currentClass" [ngStyle]="style" (click)="toggle()">

</span>

基础css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// switch.comonent.scss
.weui-switch {
display: inline-block;
position: relative;
width: 38px;
height: 23px;
border: 1px solid #DFDFDF;
outline: 0;
border-radius: 16px;
box-sizing: border-box;
background-color: #DFDFDF;
transition: background-color 0.1s, border 0.1s;
cursor: pointer;
&.disabled{
opacity: 0.6;
cursor: not-allowed;
}
}
.weui-switch:before {
content: " ";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 15px;
background-color: #FDFDFD;
transition: transform 0.35s cubic-bezier(0.45, 1, 0.4, 1);
}
.weui-switch:after {
content: " ";
position: absolute;
top: 0;
left: 0;
width: 56%;
height: 97%;
border-radius: 15px;
background-color: #FFFFFF;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
transition: transform 0.35s cubic-bezier(0.4, 0.4, 0.25, 1.35);
}
.weui-switch-on {
border-color: #1AAD19;
background-color: #1AAD19;
}
.weui-switch-on:before {
border-color: #1AAD19;
background-color: #1AAD19;
}
.weui-switch-on:after {
transform: translateX(77%);
}

效果大概就是这个样子:
自定义switch组件
(录频并转换GIF推荐使用GifGam
可以看到,组件的样式设计大多都是使用伪类:after:before来实现的,而开关的效果是通过点击的时候添
加/移除classweui-switch-on来实现的。(讲js的时候会讲到)

由于我们创建的switch组件是需要在多处使用,并且要向外输出一些值,所以在ts中我们首先要引入一下@Input@Output装饰器和EventEmitter

1
import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';

并且定义一些基础的变量

1
2
3
4
5
6
@Input() style;//{ 'width': '40px' }//外部组件输入的样式对象
@Input() isChecked: boolean = false;//开关是否打开
@Input() disabled: boolean = false;//开关是否被禁用
@Output() change: EventEmitter<any> = new EventEmitter();
_isSwitch: boolean = false;
currentClass = {}

此时我们的ts变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';

@Component({
selector: 'app-switch',
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.scss']
})
export class SwitchComponent implements OnInit, OnChanges {
constructor() { }
@Input() style;//{ 'width': '40px' }//外部组件输入的样式对象
@Input() isChecked: boolean = false;//外部组件输入进来的:开关是否打开
@Input() disabled: boolean = false;//开关是否被禁用
@Output() change: EventEmitter<any> = new EventEmitter();
_isSwitch: boolean = false;//switch组件本身的:开关是否打开
currentClass = {} //class集合

ngOnInit() {//初始化组件的生命周期

}
ngOnChanges() {//当被绑定的输入属性的值发生变化时调用

}
}

3.1 setIsSwitch()方法

组件中定义了俩个“开关是否打开”的变量isChecked_isSwitch
一个是外部组件传递进来的默认值,一个是 switch组件自身的值。
所以在组件进行初始化和发生改变的时候我们应该让其统一:

1
2
3
4
5
6
7
8
9
ngOnInit() {//初始化组件的生命周期
this.setIsSwitch();
}
ngOnChanges() {//当被绑定的输入属性的值发生变化时调用
this.setIsSwitch();
}
setIsSwitch() {//设置_isSwitch
this._isSwitch = this.isChecked;
}

3.2 setStyle()方法

由于是自定义的组件,我们当然是希望大小也可以自定义,所以我想要的效果是:
在调用组件的时候,输入一个宽度width属性,组件能够自动调节尺寸。
因此我在设计的时候就定义了一个style变量
它是一个对象,可以允许开发者输入任意的样式,格式为{ 'width': '40px' }
同时为了减少输入样式的复杂度,我们还可以来编写一个方法,让组件能够根据宽度来调节高度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setStyle() {//设置样式
if (this.style) {
if (this.style['width'] && !this.style['height']) {//若是输入了宽度没有输入高度则自动计算
let width = this.getWidth(this.style['width']);
this.style['height'] = (width * 0.55) + 'px';
}
}
}
getWidth(widthStr) {//判断用户输入的width带不带px单位
let reg = /px/;
let width = reg.test(widthStr) ? widthStr.match(/(\d*)px/)[1] : widthStr //正则获取不带单位的值
if (!width) width = 0;
return width;
}

可以看到,上面我编写的setStyle()方法是判断有没有宽度和高度,并将高度设置为0.55 * width(0.55为我找到的最合适的比例)

3.3 setClass()方法

完成了上面的步骤我们基本就完成了对组件样式的初始化,但是,最重要的一步当然是通过添加/移除一些类来进行组件的交互:

1
2
3
4
5
6
setClass() {//转换switch时切换class
this.currentClass = {
'disabled': this.disabled,
'bg_main bor_main weui-switch-on': this._isSwitch
}
}

对象currentClass存储的是组件变动的类名,对象的键名为类名,值为一个布尔类型的变量(true / false)
通过布尔类型的变量来判断添加还是移除这些类名。
第一个类disabled表示的是开关是否被禁用,也就是用户只能查看开关,并不能对其进行操作,它受disabled变量控制。
第二个类为三个类名的合写bg_mainbor_main、和weui-switch-on,他们受_isSwitch变量控制,
也就是开关打开的时候则添加这三个类。
前俩个类名是我在项目中使用的“皮肤类名”,因为客户的需要,我们产品有几套不同的主题色,用户可以进行换肤功能来切换主题色,因此就有一些类名需要用来控制主题色。
如橘色主题:

1
2
3
4
5
6
.bg_main {
background-color: #ff7920!important;
}
.bor_main {
border-color: #ff7920!important;
}

当然,你若是没有主题色的话请忽略这俩个类。

上面的几个方法我们都需要在组件初始化和变量发生改变的时候调用,所以可以整合到一个函数中:

1
2
3
4
5
6
7
8
9
10
11
ngOnInit() {
this.initComponent();
}
ngOnChanges() {
this.initComponent();
}
initComponent() {
this.setIsSwitch();
this.setStyle();
this.setClass();
}

3.4 toggle()方法

光有样式可没用,我们还需要将组件和用户的行为给结合在一起,因此给组件一个click事件来进行交互,并编写toggle()方法:

1
2
3
4
5
6
toggle() {//切换switch
if (this.disabled) return;//若是禁用时则直接返回
this._isSwitch = !this._isSwitch;
this.isChecked = this._isSwitch;
this.change.emit(this._isSwitch); //向外部传递最新的值
}

整合后的ts文件为这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';

@Component({
selector: 'app-switch',
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.scss']
})
export class SwitchComponent implements OnInit, OnChanges {

constructor() { }
@Input() onLabel: string = '';//暂无
@Input() offLabel: string = '';
@Input() style;//{ 'width': '40px' }//外部组件输入的样式对象
@Input() isChecked: boolean = false;//开关是否打开
@Input() disabled: boolean = false;//开关是否被禁用
@Output() change: EventEmitter<any> = new EventEmitter();
_isSwitch: boolean = false;
currentClass = {}

ngOnInit() {
this.initComponent();
}
ngOnChanges() {
this.initComponent();
}
initComponent() {//初始化并刷新组件
this.setIsSwitch();
this.setStyle();
this.setClass();
}
setIsSwitch() {
this._isSwitch = this.isChecked;
}
setStyle() {//设置样式
if (this.style) {
if (this.style['width'] && !this.style['height']) {//若是输入了宽度没有输入高度则自动计算
let width = this.getWidth(this.style['width']);
this.style['height'] = (width * 0.55) + 'px';
}
}
}
setClass() {//转换switch时切换class
this.currentClass = {
'disabled': this.disabled,
'bg_main bor_main weui-switch-on': this._isSwitch
}
}
getWidth(widthStr) {//判断用户输入的width带不带px单位
let reg = /px/;
let width = reg.test(widthStr) ? widthStr.match(/(\d*)px/)[1] : widthStr //正则获取不带单位的值
if (!width) width = 0;
return width;
}
toggle() {//切换switch
if (this.disabled) return;//若是禁用时则直接返回
this._isSwitch = !this._isSwitch;
this.isChecked = this._isSwitch;
this.change.emit(this._isSwitch);
}
}

四、引用switch组件

完成了上面的部分,到了我们最激动的时候了,看看我们亲手制作的组件有没有用吧,哈哈。
首先,在使用其它组件的时候,我们要将其引入进来,由于我们最开始是将switch组件引入到shared这个模块中,并从这个模块中导出的,所以想要在其它模块中使用 switch组件就得先引入shared模块。

4.1 引入shared模块

本项目中有另一个模块名为coursemanage,现在我将其作为父组件来引用一下switch组件
首先在模块里引用:

1
2
3
4
5
6
7
8
9
//coursemanage.module.ts
import { NgModule } from '@angular/core';
import { SharedModule } from "./../common/shared.module";
@NgModule({
imports: [
SharedModule
]
})
export class CourseManageModule { }

引入了shared模块就相当于是引入那个那个模块中的所有组件和方法。

4.2 使用switch组件

coursemanage模块中,有其子组件course这个组件,在course中使用switch

1
2
<!--course.component.html-->
<app-switch [isChecked]="dataStatus" (change)="changeSwitch($event)"></app-switch>

1
2
3
4
5
6
//course.component.ts

dataStatus: boolean = false;
changeSwitch($event) {
this.dataStatus = $event;
}

此时就完成了switch组件的编写和使用。
你也可以给组件设置另一个属性disabled:

1
2
<!--course.component.html-->
<app-switch [isChecked]="dataStatus" [disable]="true" (change)="changeSwitch($event)"></app-switch>

后语

上述设计的switch组件应该是UI组件中比较简单的一种UI组件了,还有更多复杂的组件有待我们的开发,通过自己设计UI组件,emmm….可以让我们更有创造力吧应该说,也促使自己多去看别人的博客与源码,最后再写上一篇总结,我认为这应该是一个正向的激励💪,哈哈,全篇废话很多,不过还是要感谢小伙的阅读🙂。

评论