zodern

Implementing Flare - Part 4

This is part 4 of a series of blog posts creating the Flare Meteor package to support single file Blaze templates.

In the past 3 posts, we have modified Blaze's syntax to be very close to what we decided on in the first post. In this post, we will switch to providing some functions we can import and use within the blaze files: $state for template state, and $parent for accessing parent context.

Normally when we want template state, we will add a reactive dictionary or reactive var to the template. $state will use a ReactiveDict, but remove the boilerplate. $state will copy the the ReactiveDict api.

To remove all boilerplate, we want to transparently create or access the Reactive Dict for the template instance. When any function is called on $state, it will:

  1. Access the current template and check if we have created a ReactiveDict for the current template instance. The reactive dict will be stored on the __flareState property in the template instance.
  2. If it does not exist, we will create a ReactiveDict.
  3. Then we will forward the call to the ReactiveDict instance. For example, if $state.get('count') is called, in $state.get we will call Template.instance().__flareState.get('count').

The implementation will look like:

function getOrCreateState(name) {
const instance = Template.instance();
if (!instance) {
throw new Error(`$state.${name} must be called with a Blaze helper, template, or event handler`);
}

if (!instance.__flareState) {
instance.__flareState = new ReactiveDict();
}

return instance.__flareState;
}

export const $state = {
get() {
const state = getOrCreateState('get');
return state.get.apply(state, arguments);
},
set() {
const state = getOrCreateState('set');
return state.set.apply(state, arguments);
},
setDefault() {
const state = getOrCreateState('setDefault');
return state.setDefault.apply(state, arguments);
},
equals() {
const state = getOrCreateState('equals');
return state.equals.apply(state, arguments);
},
all() {
const state = getOrCreateState('all');
return state.all.apply(state, arguments);
},
clear() {
const state = getOrCreateState('clear');
return state.clear.apply(state, arguments);
}
}

We didn't include destroy(). It is the same as clear, but also prevents restoring its content after hot code push. Since this reactive dictionary is not named, destroy and clear have no difference.

We can put this in a new client.js file, and add it in our package.js file. Since we are using es6, we should also add a dependency on ecmascript so our client file is compiled with Babel. Our package now also depends on reactive-dict.

// ... rest of file

Package.onUse(function (api) {
api.use('isobuild:compiler-plugin@1.0.0');
api.use(['ecmascript', 'reactive-dict'], 'client');

api.mainModule('client.js', 'client');
});

After this change, Meteor started crashing on my computer with an error about reactive-dict being an unknown package. I had to run meteor reset to fix it.

We can now update counter.blaze to import $state and use it:

<script>
import { ReactiveVar } from 'meteor/reactive-var';
import { $state } from 'meteor/flare';

export const name = 'counter';

Template.counter.onCreated(function () {
$state.set('counter', 0);
});

function increment(_event, instance) {
const current = $state.get('counter');
$state.set('counter', current + 1);
}

function counter() {
return $state.get('counter');
}

Template.registerHelper('isOdd', number => {
return number % 2 === 1;
});

const rawHtml = '<p>This is from raw html</p>'
</script>

<button on:click={increment}>Click Me</button>
{#if counter()}
<p>You've pressed the button {counter()} times.</p>
{/if}

{#if isOdd(counter())}
<p>{$state.get('counter')} is odd</p>
{/if}

{? rawHtml }

Next, we will implement $parent. Blaze doesn't recommend using the parent context, but it does provide a way to and there are some situations where it is useful. Allowing js expressions in template tags prevents accessing the parent context with the .. syntax spacebars uses. With JS, Blaze allows accessing the parent context with Template.parentData. This is much longer, so we will create a shorter alias: $parent.

In the client.js file we can add a new export:

export const $parent = Template.parentData;

Let's try it. Update counter to create parent context and then access it:

<script>
import { ReactiveVar } from 'meteor/reactive-var';
import { $state, $parent } from 'meteor/flare';

export const name = 'counter';

Template.counter.onCreated(function helloOnCreated() {
$state.setDefault('counter', 0);
});

function increment(_event, instance) {
const current = $state.get('counter');
$state.set('counter', current + 1);
}

function counter() {
return $state.get('counter');
}

</script>

<button on:click={increment}>Click Me</button>
{#if counter()}
<p>You've pressed the button {counter()} times.</p>
{/if}

{#with color="blue"}
{#with color="green"}
{#with color="red"}
<p>this: {this.color}</p>
{#each [2,3]}
<p>parent levels: {this} - {$parent(this).color}</p>
{/each}
{/with}
{/with}
{/with}

This should output

this: red
parent levels: 2 - green
parent levels: 3 - blue

If you've tried using {#each item in array}, you would have discovered it doesn't work. In the next step we will fix accessing the lexical scope created by {#each} and {#let}.

Have a comment? Please Send me an email


Want More? Join my monthly newsletter for new blog posts and news about Meteor and my projects.

Have a Meteor app in production? I run Monti APM for production monitoring to help improve performance and reduce errors.