How can I dynamically render HTML using Meteor Spacebars templates?
Asked Answered
P

6

8

So let's say I'm storing <div>{{name}}</div> and <div>{{age}}</div> in my database. Then I want to take the first HTML string and render it in a template - {{> template1}} which just renders the first string with the {{name}} handlebar in it. Then I want to give that newly generated template/html data, so that it can fill in the handlebar with the actual name from the database, so that we would get <div>John</div>. I've tried doing

<template name="firstTemplate">
    {{#with dataGetter}}
        {{> template1}}
    {{/with}}
</template>

Where template1 is defined as

<template name="template1">
    {{{templateInfo}}}
</template>

And templateInfo is the helper that returns the aforementioned html string with the handlebar in it from the database.

dataGetter is just this (just an example, I'm working with differently named collections)

Template.firstTemplate.dataGetter = function() {
    return Users.findOne({_id: Session.get("userID")});
}

I can't get the {{name}} to populate. I've tried it a couple of different ways, but it seems like Meteor doesn't understand that the handlebars in the string need to be evaluated with the data. I'm on 0.7.0 so no Blaze, I can't upgrade at the moment due to the other packages I'm using, they just don't have 0.8+ version support as of yet. Any ideas on how I can get this to work are much appreciated.

Paratroops answered 1/5, 2014 at 19:36 Comment(0)
C
1

If you need to dynamically compile complex templates, I would suggest Kelly's answer.

Otherwise, you have two options:

  1. Create every template variation, then dynamically choose the right template:

    eg, create your templates

    <template name="displayName">{{name}}</template>
    <template name="displayAge">{{age}}</template>
    

    And then include them dynamically with

    {{> Template.dynamic template=templateName}}
    

    Where templateName is a helper that returns "age" or "name"

  2. If your templates are simple, just perform the substitution yourself. You can use Spacebars.SafeString to return HTML.

    function simpleTemplate(template, values){
          return template.replace(/{{\w+}}/g, function(sub) {
              var p = sub.substr(2,sub.length-4);
              if(values[p] != null) { return _.escape(values[p]); }
              else { return ""; }
          })
    }
    Template.template1.helpers({
      templateInfo: function(){
          // In this context this/self refers to the "user" data
          var templateText = getTemplateString();
          return Spacebars.SafeString(
              simpleTemplate(templateText, this)
          );
      }
    
Crinkumcrankum answered 1/5, 2014 at 23:52 Comment(3)
Yep, I went with the second option yesterday, hopefully Blaze and whatever follows give us the option to compile html into handlebars on the fly. Still pissing me off, I have to re-implement handlebars on my own, sigh.Paratroops
JSYK eval is used internally by the templating package to create Template. The string the spacebars-compiler will parse your strings and escape them properly just as it does your templates.Communalize
As far as tags go you may want to parse out any <script> tags.Communalize
T
9

In 1.0 none of the methods described above work. I got this to work with the function below defined in the client code. The key was to pass the options { isTemplate: true} to the compile function.

var compileTemplate = function(name, html_text) {
  try {
    var compiled = SpacebarsCompiler.compile(html_text, { isTemplate:true });
      var renderer = eval(compiled);
      console.log('redered:',renderer);
      //Template[name] = new Template(name,renderer);
      UI.Template.__define__(name, renderer);
  } catch (err){
    console.log('Error compiling template:' + html_text);
    console.log(err.message);
  }
};

The you can call with something like this on the client:

compileTemplate('faceplate', '<span>Hello!!!!!!{{_id}}</span>');

This will render with a UI dynamic in your html

{{> Template.dynamic template='faceplate'}}

Tompion answered 14/11, 2014 at 5:43 Comment(3)
Be sure to add spacebars-compiler package (meteor add spacebars-compiler) so you can use SpacebarsCompilerBloodless
I get 'redered:function anonymous()' msg in console , BUT {{> UI.dynamic template='faceplate'}} doesn't do anything in my case. I'm using Meteor 1.1.0.2Dombrowski
How to render the faceplate child template i.e which in general {{> child }} within faceplate but not rendering with with dynamically adding of {{> Template.dynamic template='faceplate'}}Vale
C
8

You can actually compile strings to templates yourself using the spacebars compiler.. You just have to use meteor add spacebars-compiler to add it to your project.

In projects using 0.8.x

var compiled = Spacebars.compile("<div>{{name}}</div> and <div>{{age}}</div>");
var rendered = eval(compiled);

Template["dynamicTemplate"] = UI.Component.extend({
  kind: "dynamicTemplate",
  render: rendered
});

In projects using 0.9.x

var compiled = SpacebarsCompiler.compile("<div>{{name}}</div> and <div>{{age}}</div>");
var renderer = eval(compiled);

Template["dynamicTemplate"] = Template.__create__("Template.dynamicTemplate", rendered);
Communalize answered 2/5, 2014 at 17:5 Comment(4)
Ha, that's an even better solution, I'll look into it. Not sure if I have the UI class available in 0.7.0 though.Paratroops
It doesn't seem like that package exists - neither in meteor, nor in meteorite. I guess it is post 0.7.0 meteor stuff.Paratroops
Being that spacebars is part of Meteor UI I would think you would need Meteor > 0.8.0Communalize
My bad, was thinking that spacebars is just the Meteor name for handlebars, independent from Meteor's version. I guess they just renamed them for 0.8.x+. Still on 0.7.0 though.Paratroops
D
3

Following @user3354036's answer :

var compileTemplate = function(name, html_text) {
  try {
    var compiled = SpacebarsCompiler.compile(html_text, { isTemplate:true }),
        renderer = eval(compiled);

    console.log('redered:',renderer);
    //Template[name] = new Template(name,renderer);
    UI.Template.__define__(name, renderer);
  } catch (err) {
    console.log('Error compiling template:' + html_text);
    console.log(err.message);
  }
};

1) Add this in your HTML

 {{> Template.dynamic template=template}}

2) Call the compileTemplate method.

compileTemplate('faceplate', '<span>Hello!!!!!!{{_id}}</span>');
Session.set('templateName','faceplate');

Save the template name in a Session variable. The importance of this is explained in the next point.

3) Write a helper function to return the template name. I have used Session variable to do so. This is important if you are adding the dynamic content on a click event or if the parent template has already been rendered. Otherwise you will never see the dynamic template getting rendered.

'template' : function() {
  return Session.get('templateName');
}

4) Write this is the rendered method of the parent template. This is to reset the Session variable.

Session.set('templateName','');

This worked for me. Hope it helps someone.

Dombrowski answered 25/2, 2016 at 11:29 Comment(3)
Great! very appreciated saved my day :)Vale
after render my previous helper functions stop working have you faced thisVale
Don't forget to run meteor add spacebars-compiler to be able to use SpacebarsCompiler.Enscroll
C
1

