Skip to content

Commit

Permalink
[INTERNAL] RenderManager: Experimental DOM patching instead of
Browse files Browse the repository at this point in the history
re-rendering.

Motivation: Updating views by inserting freshly rendered HTML into the
DOM is like knocking your house down and rebuilding it when all you
really needed to do was clean your windows.

- As HTML output become larger, the amount of stuff that needs to be
destroyed with each re-render grows – and that means extra work for the
garbage collector. 
- If something causes re-rendering of the input fields during keystroke,
focus and cursor position or text selection needs to be retained which
is not possible for mobile devices since soft keyboard will be closed
and reopened again.
- Because of the re-rendering click event can be ignored.(
http://jsbin.com/givikilure/1/edit?html,js,output )
- Having to worry about the performance headaches. (Should chancing
title property re-render the table?)
- Code size of the DOM related setters.

- sap-ui-xx-domPatching=true configuration is added to test this
behavior without breaking the applications.

Change-Id: I9ddd348295276ad657bd7292b27b364b12a35044
  • Loading branch information
aborjinik committed Jun 29, 2015
1 parent c82dbbb commit 26309b5
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 9 deletions.
4 changes: 2 additions & 2 deletions src/sap.m/test/sap/m/qunit/InputBase.qunit.html
Original file line number Diff line number Diff line change
Expand Up @@ -1481,7 +1481,7 @@
jQuery(oInput.getFocusDomRef()).cursorPos(sTestCurPos);

// invalidate the control after dom value changes
oInput.setWidth("100px");
oInput.setPlaceholder("placeholder");
sap.ui.getCore().applyChanges();

// assertions
Expand Down Expand Up @@ -1518,7 +1518,7 @@
jQuery(oInput.getFocusDomRef()).cursorPos(sTestCurPos);

// invalidate the control after dom and property value changes
oInput.setWidth("100px");
oInput.setPlaceholder("placeholder");
sap.ui.getCore().applyChanges();

// assertion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
}, 1);
});

