diff --git a/cms/static/coffee/spec/views/module_edit_spec.coffee b/cms/static/coffee/spec/views/module_edit_spec.coffee
index ccf22fab40..aaefb96dad 100644
--- a/cms/static/coffee/spec/views/module_edit_spec.coffee
+++ b/cms/static/coffee/spec/views/module_edit_spec.coffee
@@ -58,7 +58,7 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod
spyOn(@moduleEdit, 'loadEdit')
spyOn(@moduleEdit, 'delegateEvents')
spyOn($.fn, 'append')
- spyOn($, 'getScript')
+ spyOn($, 'getScript').andReturn($.Deferred().resolve().promise())
window.loadedXBlockResources = undefined
diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee
index e547a43c47..fa01cda265 100644
--- a/cms/static/coffee/src/views/module_edit.coffee
+++ b/cms/static/coffee/src/views/module_edit.coffee
@@ -81,8 +81,7 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1",
headers:
Accept: 'application/json'
success: (fragment) =>
- @renderXBlockFragment(fragment, target, viewName)
- callback()
+ @renderXBlockFragment(fragment, target).done(callback)
)
render: -> @loadView('student_view', @$el, =>
diff --git a/cms/static/js/spec/views/xblock_spec.js b/cms/static/js/spec/views/xblock_spec.js
index b3e05c9ca5..4c38402e68 100644
--- a/cms/static/js/spec/views/xblock_spec.js
+++ b/cms/static/js/spec/views/xblock_spec.js
@@ -38,18 +38,22 @@ define([ "jquery", "js/spec/create_sinon", "URI", "js/views/xblock", "js/models/
var postXBlockRequest;
postXBlockRequest = function(requests, resources) {
+ var promise;
$.ajax({
url: "test_url",
type: 'GET',
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, {
html: mockXBlockHtml,
resources: resources
});
expect(xblockView.$el.select('.xblock-header')).toBeTruthy();
+ return promise;
};
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/
]);
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);
+ });
});
});
});
diff --git a/cms/static/js/views/xblock.js b/cms/static/js/views/xblock.js
index 74fcf6464a..f13d9afc03 100644
--- a/cms/static/js/views/xblock.js
+++ b/cms/static/js/views/xblock.js
@@ -21,64 +21,110 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
success: function(fragment) {
var wrapper = self.$el,
xblock;
- self.renderXBlockFragment(fragment, wrapper);
- xblock = self.$('.xblock').first();
- XBlock.initializeBlock(xblock);
+ self.renderXBlockFragment(fragment, wrapper).done(function() {
+ xblock = self.$('.xblock').first();
+ 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
* 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 element The element into which to render the fragment (defaults to this.$el)
+ * @returns {*} A promise representing the rendering process
*/
renderXBlockFragment: function(fragment, element) {
- var applyResource, i, len, resources, resource;
+ var html = fragment.html,
+ resources = fragment.resources;
if (!element) {
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];
if (!window.loadedXBlockResources) {
window.loadedXBlockResources = [];
}
if (_.indexOf(window.loadedXBlockResources, hash) < 0) {
resource = value[1];
- head = $('head');
- if (resource.mimetype === "text/css") {
- if (resource.kind === "text") {
- head.append("");
- } else if (resource.kind === "url") {
- head.append("");
- }
- } else if (resource.mimetype === "application/javascript") {
- if (resource.kind === "text") {
- head.append("");
- } else if (resource.kind === "url") {
- $.getScript(resource.data);
- }
- } else if (resource.mimetype === "text/html") {
- if (resource.placement === "head") {
- head.append(resource.data);
- }
- }
+ promise = self.loadResource(resource);
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;
- for (i = 0, len = resources.length; i < len; i++) {
- resource = resources[i];
- applyResource(resource);
+ /**
+ * Loads the specified resource into the page.
+ * @param resource The resource to be loaded.
+ * @returns {*} A promise representing the loading of the 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("");
+ } else if (kind === "url") {
+ head.append("");
+ }
+ } else if (mimetype === "application/javascript") {
+ if (kind === "text") {
+ head.append("");
+ } 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();
}
});