Xavier (Open ERP) has proposed merging 
lp:~openerp-dev/openerp-web/trunk-backbonify-widgets-xmo into lp:openerp-web.

Requested reviews:
  OpenERP R&D Web Team (openerp-dev-web)

For more details, see:
https://code.launchpad.net/~openerp-dev/openerp-web/trunk-backbonify-widgets-xmo/+merge/109844

Backboneification of the Widget API:

* $el (aliased to $element)
* el (same as $el, but raw DOM element)
* setElement(el|$el) API to programmatically set the root element of a widget
* `events` hash to declare event handlers (via jQuery delegation)
** delegateEvents and undelegateEvents to handle binding and unbinding of the 
events hash
* extension of tagName (addition of id, className and attributes) for 
template-less autogenerated DOM roots
* doc
* tests
-- 
https://code.launchpad.net/~openerp-dev/openerp-web/trunk-backbonify-widgets-xmo/+merge/109844
Your team OpenERP R&D Team is subscribed to branch 
lp:~openerp-dev/openerp-web/trunk-backbonify-widgets-xmo.
=== modified file 'addons/web/static/src/js/corelib.js'
--- addons/web/static/src/js/corelib.js	2012-05-24 15:50:15 +0000
+++ addons/web/static/src/js/corelib.js	2012-06-12 13:45:23 +0000
@@ -239,6 +239,20 @@
 };
 
 /**
+ * Utility function for APIs which can take either a value or a function which
+ * returns a value
+ *
+ * @param {Object} object
+ * @param {String} property
+ * @returns {Object}
+ */
+var getValue = function (object, property) {
+    if (object == null) { return null; }
+    var value = object[property];
+    return _.isFunction(value) ? value.call(object) : value;
+};
+
+/**
  * Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead.
  *
  * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
@@ -490,24 +504,20 @@
      *
      * 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.
      *
      * @constructs instance.web.Widget
@@ -516,14 +526,9 @@
      * @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);
     },
     /**
@@ -533,8 +538,8 @@
         _.each(this.getChildren(), function(el) {
             el.destroy();
         });
-        if(this.$element != null) {
-            this.$element.remove();
+        if(this.$el) {
+            this.$el.remove();
         }
         instance.web.PropertiesMixin.destroy.call(this);
     },
@@ -546,7 +551,7 @@
     appendTo: function(target) {
         var self = this;
         return this.__widgetRenderAndInsert(function(t) {
-            self.$element.appendTo(t);
+            self.$el.appendTo(t);
         }, target);
     },
     /**
@@ -557,7 +562,7 @@
     prependTo: function(target) {
         var self = this;
         return this.__widgetRenderAndInsert(function(t) {
-            self.$element.prependTo(t);
+            self.$el.prependTo(t);
         }, target);
     },
     /**
@@ -568,7 +573,7 @@
     insertAfter: function(target) {
         var self = this;
         return this.__widgetRenderAndInsert(function(t) {
-            self.$element.insertAfter(t);
+            self.$el.insertAfter(t);
         }, target);
     },
     /**
@@ -579,7 +584,7 @@
     insertBefore: function(target) {
         var self = this;
         return this.__widgetRenderAndInsert(function(t) {
-            self.$element.insertBefore(t);
+            self.$el.insertBefore(t);
         }, target);
     },
     /**
@@ -589,7 +594,7 @@
      */
     replace: function(target) {
         return this.__widgetRenderAndInsert(_.bind(function(t) {
-            this.$element.replaceAll(t);
+            this.$el.replaceAll(t);
         }, this), target);
     },
     __widgetRenderAndInsert: function(insertion, target) {
@@ -612,6 +617,7 @@
      * @returns {jQuery.Deferred}
      */
     start: function() {
+        return $.when();
     }
 });
 
@@ -688,12 +694,9 @@
      * @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) {
+        this._uid = _.uniqueId('-widget-');
         instance.web.WidgetMixin.init.call(this,parent);
         this.session = instance.connection;
     },
