How do I invoke Ember's component helper programmatically? - ember.js

I have a list of foos:
[
{ name: 'Foo 1' type: 'important' },
{ name: 'Foo 2' type: 'normal' },
{ name: 'Foo 3' type: 'purple' }
]
I want to render that list, but some of them have special handling -- not just a class, but entirely different markup.
My first instinct would be to use the component helper:
{{#each foos as |foo|}}
{{component (join 'foo-' foo.type) foo=foo}}
{{/each}}
The downside to that is that I must define a component for each type and the server may send me new types that I don't know about. So I'd like to fall back to a basic foo-generic component if the foo-${type} component isn't defined.
To do that, I figured I'd create a helper:
{{#each foos as |foo|}}
{{foo-entry foo}}
{{/each}}
// app/helpers/foo-entry.js
Ember.Helper.helper(function([ foo ]) {
// ...
});
But I'm left with two questions:
how do I get a container inside that helper so I can check whether component:foo-${foo.type} exists?
how do I invoke the component helper from within another helper?

I think I can do it with a component instead of a helper:
// app/pods/foo-entry/component.js
export default Ember.Component.extend({
tagName: null,
foo: null,
componentName: Ember.computed('foo.type', function() {
const type = this.get('foo.type');
const exists =
this.container.lookupFactory(`component:foo-${type}`) != null ||
this.container.lookupFactory(`template:foo-${type}`) != null;
return exists ? `foo-${type}` : 'foo-generic';
})
})
{{!-- app/pods/foo-entry/template.hbs --}}
{{component componentName foo=foo}}

Related

How to pass a model to a template rendered from a route in Ember 1.6

I'm trying to set a model in a rendered template from a route. The model is being captured correctly via the template call:
{{#each itemController="user"}}
<div {{bind-attr class="isExpanded:expanded"}}>
<div><button {{action "sayHello" this}}>{{first_name}} {{last_name}}</button></div>
and my Route:
Newkonf.UsersRoute = Ember.Route.extend({
isExpanded: false,
actions: {
sayHello: function(myModel){
alert('first name: ' + myModel.first_name); // <- this works
// this.render('edit-user', {model: myModel}); <- doesn't work but not sure since this leads me to susupec
// it might https://github.com/emberjs/ember.js/issues/1820
// this.set('model', myModel}); <- doesn't work
this.render('edit-user');
}
}
}
the handlebars is:
going to edit user here first name: {{first_name}}
Edit 1
I can update the message in UsersRoute and it will update accordingly. I kind of though since I specified UserController that it would cause UserRoute to take precedence but I guess not.
Here is my router:
Newkonf.Router.map(function() {
// this.resource('posts');
this.route('about', { path: '/about'});
this.resource('users', { path: '/users'});
});
Edit 2
I had to do my modal.js.hbs like this:
within modal for you:
{{model.first_name}}
because this didn't work:
within modal for you:
{{first_name}}
and not sure why.
Here's my Route:
Newkonf.ApplicationRoute = Ember.Route.extend({
actions:{
somePlace: function(item){
alert('here i am in somePlace: ' + item.last_name); // <- this works
var modalController = this.controllerFor('modal');
modalController.set('model', item);
var myjt=modalController.get('model');
console.log('my value:' + myjt.first_name); // <- this works
this.render('modal', {
into: 'application',
outlet: 'modal3',
controller: modalController
});
}
}
});
and there is no controller.
Rendering to a particular outlet requires both the outlet be named, and the outlet to be in view when you attempt to render to it.
In the case of a modal, having it live in the application template is generally a good choice since the application is always in view.
actions: {
openModal: function(item){
var modalController = this.controllerFor('modal');
modalController.set('model', item);
this.render('modal', { // template
into: 'application', // template the outlet lives in
outlet: 'modal', // the outlet's name
controller: modalController // the controller to use
});
}
}
App.ModalController = Em.ObjectController.extend();
In the case above I'm rendering the template modal, into application, using the outlet named modal ( {{outlet 'modal'}}) and I created a controller to back the template I'm rendering. In the process I set the model on the controller, which would be used in the template.
You can read more about modals: http://emberjs.com/guides/cookbook/user_interface_and_interaction/using_modal_dialogs/
And here's the example:
http://emberjs.jsbin.com/pudegupo/1/edit

How to pass parameter with action using Ember.Button?

In the Ember.js app that I'm working on, I want to pass the parameter from template to controller function but can not.
Template
{{#view Ember.Button target="MyApp.Controller" action="start"}}
{{/view}}
Controller
App.MyAppClass = Ember.Controller.extend({
actions: {
start: function(arg) {
console.log(arg);
}
}
});
Are there anyone used to meet this problem?
Are you certain that you are referencing the right controller?
I just did a quick mock test with -
/** templates/foobar.hbs **/
<button {{action "foo" "bar"}}>Foobar</button>
/** controllers/foobar.js **/
var FoobarController = Ember.Controller.extend({
actions: {
foo: function (args) {
alert(args); // getting an alert with "bar"
},
}
});
I suggest reading -
http://emberjs.com/guides/components/sending-actions-from-components-to-your-application/
{{#view Ember.Button target="MyApp.Controller" action="start" param="parameter"}}
{{/view}}

How do I give Marionette templates data attributes?

Here I am at the beginning of a project. I am using zurb-foundation and marionette. I have an element that is rendering a template that is supposed to be tabs. As it stands:
define([
"backbone",
"marionette"
], function(Backbone, Marionette) {
MyItem = Backbone.Marionette.ItemView.extend({
template: "#design-tabs",
className: "section-container tabs",
onRender: function() {
$(this.el).foundation();
}
});
return MyItem;
});
there are no tabs. I think this is because the <div> being rendered to replace the <script> tag in the template does not have a particular data attribute (data-section). I went looking for something like 'className' that I could add to the ItemView declaration above in order to include data-attributes, but I have come up dry. I want something like:
MyItem = Backbone.Marionette.ItemView.extend({
template: "#design-tabs",
data: {
data-section: "",
data-foo: "bar"
},
className: "section-container tabs",
.
.
.
How do I add data attributes to the <div> (or otherwise) that replaces the <script> in a template?
To add data properties, use Backbone's attributes hash:
var MyView = Backbone.Marionette.ItemView.extend({
template: "#design-tabs",
className: "section-container tabs",
attributes: {
"data-section": "",
"data-foo": "bar"
}
});
Documentation: http://backbonejs.org/#View-attributes
If you prefer or need dynamic values, you can do in this way:
attributes: function() {
return {
'src': this.model.get('avatar_src')
};
}

Collection of objects of multiple models as the iterable content in a template in Ember.js

I am trying to build a blog application with Ember. I have models for different types of post - article, bookmark, photo. I want to display a stream of the content created by the user for which I would need a collection of objects of all these models arranged in descending order of common attribute that they all have 'publishtime'. How to do this?
I tried something like
App.StreamRoute = Ember.Route.extend({
model: function() {
stream = App.Post.find();
stream.addObjects(App.Bookmark.find());
stream.addObjects(App.Photo.find());
return stream;
}
}
where the resource name is stream
But it doesn't work. I am using the latest released Ember 1.0.0 rc 2 and handlebars 1.0.0 rc 3 with jQuery 1.9.1 and ember-data.
Probably the way I am trying to achieve this whole thing is wrong. The problem is even if I am able to use the collection of objects of multiple models to iterate in the template, I would still need to distinguish between the type of each object to display its properties apart from the common property of 'publishtime'.
You can use a computed property to combine the various arrays and then use Javascript's built in sorting to sort the combined result.
Combining the arrays and sorting them
computed property to combine the multiple arrays:
stream: function() {
var post = this.get('post'),
bookmark = this.get('bookmark'),
photo = this.get('photo');
var stream = [];
stream.pushObjects(post);
stream.pushObjects(bookmark);
stream.pushObjects(photo);
return stream;
}.property('post.#each', 'bookmark.#each', 'photo.#each'),
example of sorting the resulting computed property containing all items:
//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
streamSorted: function() {
var streamCopy = this.get('stream').slice(); // copy so the original doesn't change when sorting
return streamCopy.sort(function(a,b){
return a.get('publishtime') - b.get('publishtime');
});
}.property('stream.#each.publishtime')
});
rendering items based on a property or their type
I know of two ways to do this:
add a boolean property to each object and use a handlebars {{#if}} to check that property and render the correct view
extend Ember.View and use a computed property to switch which template is rendered based on which type of object is being rendered (based on Select view template by model type/object value using Ember.js)
Method 1
JS:
App.Post = Ember.Object.extend({
isPost: true
});
App.Bookmark = Ember.Object.extend({
isBookmark: true
});
App.Photo = Ember.Object.extend({
isPhoto: true
});
template:
<ul>
{{#each item in controller.stream}}
{{#if item.isPost}}
<li>post: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isBookmark}}
<li>bookmark: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isPhoto}}
<li>photo: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{/each}}
</ul>
Method 2
JS:
App.StreamItemView = Ember.View.extend({
tagName: "li",
templateName: function() {
var content = this.get('content');
if (content instanceof App.Post) {
return "StreamItemPost";
} else if (content instanceof App.Bookmark) {
return "StreamItemBookmark";
} else if (content instanceof App.Photo) {
return "StreamItemPhoto";
}
}.property(),
_templateChanged: function() {
this.rerender();
}.observes('templateName')
})
template:
<ul>
{{#each item in controller.streamSorted}}
{{view App.StreamItemView contentBinding=item}}
{{/each}}
</ul>
JSBin example - the unsorted list is rendered with method 1, and the sorted list is rendered with method 2
It's a little complicated than that, but #twinturbo's example shows nicely how to aggregate separate models into a single array.
Code showing the aggregate array proxy:
App.AggregateArrayProxy = Ember.ArrayProxy.extend({
init: function() {
this.set('content', Ember.A());
this.set('map', Ember.Map.create());
},
destroy: function() {
this.get('map').forEach(function(array, proxy) {
proxy.destroy();
});
this.super.apply(this, arguments);
},
add: function(array) {
var aggregate = this;
var proxy = Ember.ArrayProxy.create({
content: array,
contentArrayDidChange: function(array, idx, removedCount, addedCount) {
var addedObjects = array.slice(idx, idx + addedCount);
addedObjects.forEach(function(item) {
aggregate.pushObject(item);
});
},
contentArrayWillChange: function(array, idx, removedCount, addedCount) {
var removedObjects = array.slice(idx, idx + removedCount);
removedObjects.forEach(function(item) {
aggregate.removeObject(item);
});
}
});
this.get('map').set(array, proxy);
},
remove: function(array) {
var aggregate = this;
array.forEach(function(item) {
aggregate.removeObject(item);
});
this.get('map').remove(array);
}
});

Item specific actions

Against my controller, I have an item with an array of items which I want to display a list of and have an action to remove an item.
Have taken most of the code from a similar question item-specific actions in ember.js collection views. Majority of this works, the address display works and the items render, however the action does not seem to have the correct context and therefore does not perform the remove action.
Controller:
App.EditAddressesController = Em.ArrayController.extend({
temporaryUser: {
firstName: 'Ben',
lastName: 'MacGowan',
addresses: [{
number: '24',
city: 'London'
etc...
}, {
number: '23',
city: 'London'
etc...
}]
}
});
The temporaryUser is an EmberObject (based off a User model), and each item within the addresses array is another EmberObject (based off an Address model) - this has just been simplified for purposes of displaying code.
Parent view:
{{#each address in temporaryUser.addresses}}
{{#view App.AddressView addressBinding="this"}}
{{{address.display}}}
<a {{action deleteAddress target="view"}} class="delete">Delete</a>
{{/view}}
{{/each}}
App.AddressView:
App.AddressView = Ember.View.extend({
tagName: 'li',
address: null,
deleteAddress: function() {
var address = this.get('address'),
controller = this.get('controller'),
currentAddresses = controller.get('temporaryUser.addresses');
if(currentAddresses.length > 1) {
$.each(currentAddresses, function(i) {
if(currentAddresses[i] == address) {
currentAddresses.splice(i, 1);
}
});
}
else {
//Throw error saying user must have at least 1 address
}
}
});
Found out it was an issue with my each:
{{#each address in temporaryUser.addresses}}
should have been:
{{#each temporaryUser.addresses}}
And my AddressView should use removeObject (like the original code sample)
App.AddressView = Ember.View.extend({
tagName: 'li',
address: null,
deleteAddress: function() {
var address = this.get('address'),
controller = this.get('controller'),
currentAddresses = controller.get('temporaryUser.addresses');
if(currentAddresses.length > 1) {
currentAddresses.removeObject(address);
}
else {
//Throw error saying user must have at least 1 address
}
}
});
I think your code could become more readable if you approach it that way:
1- Modify your action helper to pass the address with it as context. I modified your Handlebars a bit to reflect the way i am approaching stuff like this and works for me. I think this is the "Ember way" of doing this kind of stuff, since a the properties in a template are always a lookup against the context property of the corresponding view:
{{#each address in temporaryUser.addresses}}
{{#view App.AddressView contextBinding="address"}}
{{{display}}}
<!-- this now refers to the context of your view which should be the address -->
<a {{action deleteAddress target="view" this}} class="delete">Delete</a>
{{/view}}
{{/each}}
2 - Modify the handler in your view to accept the address as a parameter:
App.AddressView = Ember.View.extend({
tagName: 'li',
address: null,
deleteAddress: function(address) {
var controller = this.get('controller'),
currentAddresses = controller.get('temporaryUser.addresses');
if(currentAddresses.length > 1) {
currentAddresses.removeObject(address);
}
else {
//Throw error saying user must have at least 1 address
}
}
});