summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/core/mvc.js242
1 files changed, 242 insertions, 0 deletions
diff --git a/addons/web/static/src/js/core/mvc.js b/addons/web/static/src/js/core/mvc.js
new file mode 100644
index 00000000000..cdef88c4211
--- /dev/null
+++ b/addons/web/static/src/js/core/mvc.js
@@ -0,0 +1,242 @@
+odoo.define('web.mvc', function (require) {
+"use strict";
+
+/**
+ * This file contains a 'formalization' of a MVC pattern, applied to Odoo
+ * idioms.
+ *
+ * For a simple widget/component, this is definitely overkill. However, when
+ * working on complex systems, such as Odoo views (or the control panel, or some
+ * client actions), it is useful to clearly separate the code in concerns.
+ *
+ * We define here 4 classes: Factory, Model, Renderer, Controller. Note that
+ * for various historical reasons, we use the term Renderer instead of View. The
+ * main issue is that the term 'View' is used way too much in Odoo land, and
+ * adding it would increase the confusion.
+ *
+ * In short, here are the responsabilities of the four classes:
+ * - Model: this is where the main state of the system lives. This is the part
+ * that will talk to the server, process the results and is the owner of the
+ * state
+ * - Renderer: this is the UI code: it should only be concerned with the look
+ * and feel of the system: rendering, binding handlers, ...
+ * - Controller: coordinates the model with the renderer and the parents widgets.
+ * This is more a 'plumbing' widget.
+ * - Factory: setting up the MRC components is a complex task, because each of
+ * them needs the proper arguments/options, it needs to be extensible, they
+ * needs to be created in the proper order, ... The job of the factory is
+ * to process all the various arguments, and make sure each component is as
+ * simple as possible.
+ */
+
+var ajax = require('web.ajax');
+var Class = require('web.Class');
+var mixins = require('web.mixins');
+var ServicesMixin = require('web.ServicesMixin');
+var Widget = require('web.Widget');
+
+
+/**
+ * Owner of the state, this component is tasked with fetching data, processing
+ * it, updating it, ...
+ *
+ * Note that this is not a widget: it is a class which has not UI representation.
+ *
+ * @class Model
+ */
+var Model = Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
+ /**
+ * @param {Widget} parent
+ * @param {Object} params
+ */
+ init: function (parent, params) {
+ mixins.EventDispatcherMixin.init.call(this);
+ this.setParent(parent);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * This method should return the complete state necessary for the renderer
+ * to display the current data.
+ *
+ * @returns {*}
+ */
+ get: function () {
+ },
+ /**
+ * The load method is called once in a model, when we load the data for the
+ * first time. The method returns (a promise that resolves to) some kind
+ * of token/handle. The handle can then be used with the get method to
+ * access a representation of the data.
+ *
+ * @param {Object} params
+ * @returns {Promise} The promise resolves to some kind of handle
+ */
+ load: function () {
+ return $.when();
+ },
+});
+
+/**
+ * Only responsability of this component is to display the user interface, and
+ * react to user changes.
+ *
+ * @class Renderer
+ */
+var Renderer = Widget.extend({
+ /**
+ * @override
+ * @param {any} state
+ * @param {Object} params
+ */
+ init: function (parent, state, params) {
+ this._super(parent);
+ this.state = state;
+ },
+});
+
+/**
+ * The controller has to coordinate between parent, renderer and model.
+ *
+ * @class Controller
+ */
+var Controller = Widget.extend({
+ /**
+ * @override
+ * @param {Model} model
+ * @param {Renderer} renderer
+ * @param {Object} params
+ */
+ init: function (parent, model, renderer, params) {
+ this._super.apply(this, arguments);
+ this.model = model;
+ this.renderer = renderer;
+ },
+ /**
+ * @returns {Promise}
+ */
+ start: function () {
+ return $.when(
+ this._super.apply(this, arguments),
+ this._startRenderer()
+ );
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Appends the renderer in the $el. To override to insert it elsewhere.
+ *
+ * @private
+ */
+ _startRenderer: function () {
+ return this.renderer.appendTo(this.$el);
+ },
+});
+
+var Factory = Class.extend({
+ config: {
+ Model: Model,
+ Renderer: Renderer,
+ Controller: Controller,
+ },
+ /**
+ * @override
+ */
+ init: function () {
+ this.rendererParams = {};
+ this.controllerParams = {};
+ this.modelParams = {};
+ this.loadParams = {};
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Main method of the Factory class. Create a controller, and make sure that
+ * data and libraries are loaded.
+ *
+ * There is a unusual thing going in this method with parents: we create
+ * renderer/model with parent as parent, then we have to reassign them at
+ * the end to make sure that we have the proper relationships. This is
+ * necessary to solve the problem that the controller needs the model and
+ * the renderer to be instantiated, but the model need a parent to be able
+ * to load itself, and the renderer needs the data in its constructor.
+ *
+ * @param {Widget} parent the parent of the resulting Controller (most
+ * likely an action manager)
+ * @returns {Promise<Controller>}
+ */
+ getController: function (parent) {
+ var self = this;
+ var model = this.getModel(parent);
+ return $.when(this._loadData(model), ajax.loadLibs(this)).then(function (state) {
+ var renderer = self.getRenderer(parent, state);
+ var Controller = self.Controller || self.config.Controller;
+ var controllerParams = _.extend({
+ initialState: state,
+ }, self.controllerParams);
+ var controller = new Controller(parent, model, renderer, controllerParams);
+ model.setParent(controller);
+ renderer.setParent(controller);
+ return controller;
+ });
+ },
+ /**
+ * Returns a new model instance
+ *
+ * @param {Widget} parent the parent of the model
+ * @returns {Model} instance of the model
+ */
+ getModel: function (parent) {
+ var Model = this.config.Model;
+ return new Model(parent, this.modelParams);
+ },
+ /**
+ * Returns a new renderer instance
+ *
+ * @param {Widget} parent the parent of the renderer
+ * @param {Object} state the information related to the rendered data
+ * @returns {Renderer} instance of the renderer
+ */
+ getRenderer: function (parent, state) {
+ var Renderer = this.config.Renderer;
+ return new Renderer(parent, state, this.rendererParams);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Loads initial data from the model
+ *
+ * @private
+ * @param {Model} model a Model instance
+ * @returns {Promise<*>} a promise that resolves to the value returned by
+ * the get method from the model
+ * @todo: get rid of loadParams (use modelParams instead)
+ */
+ _loadData: function (model) {
+ return model.load(this.loadParams).then(function () {
+ return model.get.apply(model, arguments);
+ });
+ },
+});
+
+
+return {
+ Factory: Factory,
+ Model: Model,
+ Renderer: Renderer,
+ Controller: Controller,
+};
+
+});