diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js
index a74fe190271..b3591f534f2 100644
--- a/addons/web/static/src/js/chrome.js
+++ b/addons/web/static/src/js/chrome.js
@@ -57,9 +57,7 @@ instance.web.Dialog = instance.web.Widget.extend({
init: function (parent, options, content) {
var self = this;
this._super(parent);
- if (content) {
- this.$element = content instanceof $ ? content : $(content);
- }
+ this.setElement(content || this.make(this.tagName));
this.dialog_options = {
modal: true,
destroy_on_close: true,
@@ -589,6 +587,12 @@ instance.web.Login = instance.web.Widget.extend({
self.$(".oe_login_pane").fadeIn("fast");
self.$element.addClass("oe_login_invalid");
});
+ },
+ show: function () {
+ this.$element.show();
+ },
+ hide: function () {
+ this.$element.hide();
}
});
instance.web.client_actions.add("login", "instance.web.Login");
@@ -880,12 +884,11 @@ instance.web.Client = instance.web.Widget.extend({
},
start: function() {
var self = this;
- return instance.connection.session_bind(this.origin).then(function() {
+ return instance.connection.session_bind(this.origin).pipe(function() {
var $e = $(QWeb.render(self._template, {}));
- self.$element.replaceWith($e);
- self.$element = $e;
+ self.replaceElement($e);
self.bind_events();
- self.show_common();
+ return self.show_common();
});
},
bind_events: function() {
diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js
index a092b670ef1..846ae3e4758 100644
--- a/addons/web/static/src/js/corelib.js
+++ b/addons/web/static/src/js/corelib.js
@@ -491,23 +491,19 @@ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, {
*
* The semantics of this precisely replace closing over the method call.
*
- * @param {String} method_name name of the method to invoke
+ * @param {String|Function} method function or name of the method to invoke
* @returns {Function} proxied method
*/
- proxy: function (method_name) {
+ proxy: function (method) {
var self = this;
return function () {
- return self[method_name].apply(self, arguments);
+ var fn = (typeof method === 'string') ? self[method] : method;
+ return fn.apply(self, arguments);
}
}
});
instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
- /**
- * Tag name when creating a default $element.
- * @type string
- */
- tagName: 'div',
/**
* Constructs the widget and sets its parent if a parent is given.
*
@@ -517,14 +513,9 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
* @param {instance.web.Widget} parent Binds the current instance to the given Widget instance.
* When that widget is destroyed by calling destroy(), the current instance will be
* destroyed too. Can be null.
- * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want
- * to bind the current Widget to an already existing part of the DOM, which is not compatible
- * with the DOM insertion methods provided by the current implementation of Widget. So
- * for new components this argument should not be provided any more.
*/
init: function(parent) {
instance.web.CallbackEnabledMixin.init.call(this);
- this.$element = $(document.createElement(this.tagName));
this.setParent(parent);
},
/**
@@ -534,7 +525,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
_.each(this.getChildren(), function(el) {
el.destroy();
});
- if(this.$element != null) {
+ if(this.$element) {
this.$element.remove();
}
instance.web.PropertiesMixin.destroy.call(this);
@@ -613,6 +604,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
* @returns {jQuery.Deferred}
*/
start: function() {
+ return $.when();
}
});
@@ -673,6 +665,12 @@ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEn
* That will kill the widget in a clean way and erase its content from the dom.
*/
instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, {
+ // Backbone-ish API
+ tagName: 'div',
+ id: null,
+ className: null,
+ attributes: {},
+ events: {},
/**
* The name of the QWeb template that will be used for rendering. Must be
* redefined in subclasses or the default render() method can not be used.
@@ -689,13 +687,11 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, {
* @param {instance.web.Widget} parent Binds the current instance to the given Widget instance.
* When that widget is destroyed by calling destroy(), the current instance will be
* destroyed too. Can be null.
- * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want
- * to bind the current Widget to an already existing part of the DOM, which is not compatible
- * with the DOM insertion methods provided by the current implementation of Widget. So
- * for new components this argument should not be provided any more.
*/
init: function(parent) {
instance.web.WidgetMixin.init.call(this,parent);
+ // FIXME: this should not be
+ this.setElement(this._make_descriptive());
this.session = instance.connection;
},
/**
@@ -704,20 +700,120 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, {
* key that references `this`.
*/
renderElement: function() {
- var rendered = null;
- if (this.template)
- rendered = instance.web.qweb.render(this.template, {widget: this});
- if (_.str.trim(rendered)) {
- var elem = $(rendered);
- this.$element.replaceWith(elem);
- this.$element = elem;
+ var $el;
+ if (this.template) {
+ $el = $(_.str.trim(instance.web.qweb.render(
+ this.template, {widget: this})));
+ } else {
+ $el = this._make_descriptive();
}
+ this.replaceElement($el);
+ },
+
+ /**
+ * Re-sets the widget's root element and replaces the old root element
+ * (if any) by the new one in the DOM.
+ *
+ * @param {HTMLElement | jQuery} $el
+ * @returns {*} this
+ */
+ replaceElement: function ($el) {
+ var $oldel = this.$element;
+ this.setElement($el);
+ if ($oldel && !$oldel.is(this.$element)) {
+ $oldel.replaceWith(this.$element);
+ }
+ return this;
},
/**
- * Shortcut for $element.find() like backbone
+ * Re-sets the widget's root element (el/$el/$element).
+ *
+ * Includes:
+ * * re-delegating events
+ * * re-binding sub-elements
+ * * if the widget already had a root element, replacing the pre-existing
+ * element in the DOM
+ *
+ * @param {HTMLElement | jQuery} element new root element for the widget
+ * @return {*} this
*/
- "$": function() {
- return this.$element.find.apply(this.$element,arguments);
+ setElement: function (element) {
+ // NB: completely useless, as WidgetMixin#init creates a $element
+ // always
+ if (this.$element) {
+ this.undelegateEvents();
+ }
+
+ this.$element = (element instanceof $) ? element : $(element);
+ this.el = this.$element[0];
+
+ this.delegateEvents();
+
+ return this;
+ },
+ /**
+ * Utility function to build small DOM elements.
+ *
+ * @param {String} tagName name of the DOM element to create
+ * @param {Object} [attributes] map of DOM attributes to set on the element
+ * @param {String} [content] HTML content to set on the element
+ * @return {Element}
+ */
+ make: function (tagName, attributes, content) {
+ var el = document.createElement(tagName);
+ if (!_.isEmpty(attributes)) {
+ $(el).attr(attributes);
+ }
+ if (content) {
+ $(el).html(content);
+ }
+ return el;
+ },
+ /**
+ * Makes a potential root element from the declarative builder of the
+ * widget
+ *
+ * @return {jQuery}
+ * @private
+ */
+ _make_descriptive: function () {
+ var attrs = _.extend({}, this.attributes || {});
+ if (this.id) { attrs.id = this.id; }
+ if (this.className) { attrs['class'] = this.className; }
+ return $(this.make(this.tagName, attrs));
+ },
+ delegateEvents: function () {
+ var events = this.events;
+ if (_.isEmpty(events)) { return; }
+
+ for(var key in events) {
+ if (!events.hasOwnProperty(key)) { continue; }
+
+ var method = this.proxy(events[key]);
+
+ var match = /^(\S+)(\s+(.*))?$/.exec(key);
+ var event = match[1];
+ var selector = match[3];
+
+ event += '.widget_events';
+ if (!selector) {
+ this.$element.on(event, method);
+ } else {
+ this.$element.on(event, selector, method);
+ }
+ }
+ },
+ undelegateEvents: function () {
+ this.$element.off('.widget_events');
+ },
+ /**
+ * Shortcut for ``this.$element.find(selector)``
+ *
+ * @param {String} selector CSS selector, rooted in $el
+ * @returns {jQuery} selector match
+ */
+ $: function(selector) {
+ return this.$element.find(selector);
},
/**
* Informs the action manager to do an action. This supposes that
diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js
index 24441dc509f..12eeb1d79b4 100644
--- a/addons/web/static/src/js/coresetup.js
+++ b/addons/web/static/src/js/coresetup.js
@@ -19,15 +19,14 @@ instance.web.OldWidget = instance.web.Widget.extend({
this._super(parent);
this.element_id = element_id;
this.element_id = this.element_id || _.uniqueId('widget-');
+
var tmp = document.getElementById(this.element_id);
- this.$element = tmp ? $(tmp) : $(document.createElement(this.tagName));
+ this.setElement(tmp || this._make_descriptive());
},
renderElement: function() {
var rendered = this.render();
if (rendered) {
- var elem = $(rendered);
- this.$element.replaceWith(elem);
- this.$element = elem;
+ this.replaceElement($(rendered));
}
return this;
},
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
index c3e7ad72dcb..f9cf10e76f4 100644
--- a/addons/web/static/src/js/search.js
+++ b/addons/web/static/src/js/search.js
@@ -861,13 +861,13 @@ instance.web.search.Invalid = instance.web.Class.extend( /** @lends instance.web
);
}
});
-instance.web.search.Widget = instance.web.OldWidget.extend( /** @lends instance.web.search.Widget# */{
+instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web.search.Widget# */{
template: null,
/**
* Root class of all search widgets
*
* @constructs instance.web.search.Widget
- * @extends instance.web.OldWidget
+ * @extends instance.web.Widget
*
* @param view the ancestor view of this widget
*/
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index 1f693acbc7f..99df34d6c6c 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -101,7 +101,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
w.off('focused blurred');
w.destroy();
});
- this.$element.off('.formBlur');
+ if (this.$element) {
+ this.$element.off('.formBlur');
+ }
this._super();
},
on_loaded: function(data) {
@@ -3076,7 +3078,6 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
this.views = views;
this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
- this.viewmanager.$element.addClass("oe_view_manager_one2many");
this.viewmanager.o2m = self;
var once = $.Deferred().then(function() {
self.init_form_last_update.resolve();
@@ -4304,7 +4305,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instan
this.selection.view = this.view;
this.selection.set({force_readonly: this.get('effective_readonly')});
this.selection.on("change:value", this, this.on_selection_changed);
- this.selection.$element = $(".oe_form_view_reference_selection", this.$element);
+ this.selection.setElement(this.$(".oe_form_view_reference_selection"));
this.selection.renderElement();
this.selection.start();
this.selection
@@ -4317,7 +4318,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instan
this.m2o.view = this.view;
this.m2o.set({force_readonly: this.get("effective_readonly")});
this.m2o.on("change:value", this, this.data_changed);
- this.m2o.$element = $(".oe_form_view_reference_m2o", this.$element);
+ this.m2o.setElement(this.$(".oe_form_view_reference_m2o"));
this.m2o.renderElement();
this.m2o.start();
this.m2o
diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js
index ca889f68d8d..6c38495d0dc 100644
--- a/addons/web/static/src/js/view_list_editable.js
+++ b/addons/web/static/src/js/view_list_editable.js
@@ -96,9 +96,6 @@ openerp.web.list_editable = function (instance) {
},
on_loaded: function (data, grouped) {
var self = this;
- if (this.editor) {
- this.editor.destroy();
- }
// tree/@editable takes priority on everything else if present.
var result = this._super(data, grouped);
if (this.editable()) {
@@ -118,6 +115,7 @@ openerp.web.list_editable = function (instance) {
self.start_edition();
}
});
+ this.editor.destroy();
// Editor is not restartable due to formview not being
// restartable
this.editor = this.make_editor();
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index e47981a4284..2b79701ba4d 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -423,7 +423,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
.find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
.parent().addClass('active');
- $.when(view_promise).then(function () {
+ return $.when(view_promise).then(function () {
_.each(_.keys(self.views), function(view_name) {
var controller = self.views[view_name].controller;
if (controller) {
@@ -445,7 +445,6 @@ instance.web.ViewManager = instance.web.Widget.extend({
}
});
});
- return view_promise;
},
do_create_view: function(view_type) {
// Lazy loading of views
@@ -1175,7 +1174,7 @@ instance.web.View = instance.web.Widget.extend({
this.view_id = view_id;
this.set_default_options(options);
},
- start: function() {
+ start: function () {
return this.load_view();
},
load_view: function() {
diff --git a/addons/web/static/test/Widget.js b/addons/web/static/test/Widget.js
new file mode 100644
index 00000000000..55d9e7e7ad1
--- /dev/null
+++ b/addons/web/static/test/Widget.js
@@ -0,0 +1,237 @@
+$(document).ready(function () {
+ var $fix = $('#qunit-fixture');
+ var mod = {
+ setup: function () {
+ instance = window.openerp.init([]);
+ window.openerp.web.corelib(instance);
+
+ instance.web.qweb = new QWeb2.Engine();
+ instance.web.qweb.add_template(
+ '' +
+ '' +
+ '' +
+ '- ' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '');
+ }
+ };
+ var instance;
+
+ module('Widget.proxy', mod);
+ test('(String)', function () {
+ var W = instance.web.Widget.extend({
+ exec: function () {
+ this.executed = true;
+ }
+ });
+ var w = new W;
+ var fn = w.proxy('exec');
+ fn();
+ ok(w.executed, 'should execute the named method in the right context');
+ });
+ test('(String)(*args)', function () {
+ var W = instance.web.Widget.extend({
+ exec: function (arg) {
+ this.executed = arg;
+ }
+ });
+ var w = new W;
+ var fn = w.proxy('exec');
+ fn(42);
+ ok(w.executed, "should execute the named method in the right context");
+ equal(w.executed, 42, "should be passed the proxy's arguments");
+ });
+ test('(String), include', function () {
+ // the proxy function should handle methods being changed on the class
+ // and should always proxy "by name", to the most recent one
+ var W = instance.web.Widget.extend({
+ exec: function () {
+ this.executed = 1;
+ }
+ });
+ var w = new W;
+ var fn = w.proxy('exec');
+ W.include({
+ exec: function () { this.executed = 2; }
+ });
+
+ fn();
+ equal(w.executed, 2, "should be lazily resolved");
+ });
+
+ test('(Function)', function () {
+ var w = new (instance.web.Widget.extend({ }));
+
+ var fn = w.proxy(function () { this.executed = true; });
+ fn();
+ ok(w.executed, "should set the function's context (like Function#bind)");
+ });
+ test('(Function)(*args)', function () {
+ var w = new (instance.web.Widget.extend({ }));
+
+ var fn = w.proxy(function (arg) { this.executed = arg; });
+ fn(42);
+ equal(w.executed, 42, "should be passed the proxy's arguments");
+ });
+
+ module('Widget.renderElement', mod);
+ test('no template, default', function () {
+ var w = new (instance.web.Widget.extend({ }));
+
+ ok(!w.$element, "should not initially have a root element");
+ w.renderElement();
+ ok(w.$element, "should have generated a root element");
+ strictEqual(w.$element, w.$element, "should provide $element alias");
+ ok(w.$element.is(w.el), "should provide raw DOM alias");
+
+ equal(w.el.nodeName, 'DIV', "should have generated the default element");
+ equal(w.el.attributes.length, 0, "should not have generated any attribute");
+ ok(_.isEmpty(w.$element.html(), "should not have generated any content"));
+ });
+ test('no template, custom tag', function () {
+ var w = new (instance.web.Widget.extend({
+ tagName: 'ul'
+ }));
+ w.renderElement();
+
+ equal(w.el.nodeName, 'UL', "should have generated the custom element tag");
+ });
+ test('no template, @id', function () {
+ var w = new (instance.web.Widget.extend({
+ id: 'foo'
+ }));
+ w.renderElement();
+
+ equal(w.el.attributes.length, 1, "should have one attribute");
+ equal(w.$element.attr('id'), 'foo', "should have generated the id attribute");
+ equal(w.el.id, 'foo', "should also be available via property");
+ });
+ test('no template, @className', function () {
+ var w = new (instance.web.Widget.extend({
+ className: 'oe_some_class'
+ }));
+ w.renderElement();
+
+ equal(w.el.className, 'oe_some_class', "should have the right property");
+ equal(w.$element.attr('class'), 'oe_some_class', "should have the right attribute");
+ });
+ test('no template, bunch of attributes', function () {
+ var w = new (instance.web.Widget.extend({
+ attributes: {
+ 'id': 'some_id',
+ 'class': 'some_class',
+ 'data-foo': 'data attribute',
+ 'clark': 'gable',
+ 'spoiler': 'snape kills dumbledore'
+ }
+ }));
+ w.renderElement();
+
+ equal(w.el.attributes.length, 5, "should have all the specified attributes");
+
+ equal(w.el.id, 'some_id');
+ equal(w.$element.attr('id'), 'some_id');
+
+ equal(w.el.className, 'some_class');
+ equal(w.$element.attr('class'), 'some_class');
+
+ equal(w.$element.attr('data-foo'), 'data attribute');
+ equal(w.$element.data('foo'), 'data attribute');
+
+ equal(w.$element.attr('clark'), 'gable');
+ equal(w.$element.attr('spoiler'), 'snape kills dumbledore');
+ });
+
+ test('template', function () {
+ var w = new (instance.web.Widget.extend({
+ template: 'test.widget.template'
+ }));
+ w.renderElement();
+
+ equal(w.el.nodeName, 'OL');
+ equal(w.$element.children().length, 5);
+ equal(w.el.textContent, '01234');
+ });
+
+ module('Widget.$', mod);
+ test('basic-alias', function () {
+ var w = new (instance.web.Widget.extend({
+ template: 'test.widget.template'
+ }));
+ w.renderElement();
+
+ ok(w.$('li:eq(3)').is(w.$element.find('li:eq(3)')),
+ "should do the same thing as calling find on the widget root");
+ });
+
+ module('Widget.events', mod);
+ test('delegate', function () {
+ var a = [];
+ var w = new (instance.web.Widget.extend({
+ template: 'test.widget.template',
+ events: {
+ 'click': function () {
+ a[0] = true;
+ strictEqual(this, w, "should trigger events in widget")
+ },
+ 'click li.class-3': 'class3',
+ 'change input': function () { a[2] = true; }
+ },
+ class3: function () { a[1] = true; }
+ }));
+ w.renderElement();
+
+ w.$element.click();
+ w.$('li:eq(3)').click();
+ w.$('input:last').val('foo').change();
+
+ for(var i=0; i<3; ++i) {
+ ok(a[i], "should pass test " + i);
+ }
+ });
+ test('undelegate', function () {
+ var clicked = false, newclicked = false;
+ var w = new (instance.web.Widget.extend({
+ template: 'test.widget.template',
+ events: { 'click li': function () { clicked = true; } }
+ }));
+ w.renderElement();
+ w.$element.on('click', 'li', function () { newclicked = true });
+
+ w.$('li').click();
+ ok(clicked, "should trigger bound events");
+ ok(newclicked, "should trigger bound events");
+ clicked = newclicked = false;
+
+ w.undelegateEvents();
+ w.$('li').click();
+ ok(!clicked, "undelegate should unbind events delegated");
+ ok(newclicked, "undelegate should only unbind events it created");
+ });
+
+ module('Widget.renderElement', mod);
+ test('repeated', function () {
+ var w = new (instance.web.Widget.extend({
+ template: 'test.widget.template-value'
+ }));
+ w.value = 42;
+ w.appendTo($fix)
+ .always(start)
+ .done(function () {
+ equal($fix.find('p').text(), '42', "DOM fixture should contain initial value");
+ equal(w.$element.text(), '42', "should set initial value");
+ w.value = 36;
+ w.renderElement();
+ equal($fix.find('p').text(), '36', "DOM fixture should use new value");
+ equal(w.$element.text(), '36', "should set new value");
+ });
+ });
+});
diff --git a/addons/web/static/test/test.html b/addons/web/static/test/test.html
index cab1586f240..95518dbc8c6 100644
--- a/addons/web/static/test/test.html
+++ b/addons/web/static/test/test.html
@@ -61,5 +61,6 @@
+