summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/core/mvc.js
blob: 23f2b44b274a394f38fbcf3a32e69c80f6e4faa0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
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 Promise.resolve();
    },
});

/**
 * Only responsibility 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
     * @param {any} [params.handle=null]
     */
    init: function (parent, model, renderer, params) {
        this._super.apply(this, arguments);
        this.handle = params.handle || null;
        this.model = model;
        this.renderer = renderer;
    },
    /**
     * @returns {Promise}
     */
    start: function () {
        return Promise.all(
            [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 Promise.all([this._loadData(model), ajax.loadLibs(this)]).then(function (result) {
            const { state, handle } = result[0];
            var renderer = self.getRenderer(parent, state);
            var Controller = self.Controller || self.config.Controller;
            const initialState = model.get(handle);
            var controllerParams = _.extend({
                initialState,
                handle,
            }, 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
     * @param {Object} [options={}]
     * @param {boolean} [options.withSampleData=true]
     * @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, options = {}) {
        options.withSampleData = 'withSampleData' in options ? options.withSampleData : true;
        return model.load(this.loadParams).then(function (handle) {
            return { state: model.get(handle, options), handle };
        });
    },
});


return {
    Factory: Factory,
    Model: Model,
    Renderer: Renderer,
    Controller: Controller,
};

});