I am trying to get a menu working using {{render}} in ember.
Controller:
App.NavigationController = Ember.ArrayController.extend({
itemController: 'navSheet',
init: function() {
this._super();
this.set( 'content', [
{ name: 'Sheet 1', type: 'Sheet', id: 1 },
{ name: 'Sheet 2', type: 'Sheet', id: 2 }
]);
}
});
App.NavSheetController = Ember.ObjectController.extend({
getID: function() {
return 'sheet-' + this.get( 'id' );
}
});
Then I use {{render "navigation"}} to render using the following emblem template:
ul
each controller.content
li data-type=type! id=getID! = name
[UPDATE WITH OUTPUT]
With "controller.content" here, I do loop through the content. With use "each" this isn't available. However, in any case the itemController is not in the context inside the loop. The resulting HTML is:
<li data-type="Sheet" id="">
<script id="metamorph-8-start" type="text/x-placeholder"></script>Sheet 1
<script id="metamorph-8-end" type="text/x-placeholder"></script></li>
<li data-type="Sheet" id="">
<script id="metamorph-9-start" type="text/x-placeholder"></script>Sheet 2
<script id="metamorph-9-end" type="text/x-placeholder"></script></li>
Note in particular that "getID" value doesn't show up. In fact I can't seem to access the itemController at all. What am I doing wrong?
I've never used a emblem, so I don't totally understand the template, but if I understand correctly you're trying to get sheetId into the li and you're using the getID function to do this. I'm going to guess that Emblem will convert it into this template
<li {{bind-attr data-type=type dataid=getID}}>
But it also looks like you're doing getID!=name or something like that. That kind of logic wouldn't work in handlebars.
Now getID won't run because it needs to be a computed property
App.NavSheetController = Ember.ObjectController.extend({
getID: function() {
return 'sheet-' + this.get( 'id' );
}.property('id')
});
Related
I ran into this issue by updating from EmberJS 1.7 to 1.8:
https://github.com/emberjs/ember.js/issues/9461#issuecomment-61369680
[Update: the example jsbins can be found in the link above. I've to earn enough reputation to be allowed to post more than two links. I am sorry for the inconvenience!]
In EmberJS it seems not to be possible to combine a component with tagName 'input' and a layout.
Now I have an input element and a graphical representation sitting next to each other like:
<svg ...>
<input type="radio"...>
The image content is driven by a CSS rule which depends on the radio button state (yes, I want to style my own radio button).
My (propably very naive) components-template to achieve the rendered output:
<script type="text/x-handlebars" id="components/radio-button">
<svg><circle cx="50%" cy="50%" r="8" /></svg>
{{yield}}
</script>
[Update: Added component code]
And the component code:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
return {'radio': 0};
},
});
App.RadioButtonComponent = Ember.Component.extend({
tagName: 'input',
attributeBindings: [
'type',
'checked',
'disabled',
'tabindex',
'name',
'autofocus',
'required',
'form',
'value'
],
type: 'radio',
checked: false,
disabled: false,
indeterminate: false,
init: function() {
this._super();
this.on('change', this, this._updateElementValue);
var name = this.get('name'),
controller = this.get('radioController'),
checked = controller.get('model.%#'.fmt(name)) === this.get('value');
this.set('checked', checked);
},
group: "radio_button",
classNames: ['radio-button'],
_updateElementValue: function() {
var name = this.get('name'),
controller = this.get('radioController');
controller.set('model.%#'.fmt(name), this.get('value'));
}
});
But with EmberJS 1.8 my component [Update: added code]
<script type="text/x-handlebars">
{{radio-button name="radio" radioController=this value=0}}
</script>
now gets rendered like:
<input type="radio"...><svg ...></input>
I'am now puzzled by how to keep attribute bindings for input elements and use layouts with components in EmberJS.
Here is an solution for adding a layout to an input element:
http://emberjs.jsbin.com/nihacacebe/1/edit?html,js,output
The trick simply is to use a 'proxy' component:
<script type="text/x-handlebars" id="components/radio-button">
{{radio-button-input name=name radioController=radioController value=value}}
<i>{{name}}</i>
{{yield}}
</script>
This component allows to add some layout to the input element in the hbs and forwards everything to the 'real' component:
App.RadioButtonInputComponent = Ember.Component.extend({
tagName: 'input',
type: 'radio',
/* Add your code here... */
});
The proxy component is then used like a real component:
<script type="text/x-handlebars">
{{radio-button name="radio" radioController=this value=0}}
</script>
This solution was inspired by the work of Yapp Labs Ember-Cli Add On:
https://github.com/yapplabs/ember-radio-button
I still have a hard time to understand why using layout is not permitted for input elements in components.
I'm trying to create something really simple with ember.js, and I'm getting badly lost between old examples, new examples, and extensive documentation.
I want to create an application with no functionality, that simply shows two sets of data in two columns. The real application uses JSON data from two different sources and is more complicated, but the problem reduces to getting the below to work:
<div id="appholder">
<script type="text/x-handlebars">
{{outlet left}}
{{outlet right}}
</script>
<script type="text/x-handlebars" data-template-name="left">
<div id="left">
<ul>
{{#each item in model}}
<li>{{item}}</li>
{{/each}}
</ul>
</div>
</script>
<script type="text/x-handlebars" data-template-name="right">
<div id="right">
<ul>
{{#each item in model}}
<li>{{item}}</li>
{{/each}}
</ul>
</div>
</script>
</div>
and in the javascript something like
App = Ember.Application.create({
rootElement: '#appholder'
});
App.LeftController = Ember.ArrayController.extend({
model: function() {
return ['left one', 'left two'];
}
});
App.RightController = Ember.ArrayController.extend({
model: function() {
return ['right one', 'right two'];
}
});
App.IndexRoute = Ember.Route.extend({
renderTemplate: function() {
this.render('left', {
outlet: 'left',
controller: 'left',
});
this.render('right', {
outlet: 'right',
controller: 'right',
});
}
});
to output the hoped for result, a left column with list entries from one data source and a right column with two entries from another column.
Could somebody ideally provide a js fiddle with the above adapted to working code? Any part of it can change, to use {{render}} or {{view}} in the templates and whatever the js should be.
Thank you for any help
http://emberjs.jsbin.com/mifer/2/edit
Here is a working JSBin
First in order to use a function as a property, you must make it a computed property with the property() function:
App.LeftController = Ember.ArrayController.extend({
model: function() {
return ['left one', 'left two'];
}.property()
});
Secondly, the renderTemplate code is in the wrong place. You put it in the index route but in reality it should be in the application route. If you had {{outlet}} in your application template, the index template would have been rendered into it. Then, if you had those two named outlets inside the index template, what you had would have almost worked (you need to call this.render() or this._super() whenever you use renderTemplate if you want the route template to render.
But, you have two named outlets which you want to manually render into inside of your application template. Hence:
App.ApplicationRoute = Ember.Route.extend({
renderTemplate: function(){
this.render();
this.render('left', {outlet: 'left', into: 'application'});
this.render('right', {outlet: 'right', into: 'application'});
}
});
Now this next approach is how I tackle multiple models in my dashboard application. I am constantly needing to replace the sections of widgets so I use multiple named outlets. I've restructured your code so that the application template renders the index template into its single unnamed outlet.
Keys to this approach:
App.IndexRoute = Ember.Route.extend({
model: function(){
return Ember.RSVP.hash({
left: ['left one', 'left two'],
right: ['right one', 'right two']
});
},
renderTemplate: function(controller, model){
this.render();
this.render('left', {outlet: 'left', into: 'index', controller: 'left', model: model.left});
this.render('right', {outlet: 'right', into: 'index', controller: 'right', model: model.right});
}
});
Whenever you need to return multiple models, and you want your route to block until all models are returned, use Ember.RSVP.hash. You return multiple promises, each as properties of your returned model. RenderTemplate takes two parameters, controller and model so you access your model in the renderTemplate to manually pass the model into the controller of the template you are rendering.
As a slight alternative to this approach, if you need to render multiple datasources on the page, but you do not ever need to dynamically replace the whole template backing one of the models (ie render once and done), you can use the {{render}} helper.
<script type="text/x-handlebars" data-template-name="index">
<p>Index Template</p>
{{render 'left' model.left}}
{{render 'right' model.right}}
</script>
The benefit of this code is that our route has simplified.
App.IndexRoute = Ember.Route.extend({
model: function(){
return Ember.RSVP.hash({
left: ['left one', 'left two'],
right: ['right one', 'right two']
});
}
});
But, we have lost the ability to easily render something else here via action since we no longer have named outlets.
How do I pass a route name to a {{linkTo}} dynamically?
For example, given this code:
App.Router.map(function() {
this.resource('anon', {path: '/main'},
function() {
this.route('home', {path:'/home'});
this.route('about', { path: '/about' });
this.route('contact', { path: '/contact' });
});
});
App.NavController = Ember.ArrayController.extend({
selectedNav:'',
setNav:function(value){
var nav = App.Nav.find(value);
var items = nav.get('navItems');
this.set('content', items);
}
});
these templates:
<script type="text/x-handlebars" data-template-name="nav">
<ul class="nav">
{{#each in controller}}
{{ partial "basicNav"}}
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="_basicNav">
<li>{{#linkTo navItemPath}}{{navItemName}}{{/linkTo}}</li>
</script>
and these models with the following fixture data:
App.Nav = DS.Model.extend({
navItems:DS.hasMany('App.NavItem'),
name:DS.attr('string')
});
App.NavItem = DS.Model.extend({
nav:DS.belongsTo('App.Nav'),
navItemName:DS.attr('string'),
navItemPath:DS.attr('string')
});
App.Nav.FIXTURES = [
{
id: 10,
name: 'Anon',
navItems: [100,200,300]
}
];
App.NavItem.FIXTURES = [
{
id:100,
nav:10,
navItemName:'Home',
navItemPath:'anon.home'
},
{
id:200,
nav:10,
navItemName:'Contact',
navItemPath:'anon.contact'
},
{
id:300,
nav:10,
navItemName:'About',
navItemPath:'anon.about'
}
];
How do I pass navItemPath to the {{linkTo}} helper? In this code snippet:
{{#linkTo navItemPath}}{{navItemName}}{{/linkTo}}
ember complains that it can't find the "navItemPath" route, like it's looking for it literally. If I replace that with a valid literal route like:
{{#linkTo 'anon.home'}}{{navItemName}}{{/linkTo}}
ember will render the linkTo with the navItemName as expected, so I know the controller is passing it the right data, but of course all the routes are goofy. Am I missing something obvious?
You can't do that with LinkTo helper, you need to bind the href of your link to navItemPath using bindAttr
<a {{bindAttr href="navItemPath"}}>{{navItemName}}</a>
Make sure the the logic rending navItemPath's value takes into account the location API
UPDATE Since asking this question I have redesigned my UI such that I no longer need this feature; however I'm leaving this open and active for the sake of helping others who end up with a similar problem.
I'm listing a collection of elements inside a template and each element has a link that opens it up to the right of the list. When one is clicked, I want to hide just that element and show it again when another one is clicked. My current approach to doing this is to set an attribute (active) to true on the model. This feels wrong for three reasons:
This attribute is not actually part of the model's schema, it's just arbitrary; which makes it seem like a controller concern (see below for why that doesn't work)
I have to first set active to false on all models, forcing me to change another router's model, which may be good or bad, I'm not sure
In the recent PeepCode screencast he showed using #each.{attribute} to bind to an attributes in an array; this makes me feel like there must be something similar I could do (like this.set("#each.active", false)) to set them all in one fell swoop
I wanted to use a method on the controller but it doesn't seem I can pass arguments into functions in Handlebars if statements.
Here's the code I'm using to render the list:
{{#each note in controller}}
{{!v-- I was trying to do {{#if isCurrentNote note}} but that seems to be invalid handlebars}}
{{#unless note.active}}
<li class="sticky-list-item">
{{view Ember.TextArea classNames="sticky-note" valueBinding="note.content"}}
{{#linkTo note note classNames="sticky-permalink"}}
∞
{{/linkTo}}
</li>
{{/unless}}
{{/each}}
And here are the routes:
App.NotesController = Ember.ArrayController.extend({
// v-- this is what I was trying to do, but couldn't pass note in the template
isCurrentNote: function(note){
return this.get("currentNote") == note;
}.property("currentNote")
});
App.NoteRoute = Ember.Route.extend({
setupController: function(controller,model){
this.modelFor("notes").forEach(function(note){
note.set("active", false);
});
model.set("active", true);
}
});
Like I said, what I have works, but it feels wrong. Can anyone confirm my suspicion or help ease my soul a bit?
Thanks!
to me this looks like something that should be done mostly by the NotesView with a NotesController that stores the Note selection
Here is the fiddle: http://jsfiddle.net/colymba/UMkUL/6/
the NotesController would hold all the notes and a record of the selected one:
App.NotesController = Ember.ArrayController.extend({
content: [],
selectedNote: null,
selectNote: function(id){
var note = this.get('content').findProperty('id', id);
this.set('selectedNote', note);
}
});
with the NotesViewobserving that selection and showing/hiding elements of the list accordingly
App.NotesView = Ember.View.extend({
templateName: 'notes',
refresh: function(){
var view = this.$(),
selection = this.get('controller.selectedNote');
if (view) {
view.find('li').show();
if (selection) view.find('li.note_'+selection.id).hide();
}
}.observes('controller.selectedNote')
});
Here is the Note object and it's 2 templates (when in a list or displayed in full). The ListView handles the click event and passes the id to the NotesController.
App.Note = Ember.Object.extend({
name: '',
content: ''
});
App.NoteView = Ember.View.extend({
templateName: 'note'
});
App.NoteListItemView = Ember.View.extend({
tagName: 'li',
templateName: 'noteListItem',
classNameBindings: ['noteID'],
noteID: function(){
return 'note_' + this._context.id;
}.property(),
click: function(e){
this.get('controller').selectNote(this._context.id);
}
});
in the NotesView template everything is displayed and if there is a selectedNote, we display the Note again in full:
{{#each note in controller}}
{{#with note}}
{{view App.NoteListItemView}}
{{/with}}
{{/each}}
{{#if selectedNote}}
{{#with selectedNote}}
{{view App.NoteView}}
{{/with}}
{{/if}}
the Routes to put it together
App.Router.map(function() {
this.resource('notes', { path: "/notes" });
});
App.IndexRoute = Ember.Route.extend({
enter: function() {
this.transitionTo('notes');
}
});
App.NotesRoute = Ember.Route.extend({
model: function() {
return [
App.Note.create({id: 1, name: 'Milk', content: '15% fresh milk'}),
App.Note.create({id: 2, name: 'Juice', content: 'Orange with pulp'}),
App.Note.create({id: 3, name: 'Cereals', content: 'Kelloggs Froot Loops'}),
];
},
setupController: function(controller, model) {
controller.set('content', model);
},
renderTemplate: function(controller, model) {
this.render('notes', { outlet: 'content' });
}
});
Using:
ember-1.0.0-pre.4.js
ember-data.js REVISION:11
handlebars-1.0.rc.2.js
Please have a look at this jsFiddle illustrating the described problem.
I have a list of items that are displayed in a template. The template contain a linkTo helper that let's the controller add an item to the collection and is shown as a text input on the page.
Adding the item to the collection is done by the controller:
App.TodoItem = DS.Model.extend({
title: DS.attr('string', { defaultValue: "unknown" })
});
App.Router.map(function () {
this.resource('todo_items')
});
App.TodoItemsRoute = Em.Route.extend({
model: function () {
return App.TodoItem.find();
}
});
App.TodoItemsController = Em.ArrayController.extend({
addTodoItem: function () {
App.TodoItem.createRecord();
}
});
If I want the new item to be shown is the list, I have to pass params to createRecord, otherwise the item is not visible. The same behaviour can be reproduced by using Chrome's inspector and then the item can be made visible as follows:
// Open the jsFiddle http://jsfiddle.net/bazzel/BkFYd/ and select 'result(fiddle.jshell.net) in the inspector, then:
var item = App.TodoItem.createRecord();
// Nothing visible yet.
item.set('title', 'Whatever');
// Now the text input appear with the title as its value.
Is this expected behaviour and if so, what am I missing here?
I took time to redo your example the way i feel things should be done properly with Emberjs. You should rather make sure of transaction and properly define your views and then all your issues get taken care of. So here's how i think you should do this
Define a view for the textfield to capture the value being entered or
just bind it to the model property.
Listing items and adding a new item to the list should be done in two different views and should not be mixed together
<script type="text/x-handlebars">
{{outlet}}
<div>
{{outlet 'addItem'}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="todo_items">
{{#linkTo 'todo_items.new'}}Add Todo Item{{/linkTo}}
<ul>
{{#each item in controller}}
<li>
{{#unless item.isNew}}
{{item.title}}
{{/unless}}
</li>
{{/each}}
</ul>
</script>
Define different states for listing items and adding a new one
To benefit from automatic binding of your text field value to the
model property, you need to associate an ObjectController to the TodoItemsNew route
Finally, make use of transaction to create and commit records to the store
window.App = Em.Application.create();
App.TodoItem = DS.Model.extend({
title: DS.attr('string')
});
App.TodoItem.FIXTURES = [{
id: 1,
title: 'Lorem'
}, {
id: 2,
title: 'Ipsum'
}];
App.store = DS.Store.create({
revision: 11,
adapter: DS.FixtureAdapter.create()
});
App.Router.map(function () {
this.resource('todo_items',function(){
this.route('new');
})
});
App.IndexRoute = Em.Route.extend({
redirect: function () {
this.transitionTo('todo_items');
}
});
App.TodoItemsRoute = Em.Route.extend({
model: function () {
return App.TodoItem.find();
}
});
App.TodoItemsNewRoute = Em.Route.extend({
transaction: App.store.transaction(),
setupController:function(controller) {
console.info(controller.toString());
controller.set('content',this.transaction.createRecord(App.TodoItem));
},
renderTemplate: function() {
this.render('addItem',{
into:'application',
outlet:'addItem',
})
},
events: {
addItem: function() {
this.transaction.commit();
this.transitionTo('todo_items');
}
}
});
App.TodoItemsController = Em.ArrayController.extend();
App.TodoItemsNewController = Em.ObjectController.extend();
App.TextField = Ember.TextField.extend({
insertNewline: function () {
this.get('controller').send('addItem')
}
});
Here' is a working version of the example on jsfiddle. Hopefully, i helped with this example clarify some of your issues.
Thank you Ken for answering my question. It indeed feels like a more proper of way of doing this in Ember. However, I still think it's difficult to get the hang of which objects are accessible from where...
Your example inspired me to do a rewrite of my code. I also made some changes to your approach:
I'm not sure if it's the best practice, my I don't create a store instance. Instead I define a Store class.
The content for the TodoItemsNewController is set by calling the model property on the corresponding route.
renderTemplate in the TodoItemsNewRoute only needs the outlet key.
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="todo_items">
{{#linkTo 'todo_items.new'}}Add Todo Item{{/linkTo}}
<ul>
{{outlet "addItem"}}
{{#each controller}}
<li>
{{#unless isNew}}
{{title}}
{{/unless}}
</li>
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="todo_items/new">
{{view Ember.TextField valueBinding="title" placeholder="Enter title"}}
window.App = Em.Application.create();
App.TodoItem = DS.Model.extend({
title: DS.attr('string', {
defaultValue: "unknown"
})
});
App.TodoItem.FIXTURES = [{
id: 1,
title: 'Lorem'
}, {
id: 2,
title: 'Ipsum'
}];
App.Store = DS.Store.extend({
revision: 11,
adapter: DS.FixtureAdapter.create()
});
App.Router.map(function() {
this.resource('todo_items', function() {
this.route('new');
});
});
App.IndexRoute = Em.Route.extend({
redirect: function() {
this.transitionTo('todo_items');
}
});
App.TodoItemsRoute = Em.Route.extend({
model: function() {
return App.TodoItem.find();
}
});
App.TodoItemsNewRoute = Em.Route.extend({
model: function() {
return App.TodoItem.createRecord();
},
renderTemplate: function() {
this.render({
outlet: 'addItem'
});
}
});
App.TodoItemsNewView = Em.View.extend({
tagName: 'li'
});
The updated example is on jsFiddle.
Any reviews are welcome.