salmanhasni:blaze-component

v2.0.1Published last year

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

  1. Two new helpers self and root described below
  2. Small bug fix (statChangedStrict -> stateChangedStrict).
  3. Helper functions for binding state to data
  4. performance optimizations for this.get and this.set reactivity when called with objects/arrays that you don't want to be stringified for comparisons.
  5. added '.' notation to anywhere that you can pass in a field list (state changed, data changed, etc).
  6. Finally, adding useNonReactiveData to global helpers.
  7. 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

  1. autorun
  2. subscribe
  3. $

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.