From 65535d533ac98b2de688c1c085216fd900e376dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=89=B2?= Date: Wed, 28 Feb 2018 13:34:16 +0800 Subject: [PATCH] feat(module:avatar): add avatar component (#1028) * wip(module:avatar): add avatar component * fix tslint & Use NzUpdateHostClassService Instead of class operation * (BREAKCHANGES:nzIcon): Use ngClass Instead of string --- PROGRESS.md | 2 +- components/avatar/avatar.spec.ts | 137 +++++++++++++++++++++++ components/avatar/demo/badge.ts | 4 +- components/avatar/demo/basic.ts | 12 +- components/avatar/demo/type.ts | 6 +- components/avatar/doc/index.en-US.md | 9 +- components/avatar/doc/index.zh-CN.md | 9 +- components/avatar/index.ts | 1 + components/avatar/nz-avatar.component.ts | 122 +++++++++----------- components/avatar/public-api.ts | 2 + components/avatar/style/index.less | 7 +- 11 files changed, 219 insertions(+), 92 deletions(-) create mode 100644 components/avatar/avatar.spec.ts create mode 100644 components/avatar/index.ts create mode 100644 components/avatar/public-api.ts diff --git a/PROGRESS.md b/PROGRESS.md index d123b9bdb6c..c023ce69c73 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -42,7 +42,7 @@ | calendar | x | x | x | trotyl | - | | affix | x | x | x | cipchk | - | | transfer | x | x | x | cipchk | - | -| avatar | x | x | x | cipchk | - | +| avatar | √ | 100% | 100% | cipchk | x | | list | x | x | x | cipchk | - | | upload | x | x | x | cipchk | - | | anchor | x | x | x | cipchk | - | diff --git a/components/avatar/avatar.spec.ts b/components/avatar/avatar.spec.ts new file mode 100644 index 00000000000..5941d338db7 --- /dev/null +++ b/components/avatar/avatar.spec.ts @@ -0,0 +1,137 @@ +import { Component, DebugElement, ViewChild } from '@angular/core'; +import { fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { NzAvatarComponent } from './nz-avatar.component'; +import { NzAvatarModule } from './nz-avatar.module'; + +function getType(dl: DebugElement): string { + const el = dl.nativeElement as HTMLElement; + if (el.querySelector('img') != null) return 'image'; + if (el.querySelector('.anticon') != null) return 'icon'; + return el.innerText.trim().length === 0 ? '' : 'text'; +} + +describe('avatar', () => { + let fixture: ComponentFixture; + let context: TestAvatarComponent; + let dl: DebugElement; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ NzAvatarModule ], + declarations: [ TestAvatarComponent ] + }).compileComponents(); + fixture = TestBed.createComponent(TestAvatarComponent); + context = fixture.componentInstance; + dl = fixture.debugElement; + fixture.detectChanges(); + }); + + it('#nzSrc', () => { + expect(context).not.toBeNull(); + }); + + it('#nzIcon', () => { + context.nzSrc = null; + context.nzText = null; + fixture.detectChanges(); + expect(getType(dl)).toBe('icon'); + }); + + describe('#nzText', () => { + beforeEach(() => { + context.nzSrc = null; + context.nzIcon = null; + fixture.detectChanges(); + }); + it('property', () => { + expect(getType(dl)).toBe('text'); + }); + it('should be normal font-size', fakeAsync(() => { + context.nzText = 'a'; + fixture.detectChanges(); + tick(); + const scale = +dl.nativeElement.querySelector('.ant-avatar-string').style.transform.replace(/[^\.0-9]/ig, ''); + expect(scale).toBe(0); + })); + it('should be autoset font-size', fakeAsync(() => { + context.nzText = 'LongUsername'; + fixture.detectChanges(); + tick(); + const scale = +dl.nativeElement.querySelector('.ant-avatar-string').style.transform.replace(/[^\.0-9]/ig, ''); + expect(scale).toBeLessThan(1); + })); + }); + + describe('#nzShape', () => { + for (const type of [ 'square', 'circle' ]) { + it(type, () => { + context.nzShape = type; + fixture.detectChanges(); + expect(dl.query(By.css(`.ant-avatar-${type}`)) !== null).toBe(true); + }); + } + }); + + describe('#nzSize', () => { + for (const item of [ { size: 'large', cls: 'lg'}, { size: 'small', cls: 'sm'} ]) { + it(item.size, () => { + context.nzSize = item.size; + fixture.detectChanges(); + expect(dl.query(By.css(`.ant-avatar-${item.cls}`)) !== null).toBe(true); + }); + } + }); + + describe('order: image > icon > text', () => { + it('image priority', () => { + expect(getType(dl)).toBe('image'); + }); + it('should be show icon when image loaded error and icon exists', fakeAsync(() => { + expect(getType(dl)).toBe('image'); + context.comp.imgError(); + tick(); + fixture.detectChanges(); + expect(getType(dl)).toBe('icon'); + })); + it('should be show text when image loaded error and icon not exists', fakeAsync(() => { + expect(getType(dl)).toBe('image'); + context.nzIcon = null; + fixture.detectChanges(); + context.comp.imgError(); + tick(); + fixture.detectChanges(); + expect(getType(dl)).toBe('text'); + })); + it('should be show empty when image loaded error and icon & text not exists', fakeAsync(() => { + expect(getType(dl)).toBe('image'); + context.nzIcon = null; + context.nzText = null; + fixture.detectChanges(); + context.comp.imgError(); + tick(); + fixture.detectChanges(); + expect(getType(dl)).toBe(''); + })); + }); +}); + +@Component({ + template: ` + + `, + styleUrls: [ './style/index.less' ] +}) +class TestAvatarComponent { + @ViewChild('comp') comp: NzAvatarComponent; + nzShape = 'square'; + nzSize = 'large'; + nzIcon = 'anticon anticon-user'; + nzText = 'A'; + nzSrc = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==`; +} diff --git a/components/avatar/demo/badge.ts b/components/avatar/demo/badge.ts index dd479aef249..2a590bbf7e5 100644 --- a/components/avatar/demo/badge.ts +++ b/components/avatar/demo/badge.ts @@ -4,10 +4,10 @@ import { Component } from '@angular/core'; selector: 'nz-demo-avatar-badge', template: ` - + - + ` }) diff --git a/components/avatar/demo/basic.ts b/components/avatar/demo/basic.ts index 5e486bc7e67..0dc28343095 100644 --- a/components/avatar/demo/basic.ts +++ b/components/avatar/demo/basic.ts @@ -4,14 +4,14 @@ import { Component } from '@angular/core'; selector: 'nz-demo-avatar-basic', template: `
- - - + + +
- - - + + +
`, styles: [` diff --git a/components/avatar/demo/type.ts b/components/avatar/demo/type.ts index d3b741b8586..41f61e29ca9 100644 --- a/components/avatar/demo/type.ts +++ b/components/avatar/demo/type.ts @@ -3,12 +3,12 @@ import { Component } from '@angular/core'; @Component({ selector: 'nz-demo-avatar-type', template: ` - + - + - + `, styles: [` :host ::ng-deep .ant-avatar { diff --git a/components/avatar/doc/index.en-US.md b/components/avatar/doc/index.en-US.md index 98d7dfbb484..07b9fc76a5d 100644 --- a/components/avatar/doc/index.en-US.md +++ b/components/avatar/doc/index.en-US.md @@ -10,7 +10,8 @@ Avatars can be used to represent people or objects. It supports images, `Icon`s, | Property | Description | Type | Default | | -------- | ----------- | ---- | ------- | -| icon | the `Icon` type for an icon avatar, see `Icon` Component | string | - | -| shape | the shape of avatar | `circle` | `square` | `circle` | -| size | the size of the avatar | `large` | `small` | `default` | `default` | -| src | the address of the image for an image avatar | string | - | +| nzIcon | the `Icon` type for an icon avatar, see `Icon` | string | - | +| nzShape | the shape of avatar | `circle` | `square` | `circle` | +| nzSize | the size of the avatar | `large` | `small` | `default` | `default` | +| nzSrc | the address of the image for an image avatar | string | - | +| nzText | letter type avatar | string | - | diff --git a/components/avatar/doc/index.zh-CN.md b/components/avatar/doc/index.zh-CN.md index b8254f5a052..5bf3c77fbdc 100644 --- a/components/avatar/doc/index.zh-CN.md +++ b/components/avatar/doc/index.zh-CN.md @@ -11,7 +11,8 @@ title: Avatar | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| icon | 设置头像的图标类型,参考 `Icon` 组件 | string | - | -| shape | 指定头像的形状 | Enum{ 'circle', 'square' } | `circle` | -| size | 设置头像的大小 | Enum{ 'large', 'small', 'default' } | `default` | -| src | 图片类头像的资源地址 | string | - | +| nzIcon | 设置头像的图标类型,参考 `Icon` | string | - | +| nzShape | 指定头像的形状 | Enum{ 'circle', 'square' } | `circle` | +| nzSize | 设置头像的大小 | Enum{ 'large', 'small', 'default' } | `default` | +| nzSrc | 图片类头像的资源地址 | string | - | +| nzText | 文本类头像 | string | - | diff --git a/components/avatar/index.ts b/components/avatar/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/components/avatar/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/components/avatar/nz-avatar.component.ts b/components/avatar/nz-avatar.component.ts index 784aa4b7980..601530eb148 100644 --- a/components/avatar/nz-avatar.component.ts +++ b/components/avatar/nz-avatar.component.ts @@ -1,39 +1,29 @@ -import { - Component, - ElementRef, - Input, - OnChanges, - Renderer2, - SimpleChanges, - ViewChild -} from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; +import { NzUpdateHostClassService } from '../core/services/update-host-class.service'; export type NzAvatarShape = 'square' | 'circle'; export type NzAvatarSize = 'small' | 'large' | 'default'; @Component({ - selector : 'nz-avatar', + selector: 'nz-avatar', + template: ` + + + {{ nzText }}`, + providers: [NzUpdateHostClassService], preserveWhitespaces: false, - template : ` - - - {{ nzText }} - ` + changeDetection: ChangeDetectionStrategy.OnPush }) export class NzAvatarComponent implements OnChanges { + private el: HTMLElement; + private prefixCls = 'ant-avatar'; + private sizeMap = { large: 'lg', small: 'sm' }; + hasText: boolean = false; + hasSrc: boolean = true; + hasIcon: boolean = false; + textStyles: {}; - private _el: HTMLElement; - private _prefixCls = 'ant-avatar'; - private _classList: string[] = []; - private _sizeMap = { large: 'lg', small: 'sm' }; - - _hasText: boolean = false; - @ViewChild('textEl') _textEl: ElementRef; - _textStyles: {}; - - _isSrcExist: boolean = true; - - _hasIcon: boolean = false; + @ViewChild('textEl') textEl: ElementRef; @Input() nzShape: NzAvatarShape = 'circle'; @@ -45,76 +35,66 @@ export class NzAvatarComponent implements OnChanges { @Input() nzIcon: string; - _setClassMap(): this { - this._classList.forEach(_className => { - this._renderer.removeClass(this._el, _className); - }); - this._classList = [ - this._sizeMap[ this.nzSize ] && `${this._prefixCls}-${this._sizeMap[ this.nzSize ]}`, - this.nzShape && `${this._prefixCls}-${this.nzShape}`, - this.nzIcon && `${this._prefixCls}-icon`, - this.nzSrc && `${this._prefixCls}-image` - ].filter((item) => { - return !!item; - }); - this._classList.forEach(_className => { - this._renderer.addClass(this._el, _className); - }); + setClass(): this { + const classMap = { + [this.prefixCls]: true, + [`${this.prefixCls}-${this.sizeMap[this.nzSize]}`]: this.sizeMap[this.nzSize], + [`${this.prefixCls}-${this.nzShape}`]: this.nzShape, + [`${this.prefixCls}-icon`]: this.nzIcon, + [`${this.prefixCls}-image`]: this.nzSrc + }; + this.updateHostClassService.updateHostClass(this.el, classMap); + this.cd.detectChanges(); return this; } - _imgError(): void { - this._isSrcExist = false; - // TODO(i): need force remove [nzSrc] if broken image? - this._hasIcon = false; - this._hasText = false; + imgError(): void { + this.hasSrc = false; + this.hasIcon = false; + this.hasText = false; if (this.nzIcon) { - this._hasIcon = true; + this.hasIcon = true; } else if (this.nzText) { - this._hasText = true; + this.hasText = true; } - this._setClassMap()._notifyCalc(); + this.setClass().notifyCalc(); } - private _calcStringSize(): void { - if (!this._hasText) return; + private calcStringSize(): void { + if (!this.hasText) return; - const el = this._textEl && this._textEl.nativeElement; - if (!el) return; - - const childrenWidth = el.offsetWidth; - const avatarWidth = this._el.getBoundingClientRect().width; + const childrenWidth = this.textEl.nativeElement.offsetWidth; + const avatarWidth = this.el.getBoundingClientRect().width; const scale = avatarWidth - 8 < childrenWidth ? (avatarWidth - 8) / childrenWidth : 1; if (scale === 1) { - this._textStyles = {}; + this.textStyles = {}; } else { - this._textStyles = { + this.textStyles = { transform: `scale(${scale})`, - position : 'absolute', - display : 'inline-block', - left : `calc(50% - ${Math.round(childrenWidth / 2)}px)` + position: 'absolute', + display: 'inline-block', + left: `calc(50% - ${Math.round(childrenWidth / 2)}px)` }; } + this.cd.detectChanges(); } - private _notifyCalc(): this { + private notifyCalc(): this { // If use ngAfterViewChecked, always demands more computations, so...... setTimeout(() => { - this._calcStringSize(); + this.calcStringSize(); }); return this; } - constructor(private _elementRef: ElementRef, private _renderer: Renderer2) { - this._el = _elementRef.nativeElement; - this._renderer.addClass(this._el, this._prefixCls); + constructor(elementRef: ElementRef, private cd: ChangeDetectorRef, private updateHostClassService: NzUpdateHostClassService) { + this.el = elementRef.nativeElement; } ngOnChanges(changes: SimpleChanges): void { - this._hasText = !this.nzSrc && !!this.nzText; - this._hasIcon = !this.nzSrc && !!this.nzIcon; + this.hasText = !this.nzSrc && !!this.nzText; + this.hasIcon = !this.nzSrc && !!this.nzIcon; - this._setClassMap()._notifyCalc(); + this.setClass().notifyCalc(); } - } diff --git a/components/avatar/public-api.ts b/components/avatar/public-api.ts new file mode 100644 index 00000000000..c5da9763116 --- /dev/null +++ b/components/avatar/public-api.ts @@ -0,0 +1,2 @@ +export * from './nz-avatar.component'; +export * from './nz-avatar.module'; diff --git a/components/avatar/style/index.less b/components/avatar/style/index.less index f151cd57184..ef1023bcada 100644 --- a/components/avatar/style/index.less +++ b/components/avatar/style/index.less @@ -12,6 +12,11 @@ white-space: nowrap; position: relative; overflow: hidden; + vertical-align: middle; + + &-image { + background: transparent; + } .avatar-size(@avatar-size-base, @avatar-font-size-base); @@ -47,4 +52,4 @@ &.@{avatar-prefix-cls}-icon { font-size: @font-size; } -} +} \ No newline at end of file