Commit 2a3509f7 by Andy Armstrong

Merge pull request #3046 from edx/andya/fix-xblock-loading-in-studio

Fix Studio's XBlock dependency loading issues
parents 4801b4b3 a37d2c1b
...@@ -58,7 +58,7 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod ...@@ -58,7 +58,7 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
spyOn(@moduleEdit, 'loadEdit') spyOn(@moduleEdit, 'loadEdit')
spyOn(@moduleEdit, 'delegateEvents') spyOn(@moduleEdit, 'delegateEvents')
spyOn($.fn, 'append') spyOn($.fn, 'append')
spyOn($, 'getScript') spyOn($, 'getScript').andReturn($.Deferred().resolve().promise())
window.loadedXBlockResources = undefined window.loadedXBlockResources = undefined
......
...@@ -81,8 +81,7 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1", ...@@ -81,8 +81,7 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1",
headers: headers:
Accept: 'application/json' Accept: 'application/json'
success: (fragment) => success: (fragment) =>
@renderXBlockFragment(fragment, target, viewName) @renderXBlockFragment(fragment, target).done(callback)
callback()
) )
render: -> @loadView('student_view', @$el, => render: -> @loadView('student_view', @$el, =>
......
...@@ -38,18 +38,22 @@ define([ "jquery", "js/spec/create_sinon", "URI", "js/views/xblock", "js/models/ ...@@ -38,18 +38,22 @@ define([ "jquery", "js/spec/create_sinon", "URI", "js/views/xblock", "js/models/
var postXBlockRequest; var postXBlockRequest;
postXBlockRequest = function(requests, resources) { postXBlockRequest = function(requests, resources) {
var promise;
$.ajax({ $.ajax({
url: "test_url", url: "test_url",
type: 'GET', type: 'GET',
success: function(fragment) { success: function(fragment) {
xblockView.renderXBlockFragment(fragment, this.$el); promise = xblockView.renderXBlockFragment(fragment, this.$el);
} }
}); });
// Note: this mock response will call the AJAX success function synchronously
// so the promise variable defined above will be available.
respondWithMockXBlockFragment(requests, { respondWithMockXBlockFragment(requests, {
html: mockXBlockHtml, html: mockXBlockHtml,
resources: resources resources: resources
}); });
expect(xblockView.$el.select('.xblock-header')).toBeTruthy(); expect(xblockView.$el.select('.xblock-header')).toBeTruthy();
return promise;
}; };
it('can render an xblock with no CSS or JavaScript', function() { it('can render an xblock with no CSS or JavaScript', function() {
...@@ -87,6 +91,17 @@ define([ "jquery", "js/spec/create_sinon", "URI", "js/views/xblock", "js/models/ ...@@ -87,6 +91,17 @@ define([ "jquery", "js/spec/create_sinon", "URI", "js/views/xblock", "js/models/
]); ]);
expect($('head').html()).toContain(mockHeadTag); expect($('head').html()).toContain(mockHeadTag);
}); });
it('aborts rendering when a dependent script fails to load', function() {
var requests = create_sinon.requests(this),
mockJavaScriptUrl = "mock.js",
promise;
spyOn($, 'getScript').andReturn($.Deferred().reject().promise());
promise = postXBlockRequest(requests, [
["hash5", { mimetype: "application/javascript", kind: "url", data: mockJavaScriptUrl }]
]);
expect(promise.isRejected()).toBe(true);
});
}); });
}); });
}); });
...@@ -21,64 +21,110 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"], ...@@ -21,64 +21,110 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
success: function(fragment) { success: function(fragment) {
var wrapper = self.$el, var wrapper = self.$el,
xblock; xblock;
self.renderXBlockFragment(fragment, wrapper); self.renderXBlockFragment(fragment, wrapper).done(function() {
xblock = self.$('.xblock').first(); xblock = self.$('.xblock').first();
XBlock.initializeBlock(xblock); XBlock.initializeBlock(xblock);
self.delegateEvents();
});
} }
}); });
}, },
/** /**
* Renders an xblock fragment into the specifed element. The fragment has two attributes: * Renders an xblock fragment into the specified element. The fragment has two attributes:
* html: the HTML to be rendered * html: the HTML to be rendered
* resources: any JavaScript or CSS resources that the HTML depends upon * resources: any JavaScript or CSS resources that the HTML depends upon
* Note that the XBlock is rendered asynchronously, and so a promise is returned that
* represents this process.
* @param fragment The fragment returned from the xblock_handler * @param fragment The fragment returned from the xblock_handler
* @param element The element into which to render the fragment (defaults to this.$el) * @param element The element into which to render the fragment (defaults to this.$el)
* @returns {*} A promise representing the rendering process
*/ */
renderXBlockFragment: function(fragment, element) { renderXBlockFragment: function(fragment, element) {
var applyResource, i, len, resources, resource; var html = fragment.html,
resources = fragment.resources;
if (!element) { if (!element) {
element = this.$el; element = this.$el;
} }
// First render the HTML as the scripts might depend upon it
element.html(html);
// Now asynchronously add the resources to the page
return this.addXBlockFragmentResources(resources);
},
applyResource = function(value) { /**
var hash, resource, head; * Dynamically loads all of an XBlock's dependent resources. This is an asynchronous
* process so a promise is returned.
* @param resources The resources to be rendered
* @returns {*} A promise representing the rendering process
*/
addXBlockFragmentResources: function(resources) {
var self = this,
applyResource,
numResources,
deferred;
numResources = resources.length;
deferred = $.Deferred();
applyResource = function(index) {
var hash, resource, head, value, promise;
if (index >= numResources) {
deferred.resolve();
return;
}
value = resources[index];
hash = value[0]; hash = value[0];
if (!window.loadedXBlockResources) { if (!window.loadedXBlockResources) {
window.loadedXBlockResources = []; window.loadedXBlockResources = [];
} }
if (_.indexOf(window.loadedXBlockResources, hash) < 0) { if (_.indexOf(window.loadedXBlockResources, hash) < 0) {
resource = value[1]; resource = value[1];
head = $('head'); promise = self.loadResource(resource);
if (resource.mimetype === "text/css") {
if (resource.kind === "text") {
head.append("<style type='text/css'>" + resource.data + "</style>");
} else if (resource.kind === "url") {
head.append("<link rel='stylesheet' href='" + resource.data + "' type='text/css'>");
}
} else if (resource.mimetype === "application/javascript") {
if (resource.kind === "text") {
head.append("<script>" + resource.data + "</script>");
} else if (resource.kind === "url") {
$.getScript(resource.data);
}
} else if (resource.mimetype === "text/html") {
if (resource.placement === "head") {
head.append(resource.data);
}
}
window.loadedXBlockResources.push(hash); window.loadedXBlockResources.push(hash);
promise.done(function() {
applyResource(index + 1);
}).fail(function() {
deferred.reject();
});
} else {
applyResource(index + 1);
} }
}; };
applyResource(0);
return deferred.promise();
},
element.html(fragment.html); /**
resources = fragment.resources; * Loads the specified resource into the page.
for (i = 0, len = resources.length; i < len; i++) { * @param resource The resource to be loaded.
resource = resources[i]; * @returns {*} A promise representing the loading of the resource.
applyResource(resource); */
loadResource: function(resource) {
var head = $('head'),
mimetype = resource.mimetype,
kind = resource.kind,
placement = resource.placement,
data = resource.data;
if (mimetype === "text/css") {
if (kind === "text") {
head.append("<style type='text/css'>" + data + "</style>");
} else if (kind === "url") {
head.append("<link rel='stylesheet' href='" + data + "' type='text/css'>");
}
} else if (mimetype === "application/javascript") {
if (kind === "text") {
head.append("<script>" + data + "</script>");
} else if (kind === "url") {
// Return a promise for the script resolution
return $.getScript(data);
}
} else if (mimetype === "text/html") {
if (placement === "head") {
head.append(data);
}
} }
return this.delegateEvents(); // Return an already resolved promise for synchronous updates
return $.Deferred().resolve().promise();
} }
}); });
......
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