[ADD] JS widgets reference documentation

Also fixed Widget#alive's behavior and tested it
This commit is contained in:
Xavier Morel 2014-09-12 14:54:27 +02:00
parent 2c25fa2dc4
commit 62fcce9054
7 changed files with 484 additions and 305 deletions

View File

@ -34,7 +34,6 @@ Javascript
:maxdepth: 1
guidelines
widget
rpc
async
client_action

View File

@ -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

View File

@ -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

View File

@ -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 () {

View File

@ -6714,3 +6714,6 @@ pre {
word-break: normal;
word-wrap: normal;
}
.descclassname {
opacity: 0.5;
}

View File

@ -551,3 +551,8 @@ pre {
word-break: normal;
word-wrap: normal;
}
// lighten js namespace/class name
.descclassname {
opacity: 0.5;
}

View File

@ -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 <reference/qweb>`.
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