From 4d178debd7c714815af078eae69f755e247a9a50 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 10 May 2021 18:11:18 +0800 Subject: [PATCH] refactor(v3/avatar): refactor using composition api (#4052) * refactor(avatar): refactor using composition api * refactor: update props define --- components/avatar/Avatar.tsx | 291 +++++++++++---------- components/avatar/__tests__/Avatar.test.js | 26 +- components/avatar/index.ts | 2 + 3 files changed, 155 insertions(+), 164 deletions(-) diff --git a/components/avatar/Avatar.tsx b/components/avatar/Avatar.tsx index 212bd57cf7..47231410a1 100644 --- a/components/avatar/Avatar.tsx +++ b/components/avatar/Avatar.tsx @@ -1,170 +1,175 @@ import { tuple, VueNode } from '../_util/type'; -import { CSSProperties, defineComponent, inject, nextTick, PropType } from 'vue'; +import { + CSSProperties, + defineComponent, + ExtractPropTypes, + inject, + nextTick, + onMounted, + onUpdated, + PropType, + ref, + watch, +} from 'vue'; import { defaultConfigProvider } from '../config-provider'; -import { getComponent } from '../_util/props-util'; +import { getPropsSlot } from '../_util/props-util'; import PropTypes from '../_util/vue-types'; -export default defineComponent({ - name: 'AAvatar', - props: { - prefixCls: PropTypes.string, - shape: PropTypes.oneOf(tuple('circle', 'square')), - size: { - type: [Number, String] as PropType<'large' | 'small' | 'default' | number>, - default: 'default', - }, - src: PropTypes.string, - /** Srcset of image avatar */ - srcset: PropTypes.string, - /** @deprecated please use `srcset` instead `srcSet` */ - srcSet: PropTypes.string, - icon: PropTypes.VNodeChild, - alt: PropTypes.string, - loadError: { - type: Function as PropType<() => boolean>, - }, - }, - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), - }; - }, - data() { - return { - isImgExist: true, - isMounted: false, - scale: 1, - lastChildrenWidth: undefined, - lastNodeWidth: undefined, - }; - }, - watch: { - src() { - nextTick(() => { - this.isImgExist = true; - this.scale = 1; - // force uodate for position - this.$forceUpdate(); - }); - }, - }, - mounted() { - nextTick(() => { - this.setScale(); - this.isMounted = true; - }); +const avatarProps = { + prefixCls: PropTypes.string, + shape: PropTypes.oneOf(tuple('circle', 'square')), + size: { + type: [Number, String] as PropType<'large' | 'small' | 'default' | number>, + default: 'default', }, - updated() { - nextTick(() => { - this.setScale(); - }); + src: PropTypes.string, + /** Srcset of image avatar */ + srcset: PropTypes.string, + icon: PropTypes.VNodeChild, + alt: PropTypes.string, + loadError: { + type: Function as PropType<() => boolean>, }, - methods: { - setScale() { - if (!this.$refs.avatarChildren || !this.$refs.avatarNode) { +}; + +export type AvatarProps = Partial>; + +const Avatar = defineComponent({ + name: 'AAvatar', + props: avatarProps, + setup(props, { slots }) { + const isImgExist = ref(true); + const isMounted = ref(false); + const scale = ref(1); + const lastChildrenWidth = ref(undefined); + const lastNodeWidth = ref(undefined); + + const avatarChildrenRef = ref(null); + const avatarNodeRef = ref(null); + + const configProvider = inject('configProvider', defaultConfigProvider); + + const setScale = () => { + if (!avatarChildrenRef.value || !avatarNodeRef.value) { return; } - const childrenWidth = (this.$refs.avatarChildren as HTMLElement).offsetWidth; // offsetWidth avoid affecting be transform scale - const nodeWidth = (this.$refs.avatarNode as HTMLElement).offsetWidth; + const childrenWidth = avatarChildrenRef.value.offsetWidth; // offsetWidth avoid affecting be transform scale + const nodeWidth = avatarNodeRef.value.offsetWidth; // denominator is 0 is no meaning if ( childrenWidth === 0 || nodeWidth === 0 || - (this.lastChildrenWidth === childrenWidth && this.lastNodeWidth === nodeWidth) + (lastChildrenWidth.value === childrenWidth && lastNodeWidth.value === nodeWidth) ) { return; } - this.lastChildrenWidth = childrenWidth; - this.lastNodeWidth = nodeWidth; + lastChildrenWidth.value = childrenWidth; + lastNodeWidth.value = nodeWidth; // add 4px gap for each side to get better performance - this.scale = nodeWidth - 8 < childrenWidth ? (nodeWidth - 8) / childrenWidth : 1; - }, - handleImgLoadError() { - const { loadError } = this.$props; - const errorFlag = loadError ? loadError() : undefined; + scale.value = nodeWidth - 8 < childrenWidth ? (nodeWidth - 8) / childrenWidth : 1; + }; + + const handleImgLoadError = () => { + const { loadError } = props; + const errorFlag = loadError?.(); if (errorFlag !== false) { - this.isImgExist = false; + isImgExist.value = false; } - }, - }, - render() { - const { prefixCls: customizePrefixCls, shape, size, src, alt, srcset, srcSet } = this.$props; - const icon = getComponent(this, 'icon'); - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('avatar', customizePrefixCls); + }; - const { isImgExist, scale, isMounted } = this.$data; + watch( + () => props.src, + () => { + nextTick(() => { + isImgExist.value = true; + scale.value = 1; + }); + }, + ); - const sizeCls = { - [`${prefixCls}-lg`]: size === 'large', - [`${prefixCls}-sm`]: size === 'small', - }; + onMounted(() => { + nextTick(() => { + setScale(); + isMounted.value = true; + }); + }); - const classString = { - [prefixCls]: true, - ...sizeCls, - [`${prefixCls}-${shape}`]: shape, - [`${prefixCls}-image`]: src && isImgExist, - [`${prefixCls}-icon`]: icon, - }; + onUpdated(() => { + nextTick(() => { + setScale(); + }); + }); - const sizeStyle: CSSProperties = - typeof size === 'number' - ? { - width: `${size}px`, - height: `${size}px`, - lineHeight: `${size}px`, - fontSize: icon ? `${size / 2}px` : '18px', - } - : {}; - - let children: VueNode = this.$slots.default?.(); - if (src && isImgExist) { - children = ( - {alt} - ); - } else if (icon) { - children = icon; - } else { - const childrenNode = this.$refs.avatarChildren; - if (childrenNode || scale !== 1) { - const transformString = `scale(${scale}) translateX(-50%)`; - const childrenStyle: CSSProperties = { - msTransform: transformString, - WebkitTransform: transformString, - transform: transformString, - }; - const sizeChildrenStyle = - typeof size === 'number' - ? { - lineHeight: `${size}px`, - } - : {}; - children = ( - - {children} - - ); + return () => { + const { prefixCls: customizePrefixCls, shape, size, src, alt, srcset } = props; + const icon = getPropsSlot(slots, props, 'icon'); + const getPrefixCls = configProvider.getPrefixCls; + const prefixCls = getPrefixCls('avatar', customizePrefixCls); + + const classString = { + [prefixCls]: true, + [`${prefixCls}-lg`]: size === 'large', + [`${prefixCls}-sm`]: size === 'small', + [`${prefixCls}-${shape}`]: shape, + [`${prefixCls}-image`]: src && isImgExist.value, + [`${prefixCls}-icon`]: icon, + }; + + const sizeStyle: CSSProperties = + typeof size === 'number' + ? { + width: `${size}px`, + height: `${size}px`, + lineHeight: `${size}px`, + fontSize: icon ? `${size / 2}px` : '18px', + } + : {}; + + let children: VueNode = slots.default?.(); + if (src && isImgExist.value) { + children = {alt}; + } else if (icon) { + children = icon; } else { - const childrenStyle: CSSProperties = {}; - if (!isMounted) { - childrenStyle.opacity = 0; + const childrenNode = avatarChildrenRef.value; + + if (childrenNode || scale.value !== 1) { + const transformString = `scale(${scale.value}) translateX(-50%)`; + const childrenStyle: CSSProperties = { + msTransform: transformString, + WebkitTransform: transformString, + transform: transformString, + }; + const sizeChildrenStyle = + typeof size === 'number' + ? { + lineHeight: `${size}px`, + } + : {}; + children = ( + + {children} + + ); + } else { + children = ( + + {children} + + ); } - children = ( - - {children} - - ); } - } - return ( - - {children} - - ); + return ( + + {children} + + ); + }; }, }); + +export default Avatar; diff --git a/components/avatar/__tests__/Avatar.test.js b/components/avatar/__tests__/Avatar.test.js index a7b58b2a06..9ceeadad87 100644 --- a/components/avatar/__tests__/Avatar.test.js +++ b/components/avatar/__tests__/Avatar.test.js @@ -41,16 +41,8 @@ describe('Avatar Render', () => { props: { src: 'http://error.url', }, - sync: false, attachTo: 'body', }); - wrapper.vm.setScale = jest.fn(() => { - if (wrapper.vm.scale === 0.5) { - return; - } - wrapper.vm.scale = 0.5; - wrapper.vm.$forceUpdate(); - }); await asyncExpect(() => { wrapper.find('img').trigger('error'); }, 0); @@ -58,14 +50,7 @@ describe('Avatar Render', () => { const children = wrapper.findAll('.ant-avatar-string'); expect(children.length).toBe(1); expect(children[0].text()).toBe('Fallback'); - expect(wrapper.vm.setScale).toHaveBeenCalled(); }); - await asyncExpect(() => { - expect(global.document.body.querySelector('.ant-avatar-string').style.transform).toContain( - 'scale(0.5)', - ); - global.document.body.innerHTML = ''; - }, 1000); }); it('should handle onError correctly', async () => { global.document.body.innerHTML = ''; @@ -91,17 +76,17 @@ describe('Avatar Render', () => { }, }; - const wrapper = mount(Foo, { sync: false, attachTo: 'body' }); + const wrapper = mount(Foo, { attachTo: 'body' }); await asyncExpect(() => { // mock img load Error, since jsdom do not load resource by default // https://github.com/jsdom/jsdom/issues/1816 wrapper.find('img').trigger('error'); }, 0); await asyncExpect(() => { - expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true); + expect(wrapper.find('img')).not.toBeNull(); }, 0); await asyncExpect(() => { - expect(global.document.body.querySelector('img').getAttribute('src')).toBe(LOAD_SUCCESS_SRC); + expect(wrapper.find('img').attributes('src')).toBe(LOAD_SUCCESS_SRC); }, 0); }); @@ -126,9 +111,8 @@ describe('Avatar Render', () => { await asyncExpect(() => { wrapper.find('img').trigger('error'); }, 0); - await asyncExpect(() => { - expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(false); + expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(0); expect(wrapper.findAll('.ant-avatar-string').length).toBe(1); }, 0); @@ -136,7 +120,7 @@ describe('Avatar Render', () => { wrapper.vm.src = LOAD_SUCCESS_SRC; }); await asyncExpect(() => { - expect(wrapper.findComponent({ name: 'AAvatar' }).vm.isImgExist).toBe(true); + expect(wrapper.findComponent({ name: 'AAvatar' }).findAll('img').length).toBe(1); expect(wrapper.findAll('.ant-avatar-image').length).toBe(1); }, 0); }); diff --git a/components/avatar/index.ts b/components/avatar/index.ts index 6d94e63f26..5229cee3e8 100644 --- a/components/avatar/index.ts +++ b/components/avatar/index.ts @@ -1,4 +1,6 @@ import Avatar from './Avatar'; import { withInstall } from '../_util/type'; +export { AvatarProps } from './Avatar'; + export default withInstall(Avatar);