Commit d939a570 authored by Romain Courteaud's avatar Romain Courteaud

Always create an empty DOM element for a new gadget.

This reduces the number of DOM modifications (only one replace).

This also ensure that the element content is empty.

Previous event listener will also be discarded (and so, reduce memory usage).

declareGadget element parameter can now also be a tag name.
parent ffd3d0c2
...@@ -620,8 +620,9 @@ ...@@ -620,8 +620,9 @@
} }
function startService(gadget) { function startService(gadget) {
if ((gadget.constructor.__service_list.length === 0) && if (((gadget.constructor.__service_list.length === 0) &&
(!gadget.constructor.__job_declared)) { (!gadget.constructor.__job_declared)) ||
(gadget.hasOwnProperty('__monitor'))) {
return; return;
} }
createGadgetMonitor(gadget); createGadgetMonitor(gadget);
...@@ -888,11 +889,9 @@ ...@@ -888,11 +889,9 @@
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// privateDeclarePublicGadget // privateDeclarePublicGadget
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
function createPrivateInstanceFromKlass(Klass, options, parent_gadget) { function createPrivateInstanceFromKlass(Klass, options, parent_gadget,
old_element) {
// Get the gadget class and instanciate it // Get the gadget class and instanciate it
if (options.element === undefined) {
options.element = document.createElement("div");
}
var i, var i,
gadget_instance, gadget_instance,
template_node_list = Klass.__template_element.body.childNodes, template_node_list = Klass.__template_element.body.childNodes,
...@@ -907,10 +906,18 @@ ...@@ -907,10 +906,18 @@
} }
gadget_instance.element.appendChild(fragment); gadget_instance.element.appendChild(fragment);
setAqParent(gadget_instance, parent_gadget); setAqParent(gadget_instance, parent_gadget);
clearGadgetInternalParameters(gadget_instance);
if (old_element !== undefined) {
// Add gadget to the DOM if needed
// Do it when all DOM modifications are done
old_element.parentNode.replaceChild(options.element,
old_element);
}
return gadget_instance; return gadget_instance;
} }
function privateDeclarePublicGadget(url, options, parent_gadget) { function privateDeclarePublicGadget(url, options, parent_gadget,
old_element) {
var klass = renderJS.declareGadgetKlass(url); var klass = renderJS.declareGadgetKlass(url);
// gadget loading should not be interrupted // gadget loading should not be interrupted
// if not, gadget's definition will not be complete // if not, gadget's definition will not be complete
...@@ -918,10 +925,12 @@ ...@@ -918,10 +925,12 @@
//so loading_klass_promise can't be cancel //so loading_klass_promise can't be cancel
if (typeof klass.then === 'function') { if (typeof klass.then === 'function') {
return klass.then(function createAsyncPrivateInstanceFromKlass(Klass) { return klass.then(function createAsyncPrivateInstanceFromKlass(Klass) {
return createPrivateInstanceFromKlass(Klass, options, parent_gadget); return createPrivateInstanceFromKlass(Klass, options, parent_gadget,
old_element);
}); });
} }
return createPrivateInstanceFromKlass(klass, options, parent_gadget); return createPrivateInstanceFromKlass(klass, options, parent_gadget,
old_element);
} }
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -953,17 +962,18 @@ ...@@ -953,17 +962,18 @@
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// privateDeclareIframeGadget // privateDeclareIframeGadget
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
function privateDeclareIframeGadget(url, options, parent_gadget) { function privateDeclareIframeGadget(url, options, parent_gadget,
old_element) {
var gadget_instance, var gadget_instance,
iframe, iframe,
iframe_loading_deferred = RSVP.defer(); iframe_loading_deferred = RSVP.defer();
if (options.element === undefined) { if (old_element === undefined) {
throw new Error("DOM element is required to create Iframe Gadget " + throw new Error("DOM element is required to create Iframe Gadget " +
url); url);
} }
// Check if the element is attached to the DOM // Check if the element is attached to the DOM
if (!document.contains(options.element)) { if (!document.contains(old_element)) {
throw new Error("The parent element is not attached to the DOM for " + throw new Error("The parent element is not attached to the DOM for " +
url); url);
} }
...@@ -987,8 +997,12 @@ ...@@ -987,8 +997,12 @@
gadget_instance.__path = url; gadget_instance.__path = url;
gadget_instance.element = options.element; gadget_instance.element = options.element;
gadget_instance.state = {}; gadget_instance.state = {};
// Attach it to the DOM
options.element.appendChild(iframe); options.element.appendChild(iframe);
clearGadgetInternalParameters(gadget_instance);
// Add gadget to the DOM if needed
// Do it when all DOM modifications are done
old_element.parentNode.replaceChild(options.element,
old_element);
// XXX Manage unbind when deleting the gadget // XXX Manage unbind when deleting the gadget
...@@ -1056,7 +1070,8 @@ ...@@ -1056,7 +1070,8 @@
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// privateDeclareDataUrlGadget // privateDeclareDataUrlGadget
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
function privateDeclareDataUrlGadget(url, options, parent_gadget) { function privateDeclareDataUrlGadget(url, options, parent_gadget,
old_element) {
return new RSVP.Queue() return new RSVP.Queue()
.push(function waitForDataUrlAjax() { .push(function waitForDataUrlAjax() {
...@@ -1076,7 +1091,8 @@ ...@@ -1076,7 +1091,8 @@
return readBlobAsDataURL(blob); return readBlobAsDataURL(blob);
}) })
.push(function handleDataURL(data_url) { .push(function handleDataURL(data_url) {
return privateDeclareIframeGadget(data_url, options, parent_gadget); return privateDeclareIframeGadget(data_url, options, parent_gadget,
old_element);
}); });
} }
...@@ -1084,30 +1100,10 @@ ...@@ -1084,30 +1100,10 @@
// RenderJSGadget.declareGadget // RenderJSGadget.declareGadget
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
function setGadgetInstanceHTMLContext(gadget_instance, options, function setGadgetInstanceHTMLContext(gadget_instance, options,
parent_gadget, url) { parent_gadget, url,
old_element, scope) {
var i, var i,
scope,
queue; queue;
clearGadgetInternalParameters(gadget_instance);
// Store local reference to the gadget instance
scope = options.scope;
if (scope === undefined) {
scope = 'RJS_' + scope_increment;
scope_increment += 1;
while (parent_gadget.__sub_gadget_dict.hasOwnProperty(scope)) {
scope = 'RJS_' + scope_increment;
scope_increment += 1;
}
}
gadget_instance.element.setAttribute("data-gadget-scope",
scope);
// Put some attribute to ease page layout comprehension
gadget_instance.element.setAttribute("data-gadget-url", url);
gadget_instance.element.setAttribute("data-gadget-sandbox",
options.sandbox);
gadget_instance.element._gadget = gadget_instance;
function ready_executable_wrapper(fct) { function ready_executable_wrapper(fct) {
return function executeReadyWrapper() { return function executeReadyWrapper() {
...@@ -1116,13 +1112,14 @@ ...@@ -1116,13 +1112,14 @@
} }
function ready_wrapper() { function ready_wrapper() {
if (document.contains(gadget_instance.element)) {
startService(gadget_instance);
}
// Always set the parent reference when all ready are finished // Always set the parent reference when all ready are finished
// in case the gadget declaration is cancelled // in case the gadget declaration is cancelled
// (and ready are not finished) // (and ready are not finished)
gadget_instance.element._gadget = gadget_instance;
parent_gadget.__sub_gadget_dict[scope] = gadget_instance; parent_gadget.__sub_gadget_dict[scope] = gadget_instance;
if (document.contains(gadget_instance.element)) {
startService(gadget_instance);
}
// Always return the gadget instance after ready function // Always return the gadget instance after ready function
return gadget_instance; return gadget_instance;
} }
...@@ -1147,7 +1144,9 @@ ...@@ -1147,7 +1144,9 @@
.declareMethod('declareGadget', function declareGadget(url, options) { .declareMethod('declareGadget', function declareGadget(url, options) {
var parent_gadget = this, var parent_gadget = this,
method, method,
result; result,
scope,
old_element;
if (options === undefined) { if (options === undefined) {
options = {}; options = {};
...@@ -1155,10 +1154,39 @@ ...@@ -1155,10 +1154,39 @@
if (options.sandbox === undefined) { if (options.sandbox === undefined) {
options.sandbox = "public"; options.sandbox = "public";
} }
if (options.element === undefined) {
options.element = document.createElement('div');
} else if (typeof options.element === 'string') {
options.element = document.createElement(options.element);
} else if (options.element.parentNode) {
old_element = options.element;
// Clean up the element content
// Remove all existing event listener
options.element = old_element.cloneNode(false);
} else {
throw new Error('No need to manually provide a DOM element ' +
'without a parentNode: ' + url);
}
// transform url to absolute url if it is relative // transform url to absolute url if it is relative
url = renderJS.getAbsoluteURL(url, this.__path); url = renderJS.getAbsoluteURL(url, this.__path);
// Store local reference to the gadget instance
scope = options.scope;
if (scope === undefined) {
scope = 'RJS_' + scope_increment;
scope_increment += 1;
while (parent_gadget.__sub_gadget_dict.hasOwnProperty(scope)) {
scope = 'RJS_' + scope_increment;
scope_increment += 1;
}
}
options.element.setAttribute("data-gadget-scope", scope);
// Put some attribute to ease page layout comprehension
options.element.setAttribute("data-gadget-url", url);
options.element.setAttribute("data-gadget-sandbox", options.sandbox);
if (options.sandbox === "public") { if (options.sandbox === "public") {
method = privateDeclarePublicGadget; method = privateDeclarePublicGadget;
} else if (options.sandbox === "iframe") { } else if (options.sandbox === "iframe") {
...@@ -1169,7 +1197,7 @@ ...@@ -1169,7 +1197,7 @@
throw new Error("Unsupported sandbox options '" + throw new Error("Unsupported sandbox options '" +
options.sandbox + "'"); options.sandbox + "'");
} }
result = method(url, options, parent_gadget); result = method(url, options, parent_gadget, old_element);
// Set the HTML context // Set the HTML context
if (typeof result.then === 'function') { if (typeof result.then === 'function') {
return new RSVP.Queue() return new RSVP.Queue()
...@@ -1178,11 +1206,13 @@ ...@@ -1178,11 +1206,13 @@
}) })
.push(function setAsyncGadgetInstanceHTMLContext(gadget_instance) { .push(function setAsyncGadgetInstanceHTMLContext(gadget_instance) {
return setGadgetInstanceHTMLContext(gadget_instance, options, return setGadgetInstanceHTMLContext(gadget_instance, options,
parent_gadget, url); parent_gadget, url,
old_element, scope);
}); });
} }
return setGadgetInstanceHTMLContext(result, options, return setGadgetInstanceHTMLContext(result, options,
parent_gadget, url); parent_gadget, url, old_element,
scope);
}) })
.declareMethod('getDeclaredGadget', .declareMethod('getDeclaredGadget',
function getDeclaredGadget(gadget_scope) { function getDeclaredGadget(gadget_scope) {
......
...@@ -2337,6 +2337,51 @@ ...@@ -2337,6 +2337,51 @@
}); });
}); });
test('service started after ready is finished', function () {
// Subclass RenderJSGadget to not pollute its namespace
var gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test5011.html',
defer = RSVP.defer();
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html"
}, "<html><body></body></html>"]);
document.getElementById('qunit-fixture').innerHTML = "<div></div>";
stop();
expect(1);
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
Klass.declareService(function () {
defer.reject('Triggered before ready');
});
Klass.ready(function () {
return RSVP.delay(500)
.then(function () {
defer.resolve('Triggered before service');
});
});
return gadget.declareGadget(
html_url,
{element: document.getElementById('qunit-fixture')
.querySelector("div")}
);
})
.then(function () {
return defer.promise;
})
.then(function (result) {
equal(result, 'Triggered before service');
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('service started when gadget element added in DOM', function () { test('service started when gadget element added in DOM', function () {
// Subclass RenderJSGadget to not pollute its namespace // Subclass RenderJSGadget to not pollute its namespace
var service1 = {}, var service1 = {},
...@@ -3940,6 +3985,7 @@ ...@@ -3940,6 +3985,7 @@
// Check that gadget is not created if klass is can not be loaded // Check that gadget is not created if klass is can not be loaded
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
html_url = 'http://example.org/files/qunittest/test3.html'; html_url = 'http://example.org/files/qunittest/test3.html';
gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [404, { this.server.respondWith("GET", html_url, [404, {
"Content-Type": "text/html" "Content-Type": "text/html"
...@@ -4331,24 +4377,27 @@ ...@@ -4331,24 +4377,27 @@
// Subclass RenderJSGadget to not pollute its namespace // Subclass RenderJSGadget to not pollute its namespace
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test98.html'; html_url = 'https://example.org/files/qunittest/test98.html',
previous_fixture = document.getElementById('qunit-fixture');
gadget.__sub_gadget_dict = {}; gadget.__sub_gadget_dict = {};
this.server.respondWith("GET", html_url, [200, { this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html" "Content-Type": "text/html"
}, "<html><body><p>foo</p></body></html>"]); }, "<html><body><p>foo</p></body></html>"]);
document.getElementById('qunit-fixture').textContent = ""; previous_fixture.innerHTML = "<div>bar</div>";
stop(); stop();
expect(1); expect(3);
renderJS.declareGadgetKlass(html_url) renderJS.declareGadgetKlass(html_url)
.then(function (Klass) { .then(function (Klass) {
return gadget.declareGadget( return gadget.declareGadget(
html_url, html_url,
{element: document.getElementById('qunit-fixture')} {element: previous_fixture}
); );
}) })
.then(function () { .then(function (g) {
notEqual(document.getElementById('qunit-fixture'), previous_fixture);
equal(document.getElementById('qunit-fixture'), g.element);
equal( equal(
document.getElementById('qunit-fixture').innerHTML, document.getElementById('qunit-fixture').innerHTML,
'<p>foo</p>' '<p>foo</p>'
...@@ -5011,7 +5060,11 @@ ...@@ -5011,7 +5060,11 @@
test('Require a DOM element as option', function () { test('Require a DOM element as option', function () {
// Subclass RenderJSGadget to not pollute its namespace // Subclass RenderJSGadget to not pollute its namespace
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
html_url = 'https://example.org/files/qunittest/test98.html'; html_url = 'https://example.org/files/qunittest/test98.html',
parent_element = document.createElement("div"),
gadget_element = document.createElement("div");
gadget.__sub_gadget_dict = {};
parent_element.appendChild(gadget_element);
this.server.respondWith("GET", html_url, [200, { this.server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html" "Content-Type": "text/html"
...@@ -5023,7 +5076,7 @@ ...@@ -5023,7 +5076,7 @@
.then(function (Klass) { .then(function (Klass) {
return gadget.declareGadget(html_url, { return gadget.declareGadget(html_url, {
sandbox: 'iframe', sandbox: 'iframe',
element: document.createElement("div") element: gadget_element
}); });
}) })
.then(function () { .then(function () {
...@@ -5659,6 +5712,7 @@ ...@@ -5659,6 +5712,7 @@
// Check that declare gadget returns the gadget // Check that declare gadget returns the gadget
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
url = "./embedded_fail.html"; url = "./embedded_fail.html";
gadget.__sub_gadget_dict = {};
stop(); stop();
expect(1); expect(1);
...@@ -5681,6 +5735,7 @@ ...@@ -5681,6 +5735,7 @@
// Check that declare gadget returns the gadget // Check that declare gadget returns the gadget
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
url = "./embedded_empty.html"; url = "./embedded_empty.html";
gadget.__sub_gadget_dict = {};
stop(); stop();
expect(2); expect(2);
...@@ -5704,6 +5759,7 @@ ...@@ -5704,6 +5759,7 @@
// Check that declare gadget returns the gadget // Check that declare gadget returns the gadget
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
url = "./embedded_404.html"; url = "./embedded_404.html";
gadget.__sub_gadget_dict = {};
stop(); stop();
expect(2); expect(2);
...@@ -5754,6 +5810,7 @@ ...@@ -5754,6 +5810,7 @@
// Check that declare gadget returns the gadget // Check that declare gadget returns the gadget
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
url = "./embedded_non_renderjs.html"; url = "./embedded_non_renderjs.html";
gadget.__sub_gadget_dict = {};
stop(); stop();
expect(2); expect(2);
...@@ -6254,7 +6311,7 @@ ...@@ -6254,7 +6311,7 @@
return new RSVP.Promise(function (resolve, reject) { return new RSVP.Promise(function (resolve, reject) {
// listen for an event fired in the ready function of the parent // listen for an event fired in the ready function of the parent
// gadget // gadget
parentDiv.addEventListener("rjsready", function (e) { parentDiv.parentNode.addEventListener("rjsready", function (e) {
resolve(); resolve();
}); });
// if no event is fired within 500ms, just resolve and fail later // if no event is fired within 500ms, just resolve and fail later
......
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
(function (window, rJS) { (function (window, rJS) {
"use strict"; "use strict";
rJS(window) rJS(window)
.ready(function (gadget) { .ready(function (gadget) {
return gadget.element.dispatchEvent(new Event("rjsready")); return gadget.element.dispatchEvent(new Event("rjsready",
{bubbles: true}));
}); });
}(window, rJS)); }(window, rJS));
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment