When you ask Wapr9 to render a z-expression with warp9.render
it
parses the z-expression into a tree (reactive DOM), where each node may be
either an Element (which corresponds to a html tag) or a reactive variable. Of
cause an Element may have children (another nodes) represented as a js array or
as a reactive list. A reactive variable's value is a subtree of a tree.
When parser recognises a z-expression, it takes the first value (pseudo tag) and search a sub parser that can parse that z-expression. Warp9 provides a way to register a new sub parser to process new z-expression, so this a way you can introduce a new custom tag.
Lets reinvent a simple validation framework to understand how to create custom tags.
You may notice, that ["input-text", rv]
is a textbox which updates a
reactive variable rv
with textbox's value on each textbox update. Lets
imagine a ["input-email", rv]
, a textbox which does a email validation and
writes to rv textbox's value, a validation flag and an error message if needed.
Something similar to:
{
isValid: true,
value: "email@example.org"
}
or
{
isValid: true,
value: "email@",
message: "\"email@\" is not a valid email"
}
Also image we have a tag to display and decorate a validation message:
["validation", rv, function(message){
return ["div", {"class": "error"}, message];
}]
Having this we may write a simple UI piece to validate email
And it's source might be similar to
var email = new warp9.Cell();
warp9.render(placeholder, ["div",
["input-email", email],
["validation", email, function(message){
return ["div", {"class": "error"}, message];
}]
]);
Lets implement this and with an approach you already know - with functions. The code for UI piece would transform into
var email = new warp9.Cell();
warp9.render(placeholder, ["div",
inputEmail(email),
validation(email, function(message){
return ["div", {"class": "error"}, message];
})
]);
The
full sources
of function based VF is available. But I want to focus you attention to validation
method,
which is the first to be rewritten to custom tag.
function validation(value, builder) {
return value.when(
function(value) { return !value.isValid; },
function(value) { return builder(value.message); }
)
}
Above all we must register a new sub-parser for "validation" tag, it can be done with
warp9.ui.renderer.addTag
method. A provided handler will be called by the
parser each time it meets ["validator", ...]
and the handler must return a
reactive DOM which becomes sub-tree in the final reactive DOM.
warp9.ui.renderer.addTag("validation", function(args) {
return ...
});
args
is the tail of ["validator", ...]
array. For example
if a z-expression is ["validator", a, b]
then the args
is
[a, b]
. Also you should know that warp9.ui.renderer.parse
is
the entry point for the parser. Now we have all information to fill the ellipsis.
warp9.ui.renderer.addTag("validation", function(args) {
var value = args[0];
var builder = args[1];
return warp9.ui.renderer.parse(value.when(
function(value) { return !value.isValid; },
function(value) { return builder(value.message); }
));
});
Actually it is direct a wrap of validation function-like component into a custom tag.
Translation of inputEmail
is far more interesting - we will construct
an Element I mentioned before. The full sources are
available,
here is the main part, I hope this code will be clear to you find my comments below.
warp9.ui.renderer.addTag("input-email", function(args) {
args = warp9.ui.tags.args.parse(args);
args = warp9.ui.tags.args.tryIntercept("input-email", args);
if (args.children.length != 1) {
throw new Error();
}
if (!warp9.core.Matter.instanceOf(args.children[0], warp9.Cell)) {
throw new Error();
}
if (!args.children[0].hasValue()) {
args.children[0].set(checkEmail(""));
}
var element = new warp9.ui.ast.Element("input", args);
element.attributes.type = "text";
element.attributes.value = args.children[0].lift(function(validated){
return validated.value;
});
element.events.input = function(control, view) {
args.children[0].set(checkEmail(view.value));
};
return element;
function checkEmail(value) {
...
}
});
warp9.ui.tags.args.parse
tries to reinterpret the first element as a
element attributes (attributes, css, events). For example, if the args is
[{id: 15, "css/margin": "5px", "!click": function() {...} }, b]
the output of warp9.ui.tags.args.parse is
{
events: {
click: function() {...}
},
// onDraw has "warp9:draw" handler if it is provided
onDraw: [],
attributes: {
id: 15
},
css: {
margin: "5px"
},
children: [b]
}
if the args is [a, b]
and "a" cant be interpreted as a set of
attributes then the output is
{
events: { },
onDraw: [],
attributes: { },
css: { },
children: [a, b]
}
warp9.ui.tags.args.tryIntercept
is an another way to provide
Warp9's extensibility. You can register a handler which shuffles
attributes for a specific tag. For example, custom event "key:enter" is
mapped to keypress with filter via tryIntercept.
new warp9.ui.ast.Element("input", args);
constructs a new element
which corresponds to html "input" tag and sets attributes (css, events) from args
object (but not children).