angular自定義元件-UI元件篇-switch元件

LinDaiDai_霖呆呆發表於2018-10-21

前言

前端框架多嗎? 多! 前端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資料夾:

cd src\app\common
複製程式碼

(2) 使用建立模組的指令:

ng g m shared
複製程式碼

其實很好理解:ngangular一貫的指令,ggenerate建立的縮寫,mmodule模組的縮寫,後面接著你的模組名。(後面建立元件也是這個原理) 建立的模組實際上匯出的是一個帶有@NgModule裝飾器的類而已,其中提供了我們自定義的公有元件component,公有服務service,以及管道pipe等等。

二、建立元件

由於我們要建立的是一個switch公用元件,所以在component資料夾下在建立一個資料夾general-control,之前都是直接堆積在component資料夾下的,近期發現堆得有點多了,所以又單獨建立了一個general-control資料夾來存放一些基礎的公用元件。 此時你需要開啟命令列(使用vscode編輯器的小夥可以直接使用Ctrl+` 快捷鍵開啟終端,然後一路跳轉到general-control資料夾:

cd src\app\common\component\general-control
複製程式碼

在此目錄下執行指令:

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:

import { SwitchComponent } from './component/general-control/switch/switch.component';//模組中import引入元件

@NgModule({
declarations: [
  SwitchComponent  //模組中宣告元件
  ...
  ]
})
複製程式碼

上面倆步是你在使用ng g c switch指令時自動幫你完成的,但若是你想在其它的模組中使用這個switch元件,還得將其匯出,匯出的方式是將這個元件新增至shared.module.ts檔案的exports中:

import { SwitchComponent } from './component/general-control/switch/switch.component';//模組中import引入元件

@NgModule({
  declarations: [
    SwitchComponent  //模組中宣告元件
    ...
    ],
  exports: [
    SwitchComponent  //模組中匯出元件
    ...
  ]
})
複製程式碼

完成上面的步驟你就可以安心的來開發自己的元件了。

三、編寫switch元件

一番查詢,發現網上也有很多自定義switch元件的文章和原始碼,可能是大家都覺得原生的樣式不好看吧... 有使用input然後來進行修改樣式的,也有用其它標籤來自定義的。 博主這裡找了一個最簡單方案,一個span標籤搞定:

// switch.component.html
<span class="weui-switch" [ngClass]="currentClass" [ngStyle]="style" (click)="toggle()">
    
</span>
複製程式碼

基礎css

// 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

import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';
複製程式碼

並且定義一些基礎的變數

  @Input() style;//{ 'width': '40px' }//外部元件輸入的樣式物件
  @Input() isChecked: boolean = false;//開關是否開啟
  @Input() disabled: boolean = false;//開關是否被禁用
  @Output() change: EventEmitter<any> = new EventEmitter();
  _isSwitch: boolean = false;
  currentClass = {}
複製程式碼

此時我們的ts變成了這樣:

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元件自身的值。 所以在元件進行初始化和發生改變的時候我們應該讓其統一:

  ngOnInit() {//初始化元件的生命週期
    this.setIsSwitch();
  }
  ngOnChanges() {//當被繫結的輸入屬性的值發生變化時呼叫
    this.setIsSwitch();
  }
  setIsSwitch() {//設定_isSwitch
    this._isSwitch = this.isChecked;
  }
複製程式碼

3.2 setStyle()方法

由於是自定義的元件,我們當然是希望大小也可以自定義,所以我想要的效果是: 在呼叫元件的時候,輸入一個寬度width屬性,元件能夠自動調節尺寸。 因此我在設計的時候就定義了一個style變數 它是一個物件,可以允許開發者輸入任意的樣式,格式為{ 'width': '40px' } 同時為了減少輸入樣式的複雜度,我們還可以來編寫一個方法,讓元件能夠根據寬度來調節高度:

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()方法

完成了上面的步驟我們基本就完成了對元件樣式的初始化,但是,最重要的一步當然是通過新增/移除一些類來進行元件的互動:

 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變數控制, 也就是開關開啟的時候則新增這三個類。 前倆個類名是我在專案中使用的“皮膚類名”,因為客戶的需要,我們產品有幾套不同的主題色,使用者可以進行換膚功能來切換主題色,因此就有一些類名需要用來控制主題色。 如橘色主題:

.bg_main {
            background-color: #ff7920!important;
}
.bor_main {
            border-color: #ff7920!important;
}
複製程式碼

當然,你若是沒有主題色的話請忽略這倆個類。

上面的幾個方法我們都需要在元件初始化和變數發生改變的時候呼叫,所以可以整合到一個函式中:

  ngOnInit() {
    this.initComponent();
  }
  ngOnChanges() {
    this.initComponent();
  }
  initComponent() {
    this.setIsSwitch();
    this.setStyle();
    this.setClass();
  }
複製程式碼

3.4 toggle()方法

光有樣式可沒用,我們還需要將元件和使用者的行為給結合在一起,因此給元件一個click事件來進行互動,並編寫toggle()方法:

toggle() {//切換switch
    if (this.disabled) return;//若是禁用時則直接返回
    this._isSwitch = !this._isSwitch;
    this.isChecked = this._isSwitch;
    this.change.emit(this._isSwitch); //向外部傳遞最新的值
  }
複製程式碼

整合後的ts檔案為這樣:

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元件 首先在模組裡引用:

//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

<!--course.component.html-->
<app-switch [isChecked]="dataStatus" (change)="changeSwitch($event)"></app-switch>
複製程式碼
//course.component.ts

dataStatus: boolean = false;
changeSwitch($event) {
  this.dataStatus = $event;
}
複製程式碼

此時就完成了switch元件的編寫和使用。 你也可以給元件設定另一個屬性disabled:

<!--course.component.html-->
<app-switch [isChecked]="dataStatus" [disable]="true" (change)="changeSwitch($event)"></app-switch>
複製程式碼

後語

上述設計的switch元件應該是UI元件中比較簡單的一種UI元件了,還有更多複雜的元件有待我們的開發,通過自己設計UI元件,emmm....可以讓我們更有創造力吧應該說,也促使自己多去看別人的部落格與原始碼,最後再寫上一篇總結,我認為這應該是一個正向的激勵?,哈哈,全篇廢話很多,不過還是要感謝小夥的閱讀?。

相關文章