Ember 'needs' dependencies between unrelated controllers - ember.js

I have 2 models in Ember Data: a Tag has and belongs to many Files, both models being loaded via JSON from a Rails backend. My URLs are not deep nested because of the many-to-many nature of the relationship. Here is the code to set that up in Ember:
App.Router.map ->
#resource 'tags', ->
#resource 'tag',
path: 'tags/:tag_id'
, ->
#resource 'file',
path: 'files/:file_id'
, ->
App.Tag = DS.Model.extend
name: DS.attr('string')
files: DS.hasMany('file',
async: true
)
App.File = DS.Model.extend
name: DS.attr('string')
tags: DS.hasMany('tag',
async: true
)
In the tags page I display a list of all tags - no issues here. In the tags/:tag_id page I display the list of files having that tag - also no issues. In the files/:file_id page I want to display some information about the file, with the ability to do things such as edit the tags belonging to that file. Doing so requires me to have the list of all tags, both belonging and not belonging to that file, loaded in Ember, but since the routes are not in the same hierarchy chain, I can't for example do
App.FileController = Ember.ObjectController.extend
needs: 'tags'
tags: Ember.computed.alias('controllers.tags.content') # undefined
to obtain the data I need, although if this were possible, it would be exactly what I need. It also doesn't make sense to pass in all the JSON for all the tags (especially the ones not belonging to the file) in that route.
What are my options at this point? I'm guessing I have to restructure the code to make the tags information accessible in the ApplicationController and calling needs: 'application'. I haven't quite figured out how to do load the correct JSON in the ApplicationController. Is there an alternate/better way that I am missing?

I would set the content of the tags controller in your FileRoute.
App.FileRoute = Ember.Route.extend(
init: ()->
#_super()
#generateController('tags')
setupController : (controller, model) ->
#_super(controller, model)
#controllerFor('tags').set('model', #get('store').find('tag'))
)
That way, you can keep all your other code but I think you should use Ember.computed.alias('controllers.tags.model') instead of Ember.computed.alias('controllers.tags.content').

Related

Creating a new resource in ember

