rp-widget

Templates for rp

npm install rp-widget
26 downloads in the last month

rp-widget

Templates for rp. WORK IN PROGRESS!

They initially render to HTML and than update to DOM when anything changed.

Usage

Compile:

rp-widget template.rpw

Use with some CommonJs runtime:

// choose a runtime before requiring templates
var rpWidget = require("rp-widget/runtime/html5");

// require the compiled template
var template = require("./template");

// The rp library
var rp = require("rp");

var data = rp.variable([...]);
var param1 = rp.variable([...]);
var param2 = rp.const([...]);

rp.atomic(function() {
    // Just call the template... It'll return a HTML string
    document.body.innerHTML = template(data /* main argument */, {
        // additional arguments
        param1: param1,
        param2: param2,
        param3: "Teststring" // any argument is converted to a RP
    });
    // At the end of rp.atomic all HTML generated by templates
    //  must be part of the DOM!
});

Syntax for RPW

Just write normal HTML with some extra stuff.

Inline
{{= data.a.b.c }} Renders a text
{{{= data["xyz"].b }}} Renders a html (unsafe!!)
{{ data("p")() + data("v")() }} Renders a computed value as text
{{{ data("p")() + data("v")() }}} Renders a computed value as html (unsafe!!)

As attributes
<input value="Text" disabled=true /> Const as normal
<input checked=data.a.b.c /> Bind references
<span style={{ "background: " + color() }}> Bind computed values </span>
<span checked={{{ which() ? color : data("color") }}}> Bind delegated values </span>
<div class="one" class="two"> Multiple values are joined with space </div>

Arguments and Variables

<#arg data default> Give the main argument a name
<#arg param1> Additional arguments (required, will throw if missing)
<#arg param3 default=null> Additional arguments with default value (optional)

<#var items={{ [1,2,3] }}> Define a local variable (rp.variable) with initial value
<#var bool=true>

<#let temp={{{ items.size() }}}> Store temporary RP with <#let>

<#let temp2= items filter {{ _%2==0 }} reduce {{ a+b }} plus 1> With operations

Partials

A partial is used like a HTML element but starting with a upper case letter.

They get a main argument plus multiple arguments as attributes.

The content is the main partial given the the called partial. Other partials can given by <#name> and can take an argument.

You can name the main argument given to your provided partial with "as" plus identifier

<Partial "main argument" param1=additional.argument param2=123.456>
    The Partials main partial
<#xyz>
    In first additional partial
<#abc "main argument to partial">
    In second additional partial
</Partial>

Example: The included "If" and "ForEach" partials:

<ul>
    <ForEach items as item>
        <li>
            <If item.done>
                <span>Item {{= item.text }} is done.</span>
            <#elseif {{ item("text")().length > 0 }}>
                <span>{{= item.text }} need to be done.</span>
            <#else>
                <span style="background: red">Empty item</span>
            </If>
        </li>
    </ForEach>
</ul>

Because some stuff is reactive it need to be wrapped in a html element.

Not allowed if x is not constant:
<div>
    Text
    <If x>abc</If>
</div>

Allowed:
<div>
    Text
    <span><If x>abc</If></span>
</div>

The wrapping element (<span>) gets a special class to identify the element and exchange the html content if x is changed.

Using provided partials

The main partial can be used a "Partial": <Partial />

<#partial as Name> Give the main partial another name (instead of Partial)

<#partial type as Name> Get the additional partial <#type> (throws if missing or multiple)
And use it: <Name />

<#partial type as Name optional> (doesn't throw if missing, but is null)
<#partial type as Name default=OtherName> Defaults to another partial

<#partial type as Name with value> Get the <#type> partial and the value (throws if missing or multiple)

<#partials type as array>
<#partials type as array>  Get all matching partials as array of {p: Partial, v: RP} (RP)

<ForEach array as element>
    <#let value = element.v delegated>
    <#let partial = element.p>
    <*{{=partial}} value />
</ForEach>

Prefix

Attributes of HTML elements can have a prefix, which describe the way they are handled.

<input attr:value=xyz />
Bind xyz to the attribute "value"
Changed of "value" are not propagated to xyz

<input property:indeterminate=xyz />
The "property" prefix bind xyz to the property
instead of the attribute