@@ -703,20 +706,105 @@
      * 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;
-        }
-    },
-    /**
-     * Shortcut for $element.find() like backbone
-     */
-    "$": function() {
-        return this.$element.find.apply(this.$element,arguments);
+        var $el;
+        if (this.template) {
+            $el = $(_.str.trim(instance.web.qweb.render(
+                this.template, {widget: this})));
+        } else {
+            var attrs = _.extend({}, getValue(this, 'attributes') || {});
+            if (this.id) { attrs.id = this.id; }
+            if (this.className) { attrs['class'] = this.className; }
+            $el = $(this.make(this.tagName, attrs))
+        }
+        var $oldel = this.$el;
+        this.setElement($el);
+        if ($oldel && !$oldel.is(this.$el)) {
+            $oldel.replaceWith(this.$el);
+        }
+    },
+
+    /**
+     * 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 {*}
+     */
+    setElement: function (element) {
+        if (this.$el) {
+            this.undelegateEvents();
+        }
+
+        this.$element = this.$el = (element instanceof $) ? element : $(element);
+        this.el = this.$el[0];
+
+        this.delegateEvents();
+
+        return this;
+    },
+
+    // Backbone-ish API
+    tagName: 'div',
+    id: null,
+    className: null,
+    attributes: {},
+    /**
+     * 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;
+    },
+    events: {},
+    delegateEvents: function () {
+        var events = getValue(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];
+
+            // add namespace for easy un-delegation
+            event += '.delegated-events' + this._uid;
+            if (!selector) {
+                this.$el.on(event, method);
+            } else {
+                this.$el.on(event, selector, method);
+            }
+        }
+    },
+    undelegateEvents: function () {
+        this.$el.off('.delegated-events' + this._uid);
+    },
+    /**
+     * Shortcut for ``this.$el.find(selector)``
+     *
+     * @param {String} selector CSS selector, rooted in $el
+     * @returns {jQuery} selector match
+     */
+    $: function(selector) {
+        return this.$el.find(selector);
     },
     /**
      * Informs the action manager to do an action. This supposes that

=== modified file 'addons/web/static/src/js/coresetup.js'
--- addons/web/static/src/js/coresetup.js	2012-05-23 15:55:23 +0000
+++ addons/web/static/src/js/coresetup.js	2012-06-12 13:45:23 +0000
@@ -19,15 +19,15 @@
         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(this.tagName));
     },
     renderElement: function() {
         var rendered = this.render();
         if (rendered) {
-            var elem = $(rendered);
-            this.$element.replaceWith(elem);
-            this.$element = elem;
+            var $el = $(rendered);
+            this.setElement($el);
         }
         return this;
     },

=== modified file 'addons/web/static/src/js/search.js'
--- addons/web/static/src/js/search.js	2012-06-08 16:59:32 +0000
+++ addons/web/static/src/js/search.js	2012-06-12 13:45:23 +0000
@@ -886,13 +886,13 @@
         );
     }
 });
-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
      */

