meteor-blaze-component
v1.4.5
Published
Provides a helper class to make common patterns with blaze easier.
Readme
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:
<!-- myComponent.html -->
<template name="myComponent">
Some content
</template>// myComponent.js
import "./myComponent.html"
import { BlazeComponent } from "meteor/znewsham:blaze-component";
// define your component
export class MyComponent extends BlazeComponent {
constructor(templateInstance) {
super(templateInstance, { someInitialStateKey: "someValue" });
// the rest of your code that may have previously gone in `onCreated`
}
rendered() {
// your code that may have previously gone in `onRendered`
}
destructor() {
// disables any timeouts/intervals associated with this component instance
super.destructor();
// your code that may have previously gone in `onDestroyed`
}
}
// register your component and link it to a template
BlazeComponent.register(Template.myComponent, MyComponent);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:
export class MyComponent extends BlazeComponent {
static HelperMap() {
return ["myHelper"];
}
myHelper() {
return "something";
}
}
MyComponent.prototype.myHelper.useNonReactiveData = true;Or for all helpers in the template:
export class MyComponent extends BlazeComponent {
constructor(templInstance) {
super(templInstance);
templInstance.useNonReactiveDataForHelpers = true;
}
}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.
export class MyComponent extends BlazeComponent {
...
static HelperMap() {
return {
myHelper: "hello"
};
}
static EventMap() {
"keydown .anInput": "textChanged",
"keyup .anInput": "textChanged",
"blur .anInput": "textChanged"
}
textChanged(e, templInstance) { //this = instance of MyComponent
// in this case templInstance is a little redundant
}
hello() { // this = instance of MyComponent
// you lose this = data context, so you'll have to pass in data. Meteor says this is best practice anyway
return "Hello!"
}
}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.
export class MyComponent extends BlazeComponent {
constructor(templInstance) {
console.log("pre-init");
super(templInstance);
console.log("post-init");
}
init() {
console.log("init");
}
}The output here will be:
pre-init
init
post-initThe 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.
export class MyComponent extends BlazeComponent {
created() {
...
}
rendered () {
...
this.setTimeout(() => {
// init a 3rd party component, or trigger some other functionality
}, 1000);
...
this.setInterval(() => {
// poll some method
}, 1000);
}
}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.
export class MyComponent extends BlazeComponent {
created() {
...
}
rendered () {
this.autorun(() => {
// some reactive condition
...
this.setTimeout(() => {
// init a 3rd party component, or trigger some other functionality
}, 1000, { name: "CallMeOnce" });
...
});
}
}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.
export class MyComponent extends BlazeComponent {
created() {
...
}
rendered () {
this.on(window, "resize", () => {
console.log("resized");
});
}
}
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:
Template.MyComponent.onCreated(() => {
this.autorun(() => {
const data = Template.currentData();
Meteor.call("someExpensiveMethod", data._id, data.someOtherField, ...);
});
});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):
export class MyComponent extends BlazeComponent {
constructor(templateInstance) {
super(templateInstance);
this.autorun(() => {
const data = this.reactiveData("_id", "someOtherField");
Meteor.call("someExpensiveMethod", data._id, data.someOtherField, ...);
});
}
}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:
export class MyComponent extends BlazeComponent {
constructor(templateInstance) {
super(templateInstance);
this.dataChanged("_id", "someOtherField", (data, comp) => {
Meteor.call("someExpensiveMethod", data._id, data.someOtherField, ...);
});
}
}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
export class MyComponent extends BlazeComponent {
constructor(templateInstance) {
super(templateInstance);
this.stateChanged("_id", "someOtherField", (state, comp) => {
Meteor.call("someExpensiveMethod", state._id, state.someOtherField, ...);
});
}
}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.
export class MyComponent extends BlazeComponent {
constructor(templateInstance) {
super(templateInstance);
this.myComputation = this.once(
() => ({
fieldICareAbout: this.reactiveData().fieldICareAbout
}),
(comp, preconditionResult) => {
if (Meteor.status().connected) {
Meteor.call("somemethod", preconditionResult.fieldICareAbout, (err, res) => {
if (err) {
//handle error;
return;
}
comp.pause(res);
});
}
},
{ useGuard: true }
)
}
someReactiveFunc() {
return this.myComputation.result.get();
}
}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.
