Triggering an action in nested Components in Ember.js - ember.js

I have a Component--which is essentially a form--displaying editable prices for a set of items. Each row in this form is also a Component, so there's a nested relationship (a parent Component and child Components).
At the bottom of this form is a button to Cancel any changes the user made to the form, meaning it will need to affect all of the child Components. Ideally, I'd like to trigger an action from the Cancel button and have the child Components react to that.
I know this is against the Data Down / Actions Up methodology of Ember, but I'm not sure how else to approach this.
Is there any way to do this?

Following the data-down, actions-up approach, you'll have to do something like this.
Parent component:
App.ParentComponent = Ember.Component.extend({
isCancelled: false,
actions: {
cancel: function() {
this.set('isCancelled', true);
}
}
});
Parent template:
{{child-component isCancelled=isCancelled}}
<button {{action 'cancel'}}>Cancel</button>
Child component:
App.ChildComponent = Ember.Component.extend({
isCancelled: false,
wasCancelled: function() {
if (this.get('isCancelled')) {
// Cancelled logic here
}
}.observes('isCancelled')
});
I'm not sure exactly what you want to happen to the child component when the cancelled button is pressed, but you can probably use computed properties instead of an observer (which I think is a bit cleaner).

Related

Ember.Component's sendAction() to view target

sendAction() in an Ember.Component bubbles to controller by default which is expected. But i have 2,3 actions which i rather need to send to corresponding view which is using the component. In templates we set action to view using target=view. Can we do that?.
Update: Currently as a work around I am sending my view object to component which from there calls view.send() to send the action. But i feel this is not correct.
Ok after some thinking I believe i know that you mean. If you have a component and you have a action it will be handled by the component itself. If you want to send a action outside the component you would use sendAction.
Now to target the view of which holds your component since your component is base on a view, you can probably do this.get('parentView') to get the parent view and then chain send('nameOfAction')
So it would be this.get('parentView').send('nameOfAction') from within the component action and it will then trigger the action on the parent view of which the component is embedded.
So in your component you could have:
App.DemoCompComponent = Ember.Component.extend({
actions: {
internalTrigger: function() {
//just an alert to make sure it fired
alert('Internal action was caught on component');
//this should target parent view component is in
this.get('parentView').send('viewTriggerTest');
}
}
});
Now lets say you have you component in the index template you could:
The template would be:
<script type="text/x-handlebars" data-template-name="index">
<h2>Inside Index Template</h2>
{{demo-comp}}
</script>
The Index View Code would be:
App.IndexView = Ember.View.extend({
actions: {
viewTriggerTest: function() {
alert('View Trigger Test Worked on Index!');
}
}
});
Here is a jsbin
http://emberjs.jsbin.com/reyexuko/1/edit
For latest Ember 2.9 the recommended approach is to pass a closure action to the child component. The property target and parentView are private ones.

Ember.js - Triggering properties outside of controller

