Clear existing text content before inserting children (#8331)

Fixes a bug when updating from a single text child (or
dangerouslySetInnerHTML) to regular children, where the previous
text content never gets deleted.
This commit is contained in:
Andrew Clark
2016-11-30 18:08:41 -08:00
committed by GitHub
parent e7eed5874f
commit aa6279c41a
9 changed files with 96 additions and 18 deletions
-3
View File
@@ -32,8 +32,6 @@ src/renderers/dom/shared/__tests__/ReactDOM-test.js
* throws in render() if the update callback is not a function
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
* should empty element when removing innerHTML
* should transition from innerHTML to children in nested el
* should warn for children on void elements
* should report component containing invalid styles
* should clean up input value tracking
@@ -104,7 +102,6 @@ src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js
* throws when rendering null at the top level
src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js
* should correctly handle all possible children for render and update
* should reorder keyed text nodes
src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
+3
View File
@@ -601,8 +601,10 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
* should update arbitrary attributes for tags containing dashes
* should clear all the styles when removing `style`
* should update styles when `style` changes from null to object
* should empty element when removing innerHTML
* should transition from string content to innerHTML
* should transition from innerHTML to string content
* should transition from innerHTML to children in nested el
* should transition from children to innerHTML in nested el
* should not incur unnecessary DOM mutations for attributes
* should not incur unnecessary DOM mutations for string properties
@@ -1397,6 +1399,7 @@ src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js
* should insert non-empty children in middle where nulls were
src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js
* should correctly handle all possible children for render and update
* should throw if rendering both HTML and children
* should render between nested components and inline children
+16
View File
@@ -117,6 +117,22 @@ var DOMRenderer = ReactFiberReconciler({
updateProperties(domElement, type, oldProps, newProps, root);
},
shouldSetTextContent(props : Props) : boolean {
return (
typeof props.children === 'string' ||
typeof props.children === 'number' ||
(
typeof props.dangerouslySetInnerHTML === 'object' &&
props.dangerouslySetInnerHTML !== null &&
typeof props.dangerouslySetInnerHTML.__html === 'string'
)
);
},
resetTextContent(domElement : Instance) : void {
domElement.textContent = '';
},
createTextInstance(text : string, internalInstanceHandle : Object) : TextInstance {
var textNode : TextInstance = document.createTextNode(text);
precacheFiberNode(internalInstanceHandle, textNode);
+9
View File
@@ -68,6 +68,15 @@ var NoopRenderer = ReactFiberReconciler({
instance.prop = newProps.prop;
},
shouldSetTextContent(props : Props) : boolean {
return (
typeof props.children === 'string' ||
typeof props.children === 'number'
);
},
resetTextContent(instance : Instance) : void {},
createTextInstance(text : string) : TextInstance {
var inst = { text : text, id: instanceCounter++ };
// Hide from unit tests
@@ -53,6 +53,7 @@ var {
} = require('ReactPriorityLevel');
var {
Placement,
ContentReset,
} = require('ReactTypeOfSideEffect');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactFiberClassComponent = require('ReactFiberClassComponent');
@@ -210,14 +211,27 @@ module.exports = function<T, P, I, TI, C>(
}
function updateHostComponent(current, workInProgress) {
const nextProps = workInProgress.pendingProps;
const prevProps = current ? current.memoizedProps : null;
let nextChildren = workInProgress.pendingProps.children;
if (typeof nextChildren === 'string' || typeof nextChildren === 'number') {
const isDirectTextChild = config.shouldSetTextContent(nextProps);
if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also have access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps && (
config.shouldSetTextContent(prevProps) ||
prevProps.children === null ||
typeof prevProps.children === 'undefined' ||
typeof prevProps.children === 'boolean'
)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.effectTag |= ContentReset;
}
if (workInProgress.pendingProps.hidden &&
workInProgress.pendingWorkPriority !== OffscreenPriority) {
// If this host component is hidden, we can bail out on the children.
@@ -30,6 +30,7 @@ var {
Placement,
Update,
Callback,
ContentReset,
} = require('ReactTypeOfSideEffect');
module.exports = function<T, P, I, TI, C>(
@@ -38,6 +39,7 @@ module.exports = function<T, P, I, TI, C>(
) {
const commitUpdate = config.commitUpdate;
const resetTextContent = config.resetTextContent;
const commitTextUpdate = config.commitTextUpdate;
const appendChild = config.appendChild;
@@ -83,6 +85,17 @@ module.exports = function<T, P, I, TI, C>(
throw new Error('Expected to find a host parent.');
}
function getHostParentFiber(fiber : Fiber) : Fiber {
let parent = fiber.return;
while (parent) {
if (isHostParent(parent)) {
return parent;
}
parent = parent.return;
}
throw new Error('Expected to find a host parent.');
}
function isHostParent(fiber : Fiber) : boolean {
return (
fiber.tag === HostComponent ||
@@ -133,12 +146,29 @@ module.exports = function<T, P, I, TI, C>(
}
function commitPlacement(finishedWork : Fiber) : void {
// Clear effect from effect tag before any errors can be thrown, so that
// we don't attempt to do this again
finishedWork.effectTag &= ~Placement;
// Recursively insert all host nodes into the parent.
const parent = getHostParent(finishedWork);
const parentFiber = getHostParentFiber(finishedWork);
let parent;
switch (parentFiber.tag) {
case HostComponent:
parent = parentFiber.stateNode;
break;
case HostContainer:
parent = parentFiber.stateNode.containerInfo;
break;
case Portal:
parent = parentFiber.stateNode.containerInfo;
break;
default:
throw new Error('Invalid host parent fiber.');
}
if (parentFiber.effectTag & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.effectTag &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need recurse down its
// children to find all the terminal nodes.
@@ -49,6 +49,9 @@ export type HostConfig<T, P, I, TI, C> = {
prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean,
commitUpdate(instance : I, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void,
shouldSetTextContent(props : P) : boolean,
resetTextContent(instance : I) : void,
createTextInstance(text : string, internalInstanceHandle : OpaqueNode) : TI,
commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void,
@@ -40,6 +40,7 @@ var {
Update,
PlacementAndUpdate,
Deletion,
ContentReset,
Callback,
Err,
} = require('ReactTypeOfSideEffect');
@@ -168,11 +169,15 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
pass1: while (true) {
try {
while (effectfulFiber) {
if (effectfulFiber.effectTag & ContentReset) {
config.resetTextContent(effectfulFiber.stateNode);
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every
// possible bitmap value, we remove the secondary effects from the
// effect tag and switch on that value.
let primaryEffectTag = effectfulFiber.effectTag & ~(Callback | Err);
let primaryEffectTag = effectfulFiber.effectTag & ~(Callback | Err | ContentReset);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(effectfulFiber);
@@ -12,14 +12,15 @@
'use strict';
export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 16;
export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 16 | 32;
module.exports = {
NoEffect: 0, // 0b00000
Placement: 1, // 0b00001
Update: 2, // 0b00010
PlacementAndUpdate: 3, // 0b00011
Deletion: 4, // 0b00100
Callback: 8, // 0b01000
Err: 16, // 0b10000
NoEffect: 0, // 0b000000
Placement: 1, // 0b000001
Update: 2, // 0b000010
PlacementAndUpdate: 3, // 0b000011
Deletion: 4, // 0b000100
ContentReset: 8, // 0b001000
Callback: 16, // 0b010000
Err: 32, // 0b100000
};