I'm having a problem with Ember, although not too sure where the problem lies, whether it's within Handlebars or Ember component.
The issue is when a controller object context is passed in to an ember component, as an argument, the context is undefined. However, logging the object within the handlebars template, immediately preceding the component, shows the correct object (see index and components/component-button templates).
Templates
<script type="text/x-handlebars" data-template-name="application">
<h1>Fiddling</h1>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
{{#each sections}}
{{#each buttons}}
{{log 'source in template' ../source}}
{{component-button source=../source options=this}}
{{/each}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="components/component-button">
{{log 'source in component' source}}
<button {{action 'options.action'}}>{{options.label}}</button>
</script>
Application
App = Ember.Application.create( {} );
/**
* Component: Button
*/
App.ButtonComponent = Ember.Component.extend( {
tagName: 'button',
click: function click() {
this.get( 'source' ).send( this.get( 'options.action' ) );
}
} );
/**
* Controller: Application
*/
App.IndexController = Ember.ObjectController.extend( {
sections: Ember.A(),
actions: {
doSomething: function() {
window.console.log( 'hooray!' );
}
},
setup: function setup() {
var source = this;
var section = Ember.Object.create( {
source: source,
buttons: Ember.A( [
Ember.Object.create( {
label: 'Do something!',
action: 'doSomething'
} )
] )
} );
this.sections.pushObject( section );
}.on( 'init' )
} );
App.IndexRoute = Ember.Route.extend( {
model: function() {
return { name: 'foo' };
}
} );
See fiddle
Instead of referring to ../source in the each loop, do the following:
<script type="text/x-handlebars" data-template-name="index">
{{#each section in sections}}
{{#each section.buttons}}
{{log 'source in template' section.source}}
{{component-button source=section.source options=this}}
{{/each}}
{{/each}}
</script>
In the first each 'section' is defined, which can be used in nested each statements to refer to the section.
Related
I have the following index.html:
<!DOCTYPE html>
<html>
<body>
<script type="text/x-handlebars" id="index">
<ul>
{{#each todo in todos}}
<li>{{todo}}</li>
{{/each}}
</ul>
<button {{action 'generate'}}/>Generate a to-do</buton>
</script>
<script src="js/libs/jquery-1.10.2.js"></script>
<script src="js/libs/handlebars-1.1.2.js"></script>
<script src="js/libs/ember-1.6.1.js"></script>
<script src="js/app.js"></script>
</body>
</html>
And app.js:
App = Ember.Application.create();
App.Router.map(function() {});
App.IndexRoute = Ember.Route.extend({
model: function() {
return {todos: ['To-do 1', 'To-do 2']};
},
});
// This is a function I cannot change, because I don't own it.
// So I'm forced to get the updated model as the result of this.
// Here is some dummy-but-working implementation, for simulation purpose:
function generate(todolist) {
var n = todolist.todos.length + 1;
todolist.todos.push("To-do " + n);
return todolist;
}
App.IndexController = Ember.ObjectController.extend({
actions: {
generate: function() {
var oldToDoList = this.get('model');
var newToDoList = generate(oldToDoList);
this.set('model', newToDoList);
console.log(this.get('model').todos);
},
},
});
When I click on the generate button, I effectively see the growing to-dos array in console, but UI doesn't update.
Shouldn't #each content update automatically when completely replacing controller's model, or am I missing something?
your generate method doesn't actually generate a new array, so Ember won't notice that you've changed the property (because it's a reference to the same array). In your particular instance you should just use pushObject and Ember will know you're modifying the same array.
function generate(todolist) {
var n = todolist.todos.length + 1;
todolist.todos.pushObject("To-do " + n);
return todolist;
}
I'm expirencing some weirdness with Ember components. First problem I'm seeing is that class attribute binding is not working. I'm also witnessing properties some how being unbound after the first time they are mutated. This is just a simple tabs component that I am building. Here's a repro http://emberjs.jsbin.com/uDUfONi/2/edit
JS
App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.IndexController = Ember.Controller.extend({
selectedTab: '',
initialTab: function () {
var name = this.get( 'model' ).get('firstObject');
this.set( 'selectedTab', name );
return name;
}.property()
});
App.MyTabComponent = Ember.Component.extend({
tagName: 'li',
isSelected: false,
tabChanged: function () {
if ( this.get( 'selectedTab' ) !== this.get( 'name' ) ) {
this.set( 'isSelected', false );
} else {
this.set( 'isSelected', true );
}
}.observes('selectedTab'),
checkInitialTab: function () {
if ( this.get( 'initialTab' ) === this.get( 'name' ) ) {
this.set( 'isSelected', true);
} else {
this.set( 'isSelected', false );
}
}.on( 'didInsertElement' ),
actions: {
selectTab: function () {
if ( this.get( 'selectedTab' ) !== this.get( 'name' ) ) {
this.set( 'selectedTab', this.get( 'name' ) );
}
}
}
});
Templates
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
{{my-tabs model=model initialTab=initialTab selectedTab=selectedTab}}
</script>
<script type="text/x-handlebars" data-template-name="components/my-tabs">
Selected Tab: {{selectedTab}}
<ul class="nav nav-tabs">
{{my-tab name="control" initialTab=initialTab selectedTab=selectedTab}}
{{#each item in model}}
{{my-tab name=item initialTab=controller.initialTab selectedTab=controller.selectedTab}}
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="components/my-tab">
<li {{action selectTab name}} {{bind-attr class="isSelected:active"}}>
{{isSelected}}
{{name}}
</li>
</script>
Okay, had to consult with another Emberino on this one.
It came down to the fact that there were nested li element. When you defined the tagName on the component it injects the component with that tag. Unfortunately it was breaking ember/html/somewhere/Idunno.
http://emberjs.jsbin.com/uDUfONi/10/edit
<script type="text/x-handlebars" data-template-name="components/my-tab">
<a>{{name}}</a>
</script>
App.MyTabComponent = Ember.Component.extend({
tagName: 'li',
classNameBindings: ['isSelected:active', ':clickable'],
isSelected: function(){
return this.get('selectedTab') === this.get('name');
}.property('selectedTab', 'name'),
click: function () {
this.set('selectedTab', this.get('name'));
}
});
instead of the li being defined twice:
<script type="text/x-handlebars" data-template-name="components/my-tab2">
<li {{action selectTab name}}>
{{isSelected}}
<a href>{{name}}</a>
</li>
</script>
App.MyTabComponent = Ember.Component.extend({
tagName: 'li',
.....
});
Yeah, it's definitely the double li that is somehow breaking things. I have no idea exactly how...
I just commented out the tagName : 'li', line and the {{isSelected}} value in the template starts showing the right thing.
http://emberjs.jsbin.com/uDUfONi/12/edit
I want to insert CollectionView into View. It works but displays:
DEPRECATION: Using the defaultContainer is no longer supported. [defaultContainer#lookup]
How correctly insert CollectionView in View?
App = Ember.Application.create();
App.Router.map(function() {
this.route("index", { path: "/" });
});
App.FirstView = Em.View.extend({
templateName: 'first'
});
App.SecondView = Em.View.extend({
templateName: 'second'
});
App.MyCollection = Em.CollectionView.extend({
content: ['f','s'],
createChildView: function(viewClass, attrs){
if (attrs.content == 'f') {
viewClass = App.FirstView ;
};
if (attrs.content == 's') {
viewClass = App.SecondView ;
};
return this._super(viewClass, attrs);
}
});
App.IndexView = Em.View.extend({
myChildView: App.MyCollection.create()
});
templates:
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
{{view view.myChildView}}
</script>
<script type="text/x-handlebars" data-template-name="first">
search
</script>
<script type="text/x-handlebars" data-template-name="second">
client
</script>
Sorry for my english, i am from Russia and understand it a little))
Your IndexView is directly instantiating the view. This style of instantiation is deprecated with nested views, so as to allow child views to get their parent Container.
Change that to declare the myChildView directly, and the deprecation warning will go away.
App.IndexView = Em.View.extend({
myChildView: App.MyCollection
});
I have simple view in my Ember.js application, like this. Each "content" element consists of two objects, first and second:
{{#each App.myController.content}}
{{view for content.first}}
{{view for content.second}}
{{/each}}
I'd like to define view for each content separately (so as not to have to write it twice), in another handlebars template script. How can I pass the first and second variables to the view?
Here is a code sample, see http://jsfiddle.net/Zm4Xg/5/:
Handlebars:
<script type="text/x-handlebars" data-template-name="contact-view">
<div>{{name}}</div>
<img {{bindAttr src="avatar"}} {{bindAttr alt="name"}}>
</script>
<script type="text/x-handlebars">
{{#each App.contactsController.pair}}
<div class="menu_vertical_group">
{{#with this.first}}
{{view App.contactView}}
{{/with}}
{{#with this.second}}
{{view App.contactView}}
{{/with}}
</div>
{{/each}}
</script>
​JavaScript:
App = Ember.Application.create();
App.Contact = Em.Object.extend({
name: null,
avatar: null
});
App.contactView = Ember.View.extend({
templateName: 'contact-view'
});
App.contactsController = Em.ArrayController.create({
content: [],
initData: function(data) {
var contacts = data.map(function(contact) {
return App.Contact.create({
"name": contact.name,
"avatar": contact.avatar
});
});
this.pushObjects(contacts);
},
pair: (function() {
content = this.get('content');
var result = [];
for (ii = 0; ii < content.length; ii += 2) {
result.pushObject({
"first": content[ii],
"second": content[ii + 1] ? content[ii + 1] : null
});
}
return result;
}).property('content')
});
App.contactsController.initData([{
"name": "John Doe",
"avatar": "/john.jpg"},
{
"name": "Someone Else",
"avatar": "/else.jpg"}]);​
Something like this?
{{#each App.myController.content}}
{{view MyView contentBinding="content.first"}}
{{view MyView contentBinding="content.second"}}
{{/each}}
You can extend the View class with a templateName function that evaluates to a different view based on a property of the model, like this:
App.customView = Ember.View.extend({
templateName:function(){
if(this.get('content').get('index') === 1){
return 'view1';
}
else{
return 'view2';
}
}.property('content.index') // custom template function based on 'index' property
});
Check out this fiddle: http://jsfiddle.net/lifeinafolder/7hnc9/
I'm looking for advice on how to trigger this view function insertNewLine from a button (see view and template below). I'm guessing there's probably a better way to structure this code. Thanks for your help.
// view
App.SearchView = Ember.TextField.extend({
insertNewline: function() {
var value = this.get('value');
if (value) {
App.productsController.search(value);
}
}
});
// template
<script type="text/x-handlebars">
{{view App.SearchView placeholder="search"}}
<button id="search-button" class="btn primary">Search</button>
</script>
You could use the mixin Ember.TargetActionSupport on your TextField and execute triggerAction() when insertNewline is invoked. See http://jsfiddle.net/pangratz666/zc9AA/
Handlebars:
<script type="text/x-handlebars">
{{view App.SearchView placeholder="search" target="App.searchController" action="search"}}
{{#view Ember.Button target="App.searchController" action="search" }}
Search
{{/view}}
</script>
JavaScript:
App = Ember.Application.create({});
App.searchController = Ember.Object.create({
searchText: '',
search: function(){
console.log('search for %#'.fmt( this.get('searchText') ));
}
});
App.SearchView = Ember.TextField.extend(Ember.TargetActionSupport, {
valueBinding: 'App.searchController.searchText',
insertNewline: function() {
this.triggerAction();
}
});