diff options
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/core/mvc.js | 242 |
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, +}; + +}); |
