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:
- value (result of parsing the string returned by
parseValue
); - 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.