diff --git a/IPython/html/static/widgets/js/manager.js b/IPython/html/static/widgets/js/manager.js
index 944effd5a..242c67975 100644
--- a/IPython/html/static/widgets/js/manager.js
+++ b/IPython/html/static/widgets/js/manager.js
@@ -53,7 +53,7 @@ define([
" message was from. Widget will not be displayed");
} else {
var that = this;
- this.create_view(model, {cell: cell, callback: function(view) {
+ this.create_view(model, {cell: cell, success: function(view) {
that._handle_display_view(view);
if (cell.widget_subarea) {
cell.widget_subarea.append(view.$el);
@@ -84,7 +84,7 @@ define([
var view_name = model.get('_view_name');
var view_mod = model.get('_view_module');
- var errback = options.errback || function(err) {console.log(err);};
+ var error = options.error || function(error) { console.log(error); };
var instantiate_view = function(ViewType) {
if (ViewType) {
@@ -100,9 +100,11 @@ define([
var view = new ViewType(parameters);
view.render();
model.on('destroy', view.remove, view);
- options.callback(view);
+ if (options.success) {
+ options.success(view);
+ }
} else {
- errback({unknown_view: true, view_name: view_name,
+ error({unknown_view: true, view_name: view_name,
view_module: view_mod});
}
};
@@ -110,7 +112,7 @@ define([
if (view_mod) {
require([view_mod], function(module) {
instantiate_view(module[view_name]);
- }, errback);
+ }, error);
} else {
instantiate_view(WidgetManager._view_types[view_name]);
}
@@ -157,7 +159,7 @@ define([
handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
}
- // Create callback dict using what is known
+ // Create callback dictionary using what is known
var that = this;
callbacks = {
iopub : {
@@ -187,31 +189,85 @@ define([
WidgetManager.prototype._handle_comm_open = function (comm, msg) {
// Handle when a comm is opened.
+ this.create_model({
+ model_name: msg.content.data.model_name,
+ model_module: msg.content.data.model_module,
+ comm: comm});
+ };
+
+ WidgetManager.prototype.create_model = function (options) {
+ // Create and return a new widget model.
+ //
+ // Minimally, one must provide the model_name and widget_class
+ // parameters to create a model from Javascript.
+ //
+ // Example
+ // --------
+ // JS:
+ // IPython.notebook.kernel.widget_manager.create_model({
+ // model_name: 'WidgetModel',
+ // widget_class: 'IPython.html.widgets.widget_int.IntSlider',
+ // init_state_callback: function(model) { console.log('Create success!', model); }});
+ //
+ // Parameters
+ // ----------
+ // options: dictionary
+ // Dictionary of options with the following contents:
+ // model_name: string
+ // Target name of the widget model to create.
+ // model_module: (optional) string
+ // Module name of the widget model to create.
+ // widget_class: (optional) string
+ // Target name of the widget in the back-end.
+ // comm: (optional) Comm
+ // success: (optional) callback
+ // Callback for when the model was created successfully.
+ // error: (optional) callback
+ // Callback for when the model wasn't created.
+ // init_state_callback: (optional) callback
+ // Called when the first state push from the back-end is
+ // recieved. Allows you to modify the model after it's
+ // complete state is filled and synced.
+
+ // Make default callbacks if not specified.
+ var error = options.error || function(error) { console.log(error); };
+
+ // Create a comm if it wasn't provided.
+ var comm = options.comm;
+ if (!comm) {
+ comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
+ }
+
+ // Create a new model that is connected to the comm.
var that = this;
-
var instantiate_model = function(ModelType) {
var model_id = comm.comm_id;
- var widget_model = new ModelType(that, model_id, comm);
+ var widget_model = new ModelType(that, model_id, comm, options.init_state_callback);
widget_model.on('comm:close', function () {
delete that._models[model_id];
});
that._models[model_id] = widget_model;
+ if (options.success) {
+ options.success(widget_model);
+ }
};
- var widget_type_name = msg.content.data.model_name;
- var widget_module = msg.content.data.model_module;
-
+ // Get the model type using require or through the registry.
+ var widget_type_name = options.model_name;
+ var widget_module = options.model_module;
if (widget_module) {
+
// Load the module containing the widget model
require([widget_module], function(mod) {
if (mod[widget_type_name]) {
instantiate_model(mod[widget_type_name]);
} else {
- console.log("Error creating widget model: " + widget_type_name
+ error("Error creating widget model: " + widget_type_name
+ " not found in " + widget_module);
}
- }, function(err) { console.log(err); });
+ }, error);
} else {
+
// No module specified, load from the global models registry
instantiate_model(WidgetManager._model_types[widget_type_name]);
}
diff --git a/IPython/html/static/widgets/js/widget.js b/IPython/html/static/widgets/js/widget.js
index d60b38489..34822bd75 100644
--- a/IPython/html/static/widgets/js/widget.js
+++ b/IPython/html/static/widgets/js/widget.js
@@ -9,7 +9,7 @@ define(["widgets/js/manager",
], function(widgetmanager, _, Backbone, $, IPython){
var WidgetModel = Backbone.Model.extend({
- constructor: function (widget_manager, model_id, comm) {
+ constructor: function (widget_manager, model_id, comm, init_state_callback) {
// Constructor
//
// Creates a WidgetModel instance.
@@ -20,7 +20,11 @@ define(["widgets/js/manager",
// model_id : string
// An ID unique to this model.
// comm : Comm instance (optional)
+ // init_state_callback : callback (optional)
+ // Called once when the first state message is recieved from
+ // the back-end.
this.widget_manager = widget_manager;
+ this.init_state_callback = init_state_callback;
this._buffered_state_diff = {};
this.pending_msgs = 0;
this.msg_buffer = null;
@@ -70,6 +74,10 @@ define(["widgets/js/manager",
switch (method) {
case 'update':
this.set_state(msg.content.data.state);
+ if (this.init_state_callback) {
+ this.init_state_callback.apply(this, [this]);
+ delete this.init_state_callback;
+ }
break;
case 'custom':
this.trigger('msg:custom', msg.content.data.content);
@@ -319,7 +327,7 @@ define(["widgets/js/manager",
// to the subview without having to add it here.
var that = this;
var old_callback = options.callback || function(view) {};
- options = $.extend({ parent: this, callback: function(child_view) {
+ options = $.extend({ parent: this, success: function(child_view) {
// Associate the view id with the model id.
if (that.child_model_views[child_model.id] === undefined) {
that.child_model_views[child_model.id] = [];
diff --git a/IPython/html/tests/widgets/manager.js b/IPython/html/tests/widgets/manager.js
new file mode 100644
index 000000000..fed79fd85
--- /dev/null
+++ b/IPython/html/tests/widgets/manager.js
@@ -0,0 +1,46 @@
+// Test the widget manager.
+casper.notebook_test(function () {
+ var index;
+
+ this.then(function () {
+
+ // Check if the WidgetManager class is defined.
+ this.test.assert(this.evaluate(function() {
+ return IPython.WidgetManager !== undefined;
+ }), 'WidgetManager class is defined');
+
+ // Check if the widget manager has been instantiated.
+ this.test.assert(this.evaluate(function() {
+ return IPython.notebook.kernel.widget_manager !== undefined;
+ }), 'Notebook widget manager instantiated');
+
+ // Try creating a widget from Javascript.
+ this.evaluate(function() {
+ IPython.notebook.kernel.widget_manager.create_model({
+ model_name: 'WidgetModel',
+ widget_class: 'IPython.html.widgets.widget_int.IntSlider',
+ init_state_callback: function(model) {
+ console.log('Create success!', model);
+ window.slider_id = model.id;
+ }
+ });
+ });
+ });
+
+ // Wait for the state to be recieved.
+ this.waitFor(function check() {
+ return this.evaluate(function() {
+ return window.slider_id !== undefined;
+ });
+ });
+
+ index = this.append_cell(
+ 'from IPython.html.widgets import Widget\n' +
+ 'widget = list(Widget.widgets.values())[0]\n' +
+ 'print(widget.model_id)');
+ this.execute_cell_then(index, function(index) {
+ var output = this.get_output_cell(index).text.trim();
+ var slider_id = this.evaluate(function() { return window.slider_id; });
+ this.test.assertEquals(output, slider_id, "Widget created from the front-end.");
+ });
+});
diff --git a/IPython/html/tests/widgets/widget.js b/IPython/html/tests/widgets/widget.js
index d51a833a0..0782daed8 100644
--- a/IPython/html/tests/widgets/widget.js
+++ b/IPython/html/tests/widgets/widget.js
@@ -38,14 +38,6 @@ var recursive_compare = function(a, b) {
// Test the widget framework.
casper.notebook_test(function () {
var index;
-
- this.then(function () {
-
- // Check if the WidgetManager class is defined.
- this.test.assert(this.evaluate(function() {
- return IPython.WidgetManager !== undefined;
- }), 'WidgetManager class is defined');
- });
index = this.append_cell(
'from IPython.html import widgets\n' +
@@ -54,10 +46,6 @@ casper.notebook_test(function () {
this.execute_cell_then(index);
this.then(function () {
- // Check if the widget manager has been instantiated.
- this.test.assert(this.evaluate(function() {
- return IPython.notebook.kernel.widget_manager !== undefined;
- }), 'Notebook widget manager instantiated');
// Functions that can be used to test the packing and unpacking APIs
var that = this;
diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py
index f1571eab4..0f0c61b86 100644
--- a/IPython/html/widgets/widget.py
+++ b/IPython/html/widgets/widget.py
@@ -18,6 +18,7 @@ import collections
from IPython.core.getipython import get_ipython
from IPython.kernel.comm import Comm
from IPython.config import LoggingConfigurable
+from IPython.utils.importstring import import_item
from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
CaselessStrEnum, Tuple, CUnicode, Int, Set
from IPython.utils.py3compat import string_types
@@ -95,6 +96,13 @@ class Widget(LoggingConfigurable):
if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
Widget._widget_construction_callback(widget)
+ @staticmethod
+ def handle_comm_opened(comm, msg):
+ """Static method, called when a widget is constructed."""
+ widget_class = import_item(msg['content']['data']['widget_class'])
+ widget = widget_class(comm=comm)
+
+
#-------------------------------------------------------------------------
# Traits
#-------------------------------------------------------------------------
@@ -150,13 +158,17 @@ class Widget(LoggingConfigurable):
if self._model_id is not None:
args['comm_id'] = self._model_id
self.comm = Comm(**args)
- self._model_id = self.model_id
-
- self.comm.on_msg(self._handle_msg)
- Widget.widgets[self.model_id] = self
- # first update
- self.send_state()
+ def _comm_changed(self, name, new):
+ """Called when the comm is changed."""
+ self.comm = new
+ self._model_id = self.model_id
+
+ self.comm.on_msg(self._handle_msg)
+ Widget.widgets[self.model_id] = self
+
+ # first update
+ self.send_state()
@property
def model_id(self):
@@ -330,7 +342,7 @@ class Widget(LoggingConfigurable):
def _handle_custom_msg(self, content):
"""Called when a custom msg is received."""
self._msg_callbacks(self, content)
-
+
def _notify_trait(self, name, old_value, new_value):
"""Called when a property has been changed."""
# Trigger default traitlet callback machinery. This allows any user
@@ -341,7 +353,7 @@ class Widget(LoggingConfigurable):
# Send the state after the user registered callbacks for trait changes
# have all fired (allows for user to validate values).
if self.comm is not None and name in self.keys:
- # Make sure this isn't information that the front-end just sent us.
+ # Make sure this isn't information that the front-end just sent us.
if self._should_send_property(name, new_value):
# Send new state to front-end
self.send_state(key=name)