Skip to content

Commit

Permalink
feat(module:avatar): add avatar component (#1028)
Browse files Browse the repository at this point in the history
* wip(module:avatar): add avatar component

* fix tslint & Use NzUpdateHostClassService Instead of class operation

* (BREAKCHANGES:nzIcon): Use ngClass Instead of string
  • Loading branch information
cipchk authored and vthinkxie committed Feb 28, 2018
1 parent 4ae0bda commit 65535d5
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 92 deletions.
2 changes: 1 addition & 1 deletion PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | - |
Expand Down
137 changes: 137 additions & 0 deletions components/avatar/avatar.spec.ts
Original file line number Diff line number Diff line change
@@ -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<TestAvatarComponent>;
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: `
<nz-avatar #comp
[nzShape]="nzShape"
[nzSize]="nzSize"
[nzIcon]="nzIcon"
[nzText]="nzText"
[nzSrc]="nzSrc"></nz-avatar>
`,
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==`;
}
4 changes: 2 additions & 2 deletions components/avatar/demo/badge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { Component } from '@angular/core';
selector: 'nz-demo-avatar-badge',
template: `
<nz-badge [nzCount]="5" style="margin-right: 24px;">
<ng-template #content><nz-avatar nzIcon="user" [nzShape]="'square'"></nz-avatar></ng-template>
<ng-template #content><nz-avatar nzIcon="anticon anticon-user" [nzShape]="'square'"></nz-avatar></ng-template>
</nz-badge>
<nz-badge nzDot>
<ng-template #content><nz-avatar nzIcon="user" [nzShape]="'square'"></nz-avatar></ng-template>
<ng-template #content><nz-avatar nzIcon="anticon anticon-user" [nzShape]="'square'"></nz-avatar></ng-template>
</nz-badge>
`
})
Expand Down
12 changes: 6 additions & 6 deletions components/avatar/demo/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { Component } from '@angular/core';
selector: 'nz-demo-avatar-basic',
template: `
<div>
<nz-avatar nzSize="large" nzIcon="user"></nz-avatar>
<nz-avatar nzIcon="user"></nz-avatar>
<nz-avatar nzSize="small" nzIcon="user"></nz-avatar>
<nz-avatar nzSize="large" nzIcon="anticon anticon-user"></nz-avatar>
<nz-avatar nzIcon="anticon anticon-user"></nz-avatar>
<nz-avatar nzSize="small" nzIcon="anticon anticon-user"></nz-avatar>
</div>
<div>
<nz-avatar [nzShape]="'square'" [nzSize]="'large'" [nzIcon]="'user'"></nz-avatar>
<nz-avatar [nzShape]="'square'" [nzIcon]="'user'"></nz-avatar>
<nz-avatar [nzShape]="'square'" [nzSize]="'small'" [nzIcon]="'user'"></nz-avatar>
<nz-avatar [nzShape]="'square'" [nzSize]="'large'" [nzIcon]="'anticon anticon-user'"></nz-avatar>
<nz-avatar [nzShape]="'square'" [nzIcon]="'anticon anticon-user'"></nz-avatar>
<nz-avatar [nzShape]="'square'" [nzSize]="'small'" [nzIcon]="'anticon anticon-user'"></nz-avatar>
</div>
`,
styles: [`
Expand Down
6 changes: 3 additions & 3 deletions components/avatar/demo/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Component } from '@angular/core';
@Component({
selector: 'nz-demo-avatar-type',
template: `
<nz-avatar nzIcon="user"></nz-avatar>
<nz-avatar nzIcon="anticon anticon-user"></nz-avatar>
<nz-avatar nzText="U"></nz-avatar>
<nz-avatar nzText="USER"></nz-avatar>
<nz-avatar nzIcon="user" nzSrc="//zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"></nz-avatar>
<nz-avatar nzIcon="anticon anticon-user" nzSrc="//zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"></nz-avatar>
<nz-avatar nzText="U" style="color:#f56a00; background-color:#fde3cf;"></nz-avatar>
<nz-avatar nzIcon="user" style="background-color:#87d068;"></nz-avatar>
<nz-avatar nzIcon="anticon anticon-user" style="background-color:#87d068;"></nz-avatar>
`,
styles: [`
:host ::ng-deep .ant-avatar {
Expand Down
9 changes: 5 additions & 4 deletions components/avatar/doc/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | - |
9 changes: 5 additions & 4 deletions components/avatar/doc/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | - |
1 change: 1 addition & 0 deletions components/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public-api';
122 changes: 51 additions & 71 deletions components/avatar/nz-avatar.component.ts
Original file line number Diff line number Diff line change
@@ -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: `
<i *ngIf="nzIcon && hasIcon" [ngClass]="nzIcon"></i>
<img [src]="nzSrc" *ngIf="nzSrc && hasSrc" (error)="imgError()"/>
<span class="ant-avatar-string" #textEl [ngStyle]="textStyles" *ngIf="nzText && hasText">{{ nzText }}</span>`,
providers: [NzUpdateHostClassService],
preserveWhitespaces: false,
template : `
<i class="anticon anticon-{{nzIcon}}" *ngIf="nzIcon && _hasIcon"></i>
<img [src]="nzSrc" *ngIf="nzSrc && _isSrcExist" (error)="_imgError($event)"/>
<span class="ant-avatar-string" #textEl [ngStyle]="_textStyles" *ngIf="nzText && _hasText">{{ nzText }}</span>
`
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';

Expand All @@ -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();
}

}
2 changes: 2 additions & 0 deletions components/avatar/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './nz-avatar.component';
export * from './nz-avatar.module';
Loading

0 comments on commit 65535d5

Please sign in to comment.