uav
is a JavaScript view library for those who are skeptical of complexity.
- It's data-bound. You just change your data, and the view updates accordingly.
- It's simple. The syntax is minimal; there are no dependencies or required toolchains.
- It's small. Less than 3KB gzipped.
Table of contents:
- Hello World
- Todo App
- Creating a Component
- Template Expressions
- Events
- Child Components
- Passing Data to Children
- Creating a Model
- Binding HTML
- Boolean Attributes
- DOM Access
- Two Way Data Binding
- Script Tag Templates
- Performance Notes
- Browser Compatibility
const component = uav.component(
`<h1>{message}</h1>`,
{ message: 'Hello, world!' },
'#app'
);
// Renders into the #app element:
<h1>Hello, world!</h1>
// After running the following ...
component.message = 'Goodbye, world.';
// ... the HTML automatically updates:
<h1>Goodbye, world.</h1>
uav.component(template, model, container, callback)
Arguments:
template
(String or Element): An HTML template. Must have exactly one root node.model
(Object): A view model. Optional.container
(String or Element): The element in which to render the component. Optional.callback
(Function): A callback that will be passed the component's root node. Optional.
Returns the model, if provided. Otherwise returns the component's DOM node.
Changes to existing properties on the model will trigger an optimized re-render. Only the smallest DOM change possible will occur based on which properties on the model have changed.
By default, uav expressions use {curly}
notation. Any browser-supported JavaScript can be used in an any expression. The result of an expression can be any of the following:
- String
- Number
- Function
- Boolean
- DOM element
- uav component
- undefined or null (renders an empty string)
You can change the template tag syntax with
uav.setTag()
. For example, to use{{mustache}}
notation, calluav.setTag('{{', '}}')
before creating any components.
uav.component(
`<div>This is a content expression: {content}</div>`,
{ content: 'foo' }
);
Prepend u-
to an attribute name to tell uav
to parse it.
const component = uav.component(
`<div u-class="{className} {visible}"></div>`, {
visible: true,
className: 'component'
});
// Renders the following:
<div class="component visible"></div>
If an attribute expression evaluates to a boolean, it will render nothing if false, or the property name if true. This makes toggling the "visible" class on the above
<div>
as easy ascomponent.visble = !component.visible
.
Use the u-for
attribute to loop over an array as follows:
uav.component(`
<ul u-for="item in items">
<li>{item}</li>
</ul>
`, {
items: [ 1, 2, 'three' ]
});
This component will render the following:
<ul>
<li>1</li>
<li>2</li>
<li>three</li>
</ul>
You can set a variable for the index of the current item by adding a comma and a variable name to the attribute value:
uav.component(`
<ul u-for="item, index in items">
<li class="item-{index}">{item}</li>
</ul>
`, {
items: [ 1, 2, 'three' ]
});
Renders:
<ul>
<li class="item-0">1</li>
<li class="item-1">2</li>
<li class="item-2">three</li>
</ul>
Things you may wonder about:
- Properties of the parent model are available within a loop.
- Like a component, a loop's content must have one root node.
- Array methods that modify the array like
push
andsplice
will trigger a render.
uav.component(
`<button u-onclick="{click}">Click me</button>`,
{ click: e => console.log(e) }
);
Like any expression, you can pass data to an event handler:
uav.component(`
<ul u-for="item in items">
<li u-onclick="{click(item)}">This is {item}</li>
</ul>
`, {
click: item => e => console.log(item),
items: [ 'foo', 'bar', 'baz' ]
});
A component can be rendered into other components.
const child = uav.component(`<h3>{message}</h3>`, {
message: 'I am a child.'
});
uav.component(`
<div>
<h1>This is a component with a child.</h1>
{child}
</div>
`, { child });
This will render the following:
<div>
<h1>This is a component with a child.</h1>
<h3>I am a child.</h3>
</div>
const child = data => uav.component(
`<em>{data}</em>`,
{ data }
);
uav.component(`
<div>
This component passes data to its child.
{child}
</div>
`, {
child: child('This is passed from parent to child.')
});
The parent component above passes data when the model is created. You could just as easily pass the data through the template:
uav.component(`
<div>
This component passes data to its child.
{child('This is passed from parent to child.')}
</div>
`, { child });
Either way, it will render the following:
<div>
This component passes data to its child.
<em>This is passed from parent to child.</em>
</div>
uav supports swapping child components on the fly. For example, you could call component.child = someOtherComponent
and the view will update accordingly.
If you want to create a view model before associating it with a template, use uav.model
. This can come in handy when a model refers to itself at render time.
const model = uav.model({
blue: true,
redOrBlue: () => model.blue ? 'blue' : 'red'
});
const component = uav.component(
'<div class="{redOrBlue()}"></div>',
model
);
To render an HTML string as a DOM element, simply treat the string as the template of a child component by passing it through uav.component()
.
uav.component(
`<div>{html}</div>`, {
html: uav.component('<script>location="https://en.wikipedia.org/wiki/Cross-site_scripting"</script>')
});
Use the u-attr
attribute to bind a boolean attribute on an element.
uav.component(
`<input type="text" u-attr="{disabled}">`,
{ disabled: true }
);
// Renders:
<input type="text" disabled>
Just because these are called boolean attributes doesn't mean they have to bound to a boolean value. Here's an example that uses a string:
uav.component(
`<input type="text" u-attr="{'aria-' + ariaProp}">`,
{ ariaProp: 'hidden' }
);
// Renders:
<input type="text" aria-hidden>
Elements can be accessed directly by passing a selector to the uav
or uav.all
functions.
Access the first matched element:
uav('.item').classList.toggle('visible');
Run a callback on all matched elements:
uav('.item', item => item.classList.toggle('visible'));
Get an array of all matched elements:
uav.all('.item').forEach(item => item.classList.toggle('visible'));
Use the u-bind
attribute to two-way bind an input, select, or textarea to a model property. Whenever the user changes the value of the input, the property will be updated. Whenever you programatically change the property, the input's value will be updated.
uav.component(
`<input type="text" u-bind="value"/>`,
{ value: 'hi there' }
);
Checkboxes can be bound to arrays or booleans.
uav.component(`
<form>
<input type="checkbox" u-bind="items" value="1"> 1
<input type="checkbox" u-bind="items" value="2"> 2
<input type="checkbox" u-bind="items" value="3"> 3
</form>`, {
items: [1, 2]
});
See a live demo of two way binding
It is recommended to write your templates using ES6 template strings because A) it provides the opportunity to interpolate data without binding it, using the native template ${variable}
syntax, and B) it's easier to remove unnecessary whitespace in your templates during a build step. That said, you can also define templates within your HTML, and pass a DOM element instead of a template string to uav.component
:
<div id="app"></div>
<script type="template" id="template">
<h1>{content}</h1>
</script>
<script>
const template = uav('#template');
uav.component(template, {
content: 'Hello, world!'
}, '#app');
</script>
Avoid putting any data on the model that doesn't need to be bound to the DOM. If a particular value will never change, or changes to it don't need to update the DOM, just use a regular ES6 template variable to reference it (put a dollar sign in front of the expression).
const wontChange = 'hi!';
uav.component(`
<div>
<p>${wontChange}</p>
<p>{willChange}</p>
</div>
`, {
willChange: 'loading...'
})
When uav updates the DOM as a result of a change to a model, it automatically removes any bindings to DOM nodes that have been removed or replaced. However, if for some reason you manually remove or replace a bound DOM node, you can clean up any bindings associated with it by calling uav.unbind(<Element>)
.
Using multiline template strings creates unnecessary whitespace in your JavaScript files. To collapse this whitespace, add a step like this to your build process:
tr -s " " < dist/bundle.js > dist/tmp && mv dist/tmp dist/bundle.js
Modern browsers and IE9+.