Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Route transitions destroy rendered view object; "Error: Object in path [blah] could not be found or was destroyed."

When transitioning from one Ember route to another, I am getting the following error:

Error: Object in path item_delet could not be found or was destroyed.

In the routes' renderTemplate hooks, I'm doing a lot of this kind of thing:

this.render('item_delete', { into: 'item_parent', outlet: 'item_delete' });

... and have a rational tree of parent/child templates. However, when a template, say "item_delete", is rendered into an "routeA", then I click along into "routeB", then go back to "routeA", I get the error. I understand that the view object is getting destroyed when the router exits "routeA" for the purpose of preventing memory leaks. I am unsure why re-entering the route does not re-create/instantiate/connect the view. As a side note, when the error is presented any of the previously rendered views that gets this error message always has its path name shortened by one character, notice "item_delet" instead of "item_delete".

I am using grunt-ember-templates to compile the Handlebars templates, so posting a JSFiddle is a little bit difficult. Just wondering if anyone can "sight read" this code to flag any obvious reasons that the routes or the renderTemplate hooks may be failing to re-instantiate/connect/etc. the rendered templates. Is there some "activate/deactivate" magic that I can do to prevent views from getting destroyed? (I realize that flies in the face of the intentions behind destroying the view objects in the first place, but I'm willing to hear all options.)

I have an Ember Route map that looks like:

App.Router.map(function () {

    this.route('index', { path: '/projects' });
    this.resource('items', { path: '/projects/folders' }, function() {
        this.resource('item', { path: '/:item_id' }, function() {
            this.route('file_uploads', { path: '/file_upload' });
        });
    });

});

I have routes defined like this:

App.IndexRoute = Ember.Route.extend({
    redirect: function() {
        this.transitionTo('items');
    }
});

App.ItemsIndexRoute = Ember.Route.extend({

    model: function() {
        // Setting up the model
    }
    , setupController: function(controller, model) {
        // Setting up some controllers
    }
    , renderTemplate: function() {
        this.render('index', {
            into: 'application'
            , outlet: 'application'
            , controller: this.controllerFor('items')
        });

        this.render('navbar', {
            into: 'application'
            , outlet: 'navbar'
            , controller: this.controllerFor('currentUser')
        });

        this.render('items', {
            into: 'index'
            , outlet: 'index'
            , controller: this.controllerFor('items')
        });

        this.render('items_toolbar', {
            into: 'index'
            , outlet: 'items_toolbar'
            , controller: this.controllerFor('items')
        });

        this.render('item_rename', {
            into: 'items_toolbar'
            , outlet: 'item_rename'
            , controller: this.controllerFor('items')
        });

        this.render('item_delete', {
            into: 'items_toolbar'
            , outlet: 'item_delete'
            , controller: this.controllerFor('items')
        });

        // ... some more of these...

    }

});

App.ItemRoute = Ember.Route.extend({

    model: function (params) {
        // Building the model for the route
    }
    , setupController: function(controller, model) {
        // Setting up some controllers
    }
    , renderTemplate: function() {
        this.render('index', {
            into: 'application'
            , outlet: 'application'
            , controller: this.controllerFor('items')
        });

        this.render('navbar', {
            outlet: 'navbar'
            , into: 'application'
            , controller: this.controllerFor('application')
        });

        this.render('items', {
            into: 'index'
            , outlet: 'index'
            , controller: this.controllerFor('items')
        });

        this.render('items_toolbar', {
            into: 'index'
            , outlet: 'items_toolbar'
            , controller: this.controllerFor('items')
        });

        this.render('item_rename', {
            into: 'items_toolbar'
            , outlet: 'item_rename'
            , controller: this.controllerFor('items')
        });

        this.render('item_delete', {
            into: 'items_toolbar'
            , outlet: 'item_delete'
            , controller: this.controllerFor('items')
        });

        // ... some more of these...

    }

});

App.ItemFileUploadsRoute = Ember.Route.extend({

    model: function() {
        // Setting up the model
    }

    , setupController: function(controller, model) {
        // Setting up some controllers
    }

    , renderTemplate: function() {

        this.render('file_uploads', {
            into: 'application'
            , outlet: 'application'
            , controller: this.controllerFor('fileUploads')
        });

        this.render('navbar', {
            into: 'application'
            , outlet: 'navbar'
            , controller: this.controllerFor('application')
        });

        this.render('items_toolbar', {
            into: 'file_uploads'
            , outlet: 'items_toolbar'
            , controller: this.controllerFor('fileUploads')
        });

        this.render('item_rename', {
            into: 'items_toolbar'
            , outlet: 'item_rename'
            , controller: this.controllerFor('items')
        });

        this.render('item_delete', {
            into: 'items_toolbar'
            , outlet: 'item_delete'
            , controller: this.controllerFor('items')
        });

        // ... some more of these...
    }

});

I am reusing some templates and their outlets for different routes/resources. For example, the "items_toolbar" referred to above is in a Handlebars template like this:

<div class="row toolbar">
    <div class="col col-lg-6 text-right">
        {{outlet submission_options_button}}
        {{outlet submission_button}}
        {{outlet create_button}}
        {{outlet confirm_button}}
        {{outlet cancel_button}}
        {{outlet folder_actions}}
        {{outlet item_rename}}
        {{outlet item_delete}}
    </div>
</div>

In this template, not all the outlets will get a buffer rendered into them, and in other contexts they will. I'm doing this to avoid undesirable (confusing) conditionals in the Handlebars code (and the usual "isVisible" nonsense). I'm interested in a given template "wearing" its views as needed; in some instances there may be a "create_button" and a "cancel_button" and in other cases there may be a "cancel_button" and a "folder_actions".

Is there any sure-fire way to ensure that when re-entering a route that any objects that were previously rendered in it, then destroyed, could be reconnected, re-initialized, and/or re-rendered?

like image 895
mysterlune Avatar asked Jun 23 '13 17:06

mysterlune


2 Answers

Following up on this for those who are interested. It turns out the (over)use of {{outlet}} in my approach -- not to mention rendering templates into them within the router -- is an anti-pattern.

After a number of discussions with the gang at Tilde (thanks @peterwagenet, @codeofficer), I learned the "Ember Way" to do what I'm trying for is to use the generated routes and controllers that Ember provides and use the {{view}} helper to render the view objects within the Handlebars templates directly.

So, in my case above, there should be just about none of the stuff in App.ItemsIndexRoute.renderTemplate(), and zero {{outlet}}s in the template. Named {{outlets}} are meant for rendering routes that are "outside" of the current route, the canonical example being modals that perhaps have a different model than the parent route where the outlet is rendering. For example:

<div>
    {{view App.UsersListView}}
    {{outlet order_books_from_mars_modal}}
</div>

The routes in App.Router.map() that I had are pretty much right, though with a couple big misunderstandings on my part regarding generated controllers. When you have an App.FooRoute, Ember sets up an App.FooController which should be defined somewhere in the application code. That App.FooController can have as its model whatever you give it in the route's model hook, like:

App.FooRoute = Ember.Route.extend({

    model: function() {
        return $.get('/some/resource', function() { ... });
    }

});

And a controller that is given for all views rendered within the App.FooRoute is App.FooController, which has a model as was provided in the route's model hook earlier.

And the router will look for Ember.View objects that match the route and controller prefix, e.g. App.FooView. So, when there's something like:

App.FooView = Ember.View.extend({

    template: 'some-foo-template'

});

... the route that matches the App.FooController will render the App.FooView, and the template indicated in the view's template property. Something like:

<script type="text/x-handlebars" data-template-name="some-foo-template">
    <div class="row toolbar">
        <div class="col col-lg-6 text-right">
            {{view App.SubmissionButtonView}}
        </div>
    </div>
</script>

And in this view, an App.SubmissionButtonView is being rendered using the {{view}} helper. So, there needs to be a corresponding:

App.SubmissionButtonView = Ember.View.extend({

    template: 'the-killer-button'

});

... where the template would be something like:

<script type="text/x-handlebars" data-template-name="the-killer-button">
    <a {{action someControllerMethod}}>Do Something</a>
</script>

The someControllerMethod will be expected to be found on the App.FooController. If it is not found on the controller, the action will be bubbled up to the App.FooRoute's events object. If the method is not found on any object up the chain, an error will get logged indicating that nothing handled the action someControllerMethod.

And, consequently, the App.FooController will be what is provided to the App.SubmissionButtonView by default since it is rendering within the App.FooRoute. However, you can bind a different controller to it -- say, a App.BarController -- by doing:

{{view App.SubmissionButtonView controllerBinding='App.BarController'}}

In this case, that someControllerMethod will be expected to be found on the App.BarController, or on the App.FooRoute as mentioned earlier.

The Lesson?

At the end of all this, the lesson for me was really about properly establishing a route hierarchy, taking advantage of the generated controllers Ember provides, and rendering views using the {{view}} helper and NOT the named {{outlet}}s as I was attempting at first. Ember expects a named route to have a corresponding controller and corresponding view. Once I understood this, it all began to hang together better for me.

In my case, now because the nested resources all handle rendering their child views appropriately -- destroying them when a user navigates "away" from the current route, preserving them when the user navigates around "within" the current resource/route -- the original error I posted about is no longer occurring.

As always, the best resource for Embering yourself silly is the Ember Guides.

like image 143
mysterlune Avatar answered Sep 18 '22 23:09

mysterlune


I took a look at the ember source, the error comes from a low-level package of ember, ember-metal - property_set.js.

The code suggests that setPath is expecting a path and is getting a key without a path. It splits on the . and slices to get the parent path, which is where it's loosing the last character.

Try looking at the stack trace from that error, and double back to where the exception originates.

Posting a jsbin demo which has the stack trace would help with debugging.

like image 24
Darshan Sawardekar Avatar answered Sep 19 '22 23:09

Darshan Sawardekar