TemplateController
Supports the best practices of writing Blaze templates
Blaze is awesome but writing the Js part of templates always felt a bit awkward. This package just provides a very thin layer of syntactic sugar on top of the standard API, so you can follow best practices outlined in the Blaze guide more easily.
Now you can turn this:
1You have clicked the button {{counter}} times. 2<button>click</button>
1Template.hello.onCreated(function helloOnCreated() { 2 // counter starts at 0 3 this.counter = new ReactiveVar(0); 4}); 5 6Template.hello.helpers({ 7 counter() { 8 return Template.instance().counter.get(); 9 }, 10}); 11 12Template.hello.events({ 13 'click button'(event, instance) { 14 // increment the counter when button is clicked 15 instance.counter.set(instance.counter.get() + 1); 16 }, 17});
into that:
1You have clicked the button {{state.counter}} times. 2<button>click</button>
1TemplateController('hello', { 2 3 state: { 4 counter: 0 // default value 5 }, 6 7 events: { 8 'click button'() { 9 // increment the counter when button is clicked 10 this.state.counter += 1; 11 } 12 } 13 14});
Table of contents
Installation
meteor add space:template-controller
Compatible with Meteor 1.2.x - 1.3.x
Usage
TemplateController(templateName, options)
templateName
Just pass in the template name like you would access Template.my_template
options
The options are the same as with standard Blaze templates but
TemplateController
supports best practices and common patterns.
Here is a template with all possible options:
1TemplateController('hello', { 2 3 // Validate the properties passed to the template from parents 4 props: new SimpleSchema({ 5 messageCount: { 6 type: Number, 7 defaultValue: 0 8 } 9 }), 10 11 // Setup private reactive template state 12 state: { 13 counter: 0 // default value 14 }, 15 16 // Lifecycle callbacks work exactly like with standard Blaze 17 onCreated() {}, 18 onRendered() {}, 19 onDestroyed() {}, 20 21 // Helpers work like before but <this> is always the template instance! 22 helpers: { 23 someValue() { 24 return this.data.someValue; 25 } 26 }, 27 28 // Events work like before but <this> is always the template instance! 29 events: { 30 'click button'(event, instance) { 31 // this === instance 32 } 33 }, 34 35 // These are added to the template instance but not exposed to the html 36 private: { 37 someProperty: 5, 38 someHelperFunction() {} 39 } 40 41});
API
onCreated
, onRenderd
, onDestroyed
work exactly the same as with standard Blaze.
events
, helpers
work exactly the same as normal but this
inside the handlers is always
a reference to the Template.instance()
. In most cases that's what you want
and would expect. You can still access the data context via this.data
.
Accessing the Data Context of Child Templates
Sometimes you have child Blaze templates that trigger events but do not send the data context as event payload. Here is how you can always get a reference to the data context of the template where the event originated from:
1events: { 2 'click .from-child-template'(event) { 3 const childDataContext = Blaze.getData(event.target); 4 // do something with the data 5 }, 6}
props: { clean: Function, validate: Function}
Any data passed to your component should be validated to avoid UI bugs
that are hard to find. You can pass any object to the props
option, which
provides a clean
and validate
function. clean
is called first and can
be used to cleanup the data context before validating it (e.g: adding default
properties, transforming values etc.). validate
is called directly after
and should throw validation errors if something does not conform the schema.
This api is compatible but not limited to
SimpleSchema.
This is a best practice outlined in the
Blaze guide - validate data context
section. TemplateController
does provide a bit more functionality though:
Any property you define in the schema is turned into a template helper
that can be used as a reactive getter, also in the html template:
1TemplateController('message_count', { 2 props: new SimpleSchema({ 3 messageCount: { 4 type: Number, // allows only integers! 5 defaultValue: 0 6 } 7 }) 8});
1<template name="message_count"> 2 You have {{props.messageCount}} messages. 3</template>
… and you can access the value of messageCount
anywhere in helpers etc. with
this.props.messageCount
a parent template can provide the messageCount
prop with standard Blaze:
1<template name="parent"> 2 {{> message_count messageCount=unreadMessagesCount}} 3</template>
If the parent passes down anything else than an integer value for messageCount
our component will throw a nice validation error.
state: { myProperty: defaultValue, … }
Each state property you define is turned into a ReactiveVar
and you can get
the value with this.state.myProperty
and set it like a normal property
this.state.myProperty = newValue
. The reason why we are not using
ReactiveVar
directly is simple: we need a template helper to render it in
our html template! So TemplateController
actually adds a state
template
helper which returns this.state
and thus you can render any state var in
your templates like this:
1You have clicked the button {{state.counter}} times.
But you can also modify the state var easily in your Js code like this:
1events: { 2 'click button'() { 3 this.state.counter += 1; 4 } 5}
Since each state var is turned into a separate reactive var you do not run into any reactivity issues with re-rendering too much portions of your template.
private: { myProperty: …, myMethod: … }
Any properties and methods you define on the private
object will be added
to your template instance and are available anywhere. This way you can easily
define custom "private" helper methods to calculate or setup stuff. Do not
confuse these with "template helpers" though, they are not accessible from
the html template.
Here is a short and contrived example, but you get the point:
1TemplateController('hello', { 2 3 // These are exposed to the html template! 4 helpers: { 5 someValueForTheHtmlTemplate() { 6 // You can access the private helpers like this: 7 return this.sum(this.myValue, 5); 8 } 9 }, 10 11 // These are just available on your template instance 12 private: { 13 myValue: 5, 14 sum(first, second) { 15 return first + second; 16 } 17 } 18});
this.triggerEvent(eventName, data)
Provides syntactic sugar to trigger a custom jQuery event on the firstNode
of your template. This equivalent to this.$(this.firstNode).trigger(eventName, data)
. As you rarely need to (or
should) trigger custom events on sub-elements of the template we consider
this simple wrapper as best practice.
Another important difference is that you cannot pass multiple event arguments
like you can with the jQuery trigger
api. We only allow a single argument
on purpose, to promote the best practice of avoiding argument lists.
Here is a simple example:
1TemplateController('hello', { 2 events: { 3 'click button'() { 4 this.state.counter += 1; 5 this.triggerEvent('counterIncremented', this.state.counter); 6 } 7 } 8});
if you need to include more event data just pass an object with named properties:
1this.triggerEvent('counterIncremented', { 2 value: incrementedValue, 3 timestamp: new Date() 4});
In parent templates you can handle these custom events like this:
1TemplateController('some_parent_template', { 2 events: { 3 'counterIncremented'(event, instance, data) { 4 // do something with the event 5 } 6 } 7});
Notice in the example above that it is not necessary (and even discouraged)
to add a selector for the event handler (eg: 'counterIncremented .hello'
)!
The problem with this approach is that you are coupling the parent template
to the DOM structure and CSS classes of the child components while you are
just interested in the events. If you need to handle many different events
try to make the event names more specific like helloCounterIncremented
instead of general purpose events like incremented
which could be published
by various components.
If you have multiple instance of the same reusable sub-component that you need to manage, wrap each instance in a separate DOM element and add a unique css class to the wrapper like this:
1<template name="some_parent_template"> 2 <div class="first-counter">{{> counter}}</div> 3 <div class="second-counter">{{> counter}}</div> 4 <div class="third-counter">{{> counter}}</div> 5</template>
Then you can easily distinguish where the events come from by using selectors:
1TemplateController('some_parent_template', { 2 events: { 3 'counterIncremented .first-counter'() {}, 4 'counterIncremented .second-counter'() {}, 5 'counterIncremented .third-counter'() {} 6 } 7});
This way you are not coupling the controller code of some_parent_template
to the internal DOM of the counter
template but keep the control where it
belongs! You can refactor and improve the counter
template as you like now,
as long as you keep the API contract (events) intact!
Single Root Element for your template
this.triggerEvent
only works if you defined a single root element for
your template like this:
1<template name="my_component"> 2 <div class="my-component"> 3 // other content 4 </div> 5</template>
This is also a best practice that we recommend to avoid strange bugs when publishing jQuery events.
Internal APIs
In some situations you might need more control over your templates and want to use some of the internal helpers to reduce boilerplate even more.
Dynamically adding reactive properties to state
and props
You can dynamically add new reactive properties to props
and state
in the onCreated
hook:
1TemplateController('hello', { 2 onCreated() { 3 this.state.addProperty('counter', 0); 4 } 5});
This opens up interesting meta-programming capabilities like passing in schemas or models and generate state on the fly:
1TemplateController('hello', { 2 props: new SimpleSchema({ 3 model: { type: Object, blackbox: true } 4 }), 5 onCreated() { 6 // Generate named reactive properties on the fly 7 this.state.addProperties(this.props.model); 8 } 9});
Binding Functions to Template.instance()
There are two internal helper functions, which can be used to bind any function to run in the context of your template instance:
1// Bind a function to be bound to the Template.instance() -> returns new bound fn 2let bound = TemplateController.bindToTemplateInstance(Function); 3// Wrap all functions of the provided object -> updates object methods in-place! 4TemplateController.bindAllToTemplateInstance({ key: Function, ... });
Configuration
TemplateController.setPropsCleanConfiguration(Object)
Enables you to configure the props cleaning operation of libs like SimpleSchema. Checkout SimpleSchema clean docs to see your options.
Here is one example why removeEmptyStrings: true
is the default config:
i18n
might initially return an empty string for your translation.
This would cause an validation error because SimpleSchema removes empty strings by default when cleaning the data.
Release History
You can find the complete release history in the changelog
Related Projects
Packages
- TemplateTwoWayBinding Two-Way Binding for Blaze templates with support for
TemplateController
. - TemplateControllerModelMap Validate template state.
- Blaze Magic Events was confirmed by @comerc to work with
TemplateController
.
Examples
License
Licensed under the MIT license.