=== added file 'addons/web/static/test/Widget.js'
--- addons/web/static/test/Widget.js	1970-01-01 00:00:00 +0000
+++ addons/web/static/test/Widget.js	2012-06-12 13:45:23 +0000
@@ -0,0 +1,248 @@
+$(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(
+            '<no>' +
+                '<t t-name="test.widget.template">' +
+                    '<ol>' +
+                        '<li t-foreach="5" t-as="counter" ' +
+                            't-attf-class="class-#{counter}">' +
+                            '<input/>' +
+                            '<t t-esc="counter"/>' +
+                        '</li>' +
+                    '</ol>' +
+                '</t>' +
+                '<t t-name="test.widget.template-value">' +
+                    '<p><t t-esc="widget.value"/></p>' +
+                '</t>' +
+            '</no>');
+        }
+    };
+    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.$el, "should not initially have a root element");
+        w.renderElement();
+        ok(w.$el, "should have generated a root element");
+        strictEqual(w.$element, w.$el, "should provide $element alias");
+        ok(w.$el.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.$el.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.$el.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.$el.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.$el.attr('id'), 'some_id');
+
+        equal(w.el.className, 'some_class');
+        equal(w.$el.attr('class'), 'some_class');
+
+        equal(w.$el.attr('data-foo'), 'data attribute');
+        equal(w.$el.data('foo'), 'data attribute');
+
+        equal(w.$el.attr('clark'), 'gable');
+        equal(w.$el.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.$el.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.$el.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.$el.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('delegate(Function)', function () {
+        var clicked = false, fn = function () {
+            return { 'click li': function () { clicked = true; } }; };
+        var w = new (instance.web.Widget.extend({
+            template: 'test.widget.template',
+            events: fn
+        }));
+        w.renderElement();
+        w.$('li:first').click();
+        ok(clicked, "should accept #events to be a function");
+    });
+    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.$el.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.$el.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.$el.text(), '36', "should set new value");
+            });
+    });
+});

=== modified file 'addons/web/static/test/test.html'
--- addons/web/static/test/test.html	2012-04-26 10:29:49 +0000
+++ addons/web/static/test/test.html	2012-06-12 13:45:23 +0000
@@ -55,4 +55,5 @@
     <script type="text/javascript" src="/web/static/test/rpc.js"></script>
     <script type="text/javascript" src="/web/static/test/evals.js"></script>
     <script type="text/javascript" src="/web/static/test/search.js"></script>
+    <script type="text/javascript" src="/web/static/test/Widget.js"></script>
 </html>

=== modified file 'doc/addons.rst'
--- doc/addons.rst	2012-03-05 12:40:16 +0000
+++ doc/addons.rst	2012-06-12 13:45:23 +0000
@@ -113,103 +113,6 @@
 Creating new standard roles
 ---------------------------
 