I'm building an office reception app in Ember. When a person arrives at the office, they pop open the app and are taken through a three step wizard:
Choose a reason for visiting
Choose the person you've come to see
Confirm
The app also allows an administrator to view all of the visits and to view an individual visit.
I have Visit and Person models I've hooked up to a server using Ember Data. Here's what my routes look like:
App.Router.map () ->
#resource 'visits', ->
#resource 'visit', { path: '/:visit_id' }
#resource 'new', ->
#route 'welcome'
#route 'directory'
#route 'thanks'
This allows me to create a new Visit model in the VisitsNewRoute and use it in the welcome, directory and thanks views.
This works. However, it feels wrong to have a new resource, especially since it's conceivable I'll want at least one more new route in my application.
Is there a better way to do this?
I think that you can change the new resource to newVisit like this:
App.Router.map () ->
#resource 'visits', ->
#resource 'visit', { path: '/:visit_id' }
#resource 'newVisit', ->
#route 'welcome'
#route 'directory'
#route 'thanks'
Now you will have a NewVisitRoute where you can create a new Visit model to use in each of the child routes.
And you will be able to make a transition to this routes with the route names: newVisit.welcome, newVisit.directory and newVisit.thanks. You can use this route names in a link-to helper link this:
{{link-to "Welcome", "newVisit.welcome"}}
The recommended practice is to use a create/new route under the resource type, so new under visits, then `transitionTo('visit.welcome', newRecord). (I'm saying all of this with the assumption that welcome, directory, and thanks aren't part of the new record creation).
App.Router.map () ->
#resource 'visits', ->
#route 'new'
#resource 'visit', { path: '/:visit_id' }
#route 'welcome'
#route 'directory'
#route 'thanks'
Ember doesn't always name routes the way you want when dealing with routes nested more than one level. I would name your 'new' route as follows:
#resource 'visits.new', path: 'new', ->
There are a number of approaches you can use to structuring your routes depending on how you assign model ids and whether or not you are using localStorage to preserve user edits until they are persisted to the server.
I have my route pattern as follows:
App.Router.map () ->
#resource 'visits', ->
#route 'new'
#route 'crud', path: ':visit_id'
My 'new' routes create a new resource in the routes model callback which in my models auto-generates a v4 UUID. The new route then performs a transitionTo the crud route in the afterModel callback. In effect the 'visits.new' route acts as a trampoline and allows you to easily use {{link-to 'visits.new'}} from templates/menus etc.
The above approach allows you to to have a single crud route and crud controller that can handle all the show/create/update/delete actions for the model. The models isNew property can be used within your templates to handle any differences between create and update.
I also use localStorage so that newly created (but not yet persisted) models survive a browser refresh, the UUIDs really come in handy for both this and for persisting complex model graphs.
The above router pattern occurs quite a lot in my app so I have defined some base Route classes and a route class builder but the general pattern is as follows:
If using UUIDs:
App.VisitsNewRoute = Em.Route.extend
model: (params, transition)->
App.Visit.create(params)
afterModel: (model,transition) ->
#transitionTo 'visits.crud', model
App.VisitsCrudRoute = Em.Route.extend
model: (params,transition) ->
App.Visit.find(params['visit_id'])
If not using UUID's then the routes are different. I did something like this before I moved to UUIDs, it treats model id 'new' as a special case:
App.Router.map () ->
#resource 'visits', ->
#route 'crud', path: ':visit_id'
App.VisitsCrudRoute = App.Route.extend
model: (params, transition) ->
visit_id = params['visit_id']
if visit_id == 'new' then App.Visit.create() else App.Visit.find(visit_id)
serialize: (model, params) ->
return if params.length < 1 or !model
segment = {}
segment[params[0]] = if model.isNew() then 'new' else model.get('id')
segment
For your specific case of managing the wizard step state I would consider using Ember Query Params, which allow you specify the current step in a parameter at the controller level
Query params example:
App.VisitsCrudController = Em.ObjectController.extend
queryParams: ['step'],
step: 'welcome'
Link to next step in the view:
{{#link-to 'visits.crud' (query-params step="directory")}}Next{{/link-to}}
You may also want to define some computed properties for the next and previous steps, and some boolean properties such as isWelcome, isDirectory for your view logic.

Using transitionToRoute with a model with a custom slug

I've got an ember-data model that has an id as well as a server-generated custom slug attribute (which is prefixed with the id if that matters).
I'd like to use the slug instead of the id in all public routres. For that purpose, I have changed the router:
App.Router.map ->
#resource 'strategies', path: '/strategies', ->
#resource 'strategy', path: ':strategy_slug'
and overwrote the serialize method of the respective route:
App.StrategyRoute = Ember.Route.extend()
serialize: (model) ->
{
strategy_slug: model.get('slug')
}
Unfortunately, this does not seem to work when using transitionToRoute from a controller to transition to, e.g., /strategies/123-test:
App.ExampleController = Ember.ObjectController.extend()
[...]
actions:
showDetails: ->
#transitionToRoute("/strategies/#{#get('slug')}")
false
(where #get('slug') returns '123-test')
All output I get in the console after invoking the showDetails action is:
Transitioned into 'strategies.index'
Ember does seem to recognize the slug-based route.
Ember: 1.5.0-beta.2+pre.3ce8f9ac
Ember Data: 1.0.0-beta.6
Is there anything I may have missed?
Edit:
The following variant works and is feasible in this case, but I have another use-case where I have only access to the URL.
App.ExampleController = Ember.ObjectController.extend()
[...]
actions:
showDetails: ->
#transitionToRoute('strategy', #get('content'))
false
I eventually figured out how to solve this. The key is to add a model method to the router, which knows how to turn a slug into a model:
App.StrategyRoute = Ember.Route.extend()
model: (params, transition) ->
App.store.getById('strategy', parseInt(params.strategy_slug))
serialize: (model) ->
{
strategy_slug: model.get('slug')
}

Dynamic segments not getting passed to route params

I'm using Ember 1.0 and Ember-Data 1.0Beta. I'm trying to pass a dynamic segment to a route like so:
#resource 'organization', path: 'organizations/:organization_id', ->
#route 'edit'
Then in my edit route:
Whistlr.OrganizationEditRoute = Ember.Route.extend
model: (params) ->
#store.find('organization', params.organization_id)
Unfortunately, the params hash turns up empty. When I inspect it in the console, it's just a simple {}. In turn, params.organization_id is null. This happens even when the URL looks correct: "/organizations/1/edit`
This closely resembles the setup in the Ember guides. What could I be getting wrong?
The reason for this is the dynamic segment (:organization_id) is part of the organization resource and not the edit resource. This means that only OrganizationRoute will have access to the params.organization_id.
However, if you need the model in your OrganizationEditRoute you can use modelFor to access it.
Whistlr.OrganizationEditRoute = Ember.Route.extend
model: (params) ->
#modelFor('organization')
This lets the OrganizationRoute load the model from it's dynamic segment and then the OrganizationEditRoute can just simply access that model like so.

