Giter VIP home page Giter VIP logo

tagalong's Introduction

tagalong's People

Contributors

shawnbot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

kynetiv

tagalong's Issues

Some ideas for templated attributes

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:

  1. 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:

    • πŸ‘ Familiar syntax to for users of Mustache and similar template languages.
    • πŸ‘Ž Awkward to escape or disambiguate with many server-side templates (Django, Jinja, Liquid, Nunjucks)
    • πŸ‘Ž Requires another dependency for evaluating the templates, unless we go with unsafe eval() or new Function() implementations.
    • πŸ‘Ž Could introduce false positives for attributes that contain literal {{ .. }} character sequences. Escaping these becomes a thing.
  2. Data-bound attributes:

    <div id="template">
      <section data-bind-id="id" data-bind-class="classes">
        <h1 data-bind="name">Name</h1>
      </section>
    </div>

    Notes:

    • πŸ‘ no dependencies needed; dynamic attribute expressions could be "compiled" ahead of time.
    • πŸ‘Ž doesn't solve the need mix dynamic and static values in, for instance, 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.
    • πŸ‘Ž can't use just data-id="id" because that's a perfectly legitimate literal value.
  3. 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:

    • The 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.

keyed elements

incremental-dom supports providing a unique key to elementOpen() and like functions. We could get the appropriate key in a couple of ways:

  1. If the element has a t-id attribute, use the derived id, which should always be unique to the document.
  2. Accommodate a 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.

Add compile() method

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.

render to other elements?

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

t-as should not change the current context

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>

Update <t-template> and add tests

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.

  • Getting and setting data with .data
  • Rendering with .render([data])
  • Rendering to another element with .renderTo(element [, data])

Render to string / server API

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.

Feature ideas for v1

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);
  };
}

Evaluate incremental-dom

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
  };
}

Binding to directives that return arrays prevents sub-directives

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

Only render "static" nodes once

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.

Support multiple t-as symbols in iterators

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.

Support iterators more formally in t-each and t-foreach?

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:

  • Support ES6 iterator protocols.
  • Duck-type any object that has a forEach() method.
  • If the value is not iterable in any of the above ways, treat it as an:
    1. an Array with a single value if the value is not "empty"
    2. an empty Array if the value is "empty"

Try at an accordion

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>

Support partials by element reference

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.

Event handling

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.

A proposal for custom elements

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:

  1. Tagalong is actually made up of custom elements and type extension mixins, e.g.
    • <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.
    • Elements such as <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>).
    • Any child of a tagalong context element with a 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).
  2. Tagalong elements will have an API that allows you to get and set their data, but all of the rendering is done synchronously, and immediately. Immutable data structures make it easy to detect whether data is changed at each element's level.
  3. Form elements should have the ability to modify data and automatically trigger re-renders. E.g. <input name="foo" type="checkbox" t-action="set"> would call something like data = data.set('foo', this.value) then re-render when changed.

v2 morphdom dependency

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"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.