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
spyOn(@moduleEdit, 'loadEdit')
spyOn(@moduleEdit, 'delegateEvents')
spyOn($.fn, 'append')
spyOn($, 'getScript')
spyOn($, 'getScript').andReturn($.Deferred().resolve().promise())
window.loadedXBlockResources = undefined
......
......@@ -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, =>
......
......@@ -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);
});
});
});
});
......@@ -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("<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);
}
}
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("<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