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)