Skip to content

Commit 4d178de

Browse files
authored
refactor(v3/avatar): refactor using composition api (#4052)
* refactor(avatar): refactor using composition api * refactor: update props define
1 parent 184957e commit 4d178de

File tree

3 files changed

+155
-164
lines changed

3 files changed

+155
-164
lines changed

components/avatar/Avatar.tsx

+148-143
Original file line numberDiff line numberDiff line change
@@ -1,170 +1,175 @@
11
import { tuple, VueNode } from '../_util/type';
2-
import { CSSProperties, defineComponent, inject, nextTick, PropType } from 'vue';
2+
import {
3+
CSSProperties,
4+
defineComponent,
5+
ExtractPropTypes,
6+
inject,
7+
nextTick,
8+
onMounted,
9+
onUpdated,
10+
PropType,
11+
ref,
12+
watch,
13+
} from 'vue';
314
import { defaultConfigProvider } from '../config-provider';
4-
import { getComponent } from '../_util/props-util';
15+
import { getPropsSlot } from '../_util/props-util';
516
import PropTypes from '../_util/vue-types';
617

7-
export default defineComponent({
8-
name: 'AAvatar',
9-
props: {
10-
prefixCls: PropTypes.string,
11-
shape: PropTypes.oneOf(tuple('circle', 'square')),
12-
size: {
13-
type: [Number, String] as PropType<'large' | 'small' | 'default' | number>,
14-
default: 'default',
15-
},
16-
src: PropTypes.string,
17-
/** Srcset of image avatar */
18-
srcset: PropTypes.string,
19-
/** @deprecated please use `srcset` instead `srcSet` */
20-
srcSet: PropTypes.string,
21-
icon: PropTypes.VNodeChild,
22-
alt: PropTypes.string,
23-
loadError: {
24-
type: Function as PropType<() => boolean>,
25-
},
26-
},
27-
setup() {
28-
return {
29-
configProvider: inject('configProvider', defaultConfigProvider),
30-
};
31-
},
32-
data() {
33-
return {
34-
isImgExist: true,
35-
isMounted: false,
36-
scale: 1,
37-
lastChildrenWidth: undefined,
38-
lastNodeWidth: undefined,
39-
};
40-
},
41-
watch: {
42-
src() {
43-
nextTick(() => {
44-
this.isImgExist = true;
45-
this.scale = 1;
46-
// force uodate for position
47-
this.$forceUpdate();
48-
});
49-
},
50-
},
51-
mounted() {
52-
nextTick(() => {
53-
this.setScale();
54-
this.isMounted = true;
55-
});
18+
const avatarProps = {
19+
prefixCls: PropTypes.string,
20+
shape: PropTypes.oneOf(tuple('circle', 'square')),
21+
size: {
22+
type: [Number, String] as PropType<'large' | 'small' | 'default' | number>,
23+
default: 'default',
5624
},
57-
updated() {
58-
nextTick(() => {
59-
this.setScale();
60-
});
25+
src: PropTypes.string,
26+
/** Srcset of image avatar */
27+
srcset: PropTypes.string,
28+
icon: PropTypes.VNodeChild,
29+
alt: PropTypes.string,
30+
loadError: {
31+
type: Function as PropType<() => boolean>,
6132
},
62-
methods: {
63-
setScale() {
64-
if (!this.$refs.avatarChildren || !this.$refs.avatarNode) {
33+
};
34+
35+
export type AvatarProps = Partial<ExtractPropTypes<typeof avatarProps>>;
36+
37+
const Avatar = defineComponent({
38+
name: 'AAvatar',
39+
props: avatarProps,
40+
setup(props, { slots }) {
41+
const isImgExist = ref(true);
42+
const isMounted = ref(false);
43+
const scale = ref(1);
44+
const lastChildrenWidth = ref<number>(undefined);
45+
const lastNodeWidth = ref<number>(undefined);
46+
47+
const avatarChildrenRef = ref<HTMLElement>(null);
48+
const avatarNodeRef = ref<HTMLElement>(null);
49+
50+
const configProvider = inject('configProvider', defaultConfigProvider);
51+
52+
const setScale = () => {
53+
if (!avatarChildrenRef.value || !avatarNodeRef.value) {
6554
return;
6655
}
67-
const childrenWidth = (this.$refs.avatarChildren as HTMLElement).offsetWidth; // offsetWidth avoid affecting be transform scale
68-
const nodeWidth = (this.$refs.avatarNode as HTMLElement).offsetWidth;
56+
const childrenWidth = avatarChildrenRef.value.offsetWidth; // offsetWidth avoid affecting be transform scale
57+
const nodeWidth = avatarNodeRef.value.offsetWidth;
6958
// denominator is 0 is no meaning
7059
if (
7160
childrenWidth === 0 ||
7261
nodeWidth === 0 ||
73-
(this.lastChildrenWidth === childrenWidth && this.lastNodeWidth === nodeWidth)
62+
(lastChildrenWidth.value === childrenWidth && lastNodeWidth.value === nodeWidth)
7463
) {
7564
return;
7665
}
77-
this.lastChildrenWidth = childrenWidth;
78-
this.lastNodeWidth = nodeWidth;
66+
lastChildrenWidth.value = childrenWidth;
67+
lastNodeWidth.value = nodeWidth;
7968
// add 4px gap for each side to get better performance
80-
this.scale = nodeWidth - 8 < childrenWidth ? (nodeWidth - 8) / childrenWidth : 1;
81-
},
82-
handleImgLoadError() {
83-
const { loadError } = this.$props;
84-
const errorFlag = loadError ? loadError() : undefined;
69+
scale.value = nodeWidth - 8 < childrenWidth ? (nodeWidth - 8) / childrenWidth : 1;
70+
};
71+
72+
const handleImgLoadError = () => {
73+
const { loadError } = props;
74+
const errorFlag = loadError?.();
8575
if (errorFlag !== false) {
86-
this.isImgExist = false;
76+
isImgExist.value = false;
8777
}
88-
},
89-
},
90-
render() {
91-
const { prefixCls: customizePrefixCls, shape, size, src, alt, srcset, srcSet } = this.$props;
92-
const icon = getComponent(this, 'icon');
93-
const getPrefixCls = this.configProvider.getPrefixCls;
94-
const prefixCls = getPrefixCls('avatar', customizePrefixCls);
78+
};
9579

96-
const { isImgExist, scale, isMounted } = this.$data;
80+
watch(
81+
() => props.src,
82+
() => {
83+
nextTick(() => {
84+
isImgExist.value = true;
85+
scale.value = 1;
86+
});
87+
},
88+
);
9789

98-
const sizeCls = {
99-
[`${prefixCls}-lg`]: size === 'large',
100-
[`${prefixCls}-sm`]: size === 'small',
101-
};
90+
onMounted(() => {
91+
nextTick(() => {
92+
setScale();
93+
isMounted.value = true;
94+
});
95+
});
10296

103-
const classString = {
104-
[prefixCls]: true,
105-
...sizeCls,
106-
[`${prefixCls}-${shape}`]: shape,
107-
[`${prefixCls}-image`]: src && isImgExist,
108-
[`${prefixCls}-icon`]: icon,
109-
};
97+
onUpdated(() => {
98+
nextTick(() => {
99+
setScale();
100+
});
101+
});
110102

111-
const sizeStyle: CSSProperties =
112-
typeof size === 'number'
113-
? {
114-
width: `${size}px`,
115-
height: `${size}px`,
116-
lineHeight: `${size}px`,
117-
fontSize: icon ? `${size / 2}px` : '18px',
118-
}
119-
: {};
120-
121-
let children: VueNode = this.$slots.default?.();
122-
if (src && isImgExist) {
123-
children = (
124-
<img src={src} srcset={srcset || srcSet} onError={this.handleImgLoadError} alt={alt} />
125-
);
126-
} else if (icon) {
127-
children = icon;
128-
} else {
129-
const childrenNode = this.$refs.avatarChildren;
130-
if (childrenNode || scale !== 1) {
131-
const transformString = `scale(${scale}) translateX(-50%)`;
132-
const childrenStyle: CSSProperties = {
133-
msTransform: transformString,
134-
WebkitTransform: transformString,
135-
transform: transformString,
136-
};
137-
const sizeChildrenStyle =
138-
typeof size === 'number'
139-
? {
140-
lineHeight: `${size}px`,
141-
}
142-
: {};
143-
children = (
144-
<span
145-
class={`${prefixCls}-string`}
146-
ref="avatarChildren"
147-
style={{ ...sizeChildrenStyle, ...childrenStyle }}
148-
>
149-
{children}
150-
</span>
151-
);
103+
return () => {
104+
const { prefixCls: customizePrefixCls, shape, size, src, alt, srcset } = props;
105+
const icon = getPropsSlot(slots, props, 'icon');
106+
const getPrefixCls = configProvider.getPrefixCls;
107+
const prefixCls = getPrefixCls('avatar', customizePrefixCls);
108+
109+
const classString = {
110+
[prefixCls]: true,
111+
[`${prefixCls}-lg`]: size === 'large',
112+
[`${prefixCls}-sm`]: size === 'small',
113+
[`${prefixCls}-${shape}`]: shape,
114+
[`${prefixCls}-image`]: src && isImgExist.value,
115+
[`${prefixCls}-icon`]: icon,
116+
};
117+
118+
const sizeStyle: CSSProperties =
119+
typeof size === 'number'
120+
? {
121+
width: `${size}px`,
122+
height: `${size}px`,
123+
lineHeight: `${size}px`,
124+
fontSize: icon ? `${size / 2}px` : '18px',
125+
}
126+
: {};
127+
128+
let children: VueNode = slots.default?.();
129+
if (src && isImgExist.value) {
130+
children = <img src={src} srcset={srcset} onError={handleImgLoadError} alt={alt} />;
131+
} else if (icon) {
132+
children = icon;
152133
} else {
153-
const childrenStyle: CSSProperties = {};
154-
if (!isMounted) {
155-
childrenStyle.opacity = 0;
134+
const childrenNode = avatarChildrenRef.value;
135+
136+
if (childrenNode || scale.value !== 1) {
137+
const transformString = `scale(${scale.value}) translateX(-50%)`;
138+
const childrenStyle: CSSProperties = {
139+
msTransform: transformString,
140+
WebkitTransform: transformString,
141+
transform: transformString,
142+
};
143+
const sizeChildrenStyle =
144+
typeof size === 'number'
145+
? {
146+
lineHeight: `${size}px`,
147+
}
148+
: {};
149+
children = (
150+
<span
151+
class={`${prefixCls}-string`}
152+
ref={avatarChildrenRef}
153+
style={{ ...sizeChildrenStyle, ...childrenStyle }}
154+
>
155+
{children}
156+
</span>
157+
);
158+
} else {
159+
children = (
160+
<span class={`${prefixCls}-string`} ref={avatarChildrenRef} style={{ opacity: 0 }}>
161+
{children}
162+
</span>
163+
);
156164
}
157-
children = (
158-
<span class={`${prefixCls}-string`} ref="avatarChildren" style={{ opacity: 0 }}>
159-
{children}
160-
</span>
161-
);
162165
}
163-
}
164-
return (
165-
<span ref="avatarNode" class={classString} style={sizeStyle}>
166-
{children}
167-
</span>
168-
);
166+
return (
167+
<span ref={avatarNodeRef} class={classString} style={sizeStyle}>
168+
{children}
169+
</span>
170+
);
171+
};
169172
},
170173
});
174+
175+
export default Avatar;

components/avatar/__tests__/Avatar.test.js

+5-21
Original file line numberDiff line numberDiff line change
@@ -41,31 +41,16 @@ describe('Avatar Render', () => {
4141
props: {
4242
src: 'http://error.url',
4343
},
44-
sync: false,
4544
attachTo: 'body',
4645
});
47-
wrapper.vm.setScale = jest.fn(() => {
48-
if (wrapper.vm.scale === 0.5) {
49-
return;
50-
}
51-
wrapper.vm.scale = 0.5;
52-
wrapper.vm.$forceUpdate();
53-
});
5446
await asyncExpect(() => {
5547
wrapper.find('img').trigger('error');
5648
}, 0);
5749
await asyncExpect(() => {
5850
const children = wrapper.findAll('.ant-avatar-string');
5951
expect(children.length).toBe(1);
6052
expect(children[0].text()).toBe('Fallback');
61-
expect(wrapper.vm.setScale).toHaveBeenCalled();
6253
});
63-
await asyncExpect(() => {
64-
expect(global.document.body.querySelector('.ant-avatar-string').style.transform).toContain(
65-
'scale(0.5)',
66-
);
67-
global.document.body.innerHTML = '';
68-
}, 1000);
6954
});
7055
it('should handle onError correctly', async () => {
7156
global.document.body.innerHTML = '';
@@ -91,17 +76,17 @@ describe('Avatar Render', () => {
9176
},
9277
};
9378

94-
const wrapper = mount(Foo, { sync: false, attachTo: 'body' });
79+
const wrapper = mount(Foo, { attachTo: 'body' });
9580
await asyncExpect(() => {
9681
// mock img load Error, since jsdom do not load resource by default
9782
// https://github.com/jsdom/jsdom/issues/1816
9883
wrapper.find('img').trigger('error');
9984
}, 0);
10085
await asyncExpect(() => {
101-
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true);
86+
expect(wrapper.find('img')).not.toBeNull();
10287
}, 0);
10388
await asyncExpect(() => {
104-
expect(global.document.body.querySelector('img').getAttribute('src')).toBe(LOAD_SUCCESS_SRC);
89+
expect(wrapper.find('img').attributes('src')).toBe(LOAD_SUCCESS_SRC);
10590
}, 0);
10691
});
10792

@@ -126,17 +111,16 @@ describe('Avatar Render', () => {
126111
await asyncExpect(() => {
127112
wrapper.find('img').trigger('error');
128113
}, 0);
129-
130114
await asyncExpect(() => {
131-
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(false);
115+
expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(0);
132116
expect(wrapper.findAll('.ant-avatar-string').length).toBe(1);
133117
}, 0);
134118

135119
await asyncExpect(() => {
136120
wrapper.vm.src = LOAD_SUCCESS_SRC;
137121
});
138122
await asyncExpect(() => {
139-
expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true);
123+
expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(1);
140124
expect(wrapper.findAll('.ant-avatar-image').length).toBe(1);
141125
}, 0);
142126
});

components/avatar/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import Avatar from './Avatar';
22
import { withInstall } from '../_util/type';
33

4+
export { AvatarProps } from './Avatar';
5+
46
export default withInstall(Avatar);

0 commit comments

Comments
 (0)