CWCO logo

v1.7.6

Custom Directives

Not everything needs to be components. Directives allow you to add behavior to how a particular element gets rendered.

CWCO comes with a limited variety of directives which may not be enough for things you are trying to build. The ability to create your own directives allows you to do even more powerful things.

One thing to know about directive attributes is that they are never rendered on the DOM. That means once it activates, it stays in the background to perform the instructions you provide it with when there is a data change.

Directive

To give you the ability to create your own directive, CWCO exposes the Directive class which you can extend to define your custom directive. It is that easy!

// in the browser
const {Directive} = Window;

// in node
const {Directive} = require("cwco")

All you have to do is create your class that extends Directive.

class Wrapper extends Directive {
	// logic inside
}

Name

Like components, the name of your class is used to create the directive, and it will be all lower-cased when used on the tags.

For our Wrapper class example above, the attribute would be called wrapper.

That means that you could create a class called ContentEditable and the attribute would be called contenteditable which allows you to define your own behavior for that native attribute.

Register

Just by creating your directive class will not make it immediately usable. You must register it first by calling the static register method very similarly to how you register components.

class Wrapper extends Directive {
	// logic inside
}

Wrapper.register();
Note: It is best if you register all your directives before your components, so they get picked up when rendering the components you create.

You may also specify a name for your directive when you are registering it.

class Wrapper extends Directive {
	// logic inside
}

Wrapper.register('wrap-in');

Simply make sure that the directive name is a valid attribute name and without "dots" as these have special meaning for CWCO. This library will not validate your directive name.

parseValue()

The Directive class exposes the parseValue method you must override to handle parsing the attribute value string before it gets changed to a specific data.

It must return a valid JSON value string, and it gets called with the value (if any) and props which are the rest of the attribute name after the dot(if any).

For the below example, the attr directive the parseValue would be called with item as value and class as prop.

<button attr.class="item, true">click me</button>

For the repeat directive the parseValue would be called with 3 as value and null as prop.

<li repeat="3">{$item}</li>

Let's look at a more concrete example with our wrapper directive.

The wrapper directive takes the name of the tag to wrap the node in. The below example would create 3 li tags based on the repeat value inside a ul tag and our custom wrapper directive would then put the ul tag inside the nav tag.

<ul wrapper="nav">
	<li wrapper="ul" repeat="3">{$item}</li>
</ul>

For this example the parseValue would be called with nav as value and null as prop.

We could also support props. let's say we can tell which display to set on the wrapper tag like so.

<ul wrapper.grid="nav">
	<li wrapper="ul">{$item}</li>
</ul>

This time the parseValue would be called with nav as value and grid as prop, and we can handle it like this:

class Wrapper extends Directive {
	parseValue(value, prop) {
		return `["${value}", "${prop}"]`;
	}
}

Wrapper.register();

The parseValue must return a valid JSON value STRING and what we are returning is a string representation of an array with 2 items. The first is the value and the second is the prop. This string will be then changed into a real array and passed to the render method as first argument.

render()

The render method is what tells CWCO how to render the element. It is also where you will put the logic related to how you want the element to render.

It must return one of these:

  • The node itself (default);
  • A valid node(text, comment or any other HTML element);
  • array of nodes;
  • null.

Whatever you return will be rendered instead of the node element but if you return null the node will be simply commented out. Returning the node received simply means the node will remain rendered as is.

The render method gets called with 2 arguments:

  1. value (result of parsing the string returned by parseValue);
  2. options containing:
    • element: the element that the directive was attached to;
    • rawElementOuterHTML: the element's outerHTML as it was defined in the template. Use WebComponent.parseHTML to turn into an Element;
    • node: anything returned previously by render. Will be null on the first time.

Continuing with our wrapper example, the render method should be expecting an array containing the value and the prop as first argument, the node itself and its outer HTML as defined in the template.

We can handle that like so.

class Wrapper extends Directive {
	parseValue(value, prop) {
		return `["${value}", "${prop}"]`;
	}
	
	render([value = 'div', prop], {element}) {
		const wrapperNode = document.createElement(value);
	
		if(prop) {
			wrapperNode.style.display = prop;
		}
	
		// ALWAYS clone the element if you planing on moving
		// it in the DOM
		wrapperNode.appendChild(element.cloneNode(true));
		
		return wrapperNode;
	}
}

Wrapper.register();
Note: that the node was cloned before appended to the wrapperNode. It is not a good idea to move the node from its current place as it will result in weird behaviors. You can set attributes and even insert nodes as descendent but never remove it from the DOM or remove its children. Clone it and modify the clone as much as you want.

CWCO still keep reference of the original element around. What you are returning with render is a node to replace it with on the DOM. Your directive will be called on every change so add logic with that in consideration.

node reference

You may also create node references similar to the ref directive using the setRef method of Directive.

This method will set the reference of the node you specify in the component $refs object which you can access in component lifecycle or any other methods.

Let's say we want a directive that creates references of every node first child elements. It would look like this in the HTML:

<ul firstchildref="firstItem">
	<li wrapper="ul">{$item}</li>
</ul>

On the javascript side, all we need to do in the render is call setRef to create reference of the first child.

class FirstChildRef extends Directive {
	render(name, {element}) {
		if(element.children[0]) {
			this.setRef(name, element.children[0])
		}
	
		return element;
	}
}

FirstChildRef.register();

This will be accessed the same way you access node references inside component.

node context

You may also define context data for the node element itself. This is similar to component context which means that you can define data for the node which will be inherited by any descendent nodes.

You already have seen this in action. When you use the repeat directive it sets $item and $key context which can be accessed by anything inside the node.

<li repeat="3">
	<span>{$item}</span>
</li>

Let's say that for some reason(a weird one) we want to display inside a node how many children it has like so:

<todo-app childcount>
	<h2>Todos {$childCount - 1}</h2>
	<todo-item repeat="items" name="{$item.name}" description="{$item.description}" status="{$item.status}"></todo-item>
</todo-app>

On the javascript side, all we have to do is the following:

class ChildCount extends Directive {
	render(name, {element}) {
		this.setContext(element, '$childCount', element.children.length)
		
		return element;
	}
}

ChildCount.register();

You may also get a specific node context with the getContext(anyNode).

In general, it is good practice to mark node context data with leading dollar sign like repeat context $item and $key. It helps makes a good distinction of where data is coming from and match the cwco convention as well.

It is important to know that a node context data has precedence over the component data or context data. It will also override its ancestors nodes similarly named context data when accessed.