美文网首页
脚写Angular组件

脚写Angular组件

作者: Daeeman | 来源:发表于2021-01-21 10:10 被阅读0次
    • 参考NG-ZORRO源码

    目录

    • breadcrumb 面包屑
    • page 分页
    • tag 标签组件
    • rate 评分组件

    注意 ⚠️
    ZORRO的CSS样式直接无法修改,若要修改,需在前面加入:host ::ng-deep

    .ant-input-affix-wrapper .ant-input:not(:first-child){ 
        padding-left: 30px; 
    } 
    //1 2 3 修改上面就正常了
    :host ::ng-deep .ant-input-affix-wrapper .ant-input:not(:first-child){ 
        padding-left: 30px; 
    }
    

    1. 面包屑

    breadcrumb.component

    <div class="bread-crumb">
      <ng-content></ng-content>
    </div>
    //=============================================
    import {ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef, ViewEncapsulation} from '@angular/core';
    @Component({
      selector: 'xm-breadcrumb',
      templateUrl: './breadcrumb.component.html',
      styleUrls: ['./breadcrumb.component.scss'],
      changeDetection: ChangeDetectionStrategy.OnPush,
      encapsulation: ViewEncapsulation.None
    })
    export class BreadcrumbComponent implements OnInit {
      //父组件把数据传递给子组件,孙子组件获取子组件数据,只传一次
      //因为若直接将数据父传给孙,使用一次孙子组件需要传递一次数据
      @Input() xmSeparator: TemplateRef<any> | string = '>';
      constructor() { }
      ngOnInit(): void {}
    }
    

    breadcrumb-item.component.ts
    加入该子组件的原因是:
    解决每使用一次breadcrumb组件则需要传递一次参数的问题,把breadcrumb当成中间组件,用来传递参数

    <div class="bread-crumb-item">
      <div class="content">
        <ng-content></ng-content>
      </div>
      <div class="separator">
        <span *xmStrTplOutlet="parent.xmSeparator; context: myContext">{{ parent.xmSeparator }}</span>
      </div>
    </div>
    //===============================================================
    import {Component, OnInit, ChangeDetectionStrategy, Input, TemplateRef, Optional} from '@angular/core';
    import {BreadcrumbComponent} from '../breadcrumb.component';
    
    @Component({
      selector: 'xm-breadcrumb-item',
      templateUrl: './breadcrumb-item.component.html',
      styleUrls: ['./breadcrumb-item.component.scss'],
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class BreadcrumbItemComponent implements OnInit {
      myContext = { $implicit: 'World', my: 'svet' }; //补充,可填入变量
      constructor(@Optional() readonly parent: BreadcrumbComponent) { }
      ngOnInit(): void {}
    }
    

    app.component

    <div id="xm">
      <xm-header></xm-header>
      <div class="content">
        <div class="container">
          <div class="breadcrumb-wrap">
            <xm-breadcrumb [xmSeparator]="mySeparator">
              <xm-breadcrumb-item>
                全部
              </xm-breadcrumb-item>
              <xm-breadcrumb-item>
                一级
              </xm-breadcrumb-item>
              <xm-breadcrumb-item>
                二级
              </xm-breadcrumb-item>
              <ng-template #mySeparator let-name let-my="my">
                <b>$$—{{ name }}—{{ my }}</b>
              </ng-template>
            </xm-breadcrumb>
          </div>
        </div>
      </div>
    </div>
    

    str-tpl-outlet.directive.ts
    加入该指令的原因是:
    传递的参数只能是字符串,加入指令判断后,则可传递字符串或者template模板

    • 通过自定义指令判断传入的是template或者字符串,template直接使用,string则转化,然后创建视图容器并插入template
    import {Directive, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
    
    @Directive({
      selector: '[xmStrTplOutlet]'
    })
    export class StrTplOutletDirective implements OnChanges {
      @Input() xmStrTplOutlet: TemplateRef<any> | string;
      @Input() xmStrTplOutletContext: any;
      constructor(private viewContainer: ViewContainerRef, private templateRef: TemplateRef<any>) { }
      ngOnChanges(changes: SimpleChanges): void {
        const { xmStrTplOutlet } = changes;
        if (xmStrTplOutlet) {
          this.viewContainer.clear();
          const template = (this.xmStrTplOutlet instanceof TemplateRef) ? this.xmStrTplOutlet : this.templateRef;
          this.viewContainer.createEmbeddedView(template, this.xmStrTplOutletContext);
        }
      }
    }
    

    2. 分页组件

    <div class="xm-page-wrap"> 
      <ul class="xm-pages">
        <li
          class="xm-page"
          *ngFor="let item of listOfPageItems"
          [class.disabled]="item.disabled"
          [class.checked]="item.num === pageNum"
          (click)="pageClick(item)">
          <ng-container [ngSwitch]="item.type">
            <a *ngSwitchCase="'page'">{{ item.num }}</a>
            <ng-container *ngSwitchDefault> //其余的
              <span [ngSwitch]="item.type">
                <i *ngSwitchCase="'prev'" xmIcon="previewleft" class="icon"></i>
                <i *ngSwitchCase="'next'" xmIcon="previewright" class="icon"></i>
                <i *ngSwitchDefault xmIcon="more" class="icon"></i>
              </span>
            </ng-container>
          </ng-container>
        </li>
      </ul>
      <div class="xm-page-navigate">
          //使用+将string隐式转为number
        <input type="number" class="xm-input" #to (keyup.enter)="inputVal(+to.value)" [min]="1" [max]="lastNum" />
        <button xmBtn (click)="inputVal(+to.value)">跳转</button>
      </div>
    </div>
    
    import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core';
    import { clamp } from 'lodash';
    
    type PageItemType = 'page' | 'prev' | 'next' | 'prev5' | 'next5';
    interface PageItem {
      type: PageItemType;
      num?: number; 
      disabled?: boolean;
    }
    
    @Component({
      selector: 'xm-pagination',
      templateUrl: './pagination.component.html',
      styleUrls: ['./pagination.component.scss']
    })
    export class PaginationComponent implements OnInit, OnChanges {
      @Input() total = 0;     //总数据条数
      @Input() pageNum = 1;   //当前显示页数
      @Input() pageSize = 10;  //每个网页显示数据条数
      @Output() changed = new EventEmitter<number>();
      lastNum = 0;      //总页数
      //保存分页数据
      //type: PageItemType;
      //num?: number; 
      //disabled?: boolean;
      listOfPageItems: PageItem[] = [];  
    
      constructor() { }
      ngOnInit(): void {}
    
      ngOnChanges(changes: SimpleChanges): void {
        //向上取整,总页数,默认显示一页
        this.lastNum = Math.ceil(this.total / this.pageSize) || 1; 
        this.listOfPageItems = this.getListOfPageItems(this.pageNum, this.lastNum);
        // console.log('listOfPageItems', this.listOfPageItems);
      }
    
      private getListOfPageItems(pageNum: number, lastNum: number): PageItem[] {
        if (lastNum <= 9) {
          return concatWithPrevNext(generatePage(1, this.lastNum), pageNum, lastNum);
        } else {
          let listOfRange = [];
          const prevFiveItem = {
            type: 'prev5'
          };
          const nextFiveItem = {
            type: 'next5'
          };
          const firstPageItem = generatePage(1, 1);  //第一页
          const lastPageItem = generatePage(lastNum, lastNum); //最后一页
    //当前页小于4,显示(第1页 + 2-5页 + 三个点 + 最后一页)/1,2,3,4,5...10
          if (pageNum < 4) {
            listOfRange = [...generatePage(2, 5), nextFiveItem];
    //当前页大于总页数减4,显示第一页+ 三个点 + (lastNum-4, lastNum-1)+最后一页/1...6,7,8,9,10
          } else if (pageNum > lastNum - 4) {
            listOfRange = [prevFiveItem, ...generatePage(lastNum - 4, lastNum - 1)];
          } else {
    //其它页显示,第一页+...+ (pageNum-2,pageNum+2)+...+最后一页 /1...5,6,7,8,9...12
            listOfRange = [prevFiveItem, ...generatePage(pageNum - 2, pageNum + 2), nextFiveItem];
          }
    //拼接第一页,中间,和最后一页
          return concatWithPrevNext([...firstPageItem, ...listOfRange, ...lastPageItem], pageNum, lastNum);
        }
      }
    }
    //生产type=page类型的数据
    function generatePage(start: number, end: number): PageItem[] {
      const list = [];
      for (let i = start; i <= end; i++) {
        list.push({
          num: i,
          type: 'page'
        });
      }
      return list;
    }
    // 连接type不同的数据
    function concatWithPrevNext(listOfPage:PageItem[],pageNum:number,lastNum:number):   PageItem[] {
      return [
        {
          type: 'prev',
          disabled: pageNum === 1
        },
        ...listOfPage,
        {
          type: 'next',
          disabled: pageNum === lastNum
        }
      ];
    }
    
    inputVal(num: number): void {
        if (num > 0) {
          this.pageClick({
            type: 'page',
            num
          });
        }
      }
    //点击按钮跳转到相应页
     pageClick({ type, num, disabled }: PageItem): void {
        if (!disabled) {
          let newPageNum = this.pageNum;
          if (type === 'page') {
            newPageNum = num;
          } else {
            const diff: any = {
              next: 1,
              prev: -1,
              prev5: -5,
              next5: 5
            };
            newPageNum += diff[type];
          }
          // console.log('newPageNum', newPageNum);
          this.changed.emit(clamp(newPageNum, 1, this.lastNum));
          // clamp为lodash库的方法,这里用来限制页数变化范围
        }
      }
    

    使用

     <div class="page-wrap">
        <xm-pagination
          class="page"
          [pageNum]="searchParams.page"
          [pageSize]="searchParams.perPage"
          [total]="total"
          (changed)="changePage($event)">
        </xm-pagination>
      </div>
    //==============================================
     changePage(newPageNum: number): void {
        if (this.searchParams.page !== newPageNum) {
          this.searchParams.page = newPageNum;
          this.updateAlbums();
        }
      }
    

    3. tag标签组件

    <ng-content></ng-content>
    <i [xmIcon]="'Close'" *ngIf="xmClosable" (click)="closed.emit()" class="icon-close"></i>
    
    import {
      AfterViewInit,
      Component,
      ElementRef,
      HostBinding,
      Input,
      OnChanges,
      OnInit, Output,
      Renderer2, SimpleChange,
      SimpleChanges,
      ViewEncapsulation,
      EventEmitter
    } from '@angular/core';
    
    const ColorPresets = ['magenta', 'orange', 'green'];
    type TagMode = 'default' | 'circle';
    
    @Component({
      selector: 'xm-tag',
      templateUrl: './tag.component.html',
      styleUrls: ['./tag.component.scss'],
      encapsulation: ViewEncapsulation.None
    })
    export class TagComponent implements OnChanges {
      @Input() xmColor = '';
      @Input() xmShape: TagMode = 'default';
      @Input() xmClosable = false;
      @Output() closed = new EventEmitter<void>();
      @HostBinding('class.xm-tag-circle') get circleCls(): boolean { return this.xmShape === 'circle'; }
      @HostBinding('class.xm-tag-close') get closeCls(): boolean { return this.xmClosable; }
      @HostBinding('class.xm-tag') readonly hostCls = true;
    
      private currentPresetCls = '';
    
      constructor(private el: ElementRef, private rd2: Renderer2) { }
    
      ngOnChanges(changes: SimpleChanges): void {
        this.setStyle(changes.xmColor);
      }
    
      private setStyle(color: SimpleChange): void {
        const hostEl = this.el.nativeElement;
        if (!hostEl || !this.xmColor) { return; }
        if (this.currentPresetCls) {
          this.rd2.removeClass(hostEl, this.currentPresetCls);
          this.currentPresetCls = '';
        }
        if (ColorPresets.includes(this.xmColor)) {
          this.currentPresetCls = 'xm-tag-' + this.xmColor;
          this.rd2.addClass(hostEl, this.currentPresetCls);
          this.rd2.removeStyle(hostEl, 'color');
          this.rd2.removeStyle(hostEl, 'border-color');
          this.rd2.removeStyle(hostEl, 'background-color');
        } else {
          this.rd2.setStyle(hostEl, 'color', '#fff');
          this.rd2.setStyle(hostEl, 'border-color', 'transparent');
          this.rd2.setStyle(hostEl, 'background-color', color.currentValue);
        }
      }
    }
    
    

    rate评分组件

    <div class="xm-rate-wrap" (mouseleave)="rateLeave()">
      <xm-rate-item
        *ngFor="let item of starArray; index as i"
        [tpl]="tpl"
        [rateItemCls]="rateItemStyles[i]"
        (itemClick)="rateClick($event, i)"
        (itemHover)="rateHover($event, i)">
      </xm-rate-item>
    </div>
    
    

    c

    import {
      ChangeDetectionStrategy,
      ChangeDetectorRef,
      Component,
      EventEmitter,
      forwardRef,
      Input,
      OnInit,
      Output, TemplateRef,
      ViewEncapsulation
    } from '@angular/core';
    import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
    
    @Component({
      selector: 'xm-rate',
      templateUrl: './rate.component.html',
      styleUrls: ['./rate.component.scss'],
      changeDetection: ChangeDetectionStrategy.OnPush,
      encapsulation: ViewEncapsulation.None,
      providers: [
        {
          provide: NG_VALUE_ACCESSOR,
          useExisting: forwardRef(() => RateComponent),
          multi: true
        }
      ]
    })
    export class RateComponent implements OnInit, ControlValueAccessor {
      @Input() count = 5;
      @Input() tpl: TemplateRef<void>;
      private readonly  = false;
      starArray: number[] = [];
      private hoverValue = 0;
      private actualValue = 0;
      private hasHalf = false;
      rateItemStyles: string[] = [];
      @Output() changed = new EventEmitter<number>();
      constructor(private cdr: ChangeDetectorRef) { }
    
      ngOnInit(): void {
        this.updateStarArray();
      }
    
      rateHover(isHalf: boolean, index: number): void {
        if (this.readonly || (this.hoverValue === index + 1 && isHalf === this.hasHalf)) {
          return;
        }
        this.hoverValue = index + 1;
        this.hasHalf = isHalf;
        // console.log('hoverValue', this.hoverValue);
        this.updateStarStyle();
      }
    
      rateClick(isHalf: boolean, index: number): void {
        if (this.readonly) {
          return;
        }
        this.hoverValue = index + 1;
        this.hasHalf = isHalf;
        this.setActualValue(isHalf ? index + 0.5 : this.hoverValue);
        this.updateStarStyle();
      }
    
      private setActualValue(value: number): void {
        if (this.actualValue !== value) {
          this.actualValue = value;
          this.onChange(value);
          this.changed.emit(value);
        }
      }
      rateLeave(): void {
        this.hasHalf = !Number.isInteger(this.actualValue);
        this.hoverValue = Math.ceil(this.actualValue);
        this.updateStarStyle();
      }
    
      private updateStarArray(): void {
        this.starArray = Array(this.count).fill(0).map((item, index) => index);
        // console.log('starArray', this.starArray);
      }
    
      private updateStarStyle(): void {
        this.rateItemStyles = this.starArray.map(index => {
          const base = 'xm-rate-item';
          const value = index + 1;
          let cls = '';
          if (value < this.hoverValue || (!this.hasHalf && value === this.hoverValue)) {
            cls += base + '-full';
          } else if (this.hasHalf && value === this.hoverValue) {
            cls += base + '-half';
          }
          const midCls = this.readonly ? ' xm-rate-item-readonly ' : ' ';
          return base + midCls + cls;
        });
      }
    
      onChange: (value: number) => void = () => {};
      onTouched: () => void = () => {};
      writeValue(value: number): void {
        // console.log('writeValue', value);
        if (value) {
          this.actualValue = value;
          this.rateLeave();
          this.cdr.markForCheck();
        }
      }
      registerOnChange(fn: (value: number) => void): void {
        this.onChange = fn;
      }
      registerOnTouched(fn: () => void): void {
        this.onTouched = fn;
      }
      setDisabledState(isDisabled: boolean): void {
        this.readonly = isDisabled;
      }
    }
    
    

    c

    <div [class]="rateItemCls || 'xm-rate-item'">
      <div class="star-wrap back" (click)="clickRate(false)" (mouseenter)="hoverRate(false)">
        <ng-template [ngTemplateOutlet]="tpl || defaultTpl"></ng-template>
      </div>
    
      <div class="star-wrap front" (click)="clickRate(true)" (mouseenter)="hoverRate(true)">
        <ng-template [ngTemplateOutlet]="tpl || defaultTpl"></ng-template>
      </div>
      
      <ng-template #defaultTpl>
        <i class="icon" xmIcon="Star"></i>
      </ng-template>
    </div>
    
    

    c

    import {Component, OnInit, ChangeDetectionStrategy, Output, EventEmitter, Input, TemplateRef} from '@angular/core';
    
    @Component({
      selector: 'xm-rate-item',
      templateUrl: './rate-item.component.html',
      styleUrls: ['./rate-item.component.scss'],
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class RateItemComponent implements OnInit {
      @Input() tpl: TemplateRef<void>;
      @Input() rateItemCls = 'xm-rate-item';
      @Output() private itemHover = new EventEmitter<boolean>();
      @Output() private itemClick = new EventEmitter<boolean>();
      constructor() { }
    
      ngOnInit(): void {
      }
    
      hoverRate(isHalf: boolean): void {
        this.itemHover.emit(isHalf);
      }
    
      clickRate(isHalf: boolean): void {
        this.itemClick.emit(isHalf);
      }
    }
    
    

    相关文章

      网友评论

          本文标题:脚写Angular组件

          本文链接:https://www.haomeiwen.com/subject/cterzktx.html