EmberJs : How to pass data to a component without using a controller - ember.js

The documentation for emberjs clearly states that you should not use controllers, however sometimes you need to pass data into a component that is not the model for the corresponding route. For instance in an application I am working on I want to retrieve a list of records from the store and display them in a component so the user can select them as an attribute of the model for that route.
The advice I have received on this is to either create a controller and use it to retrieve the list in question or to add the list of records as an attribute of the model for that route, but since the former is inadvisable and the latter only makes sense if the item in question is a logical part of the model's schema (and therefore should probably be in there anyway) I am left feeling confused about how this apparently simple thing ought to be done. Can anyone help?

You can use Ember.RSVP.hash in your routes model hook. When the promise resolves, the results get passed as the second param in setupController.
// This would be in a route file like app/blogs/edit/route.js
model: function() {
return Ember.RSVP.hash({
blog: this.store.findRecord('blog', 1),
categories: this.store.findAll('category'),
});
},
setupController: function(controller, models) {
this._super(controller, models);
controller.set('model', models.blog);
controller.set('categories', models.categories);
},
OR
If you wanted all the data logic to exist in the component you can inject the data store service. This goes against the DDAU mantra (data down, actions up) but IMO it's a clean, modular solution. Useful if the extra content isn't visible immediately ie: components that open modal windows.
// This would live within the actual component
store: Ember.inject.service(),
loadCategories: function() {
this.get('store').findAll('category').then((categories) => {
this.set('categories', categories);
});
}.on('init'),
However, I would advise against this if the data (categories in this example) were immediately visible in the layout. Ember won't wait for these requests to complete before rendering so you would see blank spaces/whatever with the actual values loading in a half second later.

just be aware that components don't know anything about outside them self. The way I would solve the problem is by creating a bridge between controller and component by passing the property that you want to access to your component.
<script type="text/x-handlebars" data-template-name="sample-com">
{{sample-com
sampleRequests=sampleRequests
}}
</script>
App.MainController = Ember.Controller.extend({
//bridged properties that the controller must communicate between components/view
sampleRequests: 'hello world'
});
App.SampleComComponent = Ember.Component.extend({
sampleRequests: null
});
if there is a better way please feel free to suggest.

Related

Loading several data sources into an Ember UI

