diff --git a/packages/hippy-react/src/components/list-view.tsx b/packages/hippy-react/src/components/list-view.tsx index 1705cd49c..9e064d722 100644 --- a/packages/hippy-react/src/components/list-view.tsx +++ b/packages/hippy-react/src/components/list-view.tsx @@ -425,7 +425,9 @@ class ListView extends React.Component { } const appearEventList = [onAppear, onDisappear, onWillAppear, onWillDisappear]; nativeProps.exposureEventEnabled = appearEventList.some(func => typeof func === 'function'); - nativeProps.numberOfRows = itemList.length; + if (Device.platform.OS === 'ios') { + nativeProps.numberOfRows = itemList.length; + } (nativeProps as ListViewProps).initialListSize = initialListSize; (nativeProps as ListViewProps).style = { overflow: 'scroll', diff --git a/packages/hippy-react/src/dom/element-node.ts b/packages/hippy-react/src/dom/element-node.ts index d0d300381..ff4b1b16f 100644 --- a/packages/hippy-react/src/dom/element-node.ts +++ b/packages/hippy-react/src/dom/element-node.ts @@ -24,7 +24,6 @@ import Animation from '../modules/animation'; import AnimationSet from '../modules/animation-set'; import { colorParse, colorArrayParse, Color } from '../color'; import { updateChild, updateWithChildren, endBatch } from '../renderer/render'; -import { Device } from '../native'; import { unicodeToChar, tryConvertNumber, @@ -52,6 +51,10 @@ interface DirectionMap { [propName: string]: string; } +interface AttributeUpdateOption { + isBatchUpdate?: boolean +} + interface DegreeUnit { [propName: string]: string; } @@ -215,7 +218,7 @@ function parseTextShadowOffset(styleKey: string, styleValue: number, style: any) * get final key sent to native * @param key */ -function getEventPropKey(key: string) { +function getEventName(key: string) { if (isCaptureEvent(key)) { key = key.replace('Capture', ''); } @@ -230,6 +233,7 @@ class ElementNode extends ViewNode { public id = ''; public style: HippyTypes.Style = {}; public attributes: Attributes = {}; + public events: object = {}; public constructor(tagName: string) { super(); @@ -292,7 +296,6 @@ class ElementNode extends ViewNode { }; }); } else if (typeof style === 'object' && style) { - // TODO: Merge transform mergedStyles = { ...mergedStyles, ...style, @@ -367,8 +370,126 @@ class ElementNode extends ViewNode { }); } - /* istanbul ignore next */ - public setAttribute(key: string, value: any) { + // set node attributes in a batch + public setAttributes(attributeQueue: [][] = []) { + if (Array.isArray(attributeQueue) && attributeQueue.length > 0) { + attributeQueue.forEach((attributeList) => { + if (Array.isArray(attributeList)) { + const [key, value]: (string | any)[] = attributeList; + this.setAttribute(key, value, { isBatchUpdate: true }); + } + }); + updateChild(this); + } + } + + parseAnimationStyleProp(style) { + // Set useAnimation if animation exist in style + let useAnimation = false; + Object.keys(style).some((declare) => { + const styleVal = (style as any)[declare]; + if (styleVal && Array.isArray(styleVal) && declare === 'transform') { + for (let i = 0; i < styleVal.length; i += 1) { + const transform = styleVal[i]; + /* eslint-disable-next-line no-restricted-syntax, guard-for-in */ + for (const transformKey in (transform as any)) { + const transformValue = (transform as any)[transformKey]; + if (typeof transformValue === 'object' + && transformValue !== null + && Number.isInteger(transformValue.animationId)) { + useAnimation = true; + return true; + } + } + } + } + if (typeof styleVal === 'object' + && styleVal !== null + && Number.isInteger((styleVal as Animation).animationId)) { + useAnimation = true; + return true; + } + return false; + }); + if (useAnimation) { + this.attributes.useAnimation = true; + } else if (typeof this.attributes.useAnimation === 'boolean') { + this.attributes.useAnimation = undefined; + } + } + + parseAttributeProp(key: string, value: any): boolean { + const caseList = [ + { + match: () => ['id'].indexOf(key) >= 0, + action: () => { + if (value === this.id) { + return true; + } + this.id = value; + // update current node and child nodes + updateWithChildren(this); + return true; + }, + }, + { + match: () => ['value', 'defaultValue', 'placeholder'].indexOf(key) >= 0, + action: () => { + this.attributes[key] = unicodeToChar(value); + return false; + }, + }, + { + match: () => ['text'].indexOf(key) >= 0, + action: () => { + this.attributes[key] = value; + return false; + }, + }, + { + match: () => ['style'].indexOf(key) >= 0, + action: () => { + if (typeof value !== 'object' || value === undefined || value === null) { + return true; + } + this.setStyleAttribute(value); + return false; + }, + }, + { + match: () => true, + action: () => { + if (typeof value === 'function') { + const eventName = getEventName(key); + this.attributes[eventName] = true; + this.events[`${eventName}`] = true; + } else { + this.attributes[key] = value; + const eventName = getEventName(key); + if (this.events[`${eventName}`] === true + && typeof value !== 'function') { + this.attributes[eventName] = false; + delete this.events[`${eventName}`]; + } + } + return false; + }, + }, + ]; + + let isNeedReturn = false; + caseList.some((conditionObj: { match: Function, action: Function }) => { + if (conditionObj.match()) { + isNeedReturn = conditionObj.action(); + return true; + } + return false; + }); + return isNeedReturn; + } + + // set node attribute + public setAttribute(key: string, value: any, options: AttributeUpdateOption = {}) { try { // detect expandable attrs for boolean values // See https://vuejs.org/v2/guide/components-props.html#Passing-a-Boolean @@ -376,119 +497,13 @@ class ElementNode extends ViewNode { value = true; } if (key === undefined) { - updateChild(this); + !options.isBatchUpdate && updateChild(this); return; } - - const caseList = [ - { - match: () => ['id'].indexOf(key) >= 0, - action: () => { - if (value === this.id) { - return true; - } - this.id = value; - // update current node and child nodes - updateWithChildren(this); - return true; - }, - }, - { - match: () => ['value', 'defaultValue', 'placeholder'].indexOf(key) >= 0, - action: () => { - this.attributes[key] = unicodeToChar(value); - return false; - }, - }, - { - match: () => ['text'].indexOf(key) >= 0, - action: () => { - this.attributes[key] = value; - return false; - }, - }, - { - match: () => ['numberOfRows'].indexOf(key) >= 0, - action: () => { - this.attributes[key] = value; - return Device.platform.OS !== 'ios'; - }, - }, - { - match: () => ['style'].indexOf(key) >= 0, - action: () => { - if (typeof value !== 'object' || value === undefined || value === null) { - return true; - } - this.setStyleAttribute(value); - return false; - }, - }, - { - match: () => true, - action: () => { - if (typeof value === 'function') { - const processedKey = getEventPropKey(key); - this.attributes[processedKey] = true; - this.attributes[`__bind__${processedKey}`] = true; - } else { - this.attributes[key] = value; - const processedKey = getEventPropKey(key); - if (this.attributes[`__bind__${processedKey}`] === true - && typeof value !== 'function') { - this.attributes[processedKey] = false; - this.attributes[`__bind__${processedKey}`] = false; - } - } - return false; - }, - }, - ]; - - let isNeedReturn = false; - caseList.some((conditionObj: { match: Function, action: Function }) => { - if (conditionObj.match()) { - isNeedReturn = conditionObj.action(); - return true; - } - return false; - }); + const isNeedReturn = this.parseAttributeProp(key, value); if (isNeedReturn) return; - - // Set useAnimation if animation exist in style - let useAnimation = false; - Object.keys(this.style).some((declare) => { - const style = (this.style as any)[declare]; - if (style && Array.isArray(style) && declare === 'transform') { - for (let i = 0; i < style.length; i += 1) { - const transform = style[i]; - /* eslint-disable-next-line no-restricted-syntax, guard-for-in */ - for (const transformKey in (transform as any)) { - const transformValue = (transform as any)[transformKey]; - if (typeof transformValue === 'object' - && transformValue !== null - && Number.isInteger(transformValue.animationId)) { - useAnimation = true; - return transformValue; - } - } - } - } - if (typeof style === 'object' - && style !== null - && Number.isInteger((style as Animation).animationId)) { - useAnimation = true; - return style; - } - return false; - }); - if (useAnimation) { - this.attributes.useAnimation = true; - } else if (typeof this.attributes.useAnimation === 'boolean') { - this.attributes.useAnimation = undefined; - } - - updateChild(this); + this.parseAnimationStyleProp(this.style); + !options.isBatchUpdate && updateChild(this); } catch (e) { // noop } diff --git a/packages/hippy-react/src/renderer/host-configs.ts b/packages/hippy-react/src/renderer/host-configs.ts index 8116e672d..e570b2766 100644 --- a/packages/hippy-react/src/renderer/host-configs.ts +++ b/packages/hippy-react/src/renderer/host-configs.ts @@ -58,9 +58,10 @@ function commitUpdate( workInProgress: any, ): void { preCacheFiberNode(workInProgress, instance.nodeId); - const updatePayloadPropList: string[] = Object.keys(updatePayload); - if (updatePayloadPropList.length === 0) return; - updatePayloadPropList.forEach(propKey => instance.setAttribute(propKey, updatePayload[propKey])); + const updatePayloadList = Object.keys(updatePayload || {}); + if (updatePayloadList.length === 0) return; + const attributeQueue = updatePayloadList.map(propKey => [propKey, updatePayload[propKey]]); + instance.setAttributes(attributeQueue); } // this is the hook when commitMutationEffects begin @@ -79,49 +80,44 @@ function prepareUpdate( oldProps: Props, newProps: Props, ): UpdatePayload { - const updatePayload: { - [key: string]: any; - } = {}; - let hasFunctionProp = false; - Object.keys(newProps).forEach((key: string) => { + const updatePayload: { [key: string]: any; } = {}; + Object.keys(oldProps).forEach((key) => { + const oldPropValue = oldProps[key]; + const newPropValue = newProps[key]; + if ( + (oldPropValue !== undefined && oldPropValue !== null) + && (newPropValue === undefined || newPropValue === null) + ) { + // if oldPros existed and newPros removed(undefined/null), indicated this prop deleted. + updatePayload[key] = newPropValue; + } + }); + Object.keys(newProps).forEach((key) => { const oldPropValue = oldProps[key]; const newPropValue = newProps[key]; switch (key) { case 'children': { if (oldPropValue !== newPropValue - && (typeof newPropValue === 'number' - || typeof newPropValue === 'string' - )) { + && (typeof newPropValue === 'number' + || typeof newPropValue === 'string' + )) { updatePayload[key] = newPropValue; } break; } default: { - if (typeof oldPropValue === 'function' - && typeof newPropValue === 'function' - && !isEqual(oldPropValue, newPropValue) - ) { - hasFunctionProp = true; - } else if (!isEqual(oldPropValue, newPropValue)) { + if ( + (newPropValue !== undefined && newPropValue !== null) + && (oldPropValue === undefined || oldPropValue === null)) { + updatePayload[key] = newPropValue; + } else if (typeof newPropValue !== 'function' && !isEqual(oldPropValue, newPropValue)) { + // if newProps existed and oldProps removed(undefined/null) + // or newProps only updated if not function property updatePayload[key] = newPropValue; } } } }); - const isUpdatePayloadEmpty = Object.keys(updatePayload).length === 0; - if (isUpdatePayloadEmpty && hasFunctionProp) { - /** - * if updatePayload only has function property, - * return empty object to trigger commitUpdate for updating fiberNode Cache - */ - return {}; - } - if (isUpdatePayloadEmpty) { - /** - * prepareUpdate returning null would not trigger commitUpdate - */ - return null; - } return updatePayload; } diff --git a/packages/hippy-react/src/renderer/render.ts b/packages/hippy-react/src/renderer/render.ts index 0bd1ab533..160be26b4 100644 --- a/packages/hippy-react/src/renderer/render.ts +++ b/packages/hippy-react/src/renderer/render.ts @@ -153,12 +153,6 @@ function getTargetNodeAttributes(targetNode: Element) { id: targetNode.id, ...targetNodeAttributes, }; - // delete special __bind__event attribute, which is used in C DOM - Object.keys(attributes).forEach((key) => { - if (key.indexOf('__bind__') === 0 && typeof attributes[key] === 'boolean') { - delete attributes[key]; - } - }); delete attributes.text; delete attributes.value; return attributes; @@ -269,7 +263,6 @@ function insertChild(parentNode: ViewNode, childNode: ViewNode, atIndex = -1) { type: NODE_OPERATION_TYPES.createNode, nodes: translated, }); - // endBatch(); } } @@ -289,7 +282,6 @@ function removeChild(parentNode: ViewNode, childNode: ViewNode | null, index: nu type: NODE_OPERATION_TYPES.deleteNode, nodes: deleteNodeIds, }); - // endBatch(); } function updateChild(parentNode: Element) { @@ -304,7 +296,6 @@ function updateChild(parentNode: Element) { nodes: [translated], }); } - // endBatch(); } function updateWithChildren(parentNode: ViewNode) { @@ -317,7 +308,6 @@ function updateWithChildren(parentNode: ViewNode) { type: NODE_OPERATION_TYPES.updateNode, nodes: translated, }); - // endBatch(); } export { diff --git a/packages/hippy-vue/src/renderer/element-node.js b/packages/hippy-vue/src/renderer/element-node.js index 12c62517f..7184c8a70 100644 --- a/packages/hippy-vue/src/renderer/element-node.js +++ b/packages/hippy-vue/src/renderer/element-node.js @@ -319,6 +319,9 @@ class ElementNode extends ViewNode { default: this.attributes[key] = value; } + if (typeof this.filterAttribute === 'function') { + this.filterAttribute(this.attributes); + } updateChild(this); } catch (err) { // Throw error in development mode diff --git a/packages/hippy-vue/src/renderer/native/__tests__/index.test.js b/packages/hippy-vue/src/renderer/native/__tests__/index.test.js index 384dd9710..f5158c1e7 100644 --- a/packages/hippy-vue/src/renderer/native/__tests__/index.test.js +++ b/packages/hippy-vue/src/renderer/native/__tests__/index.test.js @@ -245,7 +245,6 @@ test('renderToNative test with children --debug mode', (t) => { }, multiline: true, onTyping: true, - __bind__onTyping: true, keyboardType: 'url', numberOfLines: 10, underlineColorAndroid: 0, @@ -362,7 +361,6 @@ test('renderToNative test with children --production mode', (t) => { props: { multiline: true, onTyping: true, - __bind__onTyping: true, keyboardType: 'url', numberOfLines: 10, underlineColorAndroid: 0, diff --git a/packages/hippy-vue/src/renderer/native/index.js b/packages/hippy-vue/src/renderer/native/index.js index f06e44e53..df5111771 100644 --- a/packages/hippy-vue/src/renderer/native/index.js +++ b/packages/hippy-vue/src/renderer/native/index.js @@ -305,12 +305,6 @@ function getTargetNodeAttributes(targetNode) { class: classInfo, ...targetNodeAttributes, }; - // delete special __bind__event attribute, which is used in C DOM - Object.keys(attributes).forEach((key) => { - if (key.indexOf('__bind__') === 0 && typeof attributes[key] === 'boolean') { - delete attributes[key]; - } - }); delete attributes.text; delete attributes.value; return attributes; @@ -355,18 +349,15 @@ function renderToNative(rootViewId, targetNode) { const nativeEventName = eventNamesMap[vueEventName]; if (nativeEventName) { events[nativeEventName] = true; - events[`__bind__${nativeEventName}`] = true; } else { const name = `on${capitalizeFirstLetter(vueEventName)}`; events[name] = true; - events[`__bind__${name}`] = true; } }); } else { vueEventNames.forEach((vueEventName) => { const name = `on${capitalizeFirstLetter(vueEventName)}`; events[name] = true; - events[`__bind__${name}`] = true; }); } }