-Widget
-++++++
-
-This is the base class for all visual components. It provides a number of
-services for the management of a DOM subtree:
-
-* Rendering with QWeb
-
-* Parenting-child relations
-
-* Life-cycle management (including facilitating children destruction when a
-  parent object is removed)
-
-* DOM insertion, via jQuery-powered insertion methods. Insertion targets can
-  be anything the corresponding jQuery method accepts (generally selectors,
-  DOM nodes and jQuery objects):
-
-  :js:func:`~openerp.base.Widget.appendTo`
-    Renders the widget and inserts it as the last child of the target, uses
-    `.appendTo()`_
-
-  :js:func:`~openerp.base.Widget.prependTo`
-    Renders the widget and inserts it as the first child of the target, uses
-    `.prependTo()`_
-
-  :js:func:`~openerp.base.Widget.insertAfter`
-    Renders the widget and inserts it as the preceding sibling of the target,
-    uses `.insertAfter()`_
-
-  :js:func:`~openerp.base.Widget.insertBefore`
-    Renders the widget and inserts it as the following sibling of the target,
-    uses `.insertBefore()`_
-
-:js:class:`~openerp.base.Widget` inherits from
-:js:class:`~openerp.base.SessionAware`, so subclasses can easily access the
-RPC layers.
-
-Subclassing Widget
-~~~~~~~~~~~~~~~~~~
-
-:js:class:`~openerp.base.Widget` is subclassed in the standard manner (via the
-:js:func:`~openerp.base.Class.extend` method), and provides a number of
-abstract properties and concrete methods (which you may or may not want to
-override). Creating a subclass looks like this:
-
-.. code-block:: javascript
-
-    var MyWidget = openerp.base.Widget.extend({
-        // QWeb template to use when rendering the object
-        template: "MyQWebTemplate",
-
-        init: function(parent) {
-            this._super(parent);
-            // insert code to execute before rendering, for object
-            // initialization
-        },
-        start: function() {
-            this._super();
-            // post-rendering initialization code, at this point
-            // ``this.$element`` has been initialized
-            this.$element.find(".my_button").click(/* an example of event binding * /);
-
-            // if ``start`` is asynchronous, return a promise object so callers
-            // know when the object is done initializing
-            return this.rpc(/* … */)
-        }
-    });
-
-The new class can then be used in the following manner:
-
-.. code-block:: javascript
-
-    // Create the instance
-    var my_widget = new MyWidget(this);
-    // Render and insert into DOM
-    my_widget.appendTo(".some-div");
-
-After these two lines have executed (and any promise returned by ``appendTo``
-has been resolved if needed), the widget is ready to be used.
-
-.. note:: the insertion methods will start the widget themselves, and will
-          return the result of :js:func:`~openerp.base.Widget.start()`.
-
-          If for some reason you do not want to call these methods, you will
-          have to first call :js:func:`~openerp.base.Widget.render()` on the
-          widget, then insert it into your DOM and start it.
-
-If the widget is not needed anymore (because it's transient), simply terminate
-it:
-
-.. code-block:: javascript
-
-    my_widget.stop();
-
-will unbind all DOM events, remove the widget's content from the DOM and
-destroy all widget data.
-
 Views
 +++++
 
@@ -541,18 +444,6 @@
 .. _promise object:
     http://api.jquery.com/deferred.promise/
 
-.. _.appendTo():
-    http://api.jquery.com/appendTo/
-
-.. _.prependTo():
-    http://api.jquery.com/prependTo/
-
-.. _.insertAfter():
-    http://api.jquery.com/insertAfter/
-
-.. _.insertBefore():
-    http://api.jquery.com/insertBefore/
-
 .. _Rosetta:
 .. _Launchpad's own translation tool:
     https://help.launchpad.net/Translations

=== modified file 'doc/index.rst'
--- doc/index.rst	2012-04-24 16:07:59 +0000
+++ doc/index.rst	2012-06-12 13:45:23 +0000
@@ -16,6 +16,7 @@
     async
     rpc
 
+    widget
     search-view
 
 Older stuff

=== added file 'doc/widget.rst'
--- doc/widget.rst	1970-01-01 00:00:00 +0000
+++ doc/widget.rst	2012-06-12 13:45:23 +0000
@@ -0,0 +1,280 @@
+User Interaction: Widget
+========================
+
+This is the base class for all visual components. It corresponds to an MVC
+view. It provides a number of services to handle a section of a page:
+
+* Rendering with QWeb
+
+* Parenting-child relations
+
+* Life-cycle management (including facilitating children destruction when a
+  parent object is removed)
+
+* DOM insertion, via jQuery-powered insertion methods. Insertion targets can
+  be anything the corresponding jQuery method accepts (generally selectors,
+  DOM nodes and jQuery objects):
+
+  :js:func:`~openerp.base.Widget.appendTo`
+    Renders the widget and inserts it as the last child of the target, uses
+    `.appendTo()`_
+
+  :js:func:`~openerp.base.Widget.prependTo`
+    Renders the widget and inserts it as the first child of the target, uses
+    `.prependTo()`_
+
+  :js:func:`~openerp.base.Widget.insertAfter`
+    Renders the widget and inserts it as the preceding sibling of the target,
+    uses `.insertAfter()`_
+
+  :js:func:`~openerp.base.Widget.insertBefore`
+    Renders the widget and inserts it as the following sibling of the target,
+    uses `.insertBefore()`_
+
+* Backbone-compatible shortcuts
+
+DOM Root
+--------
+
+A :js:class:`~openerp.web.Widget` is responsible for a section of the
+page materialized by the DOM root of the widget. The DOM root is
+available via the :js:attr:`~openerp.web.Widget.el` and
+:js:attr:`~openerp.web.Widget.$el` attributes, which are respectively
+the raw DOM Element and the jQuery wrapper around the DOM element.
+
+.. note::
+
+    both attributes are compatible with Backbone's equivalent, there
+    is also the :js:attr:`~openerp.web.Widget.$element` attribute
+    which aliases to :js:attr:`~openerp.web.Widget.$el` and remains
+    for backwards compatiblity reasons.
+
+There are two main ways to define and generate this DOM root:
+
+.. js:attribute:: openerp.web.Widget.template
+
+    Should be set to the name of a QWeb template (a
+    :js:class:`String`). If set, the template will be rendered after
+    the widget has been initialized but before it has been
+    started. The root element generated by the template will be set as
+    the DOM root of the widget.
+
+.. js:attribute:: openerp.web.Widget.tagName
+
+    Used if the widget has no template defined. Defaults to ``div``,
+    will be used as the tag name to create the DOM element to set as
+    the widget's DOM root. It is possible to further customize this
+    generated DOM root with the following attributes:
+
+    .. js:attribute:: openerp.web.Widget.id
+
+        Used to generate an ``id`` attribute on the generated DOM
+        root.
+
+    .. js:attribute:: openerp.web.Widget.className
+
+        Used to generate a ``class`` attribute on the generated DOM root.
+
+    .. js:attribute:: openerp.web.Widget.attributes
+
+        Mapping (object literal) of attribute names to attribute
+        values. Each of these k:v pairs will be set as a DOM attribute
+        on the generated DOM root.
+
+    None of these is used in case a template is specified on the widget.
+
+The DOM root can also be defined programmatically by overridding
+
+.. js:function:: openerp.web.Widget.renderElement
+
+    Renders the widget's DOM root and sets it. The default
+    implementation will render a set template or generate an element
+    as described above, and will call
+    :js:func:`~openerp.web.Widget.setElement` on the result.
+
+    Any override to :js:func:`~openerp.web.Widget.renderElement` which
+    does not call its ``_super`` **must** call
+    :js:func:`~openerp.web.Widget.setElement` with whatever it
+    generated or the widget's behavior is undefined.r
+
+    .. note::
+
+        The default :js:func:`~openerp.web.Widget.renderElement` can
+        be called repeatedly, it will *replace* the previous DOM root
+        (using ``replaceWith``). However, this requires that the
+        widget correctly sets and unsets its events (and children
+        widgets). Generally,
+        :js:func:`~openerp.web.Widget.renderElement` should not be
+        called repeatedly unless the widget advertizes this feature.
+
+Accessing DOM content
+~~~~~~~~~~~~~~~~~~~~~
+
+Because a widget is only responsible for the content below its DOM
+root, there is a shortcut for selecting sub-sections of a widget's
+DOM:
+
+.. js:function:: openerp.web.Widget.$(selector)
+
+    Applies the CSS selector specified as parameter to the widget's
+    DOM root.
+
+    .. code-block:: javascript
+
+        this.$(selector);
+
+    is functionally identical to:
+
+    .. code-block:: javascript
+
+        this.$element.find(selector);
+
+    :param String selector: CSS selector
+    :returns: jQuery object
+
+    .. note:: this helper method is compatible with
+              ``Backbone.View.$``
+
+Resetting the DOM root
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. js:function:: openerp.web.Widget.setElement(element)
+
+    Re-sets the widget's DOM root to the provided element, also
+    handles re-setting the various aliases of the DOM root as well as
+    unsetting and re-setting delegated events.
+
+    :param Element element: a DOM element or jQuery object to set as
+                            the widget's DOM root
+
+    .. note:: should be mostly compatible with `Backbone's
+              setElement`_
+
+DOM events handling
+-------------------
+
+A widget will generally need to respond to user action within its
+section of the page. This entails binding events to DOM elements.
+
+To this end, :js:class:`~openerp.web.Widget` provides an shortcut:
+
+.. js:attribute:: openerp.web.Widget.events
+
+    Events are a mapping of ``event selector`` (an event name and a
+    CSS selector separated by a space) to a callback. The callback can
+    be either a method name in the widget or a function. In either
+    case, the ``this`` will be set to the widget.
+
+    The selector is used for jQuery's `event delegation`_, the
+    callback will only be triggered for descendants of the DOM root
+    matching the selector [0]_. If the selector is left out (only an
+    event name is specified), the event will be set directly on the
+    widget's DOM root.
+
+.. js:function:: openerp.web.Widget.delegateEvents
+
+    This method is in charge of binding
+    :js:attr:`~openerp.web.Widget.events` to the DOM. It is
+    automatically called after setting the widget's DOM root.
+
+    It can be overridden to set up more complex events than the
+    :js:attr:`~openerp.web.Widget.events` map allows, but the parent
+    should always be called (or :js:attr:`~openerp.web.Widget.events`
+    won't be handled correctly).
+
+.. js:function:: openerp.web.Widget.undelegateEvents
+
+    This method is in charge of unbinding
+    :js:attr:`~openerp.web.Widget.events` from the DOM root when the
+    widget is destroyed or the DOM root is reset, in order to avoid
+    leaving "phantom" events.
+
+    It should be overridden to un-set any event set in an override of
+    :js:func:`~openerp.web.Widget.delegateEvents`.
+
+.. note:: this behavior should be compatible with `Backbone's
+          delegateEvents`_, apart from not accepting any argument.
+
+Subclassing Widget
+------------------
+
+:js:class:`~openerp.base.Widget` is subclassed in the standard manner (via the
+:js:func:`~openerp.base.Class.extend` method), and provides a number of
+abstract properties and concrete methods (which you may or may not want to
+override). Creating a subclass looks like this:
+
+.. code-block:: javascript
+
+    var MyWidget = openerp.base.Widget.extend({
+        // QWeb template to use when rendering the object
+        template: "MyQWebTemplate",
+
+        init: function(parent) {
+            this._super(parent);
+            // insert code to execute before rendering, for object
+            // initialization
+        },
+        start: function() {
+            this._super();
+            // post-rendering initialization code, at this point
+            // ``this.$element`` has been initialized
+            this.$element.find(".my_button").click(/* an example of event binding * /);
+
+            // if ``start`` is asynchronous, return a promise object so callers
+            // know when the object is done initializing
+            return this.rpc(/* … */)
+        }
+    });
+
+The new class can then be used in the following manner:
+
+.. code-block:: javascript
+
+    // Create the instance
+    var my_widget = new MyWidget(this);
+    // Render and insert into DOM
+    my_widget.appendTo(".some-div");
+
+After these two lines have executed (and any promise returned by ``appendTo``
+has been resolved if needed), the widget is ready to be used.
+
+.. note:: the insertion methods will start the widget themselves, and will
+          return the result of :js:func:`~openerp.base.Widget.start()`.
+
+          If for some reason you do not want to call these methods, you will
+          have to first call :js:func:`~openerp.base.Widget.render()` on the
+          widget, then insert it into your DOM and start it.
+
+If the widget is not needed anymore (because it's transient), simply terminate
+it:
+
+.. code-block:: javascript
+
+    my_widget.destroy();
+
+will unbind all DOM events, remove the widget's content from the DOM and
+destroy all widget data.
+
+.. [0] not all DOM events are compatible with events delegation
+
+.. _.appendTo():
+    http://api.jquery.com/appendTo/
+
+.. _.prependTo():
+    http://api.jquery.com/prependTo/
+
+.. _.insertAfter():
+    http://api.jquery.com/insertAfter/
+
+.. _.insertBefore():
+    http://api.jquery.com/insertBefore/
+
+.. _event delegation:
+    http://api.jquery.com/delegate/
+
+.. _Backbone's setElement:
+    http://backbonejs.org/#View-setElement
+
+.. _Backbone's delegateEvents:
+    http://backbonejs.org/#View-delegateEvents
+

_______________________________________________
Mailing list: https://launchpad.net/~openerp-dev-gtk
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~openerp-dev-gtk
More help   : https://help.launchpad.net/ListHelp

Reply via email to