[ADD] bit o' tutorial for web modules
authorXavier Morel <xmo@openerp.com>
Wed, 5 Dec 2012 13:28:54 +0000 (14:28 +0100)
committerXavier Morel <xmo@openerp.com>
Wed, 5 Dec 2012 13:28:54 +0000 (14:28 +0100)
bzr revid: xmo@openerp.com-20121205132854-j0r8qgh5u2qwlqj1

20 files changed:
addons/web/doc/module.rst [new file with mode: 0644]
addons/web/doc/module/__init__.py [new file with mode: 0644]
addons/web/doc/module/__openerp__.py [new file with mode: 0644]
addons/web/doc/module/__openerp__.py.1.diff [new file with mode: 0644]
addons/web/doc/module/__openerp__.py.2.diff [new file with mode: 0644]
addons/web/doc/module/__openerp__.py.3.diff [new file with mode: 0644]
addons/web/doc/module/__openerp__.py.4.diff [new file with mode: 0644]
addons/web/doc/module/__openerp__.py.5.diff [new file with mode: 0644]
addons/web/doc/module/static/src/css/web_example.css [new file with mode: 0644]
addons/web/doc/module/static/src/css/web_example.css.1.diff [new file with mode: 0644]
addons/web/doc/module/static/src/js/first_module.js [new file with mode: 0644]
addons/web/doc/module/static/src/js/first_module.js.1.diff [new file with mode: 0644]
addons/web/doc/module/static/src/js/first_module.js.2.diff [new file with mode: 0644]
addons/web/doc/module/static/src/js/first_module.js.3.diff [new file with mode: 0644]
addons/web/doc/module/static/src/js/first_module.js.4.diff [new file with mode: 0644]
addons/web/doc/module/static/src/js/first_module.js.5.diff [new file with mode: 0644]
addons/web/doc/module/static/src/js/first_module.js.6.diff [new file with mode: 0644]
addons/web/doc/module/static/src/xml/web_example.xml [new file with mode: 0644]
addons/web/doc/module/web_example.xml [new file with mode: 0644]
addons/web/doc/widget.rst

