Ember.js - CRUD scenarios - Specifying View from within a Route
Asked Answered
K

1

7

I've asked a question previously in which I wanted to bind a collection residing in the controller to the list scenario view, however, I've added details and edit templates and views to my structure producing a couple of extra sub-routes:

root.contacts.details -> /contacts/:contact_id
root.contacts.edit -> /contacts/:contact_id/edit

In my details scenarios I first started calling the connectOutlets as follows

[...]
connectOutlets: function (router, contact) {
    router.get('contactController').set('contact', contact);
    router.get('applicationController').connectOutlet('contacts');
},[...]

This would change the route in the browser navigation bar, but it would load the same view, then I changed the .connectOutlet to contact instead of contacts to the following

[...]
connectOutlets: function (router, contact) {
    router.get('contactController').set('contact', contact);
    router.get('applicationController').connectOutlet('contact');
},[...]

Because of this, I had to create a new controller as Ember couldn't find a controller named contactController, so I ended up with a contactController and a contactsController and I think I'm breaking the MVC pattern doing this, as well as creating an extra class to maintain, possible problems with syncronization (when editing a contact I'd have to manually sync with the collection in the contactsController). Also when I navigate to /#/contacts/2/edit it loads the details view since I'm using the same name in .connectOutlet('contact'). So what I'm doing can't be right. I don't want to create controller per scenario. I'm sure this is not how it's done.

I also tried setting the view (in my case App.EditContactView) instead of the resource name in the connectOutlets but I got an error saying I can pass "a name or a viewClass but not both" but I was not passing through viewClass and rather as an argument of connectOutlet.

I have also tried to set a view or an instance of my view to the route itself and I would either break my JavaScript or in some cases I'd get an error saying that "App.EditContactView does not have a method CharAt".

Then again, I got a little lost. I have seen other questions at SO and else where but the ones I've found were either using ember-routermanager by Gordon Hempton (which seems good, but I'm interested in using built-in only right now), Ember.StateManager or not using state/route at all. Documentation isn't explaining too much about these things yet.

Question: What would be the ideal approach to deal with all CRUD scenarios with Ember.Router? I want my contactsController to be able to list all, find one, edit one, add one and delete one contact. Right now I have one contactsController with findAll and one contactController with find, edit, remove, add because of naming problems.

I am currently not using ember-data so I would be more interested in examples without references to ember-data (I am doing the baby steps without any plug-in for now).

Here's the current version of my router:

JS

App.Router = Ember.Router.extend({
    enableLogging: true,
    location: 'hash',

    root: Ember.Route.extend({
        // EVENTS
        gotoHome: Ember.Route.transitionTo('home'),
        gotoContacts: Ember.Route.transitionTo('contacts.index'),

        // STATES
        home: Ember.Route.extend({
            route: '/',
            connectOutlets: function (router, context) {
                router.get('applicationController').connectOutlet('home');
            }
        }),
        contacts: Ember.Route.extend({
            route: '/contacts',
            index: Ember.Route.extend({
                route: '/',
                contactDetails: function (router, context) {
                    var contact = context.context;
                    router.transitionTo('details', contact);
                },
                contactEdit: function (router, context) {
                    var contact = context.context;
                    router.transitionTo('edit', contact);
                },
                connectOutlets: function (router, context) {
                    router.get('contactsController').findAll();
                    router.get('applicationController').connectOutlet('contacts', router.get('contactsController').content);
                }
            }),
            details: Ember.Route.extend({
                route: '/:contact_id',
                view: App.ContactView,
                connectOutlets: function (router, contact) {
                    router.get('contactController').set('contact', contact);
                    router.get('applicationController').connectOutlet('contact');
                },
                serialize: function (router, contact) {
                    return { "contact_id": contact.get('id') }
                },
                deserialize: function (router, params) {
                    return router.get('contactController').find(params["contact_id"]);
                }
            }),
            edit: Ember.Route.extend({
                route: '/:contact_id/edit',
                viewClass: App.EditContactView,
                connectOutlets: function (router, contact) {
                    router.get('contactController').set('contact', contact);
                    router.get('applicationController').connectOutlet('contact');
                },
                serialize: function (router, contact) {
                    return { "contact_id": contact.get('id') }
                },
                deserialize: function (router, params) {
                    return router.get('contactController').find(params["contact_id"]);
                }
            })
        })
    })
});
App.initialize();

Relevant templates