I am building a music site where I have multiple songs, each with it's own play/pause button, and then a global player which has a master play/pause button. I cannot figure out how to trigger the computed properties so that when I click the play button on the individual song, the master play/pause button also toggles from play to pause and vice versa.
I have the following code
Tracks.TrackController = Ember.ObjectController.extend({
currentTime: 0,
isLoaded: false,
isPlaying:false,
songStarted:false,
actions: {
play: function(){
var track_id = this.id;
var mySound = soundManager.createSound({
id: track_id,
url: 'https://api.soundcloud.com/tracks/' + track_id + '/stream?client_id=d61f17a08f86bfb1dea28539908bc9bf',
autoplay: false,
whileplaying: function() {
$('#positionBar').css('width', ((this.position/this.duration) * 100) + '%');
},
});
songStarted:true;
this.set("isPlaying", true);
this.set('mySound', mySound);
soundManager.stopAll();
mySound.play();
},
pause: function(){
var mySound = this.get('mySound');
this.set("isPlaying", false);
this.set("isPaused", true);
if(mySound && mySound.pause){
mySound.pause();
}
},
resume: function(){
var mySound = this.get('mySound');
this.set("isPlaying", true);
this.set("isPaused", false);
mySound.resume();
}
}
});
and this as the markup:
{{#if isPlaying}}
<li class="playBtn pause"><button {{action 'pause' this}} class="play-btn sm2_button" id="masterPlayBtn"></button></li>
{{else}}
{{#if isPaused}}
<li class="playBtn"><button {{action 'resume' this}} class="play-btn sm2_button" id="masterPlayBtn"></button></li>
{{else}}
<li class="playBtn"><button {{action 'play' this}} class="play-btn sm2_button" id="masterPlayBtn"></button></li>
{{/if}}
{{/if}}
I assume I need to add actions to the
Tracks.TracksController = Ember.ArrayController.extend({
});
controller, but that is where I get lost. I can't seem to figure out how to set isPlaying to the master play/pause button and also call the actions so that when I click the master pause/play button, it will find a particular song and play that on click.
You need:
A template that wraps the master button and individual songs
A component that will handle individual songs events and state
from here you handle individual song events that propagate upwards
Event bubbling from the components to the template controller
the template controller is notified of component events
here you can update the master control
Handle play/stop messages through binds
Working example: http://emberjs.jsbin.com/dopoho/1/edit?html,js,console,output
I should have used an array controller for the list of songs and I'm sure there are use cases I didn't considered. The point is to show you how you can use components to reuse code and how to pass events from the template to the component and then to the controller.
You don't need to change the actions on the individual play buttons. Only on the global button.
Update TracksController thusly:
Tracks.TracksController = Ember.ArrayController.extend({
restartTrack: null, // Need to restart with the global button? Use my model!
currentTrack: function () {
var filtered = this.get('content').filter(function (track) {
return this.get('isPlaying');
});
if(Ember.isEmpty(filtered)){
return null;
}
var track = filtered.objectAt(0);
this.set('restartTrack', track); // Should be on the item action...
return track;
}.property('#each.isPlaying'),
isPlaying: function () {
return this.get('content').any(function (track) {
return this.get('isPlaying');
});
}.property('#each.isPlaying')
});
So now your TracksController always knows if a song has individually changed, and will change its own isPlaying. It also knows which track to use if you want to restart the song with the global button...
This is just the first step to this and I don't really want to code the whole thing for you. All you really need to do at this point is do a little bit of refactoring in your handlebars and update the action on the global button.
You can also use the currentTrack in some sort of template and it will always re-render whenever you change the track. Pretty cool Ember-tastic stuff.
Good luck!
You could create hierarchy of controller objects like below:
Tracks.BaseTrackController = Ember.ObjectController.extend({...});
Tracks.TrackController = Tracks.BaseTrackController.extend({...});
With that you could place all you play pause functionality in Base controller and toggle them through actions in base itself (or child controller depending on your need)
Hope this makes sense.

Generating forms and handling submit properly with Ember.js rc1

I'm having trouble figuring out how to properly populate and accept an update from an Ember form under RC1. I've boiled it down to the bare essentials in this jsfiddle. I've made it far enough to display the form for a particular entity (user with first and last name) and the current values populate in the fields. However, as the user types, the fields actually update with each keystroke, and clicking the back button reveals that the data has already been changed without clicking the update button. I'd prefer to keep some logic in between the updates and only confirm an update after the user clicks the update button.
{{#view App.PersonFormView}}
First name: {{view Ember.TextField valueBinding="firstName"}}
Last name: {{view Ember.TextField valueBinding="lastName"}}
<button {{action "updatePerson"}}>Update</button>
{{/view}}
In the form template, I was trying to follow one of the Ember.js examples, but doing so resulted in a long delay and a monstrous deprecation warning using RC1. I think the examples are still being updated. I'd prefer a more handlebars-elegant way of coding the form if it existed.
The second problem is that I cannot capture the submit event itself, either on the form view or the controller. I don't know where this event is going.
App.PersonFormController = Ember.ObjectController.extend({
updatePerson: function(params){
// this doesn't get triggered as I would have expected
console.log('controller updatePerson: '+params);
}
});
App.PersonFormView = Ember.View.extend({
tagName: 'form',
updatePerson: function(params){
// this doesn't get triggered either!
console.log('updatePerson params: '+params);
}
});
In summary, I need to:
populate the input fields with the values without having them linked directly back to the model's data while the user is typing
catch the submit button's (or other control would be fine) clicked event along with the fields - and the entity's id - so that I can set them back on the model's data manually
There are several things:
I cannot capture the submit event itself
Events are fired in the controller and the route, not the view. The reason why your controller PersonFormController wasn't catching the event, is because the name is wrong. The controller should be named after the route: EditPersonController.
It's generally good to pass the model along with the action:
<button {{action "updatePerson" content}}>Update</button>
Here is an updated version that catches the event: http://jsfiddle.net/teddyzeenny/L9HMm/5/
populate the input fields with the values without having them linked directly back to the model's data
It's generally good practice to bind the fields directly to the model, to avoid code duplication.
Your problem is not that the fields are bound directly to the model, it's that you have no control over what is happening (saved, not saved, left the route...)
To have solid control, it's best to put your updating logic in your route. That way you can act accordingly when the user enters/leaves the route.
To catch your events in the route:
App.EditPersonRoute = Ember.Route.extend({
events: {
updatePerson: function(record) {
record.one('didUpdate', this, function() {
this.transitionTo('index');
});
record.get('transaction').commit();
}
}
});
To rollback changes if the user doesn't click on Update, use the deactivate callback in the route:
App.EditPersonRoute = Ember.Route.extend({
deactivate: function() {
this.modelFor('editPerson').get('transaction').rollback();
},
events: {
updatePerson: function(record) {
record.one('didUpdate', this, function() {
this.transitionTo('index');
});
record.get('transaction').commit();
}
}
});
Now these won't work in the fiddle since you are not using ember-data models.

binding context to action in ember textfield

I've got an ember application that needs to manage multiple chat windows. A window for each active chat is created within an {{#each}} loop. This is straightforward enough. The place that I'm having trouble is sending the chat message when the user presses enter.
The window looks like this
{{#each chats}}
... stuff to display already existing chats...
{{view Ember.TextField valueBinding="text" action="sendChat"}}
<button {{action sendChat this}}> Send </button>
{{/each}}
This works fine for the button, since I can pass this to it. By default the function defined in the textfield view action just gets the text within that textfield, which is not enough in this case. Since there can be multiple chat windows open, I need to know which window the message was typed into. Is it possible to pass this to the textfield action function? (or can you suggest a different way to solve this problem?)
Add contentBinding="this" to the definition of the view, like:
{{view Ember.TextField valueBinding="text" action=sendChat contentBinding="this"}}
EDIT
Ember master already has this change, but the official downloadable verstion still don't.. so you will need to subclass the Ember.TextField and change its insertNewline to achieve required functionality:
App.ActionTextField = Em.TextField.extend({
insertNewline: function(event) {
var controller = this.get('controller'),
action = this.get('action');
if (action) {
controller.send(action, this.get('value'), this);
if (!this.get('bubbles')) {
event.stopPropagation();
}
}
}
});
After that, the action handler will receive additional argument, the view:
{{view App.ActionTextField valueBinding="text" action=sendChat myfieldBinding="this"}}
and in controller:
sendChat: function (text, view) {
var myField = view.get('myfield');
//do stuff with my field
}
You may use ember master instead of subclassing Ember.TextField..
I hope the ember guys will release the next version soon..
I know this question has been answered but I said let me add some information that may help out someone in the situation of actions and TextField. One word "Component". TextField in Ember is a Component so if you think of TextField from that perspective it may help when it comes to sending actions and using TextField in an application.
So when you say App.SomeTextField = Ember.TexField.extend({...});App.SomeTextField is subclassing Ember.TextField (remember which is a component). You could add your logic inside and that works and you could access it from your template such as {{view App.SomeTextField}}
You may be thinking I see the word 'view' this guy sucks, TextField is a View. Well, it is sort of a View because Ember Components are a subclass of Ember.View so they have all that Views have. But there are some important things to keep in mind Components un-like Views do not absorb their surrounding context(information/data), they lock out everything and if you want to send something from the outside surrounding context you must explicitly do so.
So to pass things into App.SomeTextField in your template where you have it you would do something like {{view App.SomeTextField value=foo action="sendChat"}} where you are passing in two things value, and action in this case. You may be able to ride the fine line between View/Component for a bit but things come crashing why is your action not sending?
Now this is where things get a little trippy. Remember TextField is a Component which is subclassed from View but a View is not a Component. Since Components are their own encapsulated element when you are trying to do this.get('controller').send('someAction', someParam), "this" is referring to the Component its self, and the controller is once again the component its self in regards to this code. The action that you are hoping will go to the outside surrounding context and your application will not.
In order to fix this you have to follow the protocol for sending actions from a Component. It would be something like
App.SomeTextField = Ember.TextField.extend({
//this will fire when enter is pressed
insertNewline: function() {
//this is how you send actions from components
//we passed sendChat action in
//Your logic......then send...
this.sendAction('sendChat');
}
});
Now in the controller that is associated with where your SomeTextField component/view element is you would do
App.SomeController = Ember.Controller.extend({
//In actions hash capture action sent from SomeTextField component/view element
actions: {
sendChat: function() {
//Your logic well go here...
}
}
});
Now I said to think of TextField as a Component but I have been riding the tail of the view and declaring {{view AppSomeTextField...}}. Lets do it like a component.
So you would have in your template where you want to use it
//inside some template
`{{some-text-field}}`
Then you get a specfic template for the component with the name:
//template associated with component
<script type="text/x-handlebars" data-template-name="components/some-text-field">
Add what you want
</script>
In your JS declare your component:
//important word 'Component' must be at end
App.SomeTextFieldComponent = Ember.TextField.extend({
//same stuff as above example
});
Since we on a role you could probably get the same functionality using Ember input helpers. They are pretty powerful.
{{input action="sendChat" onEvent="enter"}}
Welp hopefully this information will help someone if they get stuck wondering why is my action not sending from this textField.
This jsBin is a sandBox for Components/Views sending actions etc....Nothing too fancy but it may help someone..
http://emberjs.jsbin.com/suwaqobo/3/
Peace, Im off this...

How can I make custom events that bubble through the view hierarchy?

I have button views that are part of a set of Ember.js form helpers I wrote. E.g. MyAddressFormView has the following template:
{{#form}}
{{textArea address}}
{{submitButton}}
{{cancelButton}}
{{/form}}
To handle the "Submit" button, I let the "submit" form event bubble, and handle it in the submit method of MyAddressFormView.
But how do I do the same for the "Cancel" button? For instance, is there any way I can trigger a custom "cancel form" event in a child view, let it bubble, and handle it in an ancestor view?
I would suggest triggering a jQuery special event and registering the event with a mapping on your Ember app. For example:
Ember.Application.create({
customEvents: {
// key is the jquery event, value is the name used in views
formcancel: 'formCancel'
}
});
And in your cancelButton view:
click: function(evt){
Ember.$().trigger('formcancel')
}
This will bubble in the same way that other mapped Ember DOM events do.
Ember.ActionHandler bubbles events through its "target" property. It's possible to override Ember.View's target to let events bubble through parentViews by default.
Include this mixin first:
(function() {
Ember.View.reopen({
// Let actions bubble to parentView by default.
target: function() {
return this.get('parentView');
}.property('parentView')
});
})();
Then just send the event like you'd normally do:
tap: function() { this.send('someEvent'); }