I'm trying to write an Ember view that has three states. Specifically, a submit button that transitions from "Submit" to "Saving..." to "Finished!" There are many ways to accomplish this goal, but I was wondering what the "best practice" would be from an Ember standpoint to accomplish this without writing crappy code.
Currently I have the following code:
UiControls.SubmitButton = Ember.View.extend({
template: function() {
var template = '{{#if view.isNotStarted}}Submit{{/if}}';
template += '{{#if view.isStarted}} <i class="icon-spinner icon-spin"></i>Saving...{{/if}}';
template += '{{#if view.isFinished}} <i class="icon-check-sign"></i>Finished!{{/if}}'
return Ember.Handlebars.compile(template);
}.property(),
isNotStarted: true,
isStarted: null,
isFinished: null,
classNames: ['btn', 'btn-green'],
isDisabled: false,
click: function(){
if (!this.get('disabled')){
this.set('isNotStarted', false);
this.set('isStarted', true);
this.set('isFinished', false);
this.timer();
}
},
/* Simulates a server call */
timer: function(){
(function(self){
setTimeout(function(){
self.set('isStarted', false);
self.set('isFinished', true);
}, 500);
})(this);
}
});
To me this is really ugly -- we're setting individual boolean values based off of events in order to work with handlebars' purposefully restricted conditional syntax.
What I want is a handlebars construct that accepts something like an Ember StateManager property (not possible with Handlebars syntax). Or, at the very least, I want to alter my template based off of a computed property from a StateManager (again, not possible). So my question is, is there any better way to write the above code to prevent code duplication handling state transitions manually through lots of little boolean flag manipulations?
To me this is really ugly -- we're setting individual boolean values based off of events in order to work with handlebars' purposefully restricted conditional syntax.
Totally agreed, this is a sign that some refactoring is needed.
What I want is a handlebars construct that accepts something like an Ember StateManager property (not possible with Handlebars syntax).
It's possible if you write a custom handlebars helper, but honestly I would not recommend that approach.
Or, at the very least, I want to alter my template based off of a computed property from a StateManager (again, not possible)
Why not? Guessing you mean even if you had that property it's not possible to alter the template without all the booleans.
So my question is, is there any better way to write the above code to prevent code duplication handling state transitions manually through lots of little boolean flag manipulations?
Yes. The reason handlebars has this restriction is to prevent complexity and logic from being part of your templates. For example any time you need to show like 1-of-3 versions based on some value. That kind of logic belongs in the view or controller layer.
So looking at your example, there are two aspects of the template that need to change
text: Should be either "Submit", "Saving..." or "Finished!"
iconClassNames: Either empty, "icon-spinner icon-spin" or "icon-check-sign"
With this in mind we can simplify the template to be:
<i {{bindAttr class="view.iconClassNames"></i>{{view.text}}
And add the properties to the view
UiControls.SubmitButton = Ember.View.extend({
template: Ember.Handlebars.compile('<i {{bindAttr class="view.iconClassNames"></i>{{view.text}}'),
classNames: ['btn', 'btn-green'],
isDisabled: false,
text: "Submitted",
iconClassNames: "",
click: function(){
if (!this.get('disabled')){
this.set('text', 'Saving...');
this.set('iconClassNames', 'icon-spinner icon-spin');
this.timer();
}
},
/* Simulates a server call */
timer: function(){
(function(self){
setTimeout(function(){
this.set('text', 'Finished!');
this.set('iconClassNames', 'icon-check-sign');
}, 500);
})(this);
}
});
This works for the simulation but is not ideal. Really you want text and iconClassNames to be bound to the stateManager. That means changing the text and iconClassNames to be computed properties. Ideally they would be computed based on the underlying state of the model object, and click() would be defined on controller, but for simulation it would be something like this:
UiControls.SubmitButton = Ember.View.extend({
template: Ember.Handlebars.compile('<i {{bindAttr class="view.iconClassNames"></i>{{view.text}}'),
classNames: ['btn', 'btn-green'],
isDisabled: false,
state: 'new',
text: function() {
//return appropriate button text based on state
}.property('state'),
iconClassNames: function() {
//calculate text based on state
}.property('state'),
/* Simulates a server call */
click: function(){
if (!this.get('disabled')){
this.set('state', 'saving');
this.timer();
}
},
/* Simulates a server call */
timer: function(){
(function(self){
setTimeout(function(){
self.set('state', 'finished');
}, 500);
})(this);
}
});
Related
Is there any way for a component to listen to, or observe, changes in yielded content?
I have a component which serves as an isotope.js wrapper, and would like to be able to call some necessary clean-up isotope methods (such as .isotope('layout')) in case wrapped content changes (e.g. through filtering).
I've been able to do something similar with a View by observing controller properties, but would like to keep things less coupled if possible.
Well, I don't know exactly what it is you want to do nor have I used isotope.js. But here's what I can tell you. The {{yield}} helper calls this function:
_yield: function(context, options, morph, blockArguments) {
var view = options.data.view;
var parentView = this._parentView;
var template = get(this, 'template');
if (template) {
Ember.assert("A Component must have a parent view in order to yield.", parentView);
view.appendChild(Ember.View, {
isVirtual: true,
tagName: '',
template: template,
_blockArguments: blockArguments,
_contextView: parentView,
_morph: morph,
context: get(parentView, 'context'),
controller: get(parentView, 'controller')
//expose parent to children components?
});
}
}
Which means in theory you could expose a handle to your child components to set properties on the parent component. This clearly couples the two components. You can also make both the children and the parent take the "wrapped content" as attributes so that you can observe and manipulate in both places. I have used both approaches to great success. Which I used is dictated by context
I have a link to User displayed from various screens(From User List, User Groups etc.). When the link is clicked, User is presented to edit. When cancel button is pressed in the edit form, I would like to transition to previous screen userlist/group. How is this generally achieved in Emberjs.
Thanks,
Murali
You need nothing more than
history.back()
One of the main design objectives of Ember, and indeed most OPA frameworks, is to work harmoniously with the browser's history stack so that back "just works".
So you don't need to maintain your own mini-history stack, or global variables, or transition hooks.
You can put a back action in your application router to which actions will bubble up from everywhere, so you can simply say {{action 'back'}} in any template with no further ado.
Here's my solution, which is very simple and high performance.
// file:app/routers/application.js
import Ember from 'ember';
export default Ember.Route.extend({
transitionHistory: [],
transitioningToBack: false,
actions: {
// Note that an action, like 'back', may be called from any child! Like back below, for example.
willTransition: function(transition) {
if (!this.get('transitioningToBack')) {
this.get('transitionHistory').push(window.location.pathname);
}
this.set('transitioningToBack', false);
},
back: function() {
var last = this.get('transitionHistory').pop();
last = last ? last : '/dash';
this.set('transitioningToBack', true);
this.transitionTo(last);
}
}
});
There is probably a way to DRY(don't repeat yourself) this up, but one way of doing it is to have 2 actions: willTransition which Ember already gives you and goBack which you define yourself. Then, there is a "global" lastRoute variable that you keep track of as follows:
App.OneRoute = Ember.Route.extend({
actions: {
willTransition: function(transition){
this.controllerFor('application').set('lastRoute', 'one');
},
goBack: function(){
var appController = this.controllerFor('application');
this.transitionTo(appController.get('lastRoute'));
}
}
});
And your template would look as follows:
<script type="text/x-handlebars" id='one'>
<h2>One</h2>
<div><a href='#' {{ action 'goBack' }}>Back</a></div>
</script>
Working example here
I am setting up a page where my user can add an orgLanguage, and I'd like to show a special message if this is the first orgLanguage being added. I'm able to get my code working, but it sure looks ugly, and I'm wondering if there's a better way to handle this?
First, here's my Handelbars template:
Handlebars Template (Simplified):
{{#if isFirstOrgLanguage}}
...display some content
{{/if}}
That variable is defined on my controller as follows.
Controller (Simplified):
export default Ember.ObjectController.extend({
isFirstOrgLanguage: function() {
// the 'orgLanguages' controller property is set in the route
var orgLanguagesPromiseArray = this.get('orgLanguages');
return orgLanguagesPromiseArray.then( function() {
var orgLanguagesRecordArray = orgLanguagesPromiseArray.get('content');
var orgLanguagesArray = orgLanguagesRecordArray.get('content');
return orgLanguagesArray ? orgLanguagesArray.length === 1 : true;
});
}.property('orgLanguages')
}
I've named my variables the data type that I receive. You'll note that this is a computed property that depends on a controller property set on my route, shown below.
Route (Simplified):
setupController: function (controller, model) {
this._super(controller, model);
controller.set('orgLanguages', this.store.find('org-language') );
},
Finally, I'd like to call some basic jQuery on this Handlebars template if isFirstOrgLanguage is true, so I set up my view as follows.
View:
export default Ember.View.extend({
didInsertElement: function() {
this.get('controller').get('isFirstOrgLanguage').then( function( isFirstOrgLanguage ) {
console.log('isFirstOrgLanguage', isFirstOrgLanguage);
});
}
});
This seems like a crazy amount of promises and async management just to answer the question "is there exactly 1 orgLanguage defined"? Although the above works, is there a simpler way, or perhaps "The Ember Way" to do this?
Update:
In doing some additional research, it seems this has been a topic for some debate. Here are relevant discussions I've seen on this. If I settle on a pattern I like, I'll post it as as an answer, but would welcome other suggestions.
http://discuss.emberjs.com/t/dashboard-type-views/5187/24
http://discuss.emberjs.com/t/the-right-way-to-load-additional-models-to-build-filtering-checkboxes/4966/4
I wanted to post how I eventually solved this.
First, it became clear that there are recommended solutions to this pattern, but no "one true way". See http://discuss.emberjs.com/t/the-right-way-to-load-additional-models-to-build-filtering-checkboxes/4966/4.
What I wound up using was this:
Route:
...
afterModel: function() {
var _this = this;
Ember.RSVP.hash({
languages: this.store.find('language'),
orgLanguages: this.store.find('org-language')
}).then( function( hash ) {
_this.set('controller.languages', hash.languages );
_this.set('controller.orgLanguages', hash.orgLanguages );
});
},
...
The key insights here are:
This is done after the page's model loads. This may or may not make sense depending on your context.
Some people like to wrap each model in its own controller, but I didn't have clean mappings to controllers like that, so I directly set these property values.
It's generally bad practice to set computed properties that are promises, so if you have to deal with promises (which with any use of this.store.find() you do, then it's best to resolve the promise in the route and then pass the "concrete" property to your controller. But keep in mind that your template will be rendering these values when they eventually resolve! So, again there is some room for debate.
I think the general takeaway is that Ember is giving you lots of options to get this done, with plenty of possibilities to use depending on your needs.
I can't figure out the correct way to handle modal states/views with the new Ember router. More generally, how do you handle states that you can enter and exit without affecting the "main" state (the URL)?
For example, a "New Message" button that is always available regardless of the current leaf state. Clicking "New Message" should open the new message modal over the current view, without affecting the URL.
Currently, I'm using an approach like this:
Routes:
App.Router.map(function() {
this.route('inbox');
this.route('archive');
});
App.IndexRoute = Em.Route.extend({
...
events: {
newMessage: function() {
this.render('new_message', { into: 'application', outlet: 'modalView' });
},
// Clicking 'Save' or 'Cancel' in the new message modal triggers this event to remove the view:
hideModal: function() {
// BAD - using private API
this.router._lookupActiveView('application').disconnectOutlet('modalView');
}
}
});
App.InboxRoute = Em.Route.extend({
...
renderTemplate: function(controller, model) {
// BAD - need to specify the application template, instead of using default implementation
this.render('inbox', { into: 'application' });
}
});
App.ArchiveRoute = ... // basically the same as InboxRoute
application.handlebars:
<button {{action newMessage}}>New Message</button>
{{outlet}}
{{outlet modalView}}
I've obviously left out some code for brevity.
This approach 'works' but has the two problems identified above:
I'm using a private API to remove the modal view in the hideModal event handler.
I need to specify the application template in all of my subroutes, because if I don't, the default implementation of renderTemplate will attempt to render into the modal's template instead of into application if you open the modal, close it, and then navigate between the inbox and archive states (because the modal's template has become the lastRenderedTemplate for the IndexRoute).
Obviously, neither of these problems are dealbreakers but it would be nice to know if there is a better approach that I'm missing or if this is just a gap in the current router API.
We do kind of the same thing but without accessing the private API.
I don't know if our solution is a best practice, but it works.
In the events of our RootRoute I have an event (same as your newMessage), where we create the view we need to render, and then append it.
events: {
showNewSomething: function(){
var newSomethingView = app.NewSomethingView.create({
controller: this.controllerFor('newSomething')
});
newSomethingView.append();
}
}
This appends the modal view into our app.
On cancel or save in the newSomethingView we call this.remove() to destroy the view and removing it from the app again.
Again, this doesn't feel like a best practice, but it works. Feel free to comment on this if someone have a better solution.
Don't know if you are using the Bootstrap Modal script or which one, but if you are, this question has a proposed solution. Haven't figured out all the pieces myself yet, but is looking for a similar type of solution myself to be able to use Colorbox in an "Ember best practices"-compliant way.
Sample code for my question is here.
It's a simple Ember app that displays the SearchView containing a TextField by default.
When the user enters some text and hits Enter, I want to transition to another state (displayUserProfile) passing the value entered in the textbox.
At first, in the Textbox's insertNewline callback, I called the transitionTo method of the application's router, passing the value as part of the parameter object:
App.SearchTextFieldView = Em.TextField.extend({
insertNewline: function() {
App.router.transitionTo('displayUserProfile', {
username: this.get('value')
});
}
});
That works fine, but then I noticed that pangratz's answer on a question about infinite scrolling, uses a different approach. Instead he invokes a method on the view's controller, which in turn calls a method on the controller's target (which is the router).
This changes my code to:
App.SearchTextFieldView = Em.TextField.extend({
insertNewline: function() {
Em.tryInvoke(this.get('controller'), 'displayUserProfile', this.get('value').w());
}
});
App.SearchController = Em.Object.extend({
displayUserProfile: function(username) {
this.get('target').transitionTo('displayUserProfile', {
username: username
});
}
});
My question is: which approach is better?
Calling transitionTo directly from the view or delegating it to the view's controller?
I would recommend a different approach. insertNewLine should trigger an action that is handled by the router, which will then transition its state.
App.SearchTextFieldView = Em.TextField.extend({
insertNewline: function() {
this.get('controller.target').send('showUser', {username: this.get('value')});
}
});
App.Router = Ember.Router.extend({
...
foo: Ember.Router.extend({
showUser: function(router, evt) {
router.transitionTo('displayUserProfile', evt);
});
}
});
You should put the showUser handler at the top-most route where it is valid in your app.
This approach follows the general pattern of events in Ember apps that views handle DOM-level events and where appropriate, turn them into semantic actions that are handled by the router.
Personally I think the second approach is better.
The first thing is that it's a bad idea to access the router statically. Then for me, you have to keep the views logic-less, so delegating to controller seems a good choice.
In your case this is only a call to the router, but you can imagine processing some algorithms on the textfield value. If you do this proccessing in you view, this will lead to a view, mixing UI code, and logic code. View should handle only UI code.