Support for theme folder #24

Open
olivercox opened this Issue Jan 11, 2013 · 1 comment

2 participants

@olivercox

I've been trying to find a way of handling template overrides with a theme folder. The project I'm working on will implement themes that can override a default template. I would like to keep the syntax of calling templates so the following statement would look for the file in a theme folder first and load it if exists.

<% layout('layouts/public') -%>

So lets say that the default views folder is 'root/views' and the theme folder is 'root/themes/mytheme'. The engine would try to find 'layouts/public.ejs' relative to the theme folder and the file the template was called from. If it doesn't exist it should attempt to load the from the views folder.

If the template was called from
root/views/mymodule/index.ejs
it would look first for the file
root/themes/mytheme/mymodule/layouts/public.ejs
if not found it would look for
root/views/layouts/public.ejs

I've found a possible way to handle this by editing the renderFile method in index.js. I'd appreciate any feedback on the change and how one would go about getting it included.

I've added the following lines just before the call to ejs.renderFile()


//Get the theme folder (options.settings['theme'])
    var themeFilePath = dirname(file).replace(resolve(options.settings.views), resolve(options.settings['theme']))
    //If the current file exists reletive to the theme folder set the file to that path
    if (fs.existsSync(resolve(themeFilePath, basename(file)))) file = resolve(themeFilePath, basename(file));`

This relies on the following being set when the view engine is declared, in my case in the server.js:


 app.set('theme', __dirname + [PATH_TO_THEME]);

The ejs-locals renderFile method now looks like:


var renderFile = module.exports = function (file, options, fn) {

    // Express used to set options.locals for us, but now we do it ourselves
    // (EJS does some __proto__ magic to expose these funcs/values in the template)
    if (!options.locals) {
        options.locals = {};
    }

    if (!options.locals.blocks) {
        // one set of blocks no matter how often we recurse
        var blocks = { scripts: new Block(), stylesheets: new Block() };
        options.locals.blocks = blocks;
        options.locals.scripts = blocks.scripts;
        options.locals.stylesheets = blocks.stylesheets;
        options.locals.block = block.bind(blocks);
        options.locals.stylesheet = stylesheet.bind(blocks.stylesheets);
        options.locals.script = script.bind(blocks.scripts);
    }
    // override locals for layout/partial bound to current options
    options.locals.layout = layout.bind(options);
    options.locals.partial = partial.bind(options);

    //Get the theme folder (options.settings['theme'])
    var themeFilePath = dirname(file).replace(resolve(options.settings.views), resolve(options.settings['theme']))
    //If the current file exists reletive to the theme folder set the file to that path
    if (fs.existsSync(resolve(themeFilePath, basename(file)))) file = resolve(themeFilePath, basename(file));

    ejs.renderFile(file, options, function (err, html) {

        if (err) {
            return fn(err, html);
        }

        var layout = options.locals._layoutFile;

        // for backward-compatibility, allow options to
        // set a default layout file for the view or the app
        // (NB:- not called `layout` any more so it doesn't
        // conflict with the layout() function)
        if (layout === undefined) {
            layout = options._layoutFile;
        }

        if (layout) {

            // use default extension
            var engine = options.settings['view engine'] || 'ejs',
          desiredExt = '.' + engine;

            // apply default layout if only "true" was set
            if (layout === true) {
                layout = path.sep + 'layout' + desiredExt;
            }
            if (extname(layout) !== desiredExt) {
                layout += desiredExt;
            }

            // clear to make sure we don't recurse forever (layouts can be nested)
            delete options.locals._layoutFile;
            delete options._layoutFile;
            // make sure caching works inside ejs.renderFile/render
            delete options.filename;

            if (layout.length > 0 && layout[0] === path.sep) {
                // if layout is an absolute path, find it relative to view options:
                layout = join(options.settings.views, layout.slice(1));
            } else {
                // otherwise, find layout path relative to current template:
                layout = resolve(dirname(file), layout);
            }

            // now recurse and use the current result as `body` in the layout:
            options.locals.body = html;
            renderFile(layout, options, fn);
        } else {
            // no layout, just do the default:
            fn(null, html);
        }
    });

};
@gsalgadotoledo
gsalgadotoledo commented Jul 24, 2016 edited

Hi @olivercox It looks like the same problem that I have,
What do you thing about my solution #49

Though your solution looks pretty awesome and cleaner

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment