Ember.js - Triggering properties outside of controller - ember.js

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.

Related

Triggering an action in nested Components in 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).

How to prevent double clicks with ember.js?

I'm trying to figure out the idiomatic way to prevent a button from being clicked multiple times.
Imagine I have a simple controller action like so ...
var FooController = Ember.ObjectController.extend({
actions: {
go: function() {
console.log("done!");
}
}
});
and in my template I have a button defined like so ...
<button {{action go}}>Click Me Fast</button>
Does the action have an option to disable it immediately / making it so only once true event will be handled by the controller (until it's disabled for example)
Edit
I'm looking for a long term / multi use solution. One idea I'm thinking about is creating a special ember-component called "button-disable" that would allow me to create a custom button type that generally disables after a single click -but will still allow me to bubble up events to a parent controller. This feels a little heavier weight than I'd like so if another option exists, or if someone has created an addon for just this - let me know
As a one-off, if you bind the disabled attribute on your button like so
<button {{action go}} {{bind-attr disabled=actionPerformed}}>
and then set up your controller like
var FooController = Ember.ObjectController.extend({
actionPerformed: false,
actions: {
go: function() {
this.set("actionPerformed", true);
console.log("done!");
}
}
});
then the button will become disabled after you click it once
If you want a reusable component I'd borrow the spinner button from http://emberjs.com/guides/cookbook/helpers_and_components/spin_button_for_asynchronous_actions/ and tweak it as you need.
So your JS would be along the lines of
window.SpinEg = Ember.Application.create({});
SpinEg.ApplicationController = Ember.Controller.extend({
isLoading:false,
buttonText:"Submit",
actions:{
saveData:function(){
var self = this;
var saveTime = Ember.run.later(function(){
self.set('isLoading', false);
}, 1000);
}
}
});
SpinEg.SpinButtonComponent = Ember.Component.extend({
classNames: ['button'],
buttonText:"Save",
isLoading:false,
actions:{
showLoading:function(){
if(!this.get('isLoading')){
this.set('isLoading', true);
this.sendAction('action');
}
}
}
});
The template for your component would be
<script type='text/x-handlebars' id='components/spin-button'>
<button {{bind-attr id=id}} {{action 'showLoading'}}>
{{#if isLoading}}
<img src="http://i639.photobucket.com/albums/uu116/pksjce/spiffygif_18x18.gif"></img>
{{else}}
{{buttonText}}
{{/if}}
</button>
</script>
and you would then just include the following where you need the button to appear
<script type='text/x-handlebars' id='application'>
{{spin-button id="forapplication" isLoading = isLoading buttonText=buttonText action='saveData'}}
</script>

Add and remove views

I would like to insert in the DOM a view that displays a form with 2 buttons: + and -;
when you click "+" another identical view is inserted, when you press "-" the current view is removed;
I've tried to create a container view and the function for adding a view is simple:
in the template:
{{view Ember.ContainerView elementId="containerView"}}
in the childView's template:
<button class="form-button" {{action "addProduct"}}>+</button>
in the route's controller:
addProduct: function() {
var container = Ember.View.views['containerView'];
var child = container.createChildView(Gmcontrolpanel.InserisciProdottoView);
container.pushObject(child);
}
But i'm not able to manage the "-" function; because for that i need to get the view that the button i'm clicking belongs to in order to remove it, and i don't know how to do this;
All the childviews can have a controller? Because from the childview's button i can only call actions from the route's controller;
Or there is a better way to get this work?
so in that case, have an action in the childview rather controller like this
<button class="form-button" {{action "deleteProduct" target="view"}}>-</button>
in the views actions handle the deleteProduct like this
deleteProduct: function() {
this.destroy();
}
If you want to handle any of the model part then send an event from above method to controller

Specify action for view in template?

I would like to know if it is possible to assign an action to a view like I could assign an action to a HTML tag:
This works:
<button {{action "show2" }}>Test 1</button>
This doesn't:
{{#view NewApp.MenuButton }}
{{action "show3" target="controller"}}
{{/view}}
I know that I could implement the click function in the view. But I would like to use the button as some sort of reusable component.
You typically want to use the Handlebars action helper on an HTML element, not on an Ember.View.
Since you want to attach an event to the NewApp.MenuButton View you, define the event in your view class definition. For example, here we handle the click event:
NewApp.MenuButton = Ember.View.extend({
click: function(event){
// When the user clicks this view,
// this function will be called.
// ... handle the click
App.myController.menuButtonWasClicked();
}
});
If the event you want to attach is not one of the built-in events, you can register your own events. Find the built-in supported events and how to register custom events here: Ember.js - Events
Edit: You say you want to be able to reuse it. You can define a mixin for attaching arbitrary events and targeting arbitrary objects:
Ember.MyEventAttacher = Ember.Mixin.create({
init: function() {
var action = this.get('action');
target = this.get('target'),
targetObj = Ember.getPath(target);
if (action && targetObj) {
var targetEventFnc = targetObj[action];
if (typeof targetEventFnc === 'function') {
var actionFnc = function(event) {
targetEventFnc(event);
}
this.set(action, actionFnc);
}
this._super();
}
});
Include the Mixin in your View:
NewApp.MenuButton = Ember.View.extend(Ember.MyEventAttacher);
And then re-use this view in your templates, making sure to define the action and target properties. Example:
{{#view NewApp.MenuButton action="show3" target="NewApp.myController"}}
<!-- ... -->
{{/view}}
Targeting:
NewApp.myController = Ember.Controller.create({
show3: function(event) {
// the event is sent here!
}
});

Controller Strategy / Garbage Collection (destroy)

Trying to figure out the "ember best practices" for my app, regarding MVC. also for reference, I'm using ember-data, ember-layout, and ember-route-manager.
I'll use User as an example:
what I feel like I want to do is to get a User model from the database... then wrap it in a UserController, and set the model on a "content" property... then in a View, I want to bind to the controller for some functionality, and to the controller.content for model-level data. so a controller might look something like:
App.UserViewController = Em.Object.create({
content: userRecord,
isFollowingBinding : 'content.you_follow',
toggleFollow: function() {
make server call to change following flag
}
});
then the view could bind to the {{controller.content.name}}, or {{#if controller.isFollowing}}, or {{action "toggleFollowing" target="controller"}}
but say I get a list of User models back from the database... I feel like what should happen is that each of those models should be wrapped with a controller, and that should be returned as a list... so the view would have a list of UserControllers
Incidentally, I've done this... and it is working nicely.... except that everytime I reload the list, I wrap all of the new model objects with new controllers... and over time, the # of controllers in memory get larger and larger. on my base Controller class, I'm logging calls to "destroy", and I dont see it ever happening
when it comes to Em.View... I know that everytime it is removed from the screen, .destroy() gets calls (I am logging those as well). so if I were to move my code into a view, i know it will get destroyed and recreated everytime... but I dont feel like the functionality like toggleFollow() is supposed to be in view...
SO QUESTIONS:
is this how MVC is supposed to work? every instance of a model wrapped in a controller for that model? where there could be lots of controller instances created for one screen?
if I go down this approach, then I'm responsible for destroy()ing all of the controllers I create?
or is the functionality I've described above really meant for a View, and them Ember would create/destroy them as they are added/removed from the screen? also allowing template designers to decide what functionality they need (if they just need the {{user.name}}, theres no need to instantiate other controller/view classes... but if they need a "toggle" button, then they could wrap that part of the template in {{#view App.UserViewController contentBinding="this"}} )
I re-wrote this a few times... hopefully it makes sense....
I wouldn't wrap every user into an own controller.
Instead I would bind the user to a view, say App.UserView and handle the action toggleFollow on that view. This action will then delegate it's action to a controller which will handle the server call, see http://jsfiddle.net/pangratz666/hSwEZ/
Handlebars:
<script type="text/x-handlebars" >
{{#each App.usersController}}
{{#view App.UserView userBinding="this" controllerBinding="App.usersController"}}
{{user.name}}
{{#if isFollowing}}
<a {{action "toggleFollowing"}} class="clickable" >stop following</a>
{{else}}
<a {{action "toggleFollowing"}} class="clickable" >start following</a>
{{/if}}
{{#if user.isSaving}}saving ...{{/if}}
{{/view}}
{{/each}}
</script>​
JavaScript:
App.usersController = Ember.ArrayProxy.create({
content: [],
toggleFollowing: function(user) {
user.set('isSaving', true);
Ember.run.later(function() {
user.toggleProperty('you_follow');
user.set('isSaving', false);
}, 1000);
}
});
App.UserView = Ember.View.extend({
isFollowingBinding: 'user.you_follow',
toggleFollowing: function() {
var user = this.get('user');
var controller = this.get('controller');
controller.toggleFollowing(user);
}
});
​