diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst index 511a271d30e..bad653e0da7 100644 --- a/addons/web/doc/index.rst +++ b/addons/web/doc/index.rst @@ -34,7 +34,6 @@ Javascript :maxdepth: 1 guidelines - widget rpc async client_action diff --git a/addons/web/doc/widget.rst b/addons/web/doc/widget.rst deleted file mode 100644 index 7c4a2374b3e..00000000000 --- a/addons/web/doc/widget.rst +++ /dev/null @@ -1,287 +0,0 @@ -Widget -====== - -.. js:class:: openerp.web.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.web.Widget.appendTo` - Renders the widget and inserts it as the last child of the target, uses - `.appendTo()`_ - - :js:func:`~openerp.web.Widget.prependTo` - Renders the widget and inserts it as the first child of the target, uses - `.prependTo()`_ - - :js:func:`~openerp.web.Widget.insertAfter` - Renders the widget and inserts it as the preceding sibling of the target, - uses `.insertAfter()`_ - - :js:func:`~openerp.web.Widget.insertBefore` - Renders the widget and inserts it as the following sibling of the target, - uses `.insertBefore()`_ - -* Backbone-compatible shortcuts - -.. _widget-dom_root: - -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. - -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. - - .. 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.$el.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: - - .. code-block:: javascript - - events: { - 'click p.oe_some_class a': 'some_method', - 'change input': function (e) { - e.stopPropagation(); - } - }, - - 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 - diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js index f190fefeba3..de8f5f660a3 100644 --- a/addons/web/static/src/js/openerpframework.js +++ b/addons/web/static/src/js/openerpframework.js @@ -258,24 +258,24 @@ openerp.ParentedMixin = { current object is destroyed. */ alive: function(promise, reject) { - var def = $.Deferred(); var self = this; - promise.done(function() { - if (! self.isDestroyed()) { - if (! reject) + return $.Deferred(function (def) { + promise.then(function () { + if (!self.isDestroyed()) { def.resolve.apply(def, arguments); - else - def.reject(); - } - }).fail(function() { - if (! self.isDestroyed()) { - if (! reject) + } + }, function () { + if (!self.isDestroyed()) { def.reject.apply(def, arguments); - else + } + }).always(function () { + if (reject) { + // noop if def already resolved or rejected def.reject(); - } - }); - return def.promise(); + } + // otherwise leave promise in limbo + }); + }).promise(); }, /** * Inform the object it should destroy itself, releasing any diff --git a/addons/web/static/test/framework.js b/addons/web/static/test/framework.js index 237bdc195ca..3fff6b7bcb3 100644 --- a/addons/web/static/test/framework.js +++ b/addons/web/static/test/framework.js @@ -400,8 +400,68 @@ ropenerp.testing.section('Widget.events', { ok(newclicked, "undelegate should only unbind events it created"); }); }); +ropenerp.testing.section('Widget.async', { -ropenerp.testing.section('server-formats', { +}, function (test) { + test("alive(alive)", {asserts: 1}, function () { + var w = new (openerp.Widget.extend({})); + return $.async_when(w.start()) + .then(function () { return w.alive($.async_when()) }) + .then(function () { ok(true); }); + }); + test("alive(dead)", {asserts: 1}, function () { + var w = new (openerp.Widget.extend({})); + + return $.Deferred(function (d) { + $.async_when(w.start()) + .then(function () { + // destroy widget + w.destroy(); + var promise = $.async_when(); + // leave time for alive() to do its stuff + promise.then(function () { + return $.async_when(); + }).then(function () { + ok(true); + d.resolve(); + }); + // ensure that w.alive() refuses to resolve or reject + return w.alive(promise); + }).always(function () { + d.reject(); + ok(false, "alive() should not terminate by default"); + }) + }); + }); + + + test("alive(alive, true)", {asserts: 1}, function () { + var w = new (openerp.Widget.extend({})); + return $.async_when(w.start()) + .then(function () { return w.alive($.async_when(), true) }) + .then(function () { ok(true); }); + }); + test("alive(dead, true)", {asserts: 1, fail_on_rejection: false}, function () { + var w = new (openerp.Widget.extend({})); + + return $.async_when(w.start()) + .then(function () { + // destroy widget + w.destroy(); + console.log('destroyed'); + return w.alive($.async_when().done(function () { console.log('when'); }), true); + }).then(function () { + console.log('unfailed') + ok(false, "alive(p, true) should fail its promise"); + }, function () { + console.log('failed') + ok(true, "alive(p, true) should fail its promise"); + }); + }); + +}); + + ropenerp.testing.section('server-formats', { dependencies: ['web.core', 'web.dates'] }, function (test) { test('Parse server datetime', function () { diff --git a/doc/_themes/odoodoc/static/style.css b/doc/_themes/odoodoc/static/style.css index 9fb81c99d74..9b5468f1bdd 100644 --- a/doc/_themes/odoodoc/static/style.css +++ b/doc/_themes/odoodoc/static/style.css @@ -6714,3 +6714,6 @@ pre { word-break: normal; word-wrap: normal; } +.descclassname { + opacity: 0.5; +} diff --git a/doc/_themes/odoodoc/static/style.less b/doc/_themes/odoodoc/static/style.less index e5c0030b1eb..ca8be90d4c2 100644 --- a/doc/_themes/odoodoc/static/style.less +++ b/doc/_themes/odoodoc/static/style.less @@ -551,3 +551,8 @@ pre { word-break: normal; word-wrap: normal; } + +// lighten js namespace/class name +.descclassname { + opacity: 0.5; +} diff --git a/doc/reference/javascript.rst b/doc/reference/javascript.rst index 07f38b77fc1..18491eae98d 100644 --- a/doc/reference/javascript.rst +++ b/doc/reference/javascript.rst @@ -1,3 +1,7 @@ +.. highlight:: javascript + +.. default-domain:: js + ========== Javascript ========== @@ -5,8 +9,400 @@ Javascript Widgets ======= -.. qweb integration: ``template`` is an (optional) automatically rendered - template +.. class:: openerp.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): + + :func:`~openerp.Widget.appendTo` + Renders the widget and inserts it as the last child of the target, uses + `.appendTo()`_ + + :func:`~openerp.Widget.prependTo` + Renders the widget and inserts it as the first child of the target, uses + `.prependTo()`_ + + :func:`~openerp.Widget.insertAfter` + Renders the widget and inserts it as the preceding sibling of the target, + uses `.insertAfter()`_ + + :func:`~openerp.Widget.insertBefore` + Renders the widget and inserts it as the following sibling of the target, + uses `.insertBefore()`_ + +* Backbone-compatible shortcuts + +.. _widget-dom_root: + +DOM Root +-------- + +A :class:`~openerp.Widget` is responsible for a section of the +page materialized by the DOM root of the widget. + +A widget's DOM root is available via two attributes: + +.. attribute:: openerp.Widget.el + + raw DOM element set as root to the widget + +.. attribute:: openerp.Widget.$el + + jQuery wrapper around :attr:`~openerp.Widget.el` + +There are two main ways to define and generate this DOM root: + +.. attribute:: openerp.Widget.template + + Should be set to the name of a :ref:`QWeb template `. + 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. + +.. attribute:: openerp.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: + + .. attribute:: openerp.Widget.id + + Used to generate an ``id`` attribute on the generated DOM + root. + + .. attribute:: openerp.Widget.className + + Used to generate a ``class`` attribute on the generated DOM root. + + .. attribute:: openerp.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 + +.. function:: openerp.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 + :func:`~openerp.Widget.setElement` on the result. + + Any override to :func:`~openerp.Widget.renderElement` which + does not call its ``_super`` **must** call + :func:`~openerp.Widget.setElement` with whatever it + generated or the widget's behavior is undefined. + + .. note:: + + The default :func:`~openerp.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, :func:`~openerp.Widget.renderElement` should + not be called repeatedly unless the widget advertizes this feature. + +Using a widget +'''''''''''''' + +A widget's lifecycle has 3 main phases: + +* creation and initialization of the widget instance + + .. function:: openerp.Widget.init(parent) + + initialization method of widgets, synchronous, can be overridden to + take more parameters from the widget's creator/parent + + :param parent: the current widget's parent, used to handle automatic + destruction and even propagation. Can be ``null`` for + the widget to have no parent. + :type parent: :class:`~openerp.Widget` + +* DOM injection and startup, this is done by calling one of: + + .. function:: openerp.Widget.appendTo(element) + + Renders the widget and inserts it as the last child of the target, uses + `.appendTo()`_ + + .. function:: openerp.Widget.prependTo(element) + + Renders the widget and inserts it as the first child of the target, uses + `.prependTo()`_ + + .. function:: openerp.Widget.insertAfter(element) + + Renders the widget and inserts it as the preceding sibling of the target, + uses `.insertAfter()`_ + + .. function:: openerp.Widget.insertBefore(element) + + Renders the widget and inserts it as the following sibling of the target, + uses `.insertBefore()`_ + + All of these methods accept whatever the corresponding jQuery method accepts + (CSS selectors, DOM nodes or jQuery objects). They all return a promise and + are charged with three tasks: + + * render the widget's root element via + :func:`~openerp.Widget.renderElement` + * insert the widget's root element in the DOM using whichever jQuery method + they match + * start the widget, and return the result of starting it + + .. function:: openerp.Widget.start() + + asynchronous startup of the widget once it's been injected in the DOM, + generally used to perform asynchronous RPC calls to fetch whatever + remote data is necessary for the widget to do its work. + + Must return a deferred_ to indicate when its work is done. + + A widget is *not guaranteed* to work correctly until its + :func:`~openerp.Widget.start` method has finished executing. The + widget's parent/creator must wait for a widget to be fully started + before interacting with it + + :returns: deferred_ object + +* widget destruction and cleanup + + .. function:: openerp.Widget.destroy() + + destroys the widget's children, unbinds its events and removes its root + from the DOM. Automatically called when the widget's parent is destroyed, + must be called explicitly if the widget has no parents or if it is + removed but its parent remains. + + A widget being destroyed is automatically unlinked from its parent. + +Because a widget can be destroyed at any time, widgets also have utility +methods to handle this case: + +.. function:: openerp.Widget.alive(deferred[, reject=false]) + + A significant issue with RPC and destruction is that an RPC call may take + a long time to execute and return while a widget is being destroyed or + after it has been destroyed, trying to execute its operations on a widget + in a broken/invalid state. + + This is a frequent source of errors or strange behaviors. + + :func:`~openerp.Widget.alive` can be used to wrap an RPC call, + ensuring that whatever operations should be executed when the call ends + are only executed if the widget is still alive:: + + this.alive(this.model.query().all()).then(function (records) { + // would break if executed after the widget is destroyed, wrapping + // rpc in alive() prevents execution + _.each(records, function (record) { + self.$el.append(self.format(record)); + }); + }); + + :param deferred: a deferred_ object to wrap + :param reject: by default, if the RPC call returns after the widget has + been destroyed the returned deferred_ is left in limbo + (neither resolved nor rejected). If ``reject`` is set to + ``true``, the deferred_ will be rejected instead. + :returns: deferred_ object + +.. function:: openerp.Widget.isDestroyed() + + :returns: ``true`` if the widget is being or has been destroyed, ``false`` + otherwise + +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: + +.. function:: openerp.Widget.$(selector) + + Applies the CSS selector specified as parameter to the widget's + DOM root. + + :: + + this.$(selector); + + is functionally identical to:: + + this.$el.find(selector); + + :param String selector: CSS selector + :returns: jQuery object + + .. note:: this helper method is compatible with + ``Backbone.View.$`` + +Resetting the DOM root +'''''''''''''''''''''' + +.. function:: openerp.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, :class:`~openerp.Widget` provides an shortcut: + +.. attribute:: openerp.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 the name of a widget's method or a function object. In either case, the + ``this`` will be set to the widget:: + + events: { + 'click p.oe_some_class a': 'some_method', + 'change input': function (e) { + e.stopPropagation(); + } + }, + + The selector is used for jQuery's `event delegation`_, the + callback will only be triggered for descendants of the DOM root + matching the selector\ [#eventsdelegation]_. If the selector is left out + (only an event name is specified), the event will be set directly on the + widget's DOM root. + +.. function:: openerp.Widget.delegateEvents + + This method is in charge of binding + :attr:`~openerp.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 + :attr:`~openerp.Widget.events` map allows, but the parent + should always be called (or :attr:`~openerp.Widget.events` + won't be handled correctly). + +.. function:: openerp.Widget.undelegateEvents + + This method is in charge of unbinding + :attr:`~openerp.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 + :func:`~openerp.Widget.delegateEvents`. + +.. note:: this behavior should be compatible with `Backbone's + delegateEvents`_, apart from not accepting any argument. + +Subclassing Widget +------------------ + +:class:`~openerp.Widget` is subclassed in the standard manner (via the +:func:`~openerp.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:: + + var MyWidget = openerp.Widget.extend({ + // QWeb template to use when rendering the object + template: "MyQWebTemplate", + events: { + // events binding example + 'click .my-button': 'handle_click', + }, + + init: function(parent) { + this._super(parent); + // insert code to execute before rendering, for object + // initialization + }, + start: function() { + var sup = this._super(); + // post-rendering initialization code, at this point + + // allows multiplexing deferred objects + return $.when( + // propagate asynchronous signal from parent class + sup, + // return own's asynchronous signal + this.rpc(/* … */)) + } + }); + +The new class can then be used in the following manner:: + + // 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 +:func:`~openerp.Widget.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 :func:`~openerp.Widget.start()`. + + If for some reason you do not want to call these methods, you will + have to first call :func:`~openerp.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:: + + my_widget.destroy(); + +will unbind all DOM events, remove the widget's content from the DOM and +destroy all widget data. + +.. _.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 + +.. _deferred: http://api.jquery.com/category/deferred-object/ RPC === @@ -15,3 +411,6 @@ RPC Web Client ========== + +.. [#eventsdelegation] not all DOM events are compatible with events delegation +