I'm trying to understand how to compose a page with multiple data sources and loading spinners in Ember.
Let's you have a UI like this:
http://postimg.org/image/6i1ko340f/
(sorry, don't have reputation to embed image)
And let's say each screen 'module' [books, shows, movies, tweets] has a separate data source at a remote API url.
somesite.com/api/books.json
somesite.com/api/shows.json
somesite.com/api/books.json
twitter.com/api/tweets.json
In order to simplify, and get to the essence of the question, I ask that you don't use:
1) Components
2) A data library (e.g. Ember Model or Ember Data) unless that's essential to the answer.
How would you approach architecting this in Ember so that each module has its own spinner and they load at their own pace (separate route hooks?).
Please help me understand how you would compose your routes, controllers, and models, or whatever else to elegantly solve this extremely common problem. I'll attach some of my approaches and thoughts in the comments :)
Well, I don't know if this is the best way by any means, but this is how I approach that problem. Dummy example using timeouts instead of async calls.
Write some sort of async wrapper. I use ic-ajax wrapper around jQuery ajax to make ajax calls that return promises in a class I call my rest-client. I use initializers to inject this object into all of my routes as restClient as a convenience. You can always just import when you need it.
I would normally block for my model retrieving via Ember.RSVP.hash() calls, but lets assume I did not as you've asked (this is our index route):
import bookModel from 'app/models/book-model';
setupController: function(controller, model){
var self = this;
//do this for each "module" call
this.restClient.get('books/url').then(function(response){
var booksController = this.controllerFor('booksTemplate');
//data is json for the properties of your Book model
var books = model.create(response.data);
booksController.set('isLoading', false);
booksController.set('model', books);
});
}
Make an asynchronous call, and on its success, set the model for the template's controller. Your template books.hbs:
{{#if isLoading}}
<img src="spinner.gif">
{{else}}
{{#each}}
{{this.title}}
{{/each}}
{{/if}}
This template shows a spinner or while loading happens. Notice in the setupController call that we set isLoading to false on complete of the ajax call. This causes the else block to display.
And your books.js controller:
export default Ember.ArrayController.extend({
isLoading: true;
});
And finally, your index.hbs:
{{render 'books'}}
{{render 'otherModel'}}
{{render 'anotherModel'}}
Caveat, if you render the same template twice with a render partial and do not specify the model, the two templates share a singleton instance of the controller meaning isLoading is shared across instances. Holler if you have any questions

How can a component respond to an action?

tl;dr: Data can be sent in and out of a component, but I only know how to send actions out. Is there a way to send actions in?
In my Ember application, I have something like the following UI from Google Maps:
The background map corresponds to a PinsRoute/PinsView/PinsController, and it shows many pins. When you click one, you enter the PinRoute, which renders the overlay to {{outlet}}. Both the big map and the thumbnail (in the Google Maps image, the picture that says "Street View") are components: FullscreenMapComponent and ThumbnailMapComponent, respectively.
In Google maps, when you click "Street view", it pans and zooms the main map to the selected point. This is essentially what I'm trying to figure out how to wire up.
When the user clicks "streeth view" on my ThumbnailMapComponent, I can send out an action, which the PinsRoute can handle. The question is, how can I then reach down to my FullscreenMapComponent and invoke the appropriate method (.panToSelected(), in this case)?
Here's a good solution. Have the component register itself to whatever controller it's being rendered in, and that way you can access the component in action handlers.
In my case, I added a method to my component
App.FullscreenMapComponent = Ember.Component.extend({
...
_register: function() {
this.set('register-as', this);
}.on('init')
});
and a property to my controller:
App.SensorsController = Ember.ArrayController.extend({
fullscreenMap: null,
...
});
In my template, I bind the two with my new register-as property:
// sensors.hbs
{{fullscreen-map data=mapData
selectedItem=currentSensor
action='selectSensor'
deselect='deselectSensor'
register-as=fullscreenMap }}
And now, let's say I click the thumbnail above and it sends an action that bubbles up to my ApplicationRoute, I can now do this:
// ApplicationRoute.js
actions: {
panTo: function(latLong, zoom) {
this.controllerFor('sensors').get('fullscreenMap').panTo(latLong, zoom);
}
}
Presto! Components responding to actions.
Update (10/6/2016)
Whew, I've learned a lot in two years.
I would no longer recommend this solution. In general, you should strive for your components to be declarative, and have their output and behavior depend solely on their state. Instead, my original answer here solved the problem using an imperative method, which is brittle, harder to understand, and not very easy to extend (what if we wanted to tie the the map's position to the URL?).
If I was refactoring this specific component, I'd probably make {{fullscreen-map}} accept latLong and zoom params. If someone from the outside sets those, the map would respond and update itself. You can use didReceiveAttrs from within the component to respond to param changes, and then from there call the imperative panTo method from Google's API.
The takeaway is that, even if you have to interact with imperative methods (maybe because of a third-party lib), strive to make your own components' APIs as declarative as possible. What first seems like "sending an action down" can usually be expressed as some function of state, and that state is what you want to identify and make explicit.
This is working example but I am not 100% sure that this approach is best
Here what you can do:
When calling action pass your component as parameter:
App.PinController = Ember.Controller.extend({
actions: {
actionThatComponentCalls: function(){
// different component will be called
new App.MyOtherComponent().send('differentAction'):
}
}
});
App.FullScreenMapComponent = Ember.Component.extend({
click: function(){
this.sendAction();
}
});
App.MyOtherComponent = Ember.Component.extend({
actions: {
differentAction: function(){
console.log('different action called');
}
}
});
Hope this helps

Make a programmatically Created Controller Delegate (Bubble) Events

I have an app with many similar views which I instantiate programmatically to "DRY-up" my app.
The problem is that controllers instantiated programmatically do not delegate actions in the actions hash further. This is clear because there is nothing from which the controller can derive the hierarchy. There should be a way, however, to tell a controller which parent controller it has to use for event bubbling. Does anyone know it?
You shouldn't be initializing controller's on your own. All controller initialization should be handled by Ember itself. Another interesting note, controller's are intended to be singletons in the application. The only exception to this being the itemController when looping over an ArrayController. You can read more about it in the guides. Quote from the guides:
In Ember.js applications, you will always specify your controllers as
classes, and the framework is responsible for instantiating them and
providing them to your templates.
This makes it super-simple to test your controllers, and ensures that
your entire application shares a single instance of each controller.
Update 1:
An example of how to do routing for a wizard:
App.Router.map(function() {
this.resource('wizard', function() {
this.route('step1');
this.route('step2');
this.route('step3');
});
});
This way, you can have a separate controller/view/template per step of the wizard. If you have logic around how much of each step should be completed prior to transitioning to the next one, you can handle that in the individual routes.
Update 2:
In the event that the number of steps aren't predetermined, but are based on the data being fed to the app, you can make a WizardController that is an ArrayController where each item in the array is a step in the wizard. Then, use the lookupItemController hook on the ArrayController, kind of like this:
App.WizardRoute = Ember.Route.extend({
model: function() {
return [
{controllerName: 'step1', templateName: 'step1'},
{controllerName: 'step2', templateName: 'step2'},
{controllerName: 'step3', templateName: 'step3'}
];
}
});
App.WizardController = Ember.ArrayController.extend({
lookupItemController: function(modelObject) {
return modelObject.controllerName;
}
});
{{#each step in controller}}
{{view Ember.View templateName=step.templateName}}
{{/each}}
As another, probably better, alternative, you can override the renderTemplate hook in the route where you're pulling down the model for the next step in the wizard and pass in the appropriate templateName and controller in the render call, kind of like you see here.
Point being, I think it should be possible to do this without having to instantiate controllers yourself.

ember new router

I have a question about Ember routing and controllers. I've just written a small App to get familiar with the new router. Therefore I've built a button that transitions to another state by clicking on it.
App.PostsView = Em.View.extend({
click: function() {
var router;
this.get('controller').transitionTo('about');
}
});
My question now is: what does the get method return?. Obviously an instance of the PostController but on the one hand the controller doesn't have a transitionTo() method and on the other hand that would not make any sense.
this.get('foo') returns a property of your Ember object. Since Views can have a "controller" property, this.get('controller') returns the controller bound to your view's controller property (by default the postsController).
this.get('controller').transitionTo() works because as sly7_7 mentioned, transitionTo() is also defined on the controller and delegates to the router. Note, it's probably going to be deprecated and one should use
this.get('controller').transitionToRoute('about');
instead.
You should not do this at the view level, this is what the router is designed for so instead of capturing a click event in the view, rather implement an action on the button and let the router handle it. I suggest you learn the best practices from the get-go, chances are your application will evolve, requiring more elaborate concepts such as handling transactions commits/rollbacks, creating/updating records. So here's my suggestion to you
In your view
<button type="button" {{action onSaveClick}} />Save</button>
In your router
App.FooRoute = App.Route.extend({
events: {
onSaveClick: function() {
this.transitionTo('bar');
}
}
})
If for any other reasons, say styling or animation, you find yourself forced to capture the click event in the view, then i suggest to, capture the event, perform your styling and finally send an event to the controller. The event can then be handled the same way in the router
App.FooView = Ember.View.extend({
click: function() {
// Do some styling
this.get('controller').send('onSaveClick')
}
})
Finally speaking of best practices, try to learn when working with ember to think of your application as a series of states interacting with each other. You'll find yourself bound to implement the concept right. Have a look at this
the controller has a transitionTo: https://github.com/emberjs/ember.js/blob/master/packages/ember-routing/lib/ext/controller.js#L36, it basically delegate to it's target (which is the router)

Ember.js 1.0-pre4 + jQ UI sortable + localstorage adapter

Day 2 learning ember.js...
I'm working on a offline app that needs to save draggable/sortable tile positions to localstorage, and if there is no existing data, load & save from a fixture.
Using: ember 1.0.0-pre4, ember-data rev11, ember-localstorage-adapter, jQ 1.9, jQ UI 1.9
https://github.com/rpflorence/ember-localstorage-adapter
It's working, but I'm a bit of a novice, feel it's not pretty and could use some community advice.
http://jsfiddle.net/Nsbcu/4/
Questions
What is the proper way to check if your DS.Store has loaded and is empty? My method of looking directly at localstorage didn't feel right.
After I createRecords from the App.Tile.DEFAULTS I feel I should commit them, but an error is thrown. I don't have to commit the known defaults, but curious what causes the error and how I should go about committing properly. Also is the App.ready() callback the right place for loading defaults? Error only happens when localstorage is empty
Uncaught Error: Attempted to handle event loadedData on <App.Tile:ember231:1> while in state rootState.loaded.created.inFlight. Called with undefined
On the TilesController I'm using sortProperties which works great until jQ UI Sortable changes the DOM and Ember wants to update my tile order, before I get a chance to set the new order. My current solution is to turn off sortProperties temporarily while updating the model. Again this feels hacky, suggestions on proper way to do this?
=== Edit Feb 3 ===
If I do an async commit the initial error in question #2 is avoided.
App.TilesRoute = Ember.Route.extend({
model: function() {
return App.Tile.find();
},
setupController: function(controller) {
if (localStorage.getItem('fusion-emberjs') == null) {
App.Tile.DEFAULTS.forEach(function(item) {
App.Tile.createRecord(item);
});
// Commit async, else generates error
var _this = this;
setTimeout(function() {
_this.store.commit();
}, 1);
}
}
});
I would put any initial code inside the application or the index Route within the setupController method
if (localStorage.getItem('fusion-emberjs') == null) {
App.Tile.DEFAULTS.forEach(function(item) {
App.Tile.createRecord(item);
});
//*** WARNING: Generates Error ***/
App.Tile.find().get('store').commit();
}
Once you move the code inside the route, replace App.Tile.find().get('store').commit(); by App.store.commit() inside your route
Create your own transaction instead of using the default one, each time you make a call to the store directly you're using the default transaction. You can create a transaction this way
var transaction = App.store.transaction()
transaction.createRecord(App.Foo);
transaction.commit()
transaction.rollback();
Any call to App.store assumes you already created a store, right now you're only extending the DS.Store. Try instead
App.Store = DS.Store.create({
revision: 11,
adapter: 'App.LSAdapter'
});
I would suggest that you do any event handling or transaction management in the router unless it's purely for styling or animation. In that case, the view is the right place for it. I like the router to orchestrate communication between all the assets (controllers, routes, models, views)
A good pattern to remember is a view talks only to a controller, a controller is a mere proxy to a model, a router orchestrates communication between controllers and manages routes.