脚写Angular组件

  • 参考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


//=============================================
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 | string = '>';
  constructor() { }
  ngOnInit(): void {}
}

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


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

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 | string;
  @Input() xmStrTplOutletContext: any;
  constructor(private viewContainer: ViewContainerRef, private templateRef: TemplateRef) { }
  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. 分页组件

//使用+将string隐式转为number
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();
  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库的方法,这里用来限制页数变化范围
    }
  }

使用

 
//============================================== changePage(newPageNum: number): void { if (this.searchParams.page !== newPageNum) { this.searchParams.page = newPageNum; this.updateAlbums(); } }

3. tag标签组件



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();
  @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评分组件

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;
  private readonly  = false;
  starArray: number[] = [];
  private hoverValue = 0;
  private actualValue = 0;
  private hasHalf = false;
  rateItemStyles: string[] = [];
  @Output() changed = new EventEmitter();
  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

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;
  @Input() rateItemCls = 'xm-rate-item';
  @Output() private itemHover = new EventEmitter();
  @Output() private itemClick = new EventEmitter();
  constructor() { }

  ngOnInit(): void {
  }

  hoverRate(isHalf: boolean): void {
    this.itemHover.emit(isHalf);
  }

  clickRate(isHalf: boolean): void {
    this.itemClick.emit(isHalf);
  }
}

你可能感兴趣的:(脚写Angular组件)