<script type="text/x-handlebars" data-template-name="contact-details">
    {{#if controller.isLoaded}} 
        <img {{bindAttr src="contact.imageUrl" alt="contact.fullName" title="contact.fullName"}} width="210" height="240" /><br />
        <strong>{{contact.fullName}}</strong><br />
        <strong>{{contact.alias}}</strong>
    {{else}}
        <img src="images/l.gif" alt="" /> Loading...
    {{/if}}
</script>

<script type="text/x-handlebars" data-template-name="contact-edit">
    <strong>Edit contact</strong><br />
    First Name: <input type="text" id="txtFirstName" {{bindAttr value="contact.firstName"}}<br />
    Lasst Name: <input type="text" id="txtLastName" {{bindAttr value="contact.lastName"}}<br />
    Email: <input type="text" id="txtEmail" {{bindAttr value="contact.email"}}<br />
</script>

<script type="text/x-handlebars" data-template-name="contact-table-row">
    <tr>
        <td>
            <img {{bindAttr src="contact.imageUrl" alt="contact.fullName" title="contact.fullName"}} width="50" height="50" /><br />{{contact.fullName}}
        </td>
        <td>
            Twitter: {{#if contact.twitter}}<a {{bindAttr href="contact.twitter"}} target='_blank'>Follow on Twitter</a>{{else}}-{{/if}}<br />
        </td>
        <td>
            <a href="#" {{action contactDetails context="contact"}}>Details</a> | 
            <a href="#" {{action contactEdit context="contact"}}>Edit</a> 
        </td>
    </tr>
</script>

Note: If there's anything unclear, please ask in the comment section and I can edit this with more details

Edit: I've added this project to GitHub even tho it's nowhere near what I'd like to expose as a learning sample. The goal is to progress on top of this and create a CRUD template in a near future. Currently using MS Web API, but might add a Rails version soon.

Kindless answered 7/7, 2012 at 18:15 Comment(2)
I'm getting a 404 for the link to the Github page. Has this changed recently?Parrnell
Yeah.. I've changed the project name. Here's the new linkKindless
S
9

There's a few things going on here, I'll try and answer them, but if I miss anything feel free to leave a comment. You seem to be reinventing a lot of stuff Ember already does for you.

Firstly, if you want to pass a view to the connectOutlet method you need to pass in a hash as the one and only argument.

router.get('applicationController').connectOutlet({
  viewClass: App.EditContactView,
  controller: router.get('contactsController'),
  context: context
})

Secondly, having two contact controllers is not frowned upon, in fact I'd recommend it. A singular ContactController that inherits from ObjectController and a ContactsController that inherits from ArrayController, this means you can easily take advantage of the content proxies built in.

Thirdly, if you add find and findAll class methods to your models you will make life much easier for yourself.

  • You won't need to define the serialize/deserialize methods you have defined, by default Ember will look for a model with the name deduced from the route so :contact_id will automatically look for App.Contact.find(:contact_id).

  • You will also be able to change your index connectOutlets to: router.get('applicationController').connectOutlet('contacts', App.Contact.findAll())

One more tip, currently your details and edit routes are almost completely identical. I would create a single route called company and then make child details and edit views inside of it.

Stelu answered 8/7, 2012 at 12:20 Comment(9)
Thanks for your answer, I'll go bit by bit on this, but before I try it out, I'd like to know more about the controllers, how do you go about to sync data? Say you have a create method in the controller which inherits from ObjectController, would you manually copy a record that was created by an ObjectController instance to the collection in the ArrayController? Maybe I'm being too strict, but I don't think I like having multiple controllers for the same resource type, is this the convention in Ember? (I had a different app using StateManager with a single controller for this resource)Kindless
If I call my edit route with this code in the connectOutlet, it throws the message Uncaught Error: assertion failed: You must supply a name or a view class to connectOutlets, but not bothKindless
I've changed viewClassName: App.EditContactView to viewClass: App.EditContactView. I no longer get the previous error message, but my route and view won't load. The url stays the same and the view stays the same, just with no data now.Kindless
Just noticed that the connectOutlet I copied from your answser is passing contactsController instead of contactController. Now after these changes I am able to load my view. Thanks :) I just wish the documentation was more explicit about the controllers, not my favorite approach right now.Kindless
Sorry, I didn't try actually double-check my code, I'm glad you worked it out. I'm currently doing everything with Ember-Data so I guess I'm kind of used to having a general store of objects abstracted outside of the controllers.Stelu
As for the singular vs plural controllers, I see it as a currentObject vs collection of Objects controllers, obviously you are going to be doing completely different things to the collection (iterating/sorting/filtering) than you are to a single object (creating/editing/deleting). Obviously you can use whatever structure suits you, but I'm this seems to be a pattern I've seen quite a bit in code samplesStelu
Yeah, although it's not what I would ideally do in a regular mvc application (my background is .net btw), I think I might adopt this pattern for controllers as well because after this question I've also started to see more and more of it being applied elsewhere. At the same time, I might go back to single controller since I can define which controller I'll be using throught connectOutlet so I could have a selectedContact property for this in the ArrayController. I'll try it out more later today. Once again, thanks!Kindless
Ember-Data looks amazing and I intend to use it as soon as I feel confident about coding with the built-in functionality. Ember is being a great experience and I think I'll stick to it over Knockout, which was my previous MV* library of choiceKindless
@Kindless Very interesting post. I think you should have a look directly to the source of controller, router, route. The documentation about this is not up to date, as the code is moving very often. Moreover, I think the examples are too small.Slop

© 2022 - 2024 — McMap. All rights reserved.