307 lines
8.9 KiB
JavaScript
307 lines
8.9 KiB
JavaScript
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
* @providesModule ReactNativeAttributePayload
|
|
* @flow
|
|
*/
|
|
'use strict';
|
|
|
|
var Platform = require('Platform');
|
|
|
|
var deepDiffer = require('deepDiffer');
|
|
var styleDiffer = require('styleDiffer');
|
|
var flattenStyle = require('flattenStyle');
|
|
|
|
type AttributeDiffer = (prevProp : mixed, nextProp : mixed) => boolean;
|
|
type AttributePreprocessor = (nextProp: mixed) => mixed;
|
|
|
|
type CustomAttributeConfiguration =
|
|
{ diff : AttributeDiffer, process : AttributePreprocessor } |
|
|
{ diff : AttributeDiffer } |
|
|
{ process : AttributePreprocessor };
|
|
|
|
type AttributeConfiguration =
|
|
{ [key : string]: (
|
|
CustomAttributeConfiguration | AttributeConfiguration /*| boolean*/
|
|
) };
|
|
|
|
function translateKey(propKey : string) : string {
|
|
if (propKey === 'transform') {
|
|
// We currently special case the key for `transform`. iOS uses the
|
|
// transformMatrix name and Android uses the decomposedMatrix name.
|
|
// TODO: We could unify these names and just use the name `transform`
|
|
// all the time. Just need to update the native side.
|
|
if (Platform.OS === 'android') {
|
|
return 'decomposedMatrix';
|
|
} else {
|
|
return 'transformMatrix';
|
|
}
|
|
}
|
|
return propKey;
|
|
}
|
|
|
|
function defaultDiffer(prevProp: mixed, nextProp: mixed) : boolean {
|
|
if (typeof nextProp !== 'object' || nextProp === null) {
|
|
// Scalars have already been checked for equality
|
|
return true;
|
|
} else {
|
|
// For objects and arrays, the default diffing algorithm is a deep compare
|
|
return deepDiffer(prevProp, nextProp);
|
|
}
|
|
}
|
|
|
|
function diffNestedProperty(
|
|
updatePayload :? Object,
|
|
prevProp, // inferred
|
|
nextProp, // inferred
|
|
validAttributes : AttributeConfiguration
|
|
) : ?Object {
|
|
// The style property is a deeply nested element which includes numbers
|
|
// to represent static objects. Most of the time, it doesn't change across
|
|
// renders, so it's faster to spend the time checking if it is different
|
|
// before actually doing the expensive flattening operation in order to
|
|
// compute the diff.
|
|
if (!styleDiffer(prevProp, nextProp)) {
|
|
return updatePayload;
|
|
}
|
|
|
|
// TODO: Walk both props in parallel instead of flattening.
|
|
|
|
var previousFlattenedStyle = flattenStyle(prevProp);
|
|
var nextFlattenedStyle = flattenStyle(nextProp);
|
|
|
|
if (!previousFlattenedStyle || !nextFlattenedStyle) {
|
|
if (nextFlattenedStyle) {
|
|
return addProperties(
|
|
updatePayload,
|
|
nextFlattenedStyle,
|
|
validAttributes
|
|
);
|
|
}
|
|
if (previousFlattenedStyle) {
|
|
return clearProperties(
|
|
updatePayload,
|
|
previousFlattenedStyle,
|
|
validAttributes
|
|
);
|
|
}
|
|
return updatePayload;
|
|
}
|
|
|
|
// recurse
|
|
return diffProperties(
|
|
updatePayload,
|
|
previousFlattenedStyle,
|
|
nextFlattenedStyle,
|
|
validAttributes
|
|
);
|
|
}
|
|
|
|
/**
|
|
* addNestedProperty takes a single set of props and valid attribute
|
|
* attribute configurations. It processes each prop and adds it to the
|
|
* updatePayload.
|
|
*/
|
|
/*
|
|
function addNestedProperty(
|
|
updatePayload :? Object,
|
|
nextProp : Object,
|
|
validAttributes : AttributeConfiguration
|
|
) {
|
|
// TODO: Fast path
|
|
return diffNestedProperty(updatePayload, {}, nextProp, validAttributes);
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* clearNestedProperty takes a single set of props and valid attributes. It
|
|
* adds a null sentinel to the updatePayload, for each prop key.
|
|
*/
|
|
function clearNestedProperty(
|
|
updatePayload :? Object,
|
|
prevProp : Object,
|
|
validAttributes : AttributeConfiguration
|
|
) : ?Object {
|
|
// TODO: Fast path
|
|
return diffNestedProperty(updatePayload, prevProp, {}, validAttributes);
|
|
}
|
|
|
|
/**
|
|
* diffProperties takes two sets of props and a set of valid attributes
|
|
* and write to updatePayload the values that changed or were deleted.
|
|
* If no updatePayload is provided, a new one is created and returned if
|
|
* anything changed.
|
|
*/
|
|
function diffProperties(
|
|
updatePayload : ?Object,
|
|
prevProps : Object,
|
|
nextProps : Object,
|
|
validAttributes : AttributeConfiguration
|
|
): ?Object {
|
|
var attributeConfig : ?(CustomAttributeConfiguration | AttributeConfiguration);
|
|
var nextProp;
|
|
var prevProp;
|
|
|
|
for (var propKey in nextProps) {
|
|
attributeConfig = validAttributes[propKey];
|
|
if (!attributeConfig) {
|
|
continue; // not a valid native prop
|
|
}
|
|
|
|
var altKey = translateKey(propKey);
|
|
if (!validAttributes[altKey]) {
|
|
// If there is no config for the alternative, bail out. Helps ART.
|
|
altKey = propKey;
|
|
}
|
|
|
|
if (updatePayload && updatePayload[altKey] !== undefined) {
|
|
// If we're in a nested attribute set, we may have set this property
|
|
// already. If so, bail out. The previous update is what counts.
|
|
continue;
|
|
}
|
|
prevProp = prevProps[propKey];
|
|
nextProp = nextProps[propKey];
|
|
|
|
// functions are converted to booleans as markers that the associated
|
|
// events should be sent from native.
|
|
if (typeof nextProp === 'function') {
|
|
nextProp = true;
|
|
// If nextProp is not a function, then don't bother changing prevProp
|
|
// since nextProp will win and go into the updatePayload regardless.
|
|
if (typeof prevProp === 'function') {
|
|
prevProp = true;
|
|
}
|
|
}
|
|
|
|
if (prevProp === nextProp) {
|
|
continue; // nothing changed
|
|
}
|
|
|
|
// Pattern match on: attributeConfig
|
|
if (typeof attributeConfig !== 'object') {
|
|
// case: !Object is the default case
|
|
if (defaultDiffer(prevProp, nextProp)) {
|
|
// a normal leaf has changed
|
|
(updatePayload || (updatePayload = {}))[altKey] = nextProp;
|
|
}
|
|
} else if (typeof attributeConfig.diff === 'function' ||
|
|
typeof attributeConfig.process === 'function') {
|
|
// case: CustomAttributeConfiguration
|
|
var shouldUpdate = prevProp === undefined || (
|
|
typeof attributeConfig.diff === 'function' ?
|
|
attributeConfig.diff(prevProp, nextProp) :
|
|
defaultDiffer(prevProp, nextProp)
|
|
);
|
|
if (shouldUpdate) {
|
|
var nextValue = typeof attributeConfig.process === 'function' ?
|
|
attributeConfig.process(nextProp) :
|
|
nextProp;
|
|
(updatePayload || (updatePayload = {}))[altKey] = nextValue;
|
|
}
|
|
} else {
|
|
// default: fallthrough case when nested properties are defined
|
|
updatePayload = diffNestedProperty(
|
|
updatePayload,
|
|
prevProp,
|
|
nextProp,
|
|
attributeConfig
|
|
);
|
|
}
|
|
}
|
|
|
|
// Also iterate through all the previous props to catch any that have been
|
|
// removed and make sure native gets the signal so it can reset them to the
|
|
// default.
|
|
for (var propKey in prevProps) {
|
|
if (nextProps[propKey] !== undefined) {
|
|
continue; // we've already covered this key in the previous pass
|
|
}
|
|
attributeConfig = validAttributes[propKey];
|
|
if (!attributeConfig) {
|
|
continue; // not a valid native prop
|
|
}
|
|
|
|
prevProp = prevProps[propKey];
|
|
if (prevProp === undefined) {
|
|
continue; // was already empty anyway
|
|
}
|
|
// Pattern match on: attributeConfig
|
|
if (typeof attributeConfig !== 'object' ||
|
|
typeof attributeConfig.diff === 'function' ||
|
|
typeof attributeConfig.process === 'function') {
|
|
|
|
// case: CustomAttributeConfiguration | !Object
|
|
// Flag the leaf property for removal by sending a sentinel.
|
|
(updatePayload || (updatePayload = {}))[translateKey(propKey)] = null;
|
|
} else {
|
|
// default:
|
|
// This is a nested attribute configuration where all the properties
|
|
// were removed so we need to go through and clear out all of them.
|
|
updatePayload = clearNestedProperty(
|
|
updatePayload,
|
|
prevProp,
|
|
attributeConfig
|
|
);
|
|
}
|
|
}
|
|
return updatePayload;
|
|
}
|
|
|
|
/**
|
|
* addProperties adds all the valid props to the payload after being processed.
|
|
*/
|
|
function addProperties(
|
|
updatePayload : ?Object,
|
|
props : Object,
|
|
validAttributes : AttributeConfiguration
|
|
) : ?Object {
|
|
return diffProperties(updatePayload, {}, props, validAttributes);
|
|
}
|
|
|
|
/**
|
|
* clearProperties clears all the previous props by adding a null sentinel
|
|
* to the payload for each valid key.
|
|
*/
|
|
function clearProperties(
|
|
updatePayload : ?Object,
|
|
prevProps : Object,
|
|
validAttributes : AttributeConfiguration
|
|
) :? Object {
|
|
return diffProperties(updatePayload, prevProps, {}, validAttributes);
|
|
}
|
|
|
|
var ReactNativeAttributePayload = {
|
|
|
|
create: function(
|
|
props : Object,
|
|
validAttributes : AttributeConfiguration
|
|
) : ?Object {
|
|
return addProperties(
|
|
null, // updatePayload
|
|
props,
|
|
validAttributes
|
|
);
|
|
},
|
|
|
|
diff: function(
|
|
prevProps : Object,
|
|
nextProps : Object,
|
|
validAttributes : AttributeConfiguration
|
|
) : ?Object {
|
|
return diffProperties(
|
|
null, // updatePayload
|
|
prevProps,
|
|
nextProps,
|
|
validAttributes
|
|
);
|
|
}
|
|
|
|
};
|
|
|
|
module.exports = ReactNativeAttributePayload;
|