I have a situation where I need to render a Handlebars template created by the user. I've been able to hack together one solution: using a view to compile the user-generated template + context into HTML, then making that resulting string available to the "final" template.
I'd prefer to be able to just expose the template as a variable to the "final" template, and have the final template evaluate it as if it were actual HBS code; but if I use triple braces {{{, the handlebars variables are (of course) escaped.
Is there another way to do this? A helper perhaps, that first compiles the variable with some context, then outputs the string? The only problem here is that Ember.Handlebars is wired up to work with Ember; I just need the final, unbound HTML.
One possible approach could be to use a view dedicated to rendering the user defined template. Then by setting its template variable and re-rendering it, the template could change dynamically.
Example,
http://emberjs.jsbin.com/yexizoyi/1/edit
hbs
<script type="text/x-handlebars">
<h2> Welcome to Ember.js</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="test">
this is the test,
{{view view.userTemplate}}
<button {{action "changeTemplate" 1 target="view"}}>change to Template 1</button>
<button {{action "changeTemplate" 2 target="view"}}>change to Template 2</button>
</script>
js
App.Router.map(function() {
this.route("test");
});
App.IndexRoute = Ember.Route.extend({
beforeModel: function() {
this.transitionTo("test");
}
});
App.UserTemplateView = Ember.View.extend({
template:Ember.Handlebars.compile("initial default template <b>{{view.parentView.parentVar}}</b>")
});
App.TestView = Ember.View.extend({
parentVar:"this is a parent variable",
userTemplate:App.UserTemplateView.create(),
actions:{
changeTemplate:function(templateId){
if(templateId===1){
this.get("userTemplate").set("template",Ember.Handlebars.compile("this is template 1 <b>{{view.parentView.parentVar}}</b>"));
this.get("userTemplate").rerender();
}else{
this.get("userTemplate").set("template",Ember.Handlebars.compile("this is template 2 <b>{{view.parentView.parentVar}}</b>"));
this.get("userTemplate").rerender();
}
}
}
});
This could also be implemented by using a ContainerView, http://emberjs.com/guides/views/manually-managing-view-hierarchy/
example,
http://emberjs.jsbin.com/luzufixi/1/edit
edit - supplement to comments and good solution of Sam Selikoff using a helper
This is one more rough example of using the previous concept, along with the model of a router and the {{view}} helper which is backed by a generic View object, i.e. PreviewTemplateView .
http://emberjs.jsbin.com/tonapaqi/1/edit
http://emberjs.jsbin.com/tonapaqi/1#/test/1
http://emberjs.jsbin.com/tonapaqi/1#/test/2
hbs - calling {{view}} helper with the desired context that if it contains a template property, the initial default template will change.
{{view App.PreviewTemplateView contextBinding="this"}}
js
App.Router.map(function() {
this.route("test",{path:"test/:tmpl_id"});
});
App.IndexRoute = Ember.Route.extend({
beforeModel: function() {
this.transitionTo("test",1);
}
});
App.TestRoute = Ember.Route.extend({
model:function(params){
if(params.tmpl_id==1){
return {template:"this is template 1 <b>{{view.parentView.parentVar}},param from context of model:{{someParams.param1}}</b>",someParams:{param1:"p1",param2:"p2"}};
}else{
return {template:"this is template 2 <b>{{view.parentView.parentVar}},param from context of model:{{someParams.param2}}</b>",someParams:{param1:"p1",param2:"p2"}};
}
}
});
App.PreviewTemplateView = Ember.View.extend({
template:Ember.Handlebars.compile("initial default template"),
init:function(){
this._super();
this.refreshTemplate();
},
refreshTemplate:function(){
this.set("template",Ember.Handlebars.compile(this.get("context").get("template")));
this.rerender();
}.observes("context.template")
});
App.TestView = Ember.View.extend({
parentVar:"this is a parent variable"
});
I ended up going with a Handlebars helper. Seems to be a bit more flexible.
I'm using EAK and Coffeescript:
helper = Ember.Handlebars.makeBoundHelper (template, context) ->
dummy = Ember.View.extend
context: context
template: Ember.Handlebars.compile template
view = dummy.create()
$elem = null
Ember.run ->
$elem = $('<div>')
view.appendTo($elem)
return new Handlebars.SafeString $elem.html()
Now in any template I can write
{{preview-template template context}}
where template is a (possibly user-generated) template, and context the data.
Related
In ember, is there a way to render a template into an outlet from a controller to get the desired effect of:
this.render('some_template', {
into: 'template_name',
outlet: 'template_outlet'
});
Or better yet, call an action in router.js from a controller?
you can put a method in the corresponding route to the controller under the actions hash (Example: posts.index controller and posts.index route) and call it using send
posts.index controller
this.send('exampleAction', record);
posts.index route
actions: {
exampleAction: function(record){
console.log(record);
}
}
Instead of outlet, you can use a component:
<script type="text/x-handlebars" id="components/x-outlet">
{{ partial template }}
</script>
Then, in your controller, you can have a template property that you can pass to the component to display your template dynamically:
App.IndexController = Ember.ArrayController.extend({
template: function(){
return 'this_one';
}.property(),
actions: {
that_one: function(){
this.set('template', 'that_one');
}
}
});
Working example here
Not completely sure what you mean by
Or better yet, call an action in router.js from a controller?
but if you are just trying to transition into a different route, you can use the transitionToRoute() method (see here)
See JSFiddle: http://jsfiddle.net/cyclomarc/aYmuJ/3/
I set a property in the application controller and want to display this property in a partial view. This does not seem to work. I can access the property in the template itself, but not in the partial view rendered within the template ..
index.html
<script type="text/x-handlebars">
<h3>Ember access to controller properties</h3>
{{#linkTo 'about'}}About{{/linkTo}} <br><br>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="about">
Access to property in index template: <br>
<b>{{controllers.application.applicationVersion}}</b>
<br><br>
{{render "_footer"}}
</script>
<script type="text/x-handlebars" data-template-name="_footer">
Footer text (partial view) with a controller property:<br>
<b>{{controllers.application.applicationVersion}} MISSING</b>
</script>
app.js
var App = Ember.Application.create({});
App.Router.map(function () {
this.resource('about', { path: "/about" });
});
App.IndexRoute = Ember.Route.extend({
redirect: function () {
this.transitionTo('about');
}
});
App.ApplicationController = Ember.Controller.extend({
//Set some properties
applicationVersion: "1.0.0"
});
App.AboutController = Ember.Controller.extend({
needs: "application"
});
render view helper have your own context.
To use the current context in a other template use the partial view helper.
{{partial "footer"}}
When you use render a new controller is created, in that case named generated _footer controller.
Using partial will preserve the controller bound to the template that called the partial template
And since you used needs in about controller, you don't have it in the new generated controller.
Here is a sample
Because I have the name of a View inside a variable I'm using a handlebars helper to get it rendered.
So, I have an object as follows (all simplified of course)
config = Ember.Object.create({ view: "MyTplView"}) ;
Next, I have the following template
<script type="text/x-handlebars" data-template-name="index">
<div id="container">
{{helperViewRenderer config}}
</div>
</script>
And the Handlebars helper
Ember.Handlebars.registerBoundHelper('helperViewRenderer', function(value, options) {
var template = App[value.view].create() ;
template.appendTo("#container") ;
}) ;
The problem is that I try to insert the template into the element with id "container", which doesn't exist at that moment. What are the possibilities to fix this. For example, is it possible to get a HTML-string from the template and return this instead ?
I have occasionally found it valuable to define a child view class inside of a parent view class instead of as a global. You can pass any path -- global or local -- to the view helper, and the current view is available as view in the current context. Thus,
App.ParentView = Ember.View.extend({
template: Ember.Handlebars.compile("The parent's child: {{view view.ChildView}}."),
ChildView: Ember.View.extend({
template: Ember.Handlebars.compile("-- child view --")
})
});
Ember tries to go the route of creating 'outlets' through the use of the new router.
If you want to use the appendTo method then obviously you have to append your template to an existing HTML element. In that way, you can't get a HTML string from a template (unless you know it is part of a parent template). However if you're not too bothered about where you're inserting your template, then you can use a straight append which will add the template to the end of the body.
I think in your case it would be better to get familiar with the router, as you seem to be wanting to render different templates inside the same main template. Therefore you can easily separate these out by using the URL as a variable, and injecting the relevant view into the outlet.
App.Router.map(function () {
this.route("index", { path: "/" });
this.route("first", { path: "/view1" });
this.route("another", { path: "/view2" });
});
App.FirstRoute = Em.Route.extend({
renderTemplate: function () {
this.render('view1', { outlet: 'main' });
}
});
App.AnotherRoute = Em.Route.extend({
renderTemplate: function () {
this.render('view2', { outlet: 'main' });
}
});
View:
<script type="text/x-handlebars" data-template-name="index">
<div id="container">
{{outlet main}}
</div>
</script>
This way you don't need any handlebars helper passing in variables and it's all neatly wrapped up in Ember conventions.
I'm trying to understand how to use nested routes.
My code:
App.Router.map(function() {
this.route("site", { path: "/" });
this.route("about", { path: "/about" });
this.resource("team", {path:'/team'}, function(){
this.resource('bob',{path:'/bob'});
});
});
And I'm trying to get to the Bob page with:
{{#linkTo 'bob'}}bob{{/linkTo}}
What am I missing?
jsbin
Thanks.
try instead
{{#linkTo 'team.bob'}}bob{{/linkTo}}
Between you can simplify your router map this way - you only need to specify the path if it's different from the route name.
App.Router.map(function() {
this.route("site", { path: "/" });
this.route("about");
this.resource("team", function(){
this.route('bob');
});
});
UPDATE
See a working example here
In summary, You need to provide an implementation of the renderTemplate function of TeamBobRoute where you explicitly specify where you want to render your template bob. Using the render option into you can override the default behaviour, rendering to the parent outlet, and pick which parent template to render to
App.TeamBobRoute = Ember.Route.extend({
renderTemplate:function(){
this.render('bob',{
into:'application',
});
}
});
<script type="text/x-handlebars" data-template-name="site-template">
This is the site template
{{#linkTo 'about'}}about{{/linkTo}}
{{#linkTo 'team'}}team{{/linkTo}}
</script>
<script type="text/x-handlebars" data-template-name="about">
This is the about page
</script>
<script type="text/x-handlebars" data-template-name="team">
This is the team page
{{#linkTo 'team.bob'}}bob{{/linkTo}}
</script>
<script type="text/x-handlebars" data-template-name="bob">
This is the bob page
</script>
<script type="text/x-handlebars">
This is the application template
{{outlet}}
</script>
FYI the render method supports the following options: into, outlet and controller as described below.
The name of the PostRoute, as defined by the router, is post.
By default, render will:
render the post template
with the post view (PostView) for event handling, if one exists
and the post controller (PostController), if one exists
into the main outlet of the application template
You can override this behavior:
App.PostRoute = App.Route.extend({
renderTemplate: function() {
this.render('myPost', { // the template to render
into: 'index', // the template to render into
outlet: 'detail', // the name of the outlet in that template
controller: 'blogPost' // the controller to use for the template
});
}
});
If you had a named template inside your application template then you would target it this way
App.TeamBobRoute = Ember.Route.extend({
renderTemplate:function(){
this.render('bob',{
into:'application',
outlet:'team-member',
});
}
});
<script type="text/x-handlebars">
This is the application template
{{outlet 'team-member'}}
{{outlet}}
</script>
You're missing the outlet in the team page. The template should look like this.
<script type="text/x-handlebars" data-template-name="team">
This is the team page
{{#linkTo 'bob'}}bob{{/linkTo}}
{{outlet}}
</script>
Each route is rendered into it's parent's template's outlet.
so when you go into "team", then "team" is rendered into the "application" outlet.
When you go to "bob", the "bob" template is rendered into the "team" outlet.
This can be overridden, but is the default behavior.
Also, each parent resources gives you two model/controller/view/template sets. So when you define:
this.resource('team',{path:'/team'});
You get the "team" template and the "team-index" template.
the "team" template is where stuff that is shared between child routes goes (this is why it needs to have the outlet) and the "team-index" template is where stuff that is specific to your "team index" would go.
my index template has two outlets, one for header, another for content. the template rendered in the content changes depending on the content being viewed.
In the old router, this could be done by calling connectOutlet on different controllers who owned that template. I can't figure out how to do the same in the new router.
any suggestions?
With my research, I came to this:
Say you have a router defined like this:
App.Router.map(function(match) {
match('/').to('index');
});
ApplicationTemplate:
<script type="text/x-handlebars">
{{outlet header}}
{{outlet}}
</script>
IndexTemplate:
<script type="text/x-handlebars" data-template-name="index">
{{outlet dashboard}}
{{outlet spaces}}
</script>
Now, What we want is that when user goes to the index router, the page should:
Render index into main outlet and header into header outlet of application template.
render dashboard, spaces template into Index Template.
To achieve this, we write the following code in indexRoute
App.IndexRoute = Em.Route.extend({
renderTemplate: function(controller, model){
//Render header into header outlet
this.render('header',{
outlet:'header'
});
//Render index into main outlet. If you comment out
//this line, the code below fails
this.render('index');
//by using into, we can render into the index template
//Note: The controller is optional.if not specified,
//ember picks up controller for the given template.
this.render('dashboard',{
outlet:'dashboard',
into:'index',
controller:this.controllerFor('somethingElse', App.TestModel.find())
});
//controller is SpacesController
this.render('spaces',{
outlet:'spaces',
into:'index'
});
}
});
you can use the renderTemplates function in the router to render mulitple views to name outlets:
renderTemplates:function () {
this.render('todos_list', {
into:'todos', //template name
outlet: 'todos', //named outlet
controller: 'listController' //controller you want to use
});
this.render('todos_test', {
into:'todos',
outlet: 'test',
controller: 'testController'
});
},
setupControllers:function (controller, model) {
this.controllerFor('list').set('content', listmodel.find());
this.controllerFor('test').set('content', testmodel.find());
}
The setupControllerControllerFor function will allow you to assign what we set as 'context' in the previous router.
In your template, you would name outlets just as before:
{{outlet list}}
{{outlet test}}
Hope this helps :)