pomerantsev / angular-training

An empty repo for creating tasks as issues
0 stars 0 forks source link

Templates are easy... Maybe not. #12

Open al-the-x opened 10 years ago

al-the-x commented 10 years ago

The concept of rendering and displaying templates is a tricky one, if you haven't encountered it before. I spent a lot of my early career in PHP, so I tend to take that for granted. Let's explore those concepts through some examples.

al-the-x commented 10 years ago

Assigned to myself so I can fill out the examples.

al-the-x commented 10 years ago

Dead simple PHP templates:

<?php

function render($template, array $context){
    extract($context);

    eval($template);
}

function renderFile($filename, array $context){
    render(
     '?>' . file_get_contents($filename), $context);
}

render('?>This is David\'s <?=$foo?> as a string!<?', [
    'foo' => 'bar',
]);

renderFile('template.phtml', [
    'foo' => 'bat',
]);

Dead simple JavaScript templates:

var assert = require('assert');

function render(template, context){
    assert(typeof template == 'string');

    return template.replace(/{{(\S+)}}/g, function (match, prop) {
        return context[prop];
    });
}

var tpl = '{{var1}} {{var2}}';

console.log(render(tpl, {var1: 'something', var2: 'something else'}));

module.exports = render;

And then someone wants to to {{var1.propA}} in a template... What else might break our assumptions?

pomerantsev commented 10 years ago

Not sure how to run arbitrary code in a specified context in JavaScript (looks like you cannot pass a context object to eval), but this will work in a broader range of cases: instead of return context[prop]; we should call return eval('context.' + prop);.

pomerantsev commented 10 years ago

And this is what happens if I try analyzing the template first and returning a function which accepts a context as an argument:

function render(template){
  assert(typeof template == 'string');

  var interpolatedTokens = [],
      stringTokens = [],
      match,
      re = /{{([^}}]*)}}/g,
      lastIndex = 0;
  while (match = re.exec(template)) {
    stringTokens.push(template.slice(lastIndex, match.index));
    interpolatedTokens.push(match[1]);
    lastIndex = re.lastIndex;
  }
  stringTokens.push(template.slice(lastIndex));
  return function (context) {
    return stringTokens.reduce(function (previous, current, index) {
      return previous + current +
             (index === stringTokens.length - 1 ? '' : eval('context.' + interpolatedTokens[index]));
    }, '');
  };
}
al-the-x commented 10 years ago
var tpl = "{{date}} | {{date.getYear()}}-{{date.getMonth()}}-{{date.getDay()}}"

assert.equals("Jan 01, 2000, 00:00:00 | 2000-01-01", render(tpl)(new Date('2000-01-01'));
// I don't know what the actual format of `Date.toString()` will be...
al-the-x commented 10 years ago
var tpl = "{{foo}} | {{foo.bar}} | {{foo.baz()}}";

assert.equals("{ bar: 'bat' }", render(tpl)({ foo: { bar: 'bat', baz: function(){ } } }));