Getting context in ember templates in singular resource index outlet

I just want to be able to display a campaign model's properties in the templates rendered in the my resource template's outlet.
My router looks like this:
App.Router.map (match)->
#resource 'campaigns', ->
#resource 'campaign',
path: ':campaign_id', ->
#route 'chat',
path: '/chat'
I set up 3 templates for my campaign resource:
campaign
campaign.index
campaign.chat
I am not sure how I am supposed to get acces to the model identified by the dynamic segment in my url. I was able to set it up successfully in my campaign route like so:
App.CampaignRoute = Ember.Route.extend
model: (params) -> App.Campaign.find(params.campaign_id)
This doesn't seem to work for the index and chat routes.
I was thinking something like {{someProperty}} or {{campaign.someProperty}} would work by default here.
Why does the context change and how do I get it?
My routes and templates are rendering fine, minus the context I want.
Setting the model this way in my campaign.index and campaign.chat routes made all the difference:
App.CampaignIndexRoute = Ember.Route.extend
model: ->
#modelFor('campaign')
App.CampaignChatRoute = Ember.Route.extend
model: ->
#modelFor('campaign')

ember.js controller properties

i'm trying to set a property of a controller
Trying to do so
{{view Ember.Select contentBinding="App.tastingsController.names"}}
it does not work
App.tastingsController = Ember.ArrayController.extend
names: ["Velato", "Abbastanza limpido", "Limpido", "Cristallino", "Brillante"]
while this version is working correctly (but gives this warning:WARNING: The immediate parent route did not render into the main outlet and the default 'into' option may not be expected )
App.tastingsController.names = ["Velato", "Abbastanza limpido", "Limpido", "Cristallino", "Brillante"]
here's my routes:
App.Router.map ->
#route "home", { path: "/" }
#route "about"
#resource "tastings", ->
#route "new"
#resource "tasting", { path: ":tasting_id"}
Can you explain me why?
( found it here)
thank you
Marco
There are a few issues with your code:
App.tastingsController should be named App.TastingsController. Controller Classes should begin with a capital letter.
You are getting a warning because you skipped a template within the route hierarchy. I need more information on the routes to help fix this.
If you need to set a property on the controller (such as names in your case), there are two ways to do it:
Either set it in the route:
App.TastingsRoute = Ember.Route.extend({
setupController: function(controller, model) {
controller.set('names', names: ['list', 'of', 'names']);
}
});
Or you can set it directly when defining the controller class:
App.TastingsController = Ember.ArrayController.extend({
content: [],
names: ['list', 'of', 'names']
});
When you need to reference a controller from the view/template, don't name the entire controller class. Just use the property you want to bind to (assuming your view's controller is App.TastingsController)
{{view Ember.Select contentBinding="names"}}
Hope this helps.