Blaze Component
A simple package to make repetitive tasks easier in blaze, and help enforce clean components. We have found this class makes it much easier to train novice developers in reactivity, particularly combining reactive external data, with the reactive internal state of a component. Additionally, we make the use of this
consistent in callbacks, helpers and events
This package has no impact on templates which dont use it, and can be used for individual components without impacting an entire project.
Usage
Define your templates as normal, and register them with blaze component:
1<!-- myComponent.html --> 2<template name="myComponent"> 3 Some content 4</template>
1// myComponent.js 2import "./myComponent.html" 3import { BlazeComponent } from "meteor/znewsham:blaze-component"; 4 5// define your component 6export class MyComponent extends BlazeComponent { 7 constructor(templateInstance) { 8 super(templateInstance, { someInitialStateKey: "someValue" }); 9 // the rest of your code that may have previously gone in `onCreated` 10 } 11 12 rendered() { 13 // your code that may have previously gone in `onRendered` 14 } 15 16 destructor() { 17 // disables any timeouts/intervals associated with this component instance 18 super.destructor(); 19 // your code that may have previously gone in `onDestroyed` 20 } 21} 22 23// register your component and link it to a template 24BlazeComponent.register(Template.myComponent, MyComponent);
New in 2.0.0
- Two new helpers
self
androot
described below - Small bug fix (statChangedStrict -> stateChangedStrict).
- Helper functions for binding state to data
- performance optimizations for
this.get
andthis.set
reactivity when called with objects/arrays that you don't want to be stringified for comparisons. - added '.' notation to anywhere that you can pass in a field list (state changed, data changed, etc).
- Finally, adding
useNonReactiveData
to global helpers. - Fixed bug in
this.reactiveData()
Major bump
The major version bump is due to a bug in the implementation of this.reactiveData()
when called with no arguments from a helper in an inner context (e.g., {{#each item}} or {{#with context}}). Consider the following:
1 {{#with innerContext}} 2 {{someHelper}} 3 {{/with}}
1export class MyComponent extends BlazeComponent { 2 static HelperMap() { 3 return ['someHelper'] 4 } 5 6 someHelper() { 7 return this.reactiveData(); 8 } 9} 10 11// register your component and link it to a template 12BlazeComponent.register(Template.myComponent, MyComponent);
In this situation (and only this situation) someHelper
would return the the value of innerContext
, when it should return the root data context. This behaviour has been fixed.
Self and Root
Additionally, two helpers have been added - self
and root
. These can be called to get the current template context, and root template instance context respectively. Both of these helpers make use of useNonReactiveData
from 1.4.0 and are wrapped in Tracker.guard
- as such they will only re-render if the actual data context they refer to changes. They both also take arguments which will limit the reactivity further to only the things you care about.
1<template name="myComponent"> 2 {{#someOtherComponent context}} 3 {{!-- existing behaviour --}} 4 {{>doSomething name}} {{!-- will return the name on the context, and will re-run even if the context doesnt change, but the dependency is triggered. --}} 5 {{> doSomething this.name}} {{!-- same as above --}} 6 7 {{!-- new behaviour --}} 8 {{> doSomething self.name}} {{!-- will rerun whenever the context actually changes (any field) --}} 9 {{> doSomething (self "name")}} {{!-- will only rerun if name actually changes - but will invoke doSomething with { name: "someName" } --}} 10 {{#let subContext=(self "name")}} 11 {{> doSomething subContext.name}} {{!-- clunky due to spacebars limitations. --}} 12 {{/let}} 13 14 {{doSomething root.name}} {{!-- will always refer to the root data passed into myComponent. Regardless of how deeply nested you are. Can be used in all the same ways --}} 15 {{/someOtherComponent}} 16</template>
Bind data to state
A common pattern is to have some value passed in and have the component update when the value changes externally, or when the value is changed internally (e.g., on select, or click). Currently this behaviour is a little repetitive:
1... 2init() { 3 this.dataChangedStrict('fieldName1', ({ fieldName1 }) => { 4 this.set('fieldName1', fieldName1); 5 }); 6 this.dataChangedStrict('fieldName2', ({ fieldName2 }) => { 7 this.set('fieldName2', fieldName2); 8 }); 9} 10...
The following is marginally cleaner:
1... 2init() { 3 this.bindDataToState('fieldName1', 'fieldName2'); 4} 5...
this.bindDataToState
has two forms. First, it can take a list of fieldNames as shown above. Second, it can take an object of kv pairs, where the key represents the data field name and the value represents the state field name:
1... 2init() { 3 this.bindDataToState({ fieldName1: "myFieldName1", fieldName2: "myFieldName2" }); 4} 5...
Additionally, you can now use the static helper BindUIToState
to bind UI changes to some state. Similar to HelperMap
and EventMap
this is evaluated at the component registration time and returns an array of objects with the following syntax:
1static BindUIToState() { 2 return [ 3 { event: 'click .something', state: 'myFieldName', valueAttribute: 'data-value' }, // will modify myFieldName to take the value present in .something[data-value] whenever .something is clicked. 4 { event: 'change select', state: 'myFieldName' }, // will modify myFieldName to take the value of any select that is changed (using $(e.currentTarget).val()) 5 { event: 'click .something', stateAttribute: 'data-field', valueAttribute: 'data-value' }, // will mdoify the state found in .something[data-field] to take the value found in .something[data-value] whenever .something is clicked. 6 { event: 'change select', state: 'myFieldName', convert: parseInt }, // will modify myFieldName to take the value of any select that is changed (using $(e.currentTarget).val()) and call parseInt 7 { event: 'change select', state: 'myFieldName', convert: 'doSomething' }, // will modify myFieldName to take the value of any select that is changed (using $(e.currentTarget).val()) and call the instance method doSomething to convert the value 8 ] 9}
in a similar vein, there is the static ExposeStateMap
function which will return an array or object and will expose the state defined there as helpers.
1static ExposeStateMap() { 2 return ['myField', 'myOtherField']; 3}
or
1static ExposeStateMap() { 2 return { 3 myHelper: 'myField', 4 myOtherHelper: 'myOtherField' 5 }; 6}
A further enhancement is in the way this.set
and this.get
function. this.set
now takes a third argument that will force the use of a reactive variable (thus not JSON cloning the value on each get/set).
Lastly, anywhere you can get a partial state/data set from a function (e.g., this.reactiveData(field)
or this.dataChanged(field...)
) you can now use . notation to further limit the data:
1init() { 2 this.dataChangedStrict('atts.index', (data) => { 3 console.log(data['atts.index']); // will only fire when `atts.index` changes. 4 }); 5}
New in 1.4.0
You can now set useNonReactiveData
on a helper function, or useNonReactiveDataForHelpers
on an entire template instance to disable data based reactivity in your component. In Blaze, by default, all helpers rerun whenever the data context changes. If your helpers are reactive on something else (e.g., ReactiveVar
or a minimongo collection) this is pointless and can cause unnecessary UI flicker and computation.
You can enable this on a single helper as follows:
1export class MyComponent extends BlazeComponent { 2 static HelperMap() { 3 return ["myHelper"]; 4 } 5 6 myHelper() { 7 return "something"; 8 } 9} 10MyComponent.prototype.myHelper.useNonReactiveData = true;
Or for all helpers in the template:
1export class MyComponent extends BlazeComponent { 2 constructor(templInstance) { 3 super(templInstance); 4 templInstance.useNonReactiveDataForHelpers = true; 5 } 6}
API
The BlazeComponent
class uses the constructor
in place of onCreated
and rendered
in place of onRendered
and destructor
in place of onDestroyed
. If you have generic code that you typically attach to the on*
methods of Template.instance()
you can still do so and they will be called correctly. If you want them to be called before the created/rendered/destroyed methods of your component class, define them before BlazeComponent.register
.
Helpers and Events are defined using the static HelperMap
and EventMap
methods respectively, Each returns a map in the form of { helperOrEventName: 'nameOfFunction' }
. While this may appear to be (and might actually be) quite clunky, it means you can easily re-use helpers and trivially extract common functionality of events and/or helpers to instance methods in your class. It also ensures that this
is always the instance of your component whether in a helper, constructor, rendered callback or event. For the sake of brevity, HelperMap
can also return an array of strings where each string is both the helpername, and the corresponding function name. This makes the common use case slightly less clunky.
1export class MyComponent extends BlazeComponent { 2 ... 3 static HelperMap() { 4 return { 5 myHelper: "hello" 6 }; 7 } 8 9 static EventMap() { 10 "keydown .anInput": "textChanged", 11 "keyup .anInput": "textChanged", 12 "blur .anInput": "textChanged" 13 } 14 15 textChanged(e, templInstance) { //this = instance of MyComponent 16 // in this case templInstance is a little redundant 17 } 18 19 hello() { // this = instance of MyComponent 20 21 // you lose this = data context, so you'll have to pass in data. Meteor says this is best practice anyway 22 return "Hello!" 23 } 24}
Rather than overriding the constructor, it is possible to just define an init
method - which will be get called by the constructor AFTER setting up the initial state.
1export class MyComponent extends BlazeComponent { 2 constructor(templInstance) { 3 console.log("pre-init"); 4 super(templInstance); 5 console.log("post-init"); 6 } 7 8 init() { 9 console.log("init"); 10 } 11}
The output here will be:
pre-init init post-init
The class also provides an interface similar to that of Template.instance()
to allow easy usage, the following are methods that directly expose their Template.instance()
equivalents
- autorun
- subscribe
- $
In addition to these trivial pass-thru methods, we also define helper methods for common occurrences.
Template.instance() level internal state
A common pattern in blaze is to assign a reactive dictionary, or a set of reactive variables to the template instance to store internal state - we provide some trivial helper methods to make this more obvious.
Initial state can be set by calling super(templInstance, {...})
in the constructor of your component, then calling this.get("settingName")
or this.set("settingName", "value")
will update the state. These methods internally resolve to a ReactiveDict
.
Timeouts and intervals
In some cases it is necessary (particularly when integrating with 3rd party, non-meteor JS packages) to initialize some setup after a delay, or at a certain interval. If care is not taken, this can lead to memory and performance leaks as more code blocks are created and not destroyed along with your templates. This can also lead to unusual behaviour. The BlazeComponent makes this trviial, in the below code the timeout and interval will be cancelled with the components destruction.
1export class MyComponent extends BlazeComponent { 2 created() { 3 ... 4 } 5 6 rendered () { 7 ... 8 this.setTimeout(() => { 9 // init a 3rd party component, or trigger some other functionality 10 }, 1000); 11 ... 12 13 this.setInterval(() => { 14 // poll some method 15 }, 1000); 16 } 17}
In some cases, you may need to reactively create timeouts that should be called exactly once, some milliseconds after the reactive change. The below code will trigger the timeout 1 second after the reactive condition triggers, if the reactive condition re-triggers after the timeout is created, but before the timeout is fired, the initial timeout is removed, and re-created.
1export class MyComponent extends BlazeComponent { 2 created() { 3 ... 4 } 5 6 rendered () { 7 this.autorun(() => { 8 9 // some reactive condition 10 ... 11 this.setTimeout(() => { 12 // init a 3rd party component, or trigger some other functionality 13 }, 1000, { name: "CallMeOnce" }); 14 ... 15 }); 16 } 17}
Temporary non-template jquery listeners
Sometimes you find yourself needing to listen to jquery events that cannot be bound to a template, for example rescaling some content when the window resizes. You can use this.on
, its first argument is either an element or a selector (which will be passed to jQuery), the second argument is the event to listen to (paseed to $.fn.on
) and the final argument is the callback.
1export class MyComponent extends BlazeComponent { 2 created() { 3 ... 4 } 5 6 rendered () { 7 this.on(window, "resize", () => { 8 console.log("resized"); 9 }); 10 } 11} 12
Obviously reactive or non-reactive data
Many novices struggle with the concept of reactive and non-reactive data, when should I use this
, this.data
, Template.instance().data
or Template.currentData()
? BlazeComponent provides two methods: this.nonReactiveData()
as the name suggests, returns the entire data context passed into the template and is non-reactive. this.reactiveData()
returns (optionally) the entire data context passed into the component, and is reactive. For fine-grained data changes, see below.
Fine-grained reactivity
One problem we found occurred more often than we'd like was running some "expensive" code when the data context of a component changed, when in reality we only cared about some subset of the changed data. Consider the following code, whenever any field of the data changes, we'll re-run the code - even though we only depend on the _id
and someOtherField
properties:
1Template.MyComponent.onCreated(() => { 2 this.autorun(() => { 3 const data = Template.currentData(); 4 Meteor.call("someExpensiveMethod", data._id, data.someOtherField, ...); 5 }); 6});
A better approach would be to only depend on the fields we care about - you could do this manually of course, or you could use this.reactiveData(...fieldList)
:
1export class MyComponent extends BlazeComponent { 2 constructor(templateInstance) { 3 super(templateInstance); 4 this.autorun(() => { 5 const data = this.reactiveData("_id", "someOtherField"); 6 Meteor.call("someExpensiveMethod", data._id, data.someOtherField, ...); 7 }); 8 } 9}
Obvious autoruns on data changes
In some cases autorun blocks are exclusively dependent on changes to the data context - for the sake of readability it's nice to be clear about this! Let's rewrite the above example:
1export class MyComponent extends BlazeComponent { 2 constructor(templateInstance) { 3 super(templateInstance); 4 this.dataChanged("_id", "someOtherField", (data, comp) => { 5 Meteor.call("someExpensiveMethod", data._id, data.someOtherField, ...); 6 }); 7 } 8}
this.dataChanged
will trigger an invalidation whenever the data changes, or whenever a dependency within the callback changes. To JUST invalidate on data changes use this.dataChangedStrict
Obvious autoruns on state changes
Same as with dataChanged
but with stateChanged
and tracks internal component state
1export class MyComponent extends BlazeComponent { 2 constructor(templateInstance) { 3 super(templateInstance); 4 this.stateChanged("_id", "someOtherField", (state, comp) => { 5 Meteor.call("someExpensiveMethod", state._id, state.someOtherField, ...); 6 }); 7 } 8}
this.stateChanged
will trigger an invalidation whenever the state changes, or whenever a dependency within the callback changes. To JUST invalidate on state changes use this.stateChangedStrict
Pausable autoruns
Sometimes you might want an autorun block to run exactly once to "completion" whatever that might be, for example, waiting for previous subscriptions to finish then calling a method to get data. You could accomplish this with stop, but what if you want it to run to completion exactly once whenever some external state changes, e.g., the data to the template changes.
1export class MyComponent extends BlazeComponent { 2 constructor(templateInstance) { 3 super(templateInstance); 4 this.myComputation = this.once( 5 () => ({ 6 fieldICareAbout: this.reactiveData().fieldICareAbout 7 }), 8 (comp, preconditionResult) => { 9 if (Meteor.status().connected) { 10 Meteor.call("somemethod", preconditionResult.fieldICareAbout, (err, res) => { 11 if (err) { 12 //handle error; 13 return; 14 } 15 comp.pause(res); 16 }); 17 } 18 }, 19 { useGuard: true } 20 ) 21 } 22 23 someReactiveFunc() { 24 return this.myComputation.result.get(); 25 } 26}
In the above example we'll keep trying to call somemethod
until meteor is connected, and we get a result. Then we'll stop running until fieldICareAbout
changes, at which point we'll try to call somemethod
once. You could also manually trigger a rerun by calling this.myComputation.resume(force)
. resume(true)
will trigger a rerun immediately resume(false)
will just allow a rerun to occur the next time the computation is invalidated. Passing in { useGuard: true }
will wrap the precondition function in a Tracker.guard
. The precondition function is optional.