0

I wrote this fast-templating function:

var templatize = function(string) {
    return function (string) {
      return string.replace(/{{(.*?)}}/g, function(pattern, match) {
        value = this[match];
        if (value) {
          return value;
        } else {
          return pattern;
        }
      });
    }.call(this, string);
}

Which does this:

var foo = "bar", bar = "foo";
templatize("We are {{foo}} and {{bar}}, but not {{crazy}}"); // "We are bar and foo but not {{crazy}}"

I'm quite happy with this except that I have scoping problem. For sure, the templatize method will be accessible through namedscope, but then, the current context of execution of templatize is not accessible in my function automatically.

Something like calling $.proxy(templatize, this)("We are {{foo}} and {{bar}}, but not {{crazy}}") should work, right?

But I'd like to achieve this without needing to call $.proxy() (and without any jQuery preferably) so that context is automatically transfered to the execution one.

I'm struggling with .call(), .apply(), and other closures, but I think I read somewhere over the internet that it was possible. Thanks

Augustin Riedinger
  • 16,966
  • 22
  • 99
  • 173
  • Have you checked the source code of mustache or handlebars for ideas? – Brad M Jan 02 '14 at 14:48
  • possible duplicate of [Javascript OO reference this](http://stackoverflow.com/questions/14282605/javascript-oo-reference-this) – Matt Ball Jan 02 '14 at 14:50

2 Answers2

2

You can avoid using jQuery doing this :

var templatize = function(string) {
    var me = this; // the data source
    return string.replace(/{{(.*?)}}/g, function (full, key) {
        // "this" refers to the string itself
        return me[key] || full;
    });
}

In case you want to use jQuery.proxy(), wrap the replacement function :

var templatize = function(string) {
    return string.replace(/{{(.*?)}}/g, jQuery.proxy(function (full, key) {
        // "this" now refers permanently to the data source
        return this[key] || full;
    }, this));
}

In both cases you can bind the data source to this using call :

templatize.call({ hello: 'Hi!' }, '{{hello}}');

Going further

You could optimize by compiling the template for reuse :

function compile(tpl) {
    var i = -1, tmp = [];
    tpl = tpl.split(/{{([^{}]+)}}/);
    while (++i < tpl.length) {
        if (i % 2) tmp.push('this["' + tpl[i] + '"]');
        else if (tpl[i]) tmp.push('"' + tpl[i].replace(/"/g, '\\"') + '"');
    }
    return new Function(
        'return [' + tmp.join() + '].join("");'
    );
}

Usage example :

var tpl = compile('{{hello}} {{hello}}');
tpl.call({ hello: 'Hi!' }); // "Hi! Hi!"
tpl.call({ hello: 'Yo!' }); // "Yo! Yo!"

Regarding the example above, here is the function returned by compile :

function () {
    return [this["hello"]," ",this["hello"]].join("");
}

Note that you can use an array as well :

var tpl = compile('{{1}} {{0}}');
tpl.call(['a', 'b']); // "b a"

Performance test : http://jsperf.com/template-compiling.

Community
  • 1
  • 1
leaf
  • 14,210
  • 8
  • 49
  • 79
1

why don't you pass an object containing the view variables? would be cleaner then potentially displaying any existing variable in your view.

var templatize = function(string, variables) {
  return function (string) {
    return string.replace(/{{(.*?)}}/g, function(pattern, match) {
      value = variables[match];
      if (value) {
        return value;
      } else {
        return pattern;
      }
    });
  }.call(this, string);
}
Dominik Goltermann
  • 4,169
  • 2
  • 24
  • 32
  • At the end, that's what I ended up doing. Thanks! – Augustin Riedinger Jan 03 '14 at 10:11
  • 1
    @AugustinRiedinger You can remove this wrapper function : `return function (string) {...}.call(this, string);`, it does nothing. – leaf Jan 03 '14 at 12:24
  • 1
    @AugustinRiedinger Then you can replace the six lines inside the replacement function with a single one : `return variables[match] || pattern;`, which means "value OR pattern". – leaf Jan 03 '14 at 18:49