<input bpc:value=xyz />
The "bpc" prefix bind xyz to the attribute value
and the property value to xyz (bpc = on change)
bpc: "Bind Property Change"
bpku: "Bind Property Key Up"
bpkd: "Bind Property Key Down"
bpkp: "Bind Property Key Press"
bpi: "Bind Property Input"

<button on:click=someFn />
The "on" prefix just bind a event.
The argument should be a RP resolving to a function
The "on" prefix allow some special syntax for inlined functions:
<button on:click={{{ xyz.increment() }}} />

<div style:height="50px" class:my-class=true class:other-class=x></div>
Shortcuts for style and class

There are some useful defaults if you don't set the prefix. I. e. "value" defaults to "bpc:value".

id attribute

Use the id attribute on html elements. They are bound to variables. You can access them in your code.

Example:

<button id="myButton" on:click={{{
    myButton === this; // Depends on runtime...
    myButton.disabled = true;
}}}>Button</button>

But better do it this way:

<#var buttonDisabled=false>
<button disabled=buttonDisabled on:click={{{
    buttonDisabled.set(true);
}}}>Button</button>

<script>

Custom code...

<script> __buf.push("<!!>"); </script>
Run when generating html

<script load></script>
Run at end of atomic. The HTML is now in the DOM.

<script changed data.a.b.c></script>
Handle changed (or updated, added, removed) event of RPs.

<script private myFunction(a, b)></script>
Define a local function, which can be used in other scripts or handler

<script public myFunction(a, b)></script>
Define a exported function, which can be used from outside too.
You need to instanciate the widgets with a "id" property.
<Xyz id="abc" /> <script> abc.myFunction(1,2); </script>

<script dispose>clearTimeout(t)</script>
Run when widget is no longer needed.

Use with webpack

With webpack you don't need to compile your template. It will do it for you. Just configure the loader and the extension with:

module.exports = {
    module: {
        loaders: [
            { test: /\.rpw$/, loader: "rp-widget" }
        ]
    },
    resolve: {
        extensions: ["", ".webpack.js", ".web.js", ".rpw", ".js"]
    }
}

And require the template with require("./template").

FAQ

Why another templating language?

It's not just a templating language, it's reactive template. The DOM will ever reflect the current value of the data. But it looks like a templating language, to atract users and be simple to use.

Why not in Jade style?

rp-widget is modular (parser -AST-> (optimizer) -AST-> generator). So it possible (and planed) to make a jade style parser too.

What is the difference to React?

  • React can have templates inlined in javascript. rp-widget cannot do this.
  • React rerenders templates and compute the difference. rp-widget only renders and updates changed elements.
  • React and rp-widget are both declarative. React has one-way bindings, rp-widget two-way bindings.

Hello World

/** @jsx React.DOM */
var HelloMessage = React.createClass({
  render: function() {
    return <div>{'Hello ' + this.props.name}</div>;
  }
});
<#arg name>
<div>Hello {{= name }}</div>

TODO App

/** @jsx React.DOM */
var TodoList = React.createClass({
  render: function() {
    var createItem = function(itemText) {
      return <li>{itemText}</li>;
    };
    return <ul>{this.props.items.map(createItem)}</ul>;
  }
});
var TodoApp = React.createClass({
  getInitialState: function() {
    return {items: [], text: ''};
  },
  onChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var nextItems = this.state.items.concat([this.state.text]);
    var nextText = '';
    this.setState({items: nextItems, text: nextText});
  },
  render: function() {
    return (
      <div>
        <h3>TODO</h3>
        <TodoList items={this.state.items} />
        <form onSubmit={this.handleSubmit}>
          <input onChange={this.onChange} value={this.state.text} />
          <button>{'Add #' + (this.state.items.length + 1)}</button>
        </form>
      </div>
    );
  }
});
<#define TodoList>
    <#arg items>
    <ul>
        <ForEach items as item>
            <li>{{= item }}</li>
        </ForEach>
    </ul>
</define>
<#var items = {{ [] }}>
<#var text = "">
<div>
    <h3>TODO</h3>
    <TodoList items=items />
    <form on:submit={{{
        event.preventDefault();
        items.push(text());
        text.set("");
    }}}>
        <input value=text />
        <#let nextNum = items size plus 1>
        <button>Add #{{= nextNum }}</button>
    </form>
</div>

TODO

  • More included Widgets
  • parse basic expressions in attributes <a href="xyz"+data.x>
  • remove whitespace when appropriate
  • optimize the resulting AST

License

MIT

npm loves you