Skip to content

Instantly share code, notes, and snippets.

@caridy
Last active December 17, 2015 04:08
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save caridy/5548076 to your computer and use it in GitHub Desktop.
Save caridy/5548076 to your computer and use it in GitHub Desktop.
Template registration process in YUI

This gist was updated based on the discussion here : https://groups.google.com/forum/?fromgroups=#!topic/yui-contrib/cUpVvtoUBa8

Template Registration

With the ability to precompile templates into javascript and the abtraction layer provided by Y.Template to normalize the api to render those templates, we got one step closer to create applications that can be template language agnostic.

The premise here is to create a YUI Application that references templates by name and call for render when needed without having to know what engine to use, or what file generated the compiled template, or what api should be used for a particular template.

In order to facilitate this, we should have a centralized registration mechanism used by the application to register any template that is provisioned, in which case we can decouple the provisioning process from the actual rendering process.

Proposal

The initial proposal is to utilize Y.Template as the central hub for all those templates, and doing so by introducing three new static methods, register, unregister and render.

Register a template

To register a compiled template:

var revivedTemplate = Y.Template.register('templateName', template);

note: the revivedTemplate is probably the same as argument template, but just a guarantee that the function complies with the revive API in Y.Template.

note: the register method override any existing template with the same name. this will help with live updates in development mode.

Unregister a template

To unregister a template by name:

var wasRegisteredBool = Y.Template.unregister('templateName');

which returns a boolean in case you want to know if the template was previously registered.

Render a template

To render a registered template:

var html = Y.Template.render('templateName', data);

All together (in code):

var revivedTemplate = Y.Template.register('name', function (data) {
    return 'some string';
});
var someString = revivedTemplate({foo: 'foo'});

The means the function passed to register() should be a function which conforms to the above contract. It is up to the thing which precompiles it to encapsulate or close-over any underly template engine specifics.

var someString = Y.Template.render('name', {foo: 'foo'});
var wasRegisteredBool = Y.Template.unregister('name');

The example

A good example of this will be a nodejs application that uses YUI on the server side to precompile templates generating YUI Modules that can be use on-demand or required by our business logic, where those modules can do the provision of templates into the internal register, which guarantees that our business logic can render them. Some code:

Pre-compiled template into a module:

YUI.add('foo', function (Y, NAME) {
  var compiled = function (data) {
    /* compiled template */
    return '<html fragment>';
  };
  Y.Template.register('foo', compiled);
}, '', {requires: ['template-base']});

Where the business logic can require foo to guarantee the provision:

YUI.add('bar', function (Y, NAME) {
  var html = Y.Template.render('foo', {
    tagline: 'bar is now template language agnostic'
  });
}, '', {requires: ['foo']});

Few more notes

  • The internal cache mechanism should not be expensive
  • Y.Template._cache = {}; as the cache mechanism should be just fine.
  • Throwing when unregistered template is invoked
  • Y.Template.render vs Y.Template.prototype.render might be confused, we need to clearly state this in the docs.
  • The nature of the render method is syncrounous, hence any register template should be syncronous.
@rgrove
Copy link

rgrove commented May 9, 2013

Good stuff. I like the general idea of this template registration mechanism, although there are a few unanswered questions and potential problems.

Problems

  • Y.Template already has a prototype render() method that both compiles and renders a template. Adding a static render() method that doesn't perform a compilation step could confuse people.
  • This proposal assumes that all registered templates will be functions that accept a data object as the first argument, but this may not always be the case. It's a common pattern, but nothing in Y.Template enforces it, and template engines are free to do as they choose. Since the static Y.Template.render() method doesn't have any way of knowing what type of template it's rendering, it can't handle special cases.

Questions

  • What happens if I register template 'foo', then try to register another template named 'foo'? Does it get overwritten?
  • How can I tell (via the public interface) whether or not a template has been registered without actually attempting to render it?
  • How can I unregister a template if it's no longer needed, such as when the component that registered it is destroyed?

Suggestions

Wherein I solve my own problems and answer my own questions... :)

I'd recommend changing the register() method to accept an optional third options argument, like this:

Y.Template.register('name', template, {engine: Y.Template.Micro});

Have Y.Template's constructor accept a template name as its first param (it currently accepts an engine class, but could use a typeof check to support both). When a template name is passed, you get back a Template instance that wraps the named template and uses the template engine that was specified at registration time, so it understands how to render the template. The engine will default to Y.Template.Micro if it wasn't specified.

var template = new Y.Template('name');

Make Y.Template.prototype.render() a little smarter, so that it will only compile the template if it's not already compiled. Now it can be used to render a registered template.

Add a static Y.Template.unregister() method that does exactly what it sounds like.

So, a complete use case might look like this:

// Register a Handlebars template.
Y.Template.register('myTemplate', template, {engine: Y.Template.Handlebars});

// Instantiate the registered template.
var tmpl = new Y.Template('myTemplate');

// Render it (compiling only if the registered template wasn't already compiled).
var html = tmpl.render(data);

// When we're done and know that we'll never need this template again...
Y.Template.unregister('myTemplate');

@tivac
Copy link

