view/template.js

/*
 * Your installation or use of this SugarCRM file is subject to the applicable
 * terms available at
 * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
 * If you do not agree to all of the applicable terms or do not have the
 * authority to bind the entity as an authorized representative, then do not
 * install or use this SugarCRM file.
 *
 * Copyright (C) SugarCRM Inc. All rights reserved.
 */

//Pull the precompile header and footer from the node precompile implementation for handlebars
var _header = "(function() {\n  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};\n",
    _footer = '})();',
    _templates = {},
    _sources = {};

/**
 * Manages {@link http://handlebarsjs.com|Handlebars} templates.
 *
 * @module View/Template
 */

/**
 * @alias module:View/Template
 */
const TemplateManager = {
    /**
     * Loads templates from local storage and populates the `Handlebars.templates` collection.
     */
    init: function() {
        _templates = SUGAR.App.config.cacheMeta ? SUGAR.App.cache.get('templates') || {} : {};
        var src = '';
        _.each(_templates, function(t) {
            src += t;
        });

        try {
            eval(_header + src + _footer);
        }
        catch (e) {
            SUGAR.App.logger.error('Failed to eval templates retrieved from local storage:\n' + e);
            // TODO: Trigger app:error event
        }
    },

    /**
     * Used to register a template src without compiling the template yet.
     * The template will be compiled the first time it is used (requested
     * with `getView`/`getLayout`/`getField`).
     *
     * @param {Array} tpl The template as `[key, compiled]` pair.
     * @param {string} src The template content (.hbs file).
     * @param {boolean} [force=false] `true` to force recompilation on it.
     * @private
     */
    _add: function(tpl, src, force) {
        var key = tpl[0],
            loaded = this.get(key, false);
        if (loaded && !force) {
            return;
        }
        //If we have already loaded this template but with a different source, we need to mark it for recompilation
        if (loaded && force && _sources[key] != src) {
            _templates[key] = Handlebars.templates[key] = null;
        }
        _sources[key] = src;
    },

    /**
     * Caches a template and compiles it if necessary.
     *
     * @param {Array} tpl The first item is the template key. The second item
     *   is optional and is the precompiled template function.
     * @param {string} src Template source code.
     * @param {boolean} [force=false] Flag indicating if the template must
     *   be re-compiled.
     * @return {Function} The compiled template.
     * @private
     */
    _compile: function(tpl, src, force) {
        Handlebars.templates = Handlebars.templates || {};
        _templates[tpl[0]] = Handlebars.templates[tpl[0]] = (force || !tpl[1]) ? this.compile(tpl[0], src) : tpl[1];
        return _templates[tpl[0]];
    },

    /**
     * Compiles a template.
     *
     * This method caches the precompiled version of the template
     * and returns the compiled template. The template can be accessed
     * directly via `Handlebars.templates[key]`.
     *
     * @param {string} key Identifier of the template to be compiled.
     * @param {string} src The actual template source to be compiled.
     * @return {Function} The compiled template.
     */
    compile: function(key, src) {
        try {
            _templates[key] = "templates['" + key + "'] = template(" + Handlebars.precompile(src) + ");\n";
            eval(_header + _templates[key] + _footer); // jshint ignore:line
        } catch (e) {
            // Invalid templates will cause a JS error when they either pre-compile or compile.
            SUGAR.App.logger.error("Failed to compile or eval template " + key + ".\n" + e);
        }
        return this.get(key, false) || this.empty;
    },

    /**
     * Retrieves a compiled Handlebars template.
     *
     * @param {string} key Identifier of the template to be retrieved.
     * @param {boolean} [compile=true] Force the template to compile if we
     *   have uncompiled source.
     * @return {Function} The compiled template.
     */
    get: function(key, compile) {
        //Undefined should default to true for compiled (not passed means compile)
        compile = _.isUndefined(compile) || compile;
        if (compile && !Handlebars.templates[key] && _sources[key]) {
            this._compile([key], _sources[key]);
        }
        return Handlebars.templates ? Handlebars.templates[key] : null;
    },

    // Convenience private method
    _getView: function(name, module, compile) {
        var key = name + (module ? ('.' + module) : '');
        return [key, this.get(key, compile)];
    },

    /**
     * Gets the compiled template for a view.
     *
     * @param {string} name View name.
     * @param {string} [module] Module name.
     * @return {Function} The compiled template.
     */
    getView: function(name, module) {
        return this._getView(name, module, true)[1];
    },

    // Convenience private method
    _getField: function(type, view, module, fallbackTemplate, skipFallbacks, compile) {
       var foundTemplate,
           prefix = "f." + type + ".",
           key = prefix + (module ? (module + ".") : "") + view;

        module += ".";

       // get the module specific one first, then try the base one for this view
       foundTemplate = this.get(prefix + module + view, compile) || this.get(prefix + view, compile);
        //skipfallbacks indicates we should only check for the requested field,
       if (!foundTemplate && !skipFallbacks)
       {
           foundTemplate = this.get(prefix + module + fallbackTemplate, compile) || this.get(prefix + fallbackTemplate, compile);
           // If we got nothing for the requested fallback, use base as the last ditch fallback
           if (!foundTemplate) {
               foundTemplate = this.get('f.base.' + view, compile) || this.get('f.base.' + fallbackTemplate, compile);
           }
       }
       return [key, foundTemplate];
   },

    /**
     * Gets the compiled template for a field.
     *
     * @param {string} type Field type.
     * @param {string} view View name.
     * @param {string} module The module the field is from.
     * @param {boolean} [fallbackTemplate=true] Template name to fall back
     *   to if the template for `view` is not found.
     * @return {Function} The compiled template.
     */
    getField: function(type, view, module, fallbackTemplate) {
        return this._getField(type, view, module, fallbackTemplate, false, true)[1];
    },

    // Convenience private method
    _getLayout: function(name, moduleName, compile) {
        var key = 'l.' + (moduleName ? (moduleName + '.') : '') + name;
        return [key, this.get(key, compile)];
    },

    /**
     * Gets the compiled template for a layout.
     *
     * @param {string} [name] Layout name.
     * @param {string} [moduleName] Module name.
     * @return {Function} The compiled template.
     */
    getLayout: function(name, moduleName) {
        return this._getLayout(name, moduleName, true)[1];
    },

    /**
     * Compiles a view template and puts it into local storage.
     *
     * @param {string} name View name.
     * @param {string} module Module name.
     * @param {string} src Template source code.
     * @param {boolean} [force=false] Flag indicating if the template must
     *   be re-compiled.
     * @return {Function} The compiled template.
     */
    setView: function(name, module, src, force) {
        return this._add(this._getView(name, module, false), src, force);
    },

    /**
     * Compiles a field template and puts it into local storage.
     *
     * @param {string} type Field type.
     * @param {string} view View name.
     * @param {string} module The module the field is from.
     * @param {string} src Template source code.
     * @param {boolean} [force=false] Flag indicating if the template must
     *   be re-compiled.
     * @return {Function} The compiled template.
     */
    setField: function(type, view, module, src, force) {
        // Don't fall back to default template (false flag)
        return this._add(this._getField(type, view, module, null, true, false), src, force);
    },

    /**
     * Compiles a layout template and puts it into local storage.
     *
     * @param {string} name Layout name.
     * @param {string} [moduleName] Module Name.
     * @param {string} src Template source code.
     * @param {boolean} [force=false] Flag indicating if the template must
     *   be re-compiled.
     * @return {Function} The compiled template.
     */
    setLayout: function(name, moduleName, src, force) {
        return this._add(this._getLayout(name, moduleName, false), src, force);
    },

    /**
     * Registers view, layout, and field templates from metadata payload
     * for later "lazy" on-demand compilation.
     *
     * The metadata must contain the following sections:
     *
     * ```
     * {
     *    // This should now be deprecated
     *    "view_templates": {
     *       "detail": HB template source,
     *       "list": HB template source,
     *       // etc.
     *    },
     *
     *    "sugarFields": {
     *        "text": {
     *            "templates": {
     *               "default": HB template source,
     *               "detail": HB template source,
     *               "edit": ...,
     *               "list": ...
     *            }
     *        },
     *        "bool": {
     *           // templates for boolean field
     *        },
     *        // etc.
     *    }
     *
     *    "views": {
     *      "text": {
     *          "templates" {
     *              "view": HB template source...
     *              "view2": HB template source..
     *          }.
     *    }
     * }
     * ```
     *
     * @param {Object} metadata Metadata payload.
     * @param {boolean} [force=false] Flag indicating if the cache is
     *   ignored and the templates are to be recompiled.
     */
    set: function(metadata, force) {
        if (metadata.views) {
            _.each(metadata.views, function(view, name) {
                if (name != '_hash') {
                    _.each(view.templates, function(src, key) {
                        key = name == key ? key : name + '.' + key;
                        this.setView(key, null, src, force);
                    }, this);
                }
            }, this);
        }

        if (metadata.fields) {
            _.each(metadata.fields, function(field, type) {
                if (type != '_hash') {
                    _.each(field.templates, function(src, view) {
                        this.setField(type, view, null, src, force);
                    }, this);
                }
            }, this);
        }

        if (metadata.layouts) {
            _.each(metadata.layouts, function(layout, type) {
                if (type != '_hash') {
                    _.each(layout.templates, function(src, key) {
                        key = type == key ? key : type + '.' + key;
                        this.setLayout(key, null, src, force);
                    }, this);
                }
            }, this);
        }

    },

    /**
     * A precompiled empty template function.
     *
     * @return {string} The empty string.
     */
    empty: function() {
        return '';
    }
};

module.exports = TemplateManager;