Skip to content

Latest commit

 

History

History
250 lines (184 loc) · 6.36 KB

directives.md

File metadata and controls

250 lines (184 loc) · 6.36 KB

Directives

The crux of Strawberry is reactivity which is easily described as: When my data changes do x

In Strawberry things (i.e. x) are done by using directives. For example:

  • sb-mark is a directive that sets the inner text of an element when the value in the RDO changes.
  • sb-if is a directive that inserts or removes an element depending on the truthy-ness of the value.

Directives can be used to extend the functionality of strawberry.

Index

  1. Directive is a function
  2. Directive is called on data change
  3. Directives can be registered using sb.directive
  4. Example: Two-way binding using a directive
  5. Directives can have parameters
  6. Example: Event listeners using a directive

Directive is a function

You can add all sorts of additional functionality to Strawberry by using directives. A directive is a function with the following signature:

type Directive = (params: DirectiveParams) => void;

type DirectiveParams = {
  // Element on which the directive has been set.
  el: Element;

  // Updated value.
  value: unknown;

  // Period '.' delimited key of the value in the RDO.
  key: string;

  // Whether the value was deleted `delete data.prop`.
  isDelete: boolean;

  // Parent object to which the value belongs (the proxied object).
  parent: Record<string, unknown>;

  // Property of the parent which points to the value `parent[prop] ≈ value`
  prop: string;
};

Directive is called on data change

A directive function is called whenever the value of the mentioned key changes in the RDO.

For example, in sb-mark="form.title", form.title is the key of a value in the RDO i.e. data.form.title and when this is set or changes or is deleted, the directive is called.

Directives can be registered using sb.directive

sb.directive(
  // Name of the directive
  name: string,

  // The callback function of the directive
  cb: Directive,

  // Whether the directive is parametric
  isParametric: boolean
): void;

To register a directive, you can use the sb.directive function.

sb.directive('somedirective', () => {
  /* ... */
});

This can now be used like so:

<p sb-somedirective="value"></p>
<script>
  data.value = '...';
</script>

Example: Two-way binding using a directive

Let's implement two-way binding by using directives.

What is two-way binding?

In short, two-way binding is a binding between data and a some input element, so:

  1. When the data value changes, the value in the input should change.
  2. When the input value changes, it should update the data value.

For example:

<input type="text" for="name" />
<script>
  data.name = '';
</script>

When user changes the value in input[for="name"] the value of data.name should change, and when we change the value of data.name the value of input[for="name"] should change.

For this we'll write a simple directive called bind:

sb.directive('bind', ({ el, value, parent, prop }) => {
  el.value = value;
  el.oninput ??= (e) => {
    parent[prop] = e.target.value;
  };
});

Let's go over what's happening here:

1. Setting the input value

el.value = value;

Since the directive is called only when the value in the RDO changes, we can directly use this value, stored in the value arg to set the input elements value.

2. Setting the data value

el.oninput ??= (e) => {
  parent[prop] = e.target.value;
};

Since the directive can be called multiple times, we want to add an oninput listener to the inputElement only once (Hence the ??= which checks if a value is set before setting it).

And in the input listener, we set the data value, here parent is the RDO that contains value, i.e. parent[prop] === value and since parent is an RDO setting a value to it will trigger changes related to parent[prop]

Using the bind directive

Once, it has been defined, you can use it like so:

<p>Hello, <span sb-mark="name"></span></p>
<input type="text" sb-bind="name" />

<script>
  data.name = 'Fyo';
</script>

Now if you update input, the value in the span will change too. And if you update data.name both the input and the span values will change.

Warning

Theparent arg passed to a directive is the reactive object. So setting a value on it without checks can cause recursion.

In the bind example above since parent[prop] is set inside the input listener, recursion does not take place.

Directives can have parameters

Parametric directives are directives that can have parameters, it can be defined by passing true as the third arg of the sb.directive function:

sb.directive(
  'parametric',
  ({ el, value, key, param }) => {
    /* parametric directive logic */
  },
  true // registers directive as a parametric directive
);

and then can be used like so:

<p sb-parametric="directiveKey:directiveParameter"></p>
<script>
  sb.directiveKey = 'directiveValue';
</script>

In the above example the directive call back function will receive the following values:

{
  el:    HTMLParagraphElement,
  value: 'directiveValue',
  key:   'directiveKey',
  param: 'directiveParameter'
}

Example: Event listeners using a directive

You can make use of parametric directives to create a generic directive that attaches an event listeners to an element.

Here we'll use the parameter to store the event name.

sb.directive(
  'listen',
  ({ el, value, param }) => {
    el.addEventListener(param, value);
  },
  true
);

We can now set this directive on an element:

<button sb-listen="clicHandler:click">Click</button>

<script>
  data.clickHandler = () => () => console.log('button clicked');
</script>

Info

Here we're using a computed function that returns a function, these aren't executed by strawberry and so will be passed to the directive as a value.

Check the computed documentation for more info.