tivac commented May 9, 2013

This is somewhat similar to what we already do. This gist explains it though it's gotten smoother & the gist is out of date. I'm definitely in favor of providing a little library structure to this flow. We've just been using Y.namespace() generated objects to provide our template "cache" which IMO has worked out really well. I don't think the cache layer needs to be complicated.

I definitely prefer a static method on Y.Template vs something instance-based, I already find the instantiation step for using Y.Template a bit weird. Not sure what the intent behind that was since I'm already loading template functions as full YUI modules that just add themselves to an object vs run-time compilation.

To @rgrove's questions:

  • I think registering a new template w/ the same name as the old one should definitely overwrite it.
  • It seems like it should be possible to publicly access the template cache object so to check for template existence you could just use in.
  • Again, if it's a simple object you can access it's as simple as delete or setting that template to null, which I like.

Edit I think I might understand the instantiation step now, it's for tracking the engine being used. Is that really necessary for precompiled & revived templates that (at least in all examples I've seen) are already a function ready to be called?

@rgrove
Copy link

rgrove commented May 9, 2013

I don't like the idea of exposing the template cache object publicly and encouraging people to modify it. It's a behind-the-scenes implementation detail that users shouldn't need to worry about, and that we need control over to ensure we don't end up in an inconsistent or unexpected state. We should provide public methods for checking for template existence and unregistering templates.

@tivac
Copy link

tivac commented May 9, 2013

@rgrove I'm fine with that as well, more of a thought experiment to see how far we could take stripping things down :D

@caridy
Copy link
Author

caridy commented May 9, 2013

few notes:

  • The whole point here is to organize compiled templates WITHOUT having to load any engine, just template-base, and this has to be paramount.
  • This proposal is more of a guidelines than creating a solid/regid infrastructure, a la express! at the end of the day we do have a solution for this by doing the namespacing as @tivac mentioned, so this becomes more about guiding new folks to use a very simple schema to handle their app-specific templates.
  • unregister: it is a nice sugar, I will update the gist.
  • new Y.Template(): I'm reluctant to create one object per view (unless we figure out how to make that object very lean), too much complexity, when at the end, a compiled template is just a function.
  • Y.Template.Micro as the default fallback engine means that, to use this thing, you will have to load template, instead of template-base even when I will not need it (yes, it is small, but I don't want it there if I don't need it).
  • templates are (most of the time) app-specific, and abstracting the call while supporting custom arguments should be just fine. What I'm saying here is the fact that you can still revive a template (if needed) where the template can have a custom set of arguments, and Y.Template.render('foo', data, options, whatever) will just propagated that to the actual function.
  • One of the main reasons to formalize this is the fact that we can have a Y at the server side that provisions templates in the same way we do in the client side, so performance is paramount.
  • I'm ok with exposing the internal cache (Y.Template._cache maybe) so people can do whatever they want, it is about their templates. I don't expect templates to be pushed into gallery to become shareable or anything along those lines.

Bottomline, I'm leaning toward @tivac comments on this one, they resonate well with the modown experiment where extensions of Y.Template are really only needed during the build process to compile things into a YUI Module that registers template(s), which btw, it is something we want to support out of the box for anyone using express.

update 1: I will remove the Micro from the example.

@rgrove
Copy link

rgrove commented May 9, 2013

@caridy:

The whole point here is to organize compiled templates WITHOUT having to load any engine, just template-base, and this has to be paramount.

Then maybe this shouldn't be part of Y.Template, since the implication of it being there is that it will work with any Y.Template-wrapped engine. We can't expect all compiled template functions, regardless of engine, to have the exact same arguments and behavior.

Y.Template exists to provide a generic abstraction over template engines with different APIs.

This proposal is more of a guidelines than creating a solid/regid infrastructure, a la express! at the end of the day we do have a solution for this by doing the namespacing as @tivac mentioned, so this becomes more about guiding new folks to use a very simple schema to handle their app-specific templates.

This proposal proposes new APIs and functionality. That's solid infrastructure, not guidelines. :)

new Y.Template(): I'm reluctant to create one object per view (unless we figure out how to make that object very lean), too much complexity, when at the end, a compiled template is just a function.

I'm open to other solutions, but I do think it's important that engine abstractions be supported for registered templates. We can't just blindly assume that all registered template functions follow the function (data) { } => html pattern.

Y.Template.Micro as the default fallback engine means that, to use this thing, you will have to load template, instead of template-base even when I will not need it (yes, it is small, but I don't want it there if I don't need it).

Not true. You only need to load Y.Template.Micro if you aren't going to specify another engine.

templates are (most of the time) app-specific, and abstracting the call while supporting custom arguments should be just fine. What I'm saying here is the fact that you can still revive a template (if needed) where the template can have a custom set of arguments, and Y.Template.render('foo', data, options, whatever) will just propagated that to the actual function.

This requires the calling code to know what type of template it's rendering, and means the template engine is explicitly linked to the component that uses those templates.

One of the great things about Y.Template right now is that even if I write a component that uses Template.Micro, you can override the component's default templates with your own custom templates using Handlebars or anything else, and you don't have to modify the component itself to call them differently. They just work.

