Custom tags

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.

Validation framework

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).