Hey, I'm Shawn π
- I'm a web nerd doing my best to improve digital services for the City of San Francisco.
- I'm a dad with two awesome boys.
- I like bicycles and synthesizers.
- Black lives matter.
Progressively enhance your HTML with dynamic data
Home Page: http://shawnbot.github.io/tagalong/
License: Other
Currently, you can only dynamically assign attributes with directives. There should be a way to dynamically assign attributes via the template. Here are some proposals:
JavaScript {{ expressions }}
in attributes and text nodes:
<div id="template">
<section id="{{ id }}" class="{{ foo ? 'foo' : '' }} bar">
<h1>{{ name.substr(0, 10) }}</h1>
</section>
</div>
Notes:
eval()
or new Function()
implementations.{{ .. }}
character sequences. Escaping these becomes a thing.Data-bound attributes:
<div id="template">
<section data-bind-id="id" data-bind-class="classes">
<h1 data-bind="name">Name</h1>
</section>
</div>
Notes:
class
attributes.data-bind-attr="key"
attributes are awkward. You'd need to use data-bind-data-foo="bar"
to set the data-foo
attribute.data-id="id"
because that's a perfectly legitimate literal value.Maybe we need a namespace for tagalong-specific attributes? The t-
prefix is nice because it could also mean "template":
<div id="template">
<section t-id="id" class="foo" t-class="classes">
<h1 t-key="name">Name</h1>
<ul t-if="items" t-key="items">
<li><a t-href="url" t-key="text">link</a></li>
</ul>
</section>
</div>
Notes:
t-class
attribute could be a special case, allowing you to specify classes to add/subtract to the classList
. It'd be nice if the key expression could evaluate to a list (['foo', 'bar']
) or map ({foo: true, bar: false}
) and we'd do the right thing under the hood.I've been working around this with render.bind(null, data)
so that I don't have to pass the data on each call:
var model = { /* ... */ };
var render = tagalong.render('#root', model).bind(null, model);
// ...
render();
but this feels like a hack.
incremental-dom supports providing a unique key to elementOpen()
and like functions. We could get the appropriate key in a couple of ways:
t-id
attribute, use the derived id, which should always be unique to the document.t-key
expression that should evaluate to a unique key.Both seems like the best option: use the t-id
expression by default, or the t-key
if it exists.
Right now we have an undocumented createRenderer(element [, context])
method, but this should really just be called compile()
and made "public". We could even consider using privates to stash compiled render functions for each node.
Right now you can't render a "compiled" template to a different element. This probably means adding (back) a compile()
function that doesn't bind the render function to the template node:
// compile a template into a render function
var render = tagalong.compile('#template');
// optionally, bind to a specific context object
var render = tagalong.compile('#template');
// then, render to a target node (by CSS selector or element reference)
render('#target', data);
render(document.querySelector('.data'), data);
As it is right now, using t-as="symbol"
will alias an iterator (t-each
, t-foreach
) or t-with
expression and change the execution context, which makes the "higher-level" data unavailable in nested expressions unless it's explicitly aliased with another t-as
in an ancestor:
<div id="template">
<section t-as="$parent">
<ul t-foreach="ids" t-as="id">
<li t-text="$parent.things[id].name"></li>
</ul>
</section>
</div>
<script>
tagalong.render('#template', {
things: {foo: {name: 'Foo'}},
ids: ['foo']
});
</script>
Ideally, the context would not change but the t-as
symbol would be made available so you could do this:
<div id="template">
<section>
<ul t-foreach="ids" t-as="id">
<li t-text="things[id].name"></li>
</ul>
</section>
</div>
<script>
tagalong.render('#template', {
things: {foo: {name: 'Foo'}},
ids: ['foo']
});
</script>
The tests might be tricky because I'm not sure if the custom element polyfill works in jsdom, but the ability to run these tests in a real browser would be helpful too.
.data
.render([data])
.renderTo(element [, data])
In order to really compete with React, we're going to need to be isomorphic. How about:
var buffer = fs.readFileSync('path/to/template.html', 'utf8');
var html = tagalong.renderToString(buffer, data, context, function(error, html) {
fs.writeFileSync('path/to/output.html', 'utf8', html);
});
Should we use jsdom behind the scenes to parse the HTML into a proper document? Currently, several different parts of the library refer to the document
global, so we'd either need to make the whole thing async, or make only the Node API async and do a jsdom.env()
call the first time that a document is needed, e.g. with a function wrapper:
var jsdom = require('jsdom');
var getDocument = function(fn) {
return function() {
var args = [].slice.call(arguments);
var done = args.pop();
if (global.document) {
return done(null, fn.apply(this, args));
}
var that = this;
jsdom.env('<!DOCTYPE html><body></body>', function(errors, window) {
global.document = window.document;
return done(null, fn.apply(that, args));
});
};
};
// wrap all of the API methods in a :document getting" function that takes an
// additional done callback
tagalong.render = getDocument(tagalong.render);
tagalong.createRenderer = getDocument(tagalong.createRenderer);
// renderToString() needs a DOM into which to parse the template string
tagalong.renderToString = getDocument(function(template, data, context, done) {
var el = document.createElement('div');
el.innerHTML = String(template);
tagalong.render(el, data, context, done);
return el.innerHTML;
});
There might be a synchronous way to create a document in jsdom too, if we don't need to parse any HTML from the get-go.
Right now, data-bound elements are only touched if the corresponding key (property) exists in the data. This means that if some (or all) of your data lacks the named keys, those elements won't be touchedβeven if they explicitly use the data-bind
attribute. You can work around this with directives, but it feels like a hack.
At the very least, elements with data-bind
should be addressed explicitly, and have null (or undefined) values bound to them if the corresponding key is missing in the data.
It might be better to do recursion (walking the node tree), which also solves another problem: Namely, that you currently have to nest your elements the same number of levels deep that your data goes in order to access nested object properties. For instance, if you have this:
var data = {
foo: {
bar: {
beep: 'boop'
}
}
};
Then you have to do this to access data.foo.bar.beep
:
<div class="foo">
<div class="bar">
<b class="beep"></b>
</div>
</div>
Another thing that would be good to support is, essentially, wiping out elements that lack corresponding data. So if the above were written as:
<div data-bind="baz"><!-- note: "baz" instead of "foo" -->
<div data-bind="bar">
<b data-bind="beep"></b>
</div>
</div>
then I would expect that outer <div>
to be either removed, hidden (style="display: none"
?), or have its contents wiped out, possibly recursively.
Another thing that would be nice to support is dot-notation, so you could use data-bind="foo.bar.beep"
to access a deep property. Properly does this, as do dot-map and keypather, though a simpler (and more file-size-conscious) solution would be to just include something like:
function accessor(key) {
var keys = key.split('.');
return function(d) {
return keys.reduce(function(key, d) {
return (typeof d === 'object') ? d[key] : undefined;
}, d);
};
}
This is slightly tricky because the conditions need to short-circuit.
var dom = require('incremental-dom');
var data = {
title: 'foo',
items: [
'bar',
'baz'
]
};
var template = document.querySelector('.template');
dom.patch(template, binder(template, data));
function binder(node, data, directives) {
return function() {
// magic
};
}
Right now if you use a directive to generate an array for list binding, there's no way to specify sub-directives for those elements. This means that you can't set attributes on elements bound to lists, or any of their children.
One solution would be to add a special case for arrays that treats directive-generated lists as directives, rather than data. This seems like an ugly hack.
Another would be to revive the idea of nested directive keys:
tagalong('#target', {
foo: 'bar'
}, {
links: function(d) {
return [{name: d.foo}];
},
'links.text': 'name',
'links.@href': function(d) {
return '#' + d.name;
}
});
This would mean that resolveKey()
(in the current incremental-dom approach) would need to dig into the directives object and search for nested keys in addition to the specific one and interpolate them at least one level deep. So:
resolveKey('links', {foo: 'bar'}, {
links: function(d) { return [{name: d.foo}]; },
'links.text': 'name',
'links.@href': function(d) {
return '#' + d.name;
}
});
// should produce:
{
data: [
{name: 'bar'}
],
directives: {
text: 'name',
'@href': function(d) { ... }
}
}
Tagalong should recognize (when creating a render function) when elements don't have any dynamic attributes or content (after descending into their children) and return a function that always returns the same (cloned) node.
Right now the docs aren't clear how you gain access to the iteration index in a t-each
or t-foreach
loop. It'd be nice if you could just do this:
<li t-each="items" t-as="item, index">
This is item number {{ index + 1 }}.
</li>
This would mean updating the symbolSetter()
function to take one or more arguments and update the all of the corresponding symbols when called.
I'm scratching my head over this, but dynamic attributes (t-title="xxx"
, etc.) aren't updated the second time you call a render function. It seems like something in incremental-dom's elementOpen()
isn't invalidating our attributes and updating any of them. There are tests for this, but they're currently pending.
Currently there's some silly duck typing in the forEach()
implementation that backs the t-each
and t-foreach
constructs, including iteration over a string's characters. Here are some better things we could do:
forEach()
method.Heres my try at building something. It's really easy to bind data to elements. I did have trouble when it came to more evented code. The click handler on this accordion isnt' working yet, I haven't figured out why. It might also be nice to be able to put data into html attributes to make id-ing them easy, although maybe you thought of a better solution to that. I wrote notes in the HTML here too:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The HTML5 Herald</title>
</head>
<body>
<div class="accordion" id="accordion">
<ul>
<li t-each="people">
<!-- might be kinda nice to have a way to set a data-attribute based
on data
-->
<button data-id="">+</button>
<span style="display:none" class="eId" t-text="id"></span>
<!--
So here, I want to be able to do an if, else with the aria
attribute: like if showing then aria-hidden=false, etc.
-->
<div t-if="showing" aria-hidden="false">
<h4 t-text="name">A name</h4>
<p t-text="occupation">some content</p>
</div>
</li>
</ul>
</div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
<script src="./node_modules/tagalong/tagalong.js"></script>
<script>
var initialData = {
people: [
{id: 1, name: 'Rosie', occupation: 'Riveter', showing: false},
{id: 2, name: 'Joe', occupation: 'Plumber', showing: true},
{id: 3, name: 'Jill', occupation: 'Astrophysicist', showing: true},
]
};
tagalong.render('#accordion', initialData);
var accordion = document.getElementById('accordion');
var buttons = accordion.querySelectorAll('button');
var onClick = function(ev) {
var target = ev.currentTarget.parentElement;
var idEl = target.querySelector('.eId');
var cid = parseInt(idEl.innerHTML, 10);
var clone = initialData.people.slice(0);
for (var i = 0, ilen = clone.length; i < ilen; i++) {
var d = clone[i];
if (d.id === cid) {
d.showing = !d.showing
}
}
console.log('peop', {people: clone});
tagalong.render('#accordion', {people: clone});
};
[].forEach.call(buttons, function(button) {
button.addEventListener('click', onClick);
});
</script>
</body>
</html>
Partials are probably best expressed as hrefs to other nodes in the document:
<ul>
<li t-each="people" t-href="list-item-template"></li>
</ul>
<!-- hide this item by default, but show it when it renders -->
<li id="list-item-template" style="display: none" t-style="">
<b>{{ name }}</b> is {{ age }} year{{ age == 1 ? '' : 's' }} old.
</li>
This could get a little weird because it's not clear from the above example whether the <li>
that references the template would be "replaced" by the template element, or the latter would be inserted into it. The replacement behavior feels more intuitive to me.
Also, what happens if the referencing and referenced elements have different node names? I could see an argument for either one "winning out", but my gut tells me that the node name and attributes of the referencing element (with the t-href
attribute) should be respected, and the attributes and children of the reference element should be merged in at compile time.
This is in the works in #18, but I wanted to have an issue to track it just in case it doesn't make the next release. Here's how it should work:
<div id="template">
<button t-each="buttons" t-onclick="(d, e) => click(d)">{{ . }}</button>
</div>
<script>
tagalong.render('#template', ['a', 'b', 'c'], {
click: function(data, event) {
alert('clicked: ' + data);
}
});
</script>
You have to use the fat arrow syntax to get access to the event object, which is passed as the second argument. (We could do things like D3 and just stash the event in tagalong.event
, too.) If you don't need access to the event and only need access to properties of the data and/or context, you can write a handler like:
<button t-onclick="click(foo)">{{ foo }}</button>
...which assumes that the data being rendered in that node is an object with a foo
property, and the click
property is a function of either the data or the rendering context.
I still think the fundamental idea of tagalong is a good one, but lately I've been thinking about a reorganization of the code around a smaller, tighter core with React-like innards and a more designer-friendly, pure markup API:
<t-context data="expression">
(working title) establishes a new data context with an optional initial state determined by the JavaScript expression
. Under the hood, the data is always stored as an immutable data structure.<t-if data="...">
and <t-each data="...">
are your control structures, and the optional data
expression is evaluated in the context of the closest tagalong ancestor that establishes a new context (<t-context>
, <t-each>
).t-value="expression"
attribute will have its text content set to the stringified return value of the expression (except that "empty" values like null
and undefined
will be treated as empty strings).<input name="foo" type="checkbox" t-action="set">
would call something like data = data.set('foo', this.value)
then re-render when changed.I pulled down v2 of tagalong to take a look around but npm is throwing some error. I'm thinking the morphdom repo or tag is now missing? I see this dependency in the package.json
but I don't see a public repo available for it:
"morphdom": "github:shawnbot/morphdom#namespace-aware"
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.