I'm ok with exposing the internal cache (Y.Template._cache maybe) so people can do whatever they want, it is about their templates. I don't expect templates to be pushed into gallery to become shareable or anything along those lines.

A protected property (_cache) is fine, because it indicates that people shouldn't mess with this thing unless they're willing to accept the consequences. We absolutely should not make this a public property, though. It's internal state, not a public API.

@tivac
Copy link

tivac commented May 9, 2013

@rgrove I'm curious about the assertion that we can't assume that template engines will be function(data) => html, isn't that what Y.Template already does for Y.Template.render? It does it via the template engine "interface", but still.

I agree with you conceptually that supporting other styles of template usage is important, but wouldn't those other template engines still need to implement the engine interface that Y.Template expects? I guess the argument can be made that since we're dealing with precompiled templates that the engine then becomes less important as you wouldn't want to load it anyways. It seems like any engine that is so different from what seems like the "standard" could be expected to provide a function(data) => html flow as part of the precompilation/revival process to use this API. Even if it was just a small translation module it'd still presumably be able to fit into this very basic model w/ a minimum of code.

Or maybe I'm totally off base here, I just don't know of any templating engines that work any other way off the top of my head so I'm having a hard time seeing how they'd fit. Granted, I haven't done super-extensive of research into templating solutions beyond the popular ones.

@rgrove
Copy link

rgrove commented May 9, 2013

@tivac Good point. I suppose if you assume that a registered template will always have been compiled with a Y.Template engine, then we can leave it up to the engines to ensure that their compiled functions always follow the same signature, regardless of the underlying implementation.

@caridy
Copy link
Author

caridy commented May 9, 2013

In my mind, Y.Template is not different from View constructor in express (aka app.set('view')), this thing defines a simple API for view engines to follow, but doesn't enforce too many things, it just have two rules:

  • when a new instance is created, name and options will be provided as constructor arguments.
  • the object requires to have path member and render() method that receive data and a callback function.

that's about it, nothing fancy, but the trick here is what @tivac just mentioned, if you have an engine that is too crazy to fit into that, just create a function that takes care of that normalization. In the context of Y.Template that's precisely what the revive process should do. E.g:

YUI.add('foo', function (Y, NAME) {
  var compiled = function (format, data) {
    /* compiled template */
    return '<html fragment>';
  }; 
  var helpers = {}; // collect helpers from somewhere
  Y.Template.register('foo', function (data, options) {
       compiled('http', Y.merge(data, helpers, options));
  });
}, '', {requires: ['template-base']});

and that is probably more efficient that brining the cavalry to normalize that compiled template, which is another option:

YUI.add("foo",function(Y, NAME){
   var revived = Y.Template.Handlebars.revive(function (Handlebars,depth0,helpers,partials,data) {
     // a lot of generated code goes here...
     return buffer;
  }),
  Y.Template.register('foo', revived);
}, "", {requires: ["handlebars-base"]});

These are equivalent, since you will do:

YUI().use('foo', function (Y) {
   var html = Y.Template.render('foo', data);
});

It doesn't matter much how the template was provisioned, but obviously having the ability to rely on the build process to produce a YUI module that does not require you to load any particular engine when in the runtime, is going to be a big plus for many, while the revive workflow, just like the render workflow is still on the table for those who don't mind to load those engines.

@caridy
Copy link
Author

caridy commented May 9, 2013

@rgrove, about this:

Y.Template.Micro as the default fallback engine means that, to use this thing, you will have to load template, instead of template-base even when I will not need it (yes, it is small, but I don't want it there if I don't need it).

Not true. You only need to load Y.Template.Micro if you aren't going to specify another engine.

Not accurate. According to what you said "The engine will default to Y.Template.Micro if it wasn't specified.", that means Y.Template.register should have access to Y.Template.Micro if needed but not by requiring it, which sounds like a weird decoupling. Who will guarantee that this is going to be in place if needed? developers?

@rgrove
Copy link

rgrove commented May 9, 2013

@caridy

Not accurate. According to what you said "The engine will default to Y.Template.Micro if it wasn't specified.", that means Y.Template.register should have access to Y.Template.Micro if needed but not by requiring it, which sounds like a weird decoupling. Who will guarantee that this is going to be in place if needed? developers?

This fork of the conversation no longer seems to have a point given that we both agree with what @tivac said earlier, which means specifying an engine is no longer necessary, but as far as what I was thinking earlier...

As a developer, if you don't specify an engine, you're opting into Template.Micro. If you don't specify an engine and Template.Micro isn't loaded, that's an error, and it's your fault. If you don't want to load Template.Micro, specify another engine that you do want to load. Pretty simple.

But again, no longer necessary.

@derek
Copy link

derek commented May 10, 2013

Note: Discussion has moved to [Proposal] Y.Template.register / Y.Template.render as static methods on the yui-contrib mailing list.

@caridy
Copy link
Author

caridy commented May 13, 2013

The comments are closed now, any follow up should go thru: https://groups.google.com/forum/?fromgroups=#!topic/yui-contrib/cUpVvtoUBa8

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