asyncTest("Execution of a Handler after re-rendering", 2, function() {
asyncTest("Execution of a Handler after re-rendering", function() {
oBtn2.getDomRef().focus();

// register handler first
Expand All @@ -143,11 +143,11 @@
// re-render
var oldBtn = oBtn.getDomRef();
oBtn.invalidate();
sap.ui.getCore().applyChanges();
var newBtn = oBtn.getDomRef();
ok(oldBtn != newBtn, "Button should be re-rendered and replaced by a new DOM element");
sap.ui.getCore().applyChanges();
var newBtn = oBtn.getDomRef();
//ok(oldBtn != newBtn, "Button should be re-rendered and replaced by a new DOM element");

// now trigger the event
// now trigger the event
newBtn.focus();
oBtn2.getDomRef().focus();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@
equal(jQuery(oSearchField.getFocusDomRef()).val(), bNoChange ? "" : "SapUi5", "SearchField' value after user action (HTML)");
equal(oSearchField.getValue(), bNoChange ? "" : "SapUi5", "SearchField' value after user action (Property)");
checkFocus(oSearchField.getFocusDomRef().id, "SearchField has "+(bReadOnly ? "no " : "")+"focus after user action", !bReadOnly);

// focus is removed for the next test
document.activeElement.blur();
}, 10);
};

Expand Down
120 changes: 120 additions & 0 deletions src/sap.ui.core/src/jquery.sap.dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,126 @@ sap.ui.define(['jquery.sap.global', 'sap/ui/Device'],
return removeFromAttributeList.call(this, "aria-describedby", sId);
};


/**
* This method try to patch two HTML elements according to changed attributes.
*
* @param {HTMLElement} oOldDom existing element to be patched
* @param {HTMLElement} oNewDom is the new node to patch old dom
* @return {Boolean} true when patch is applied correctly or false when nodes are replaced.
* @author SAP SE
* @since 1.30.0
* @private
*/
function patchDOM(oOldDom, oNewDom) {

// start checking with most common use case and backwards compatible
if (oOldDom.childElementCount != oNewDom.childElementCount ||
oOldDom.tagName != oNewDom.tagName) {
oOldDom.parentNode.replaceChild(oNewDom, oOldDom);
return false;
}

// go with native... if nodes are equal there is nothing to do
// http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-isEqualNode
if (oOldDom.isEqualNode(oNewDom)) {
return true;
}

// remove outdated attributes from old dom
var aOldAttributes = oOldDom.attributes;
for (var i = 0, ii = aOldAttributes.length; i < ii; i++) {
var sAttrName = aOldAttributes[i].name;
if (oNewDom.getAttribute(sAttrName) === null) {
oOldDom.removeAttribute(sAttrName);
ii = ii - 1;
i = i - 1;
}
}

// patch new or changed attributes to the old dom
var aNewAttributes = oNewDom.attributes;
for (var i = 0, ii = aNewAttributes.length; i < ii; i++) {
var sAttrName = aNewAttributes[i].name,
vOldAttrValue = oOldDom.getAttribute(sAttrName),
vNewAttrValue = oNewDom.getAttribute(sAttrName);

if (vOldAttrValue === null || vOldAttrValue !== vNewAttrValue) {
oOldDom.setAttribute(sAttrName, vNewAttrValue);
}
}

// check whether more child nodes to continue or not
var iNewChildNodesCount = oNewDom.childNodes.length;
if (!iNewChildNodesCount && !oOldDom.hasChildNodes()) {
return true;
}

// maybe no more child elements
if (!oNewDom.childElementCount) {
// but child nodes(e.g. Text Nodes) still needs to be replaced
if (!iNewChildNodesCount) {
// new dom does not have any child node, so we can clean the old one
oOldDom.textContent = "";
} else if (iNewChildNodesCount == 1 && oNewDom.firstChild.nodeType == 3 /* TEXT_NODE */) {
// update the text content for the first text node
oOldDom.textContent = oNewDom.textContent;
} else {
// in case of comments or other node types are used
oOldDom.innerHTML = oNewDom.innerHTML;
}
return true;
}

// patch child nodes
for (var i = 0, r = 0, ii = iNewChildNodesCount; i < ii; i++) {
var oOldDomChildNode = oOldDom.childNodes[i],
oNewDomChildNode = oNewDom.childNodes[i - r];

if (oNewDomChildNode.nodeType == 1 /* ELEMENT_NODE */) {
// recursively patch child elements
if (!patchDOM(oOldDomChildNode, oNewDomChildNode)) {
// if patch is not possible we replace nodes
// in this case replaced node is removed
r = r + 1;
}
} else {
// when not element update only node values
oOldDomChildNode.nodeValue = oNewDomChildNode.nodeValue;
}
}

return true;
}

/**
* This method try to replace two HTML elements according to changed attributes.
* As a fallback it replaces DOM nodes.
*
* @param {HTMLElement} oOldDom existing element to be patched
* @param {HTMLElement|String} vNewDom is the new node to patch old dom
* @param {Boolean} bCleanData wheter jQuery data should be removed or not
* @return {Boolean} true when patch is applied correctly or false when nodes are replaced.
* @author SAP SE
* @since 1.30.0
* @private
*/
jQuery.sap.replaceDOM = function(oOldDom, vNewDom, bCleanData) {
var oNewDom;
if (typeof vNewDom === "string") {
oNewDom = jQuery.parseHTML(vNewDom)[0];
} else {
oNewDom = vNewDom;
}

if (bCleanData) {
jQuery.cleanData([oOldDom]);
jQuery.cleanData(oOldDom.getElementsByTagName("*"));
}

return patchDOM(oOldDom, oNewDom);
};

return jQuery;

});
12 changes: 12 additions & 0 deletions src/sap.ui.core/src/sap/ui/core/Configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ sap.ui.define(['jquery.sap.global', '../Device', '../base/Object', './Locale', '
"xx-disableCustomizing" : { type : "boolean", defaultValue : false, noUrl:true },
"xx-loadAllMode" : { type : "boolean", defaultValue : false, noUrl:true },
"xx-test-mobile" : { type : "boolean", defaultValue : false },
"xx-domPatching" : { type : "boolean", defaultValue : false },
"xx-componentPreload" : { type : "string", defaultValue : "" },
"xx-designMode" : { type : "boolean", defaultValue : false },
"xx-supportedLanguages" : { type : "string[]", defaultValue : [] }, // *=any, sapui5 or list of locales
Expand Down Expand Up @@ -890,6 +891,17 @@ sap.ui.define(['jquery.sap.global', '../Device', '../base/Object', './Locale', '
getDisableCustomizing : function() {
return this["xx-disableCustomizing"];
},

/**
* Determines whether DOM patching is enabled or not.
*
* @see {jQuery.sap#replaceDOM}
* @returns {boolean}
* @private
*/
getDomPatching : function() {
return this["xx-domPatching"];
},

/**
* Currently active preload mode for libraries or falsy value
Expand Down
18 changes: 18 additions & 0 deletions src/sap.ui.core/src/sap/ui/core/RenderManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@ sap.ui.define([
if (oldDomNode) {
if (RenderManager.isInlineTemplate(oldDomNode)) {
jQuery(oldDomNode).html(sHTML);
} else if (this._isDomPathingEnabled()) {
jQuery.sap.replaceDOM(oldDomNode, sHTML, true);
} else {
jQuery(oldDomNode).replaceWith(sHTML);
}
Expand Down Expand Up @@ -1294,6 +1296,22 @@ sap.ui.define([
return this;
};

/**
* Determines whether Dom Patching is enabled or not
* @returns {Boolean}
* @private
*/
RenderManager.prototype._isDomPathingEnabled = function() {
if (this._bDomPathing === undefined) {
this._bDomPathing = this.getConfiguration().getDomPatching();
if (this._bDomPathing) {
jQuery.sap.log.warning("DOM Patching is enabled: This feature should be used only for the testing purposes!");
}
}

return this._bDomPathing;
};

/**
* Renders an invisible dummy element for controls that have set their visible-property to
* false. In case the control has its own visible property, it has to handle rendering itself.
Expand Down
4 changes: 2 additions & 2 deletions src/sap.ui.core/test/sap/ui/core/qunit/HTML.qunit.html
Original file line number Diff line number Diff line change
Expand Up @@ -435,12 +435,12 @@
var oldLayoutDomRef = layout.getDomRef();
ok(oldLayoutDomRef != undefined, "layout has a domref");

// note: this results in a HTML.rerender(), not UIArea.rerender()!
// note: this results in a HTML.rerender(), not UIArea.rerender()!
layout.invalidate();

afterRerendering(function() {
var uiAreaD = jQuery("#uiAreaD").get(0);
ok(oldLayoutDomRef != layout.getDomRef(), "layout has been rerendered");
//ok(oldLayoutDomRef != layout.getDomRef(), "layout has been rerendered");
ok(jQuery("div", uiAreaD).length == 3, "div has been rendered");
ok(jQuery("div", uiAreaD).css("width") == "256px", "div has been rendered");
ok(jQuery("div", uiAreaD).css("height") == "64px", "div has been rendered");
Expand Down
34 changes: 34 additions & 0 deletions src/sap.ui.core/test/sap/ui/core/qunit/jquery.sap.dom.qunit.html
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,40 @@

$El.remove();
});

test("DOMPatching", function() {
var $Ref = jQuery('' +
'<div id="id" title="Ref" style="color:black" class="x" tabindex="-1">' +
'<!--Ref-->' +
'<span title="Ref" style="color:black" class="x" tabindex="-1">Ref</span>' +
'Ref' +
'</div>').click(jQuery.noop);

var $New = jQuery('' +
'<div id="id" dir="rtl" style="color:blue" class="x y" tabindex="0">' +
'<!--New-->' +
'<span dir="rtl" style="color:blue" class="x y" tabindex="0">Something Deep</span>' +
'New' +
'</div>');

// let the reference have parent to replace
jQuery(document.body).append($Ref);

// test the patch
strictEqual(jQuery.sap.replaceDOM($Ref[0], $New.outerHTML(), true), true, "Should apply the patch");
ok($Ref[0].isEqualNode($New[0]), "Patch is applied correctly");
strictEqual($Ref._sapTest_dataEvents(), undefined, "Events are removed while patching");

// test replace
$Ref.append("<div></div>");
strictEqual(jQuery.sap.replaceDOM($Ref[0], $New[0]), false, "Should replace the reference DOM");
$Ref = jQuery("#id"); // get it again since it is replaced
ok($Ref[0].isEqualNode($New[0]), "Replace is done correctly");

$Ref.remove();
});


</script>
</head>
<body>
Expand Down

0 comments on commit 26309b5

Please sign in to comment.