If you need to dynamically compile complex templates, I would suggest Kelly's answer.

Otherwise, you have two options:

  1. Create every template variation, then dynamically choose the right template:

    eg, create your templates

    <template name="displayName">{{name}}</template>
    <template name="displayAge">{{age}}</template>
    

    And then include them dynamically with

    {{> Template.dynamic template=templateName}}
    

    Where templateName is a helper that returns "age" or "name"

  2. If your templates are simple, just perform the substitution yourself. You can use Spacebars.SafeString to return HTML.

    function simpleTemplate(template, values){
          return template.replace(/{{\w+}}/g, function(sub) {
              var p = sub.substr(2,sub.length-4);
              if(values[p] != null) { return _.escape(values[p]); }
              else { return ""; }
          })
    }
    Template.template1.helpers({
      templateInfo: function(){
          // In this context this/self refers to the "user" data
          var templateText = getTemplateString();
          return Spacebars.SafeString(
              simpleTemplate(templateText, this)
          );
      }
    
Crinkumcrankum answered 1/5, 2014 at 23:52 Comment(3)
Yep, I went with the second option yesterday, hopefully Blaze and whatever follows give us the option to compile html into handlebars on the fly. Still pissing me off, I have to re-implement handlebars on my own, sigh.Paratroops
JSYK eval is used internally by the templating package to create Template. The string the spacebars-compiler will parse your strings and escape them properly just as it does your templates.Communalize
As far as tags go you may want to parse out any <script> tags.Communalize
E
1

Luckily, the solution to this entire problem and any other problems like it has been provided to the Meteor API in the form of the Blaze package, which is the core Meteor package that makes reactive templates possible. If you take a look at the linked documentation, the Blaze package provides a long list of functions that allow for a wide range of solutions for programmatically creating, rendering, and removing both reactive and non-reactive content.

In order to solve the above described problem, you would need to do the following things:

First, anticipate the different HTML chunks that would need to be dynamically rendered for the application. In this case, these chunks would be <div>{{name}}</div> and <div>{{age}}</div>, but they could really be anything that is valid HTML (although it is not yet part of the public API, in the future developers will have more options for defining this content in a more dynamic way, as mentioned here in the documentation). You would put these into small template definitions like so:

<template name="nameDiv">
    <div>{{name}}</div>
</template>

and

<template name="ageDiv">
    <div>{{age}}</div>
</template>

Second, the definition for the firstTemplate template would need to be altered to contain an HTML node that can be referenced programmatically, like so:

<template name="firstTemplate">
    <div></div>
</template>

You would then need to have logic defined for your firstTemplate template that takes advantage of some of the functions provided by the Blaze package, namely Blaze.With, Blaze.render, and Blaze.remove (although you could alter the following logic and take advantage of the Blaze.renderWithData function instead; it is all based on your personal preference for how you want to define your logic - I only provide one possible solution below for the sake of explanation).

Template.firstTemplate.onRendered(function() {
    var dataContext = Template.currentData();
    var unrenderedView = Blaze.With(dataContext, function() {
        // Define some logic to determine if name/age template should be rendered
        // Return either Template.nameDiv or Template.ageDiv
    });
    var currentTemplate = Template.instance();
    var renderedView = Blaze.render(unrenderedView, currentTemplate.firstNode);

    currentTemplate.renderedView = renderedView;
});

Template.firstTemplate.onDestroyed(function() {
    var renderedView = Template.instance().renderedView;
    Blaze.remove(renderedView);
});

So what we are doing here in the onRendered function for your firstTemplate template is dynamically determining which of the pieces of data that we want to render onto the page (either name or age in your case) and using the Blaze.With() function to create an unrendered view of that template using the data context of the firstTemplate template. Then, we select the firstTemplate template element node that we want the dynamically generated content to be contained in and pass both objects into the Meteor.render() function, which renders the unrendered view onto the page with the specified element node as the parent node of the rendered content.

If you read the details for the Blaze.render() function, you will see that this rendered content will remain reactive until the rendered view is removed using the Blaze.remove() function, or the specified parent node is removed from the DOM. In my example above, I am taking the reference to the rendered view that I received from the call to Blaze.render() and saving it directly on the template object. I do this so that when the template itself is destroyed, I can manually remove the rendered view in the onDestroyed() callback function and be assured that it is truly destroyed.

Eightfold answered 18/8, 2015 at 1:56 Comment(0)
W
1

A very simple way is to include in the onRendered event a call to the global Blaze object.

Blaze.renderWithData(Template[template_name], data ,document.getElementById(template_id))
Worrell answered 26/1, 2016 at 21:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.