diff --git a/addons/web/doc/module.rst b/addons/web/doc/module.rst
new file mode 100644 (file)
index 0000000..1bdb7c1
--- /dev/null
@@ -0,0 +1,281 @@
+Building an OpenERP Web module
+==============================
+
+There is no significant distinction between an OpenERP Web module and
+an OpenERP module, the web part is mostly additional data and code
+inside a regular OpenERP module. This allows providing more seamless
+features by integrating your module deeper into the web client.
+
+A Basic Module
+--------------
+
+A very basic OpenERP module structure will be our starting point:
+
+.. code-block:: text
+
+    web_example
+    ├── __init__.py
+    └── __openerp__.py
+
+.. literalinclude:: module/__openerp__.py
+    :language: python
+
+This is a sufficient minimal declaration of a valid OpenERP module.
+
+Web Declaration
+---------------
+
+There is no such thing as a "web module" declaration. An OpenERP
+module is automatically recognized as "web-enabled" if it contains a
+``static`` directory at its root, so:
+
+.. code-block:: text
+
+    web_example
+    ├── __init__.py
+    ├── __openerp__.py
+    └── static
+
+is the extent of it. You should also change the dependency to list
+``web``:
+
+.. literalinclude:: module/__openerp__.py.1.diff
+    :language: diff
+
+.. note::
+
+    This does not matter in normal operation so you may not realize
+    it's wrong (the web module does the loading of everything else, so
+    it can only be loaded), but when e.g. testing the loading process
+    is slightly different than normal, and incorrect dependency may
+    lead to broken code.
+
+This makes the "web" discovery system consider the module as having a
+"web part", and check if it has web controllers to mount or javascript
+files to load. The content of the ``static/`` folder is also
+automatically made available to web browser at the URL
+``$module-name/static/$file-path``. This is sufficient to provide
+pictures (of cats, usually) through your module. However there are
+still a few more steps to running javascript code.
+
+Getting Things Done
+-------------------
+
+The first one is to add javascript code. It's customary to put it in
+``static/src/js``, to have room for e.g. other file types, or
+third-party libraries.
+
+.. literalinclude:: module/static/src/js/first_module.js
+    :language: javascript
+
+The client won't load any file unless specified, thus the new file
+should be listed in the module's manifest file, under a new key ``js``
+(a list of file names, or glob patterns):
+
+.. literalinclude:: module/__openerp__.py.2.diff
+    :language: diff
+
+At this point, if the module is installed and the client reloaded the
+message should appear in your browser's development console.
+
+.. note::
+
+    Because the manifest file has been edited, you will have to
+    restart the OpenERP server itself for it to be taken in account.
+
+    You may also want to open your browser's console *before*
+    reloading, depending on the browser messages printed while the
+    console is closed may not work or may not appear after opening it.
+
+.. note::
+
+    If the message does not appear, try cleaning your browser's caches
+    and ensure the file is correctly loaded from the server logs or
+    the "resources" tab of your browser's developers tools.
+
+At this point the code runs, but it runs only once when the module is
+initialized, and it can't get access to the various APIs of the web
+client (such as making RPC requests to the server). This is done by
+providing a `javascript module`_:
+
+.. literalinclude:: module/static/src/js/first_module.js.1.diff
+    :language: diff
+
+If you reload the client, you'll see a message in the console exactly
+as previously. The differences, though invisible at this point, are:
+
+* All javascript files specified in the manifest (only this one so
+  far) have been fully loaded
+* An instance of the web client and a namespace inside that instance
+  (with the same name as the module) have been created and are
+  available for use
+
+The latter point is what the ``instance`` parameter to the function
+provides: an instance of the OpenERP Web client, with the contents of
+all the new module's dependencies loaded in and initialized. These are
+the entry points to the web client's APIs.
+
+To demonstrate, let's build a simple :doc:`client action
+<client_action>`: a stopwatch
+
+First, the action declaration:
+
+.. literalinclude:: module/__openerp__.py.3.diff
+    :language: diff
+
+.. literalinclude:: module/web_example.xml
+    :language: xml
+
+then set up the :doc:`client action hook <client_action>` to register
+a function (for now):
+
+.. literalinclude:: module/static/src/js/first_module.js.2.diff
+    :language: diff
+
+Updating the module (in order to load the XML description) and
+re-starting the server should display a new menu *Example Client
+Action* at the top-level. Opening said menu will make the message
+appear, as usual, in the browser's console.
+
+Paint it black
+--------------
+
+The next step is to take control of the page itself, rather than just
+print little messages in the console. This we can do by replacing our
+client action function by a :doc:`widget`. Our widget will simply use
+its :js:func:`~openerp.web.Widget.start` to add some content to its
+DOM:
+
+.. literalinclude:: module/static/src/js/first_module.js.3.diff
+    :language: diff
+
+after reloading the client (to update the javascript file), instead of
+printing to the console the menu item clears the whole screen and
+displays the specified message in the page.
+
+Since we've added a class on the widget's :ref:`DOM root
+<widget-dom_root>` we can now see how to add a stylesheet to a module:
+first create the stylesheet file:
+
+.. literalinclude:: module/static/src/css/web_example.css
+    :language: css
+
+then add a reference to the stylesheet in the module's manifest (which
+will require restarting the OpenERP Server to see the changes, as
+usual):
+
+.. literalinclude:: module/__openerp__.py.4.diff
+    :language: diff
+
+the text displayed by the menu item should now be huge, and
+white-on-black (instead of small and black-on-white). From there on,
+the world's your canvas.
+
+.. note::
+
+    Prefixing CSS rules with both ``.openerp`` (to ensure the rule
+    will apply only within the confines of the OpenERP Web client) and
+    a class at the root of your own hierarchy of widgets is strongly
+    recommended to avoid "leaking" styles in case the code is running
+    embedded in an other web page, and does not have the whole screen
+    to itself.
+
+So far we haven't built much (any, really) DOM content. It could all
+be done in :js:func:`~openerp.web.Widget.start` but that gets unwieldy
+and hard to maintain fast. It is also very difficult to extend by
+third parties (trying to add or change things in your widgets) unless
+broken up into multiple methods which each perform a little bit of the
+rendering.
+
+The first way to handle this method is to delegate the content to
+plenty of sub-widgets, which can be individually overridden. An other
+method [#DOM-building]_ is to use `a template
+<http://en.wikipedia.org/wiki/Web_template>`_ to render a widget's
+DOM.
+
+OpenERP Web's template language is :doc:`qweb`. Although any
+templating engine can be used (e.g. `mustache
+<http://mustache.github.com/>`_ or `_.template
+<http://underscorejs.org/#template>`_) QWeb has important features
+which other template engines may not provide, and has special
+integration to OpenERP Web widgets.
+
+Adding a template file is similar to adding a style sheet:
+
+.. literalinclude:: module/static/src/xml/web_example.xml
+    :language: xml
+
+.. literalinclude:: module/__openerp__.py.5.diff
+    :language: diff
+
+The template can then easily be hooked in the widget:
+
+.. literalinclude:: module/static/src/js/first_module.js.4.diff
+    :language: diff
+
+And finally the CSS can be altered to style the new (and more complex)
+template-generated DOM, rather than the code-generated one:
+
+.. literalinclude:: module/static/src/css/web_example.css.1.diff
+    :language: diff
+
+.. note::
+
+    The last section of the CSS change is an example of "state
+    classes": a CSS class (or set of classes) on the root of the
+    widget, which is toggled when the state of the widget changes and
+    can perform drastic alterations in rendering (usually
+    showing/hiding various elements).
+
+    This pattern is both fairly simple (to read and understand) and
+    efficient (because most of the hard work is pushed to the
+    browser's CSS engine, which is usually highly optimized, and done
+    in a single repaint after toggling the class).
+
+The last step (until the next one) is to add some behavior and make
+our stopwatch watch. First hook some events on the buttons to toggle
+the widget's state:
+
+.. literalinclude:: module/static/src/js/first_module.js.5.diff
+    :language: diff
+
+This demonstrates the use of the "events hash" and event delegation to
+declaratively handle events on the widget's DOM. And already changes
+the button displayed in the UI. Then comes some actual logic:
+
+.. literalinclude:: module/static/src/js/first_module.js.6.diff
+    :language: diff
+
+* An initializer (the ``init`` method) is introduced to set-up a few
+  internal variables: ``_start`` will hold the start of the timer (as
+  a javascript Date object), and ``_watch`` will hold a ticker to
+  update the interface regularly and display the "current time".
+
+* ``update_counter`` is in charge of taking the time difference
+  between "now" and ``_start``, formatting as ``HH:MM:SS`` and
+  displaying the result on screen.
+
+* ``watch_start`` is augmented to initialize ``_start`` with its value
+  and set-up the update of the counter display every 33ms.
+
+* ``watch_stop`` disables the updater, does a final update of the
+  counter display and resets everything.
+
+* Finally, because javascript Interval and Timeout objects execute
+  "outside" the widget, they will keep going even after the widget has
+  been destroyed (especially an issue with intervals as they repeat
+  indefinitely). So ``_watch`` *must* be cleared when the widget is
+  destroyed (then the ``_super`` must be called as well in order to
+  perform the "normal" widget cleanup).
+
+Starting and stopping the watch now works, and correctly tracks time
+since having started the watch, neatly formatted.
+
+.. [#DOM-building] they are not alternative solutions: they work very
+                   well together. Templates are used to build "just
+                   DOM", sub-widgets are used to build DOM subsections
+                   *and* delegate part of the behavior (e.g. events
+                   handling).
+
+.. _javascript module:
+    http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript
diff --git a/addons/web/doc/module/__init__.py b/addons/web/doc/module/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/addons/web/doc/module/__openerp__.py b/addons/web/doc/module/__openerp__.py
new file mode 100644 (file)
index 0000000..d8c23c3
--- /dev/null
@@ -0,0 +1,7 @@
+# __openerp__.py
+{
+    'name': "Web Example",
+    'description': "Basic example of a (future) web module",
+    'category': 'Hidden',
+    'depends': ['base'],
+}
diff --git a/addons/web/doc/module/__openerp__.py.1.diff b/addons/web/doc/module/__openerp__.py.1.diff
new file mode 100644 (file)
index 0000000..dbab2e6
--- /dev/null
@@ -0,0 +1,11 @@
+--- web_example/__openerp__.py
++++ web_example/__openerp__.py
+@@ -1,7 +1,7 @@
+ # __openerp__.py
+ {
+     'name': "Web Example",
+     'description': "Basic example of a (future) web module",
+     'category': 'Hidden',
+-    'depends': ['base'],
++    'depends': ['web'],
+ }
diff --git a/addons/web/doc/module/__openerp__.py.2.diff b/addons/web/doc/module/__openerp__.py.2.diff
new file mode 100644 (file)
index 0000000..77025f1
--- /dev/null
@@ -0,0 +1,11 @@
+--- web_example/__openerp__.py
++++ web_example/__openerp__.py
+@@ -1,7 +1,8 @@
+ # __openerp__.py
+ {
+     'name': "Web Example",
+     'description': "Basic example of a (future) web module",
+     'category': 'Hidden',
+     'depends': ['web'],
++    'js': ['static/src/js/first_module.js'],
+ }
diff --git a/addons/web/doc/module/__openerp__.py.3.diff b/addons/web/doc/module/__openerp__.py.3.diff
new file mode 100644 (file)
index 0000000..e100098
--- /dev/null
@@ -0,0 +1,12 @@
+--- web_example/__openerp__.py
++++ web_example/__openerp__.py
+@@ -1,8 +1,9 @@
+ # __openerp__.py
+ {
+     'name': "Web Example",
+     'description': "Basic example of a (future) web module",
+     'category': 'Hidden',
+     'depends': ['web'],
++    'data': ['web_example.xml'],
+     'js': ['static/src/js/first_module.js'],
+ }
diff --git a/addons/web/doc/module/__openerp__.py.4.diff b/addons/web/doc/module/__openerp__.py.4.diff
new file mode 100644 (file)
index 0000000..78fc90c
--- /dev/null
@@ -0,0 +1,13 @@
+--- web_example/__openerp__.py
++++ web_example/__openerp__.py
+@@ -1,9 +1,10 @@
+ # __openerp__.py
+ {
+     'name': "Web Example",
+     'description': "Basic example of a (future) web module",
+     'category': 'Hidden',
+     'depends': ['web'],
+     'data': ['web_example.xml'],
+     'js': ['static/src/js/first_module.js'],
++    'css': ['static/src/css/web_example.css'],
+ }
diff --git a/addons/web/doc/module/__openerp__.py.5.diff b/addons/web/doc/module/__openerp__.py.5.diff
new file mode 100644 (file)
index 0000000..c949242
--- /dev/null
@@ -0,0 +1,14 @@
+--- web_example/__openerp__.py
++++ web_example/__openerp__.py
+@@ -1,10 +1,11 @@
+ # __openerp__.py
+ {
+     'name': "Web Example",
+     'description': "Basic example of a (future) web module",
+     'category': 'Hidden',
+     'depends': ['web'],
+     'data': ['web_example.xml'],
+     'js': ['static/src/js/first_module.js'],
+     'css': ['static/src/css/web_example.css'],
++    'qweb': ['static/src/xml/web_example.xml'],
+ }
diff --git a/addons/web/doc/module/static/src/css/web_example.css b/addons/web/doc/module/static/src/css/web_example.css
new file mode 100644 (file)
index 0000000..b861af8
--- /dev/null
@@ -0,0 +1,6 @@
+.openerp .oe_web_example {
+    color: white;
+    background-color: black;
+    height: 100%;
+    font-size: 400%;
+}
diff --git a/addons/web/doc/module/static/src/css/web_example.css.1.diff b/addons/web/doc/module/static/src/css/web_example.css.1.diff
new file mode 100644 (file)
index 0000000..442211a
--- /dev/null
@@ -0,0 +1,17 @@
+--- web_example/static/src/css/web_example.css
++++ web_example/static/src/css/web_example.css
+@@ -1,6 +1,13 @@
+ .openerp .oe_web_example {
+     color: white;
+     background-color: black;
+     height: 100%;
+-    font-size: 400%;
+ }
++.openerp .oe_web_example h4 {
++    margin: 0;
++    font-size: 200%;
++}
++.openerp .oe_web_example.oe_web_example_started .oe_web_example_start button,
++.openerp .oe_web_example.oe_web_example_stopped .oe_web_example_stop button {
++    display: none
++}
diff --git a/addons/web/doc/module/static/src/js/first_module.js b/addons/web/doc/module/static/src/js/first_module.js
new file mode 100644 (file)
index 0000000..74580d1
--- /dev/null
@@ -0,0 +1,2 @@
+// static/src/js/first_module.js
+console.log("Debug statement: file loaded");
diff --git a/addons/web/doc/module/static/src/js/first_module.js.1.diff b/addons/web/doc/module/static/src/js/first_module.js.1.diff
new file mode 100644 (file)
index 0000000..53c153e
--- /dev/null
@@ -0,0 +1,8 @@
+--- web_example/static/src/js/first_module.js
++++ web_example/static/src/js/first_module.js
+@@ -1,2 +1,4 @@
+ // static/src/js/first_module.js
+-console.log("Debug statement: file loaded");
++openerp.web_example = function (instance) {
++    console.log("Module loaded");
++};
diff --git a/addons/web/doc/module/static/src/js/first_module.js.2.diff b/addons/web/doc/module/static/src/js/first_module.js.2.diff
new file mode 100644 (file)
index 0000000..88a80f5
--- /dev/null
@@ -0,0 +1,11 @@
+--- web_example/static/src/js/first_module.js
++++ web_example/static/src/js/first_module.js
+@@ -1,4 +1,7 @@
+ // static/src/js/first_module.js
+ openerp.web_example = function (instance) {
+-    console.log("Module loaded");
++    instance.web.client_actions.add('example.action', 'instance.web_example.action');
++    instance.web_example.action = function (parent, action) {
++        console.log("Executed the action", action);
++    };
+ };
diff --git a/addons/web/doc/module/static/src/js/first_module.js.3.diff b/addons/web/doc/module/static/src/js/first_module.js.3.diff
new file mode 100644 (file)
index 0000000..295e90b
--- /dev/null
@@ -0,0 +1,18 @@
+--- web_example/static/src/js/first_module.js
++++ web_example/static/src/js/first_module.js
+@@ -1,7 +1,11 @@
+ // static/src/js/first_module.js
+ openerp.web_example = function (instance) {
+-    instance.web.client_actions.add('example.action', 'instance.web_example.action');
+-    instance.web_example.action = function (parent, action) {
+-        console.log("Executed the action", action);
+-    };
++    instance.web.client_actions.add('example.action', 'instance.web_example.Action');
++    instance.web_example.Action = instance.web.Widget.extend({
++        className: 'oe_web_example',
++        start: function () {
++            this.$el.text("Hello, world!");
++            return this._super();
++        }
++    });
+ };
diff --git a/addons/web/doc/module/static/src/js/first_module.js.4.diff b/addons/web/doc/module/static/src/js/first_module.js.4.diff
new file mode 100644 (file)
index 0000000..78a1921
--- /dev/null
@@ -0,0 +1,15 @@
+--- web_example/static/src/js/first_module.js
++++ web_example/static/src/js/first_module.js
+@@ -1,11 +1,7 @@
+ // static/src/js/first_module.js
+ openerp.web_example = function (instance) {
+     instance.web.client_actions.add('example.action', 'instance.web_example.Action');
+     instance.web_example.Action = instance.web.Widget.extend({
++        template: 'web_example.action'
+-        className: 'oe_web_example',
+-        start: function () {
+-            this.$el.text("Hello, world!");
+-            return this._super();
+-        }
+     });
+ };
diff --git a/addons/web/doc/module/static/src/js/first_module.js.5.diff b/addons/web/doc/module/static/src/js/first_module.js.5.diff
new file mode 100644 (file)
index 0000000..f345ea3
--- /dev/null
@@ -0,0 +1,23 @@
+--- web_example/static/src/js/first_module.js
++++ web_example/static/src/js/first_module.js
+@@ -1,7 +1,19 @@
+ // static/src/js/first_module.js
+ openerp.web_example = function (instance) {
+     instance.web.client_actions.add('example.action', 'instance.web_example.Action');
+     instance.web_example.Action = instance.web.Widget.extend({
+-        template: 'web_example.action'
++        template: 'web_example.action',
++        events: {
++            'click .oe_web_example_start button': 'watch_start',
++            'click .oe_web_example_stop button': 'watch_stop'
++        },
++        watch_start: function () {
++            this.$el.addClass('oe_web_example_started')
++                    .removeClass('oe_web_example_stopped');
++        },
++        watch_stop: function () {
++            this.$el.removeClass('oe_web_example_started')
++                    .addClass('oe_web_example_stopped');
++        },
+     });
+ };
diff --git a/addons/web/doc/module/static/src/js/first_module.js.6.diff b/addons/web/doc/module/static/src/js/first_module.js.6.diff
new file mode 100644 (file)
index 0000000..7dd17af
--- /dev/null
@@ -0,0 +1,55 @@
+--- web_example/static/src/js/first_module.js
++++ web_example/static/src/js/first_module.js
+@@ -1,19 +1,52 @@
+ // static/src/js/first_module.js
+ openerp.web_example = function (instance) {
+     instance.web.client_actions.add('example.action', 'instance.web_example.Action');
+     instance.web_example.Action = instance.web.Widget.extend({
+         template: 'web_example.action',
+         events: {
+             'click .oe_web_example_start button': 'watch_start',
+             'click .oe_web_example_stop button': 'watch_stop'
+         },
++        init: function () {
++            this._super.apply(this, arguments);
++            this._start = null;
++            this._watch = null;
++        },
++        update_counter: function () {
++            var h, m, s;
++            // Subtracting javascript dates returns the difference in milliseconds
++            var diff = new Date() - this._start;
++            s = diff / 1000;
++            m = Math.floor(s / 60);
++            s -= 60*m;
++            h = Math.floor(m / 60);
++            m -= 60*h;
++            this.$('.oe_web_example_timer').text(
++                _.str.sprintf("%02d:%02d:%02d", h, m, s));
++        },
+         watch_start: function () {
+             this.$el.addClass('oe_web_example_started')
+                     .removeClass('oe_web_example_stopped');
++            this._start = new Date();
++            // Update the UI to the current time
++            this.update_counter();
++            // Update the counter at 30 FPS (33ms/frame)
++            this._watch = setInterval(
++                this.proxy('update_counter'),
++                33);
+         },
+         watch_stop: function () {
++            clearInterval(this._watch);
++            this.update_counter();
++            this._start = this._watch = null;
+             this.$el.removeClass('oe_web_example_started')
+                     .addClass('oe_web_example_stopped');
+         },
++        destroy: function () {
++            if (this._watch) {
++                clearInterval(this._watch);
++            }
++            this._super();
++        }
+     });
+ };
diff --git a/addons/web/doc/module/static/src/xml/web_example.xml b/addons/web/doc/module/static/src/xml/web_example.xml
new file mode 100644 (file)
index 0000000..b5a9918
--- /dev/null
@@ -0,0 +1,11 @@
+<templates>
+<div t-name="web_example.action" class="oe_web_example oe_web_example_stopped">
+    <h4 class="oe_web_example_timer">00:00:00</h4>
+    <p class="oe_web_example_start">
+        <button type="button">Start</button>
+    </p>
+    <p class="oe_web_example_stop">
+        <button type="button">Stop</button>
+    </p>
+</div>
+</templates>
diff --git a/addons/web/doc/module/web_example.xml b/addons/web/doc/module/web_example.xml
new file mode 100644 (file)
index 0000000..dfcc161
--- /dev/null
@@ -0,0 +1,11 @@
+<!-- web_example/web_example.xml -->
+<openerp>
+    <data>
+        <record model="ir.actions.client" id="action_client_example">
+            <field name="name">Example Client Action</field>
+            <field name="tag">example.action</field>
+        </record>
+        <menuitem action="action_client_example"
+                  id="menu_client_example"/>
+    </data>
+</openerp>
index 7466eba..1ebc084 100644 (file)
@@ -33,6 +33,8 @@ view. It provides a number of services to handle a section of a page:
 
 * Backbone-compatible shortcuts
 
+.. _widget-dom_root:
+
 DOM Root
 --------