What is a Helper?

A Markup helper is simply a function, which the main purpose is to help with some render logic or value in the template.

It can be used to check data, wrap or render templates, manipulate data, etc. You can create simple functions and use the template update method to trigger them all or wrap functions in helper call to make it work with template states.

Simple helper

Bellow is a simple example of a helper. For this case this code is using the template update method to trigger updates on the DOM. In this case, any function in the template will get called with every update.

let count = 0;

// simple count helper function to display "even" or "odd" 
const evenOddCountLabel = () => count % 2 === 0 ? 'even' : 'odd';

const counter = html`
  <p>${() => count}</p>
  <p>${evenOddCountLabel} count</p>
  <button type="button" onclick="${countUp}">+</button>
`;

function countUp() {
  count += 1;
  counter.update()
}

counter.render(document.body)

As you can see, a helper is simply a function that does a particular calculation and returns something for a part of the template. This can be an attribute value or a template content.

For contrast, this is how it would look like if I was using state count instead.

let [count, setCount] = state(0);

const evenOddCountLabel = () => count() % 2 === 0 ? 'even' : 'odd';

const counter = html`
  <p>${count}</p>
  <p>${effect(count, evenOddCountLabel)} count</p>
  <button type="button" onclick="${countUp}">+</button>
`;

function countUp() {
  setCount(prev => prev + 1)
}

counter.render(document.body)

The difference is the usage of the effect helper to tell the template to execute the helper whenever the count changes since this particular helper uses the count state inside.

Helper wrapper

We can simplify the helper above with the helper wrapper and remove the need for the effect helper when using it in the template.

To use the helper wrapper, all you need to do is wrap a function with it.

const evenOddLabel = helper((x: StateGetter<number>) => x() % 2 === 0 
   ? 'even' 
   : 'odd');

Notice that now it is a pure function that takes the x input which must be a StateGetter. What this mean is that, whenever the state changes, the helper will tell the template to update that specific part of the DOM where it is used.

Here is the full result:

let [count, setCount] = state(0);

const evenOddLabel = helper((x: StateGetter<number>) => x() % 2 === 0 
   ? 'even' 
   : 'odd');

const counter = html`
  <p>${count}</p>
  <p>${evenOddLabel(count)} count</p>
  <button type="button" onclick="${countUp}">+</button>
`;

function countUp() {
  setCount(prev => prev + 1)
}

counter.render(document.body)

You can learn to improve it further by checking how to create your custom helpers.

Helper value and arguments

The helper wrapper returns an instance of Helper that the template itself knows how to handle by accessing the value and args properties.

evenOddLabel(count).value;
evenOddLabel(count).args;

By knowing these two properties, you can use them anywhere in your code for any type of functionality you want to create.

Working with helpers

Helpers are just functions, and you can do anything you would with functions including returning or passing them around.

Below is a simple example of nesting helpers, in this case the is, when, and repeat helpers.

html`${repeat(
  when(is(userType, 'user'), ['name', 'status'], ['name', 'role']), 
  part => html`<strong>${part}</strong>`)
}`

What this is illustrating is the functional oriented nature of Markup, specifically the ability to compose simple functions to create complex renders and logic handlers.

Helper scope state

Helpers can also keep internal state through JavaScript closures, and it should not be a problem. Your helper function can be up to one level nested function.

The following filter use the outer function to "cache" the condition, and the filtered list results, so it can save unnecessary computation. The inner returned function is used as the handler.

const filterList = helper((list, filterer, condition = () => null) => {
  let val = [];
  let cond;
  
  return () => {
    if(condition() !== cond) {
      val = list().filter(filterer);
      cond = condition();
    }
    
    return val;
  }
})

This is just a simple example, but it shows that you can nest functions up to one level (outer and inner) and the helper would still function well.

This capability will allow helpers to keep data between calculations, and you to build more powerful helpers to support your project.