Ember.js — How do I target outlets in nested/repea

2019-03-08 09:38发布

问题:

I'm working on refactoring an inherited Ember application, with quite a bit of non-mvc disorder to it. I'm looking to keep things as modular as possible, and am hoping to reuse various ui components in multiple screens to help prevent code duplication.

It seems like outlets are the best way to do this.

Right now, I have a UI displaying a number of elements, each rendered using a templatized view.

{{#each item in controller}}
      {{view App.ItemThumbView}}
{{/each}}

The right sidebar of this view is an outlet that changes based on the selection. When I select an item, I would like to display a list of edit operations within the templatized sub-view, which when selected, reveal the proper edit UI through a nested outlet.

Essentially

+---------------------------------------------------+
|        Parent Pane
|
| +------------------------------+ +----------+
| |  Device Section              | | Sidebar  |
| |                              | | [Outlet] |
| |  +--------+ +---------+      | |          |
| |  | Dev 1  | |  Dev 2  |      | |          |
| |  |[outlet]| | [outlet]|      | +----------+
| |  +--------+ +---------+      |
| +------------------------------+ 
+--------------------------------------------------+

The nested views all share the same controller -- which makes sense -- but I need to be able to connect a selected view to its corresponding outlet. My initial attempts at connecting outlets never display. The code doesn't fail at all, so the controller's just updating a hidden outlet.

How do I target the correct outlet for a nested view in Ember? (At best, I seem to be able to hit the sidebar outlet, but not the outlet within a nested device template.) And is this a reasonable structure for implementing contextual menus in ember in the first place?

* For clarification With my current setup, each device item is rendered using the same template. When selected, the sidebar outlet would update with some device's meta information, while the selected device view would also connect its outlet to an edit menu. Only one device item would have its 'edit' outlet connected at a time.

Does it make sense to even use an outlet here, or should I be dropping conditional logic into the template in order to display the edit menus as necessary?

Update to restate the best practices portion of the question:

Outlets seem to be great for decoupling components, and future-proofing potential view nestings. But right now it seems like accessing the correct outlet for a nested view is a bit cumbersome. Further, if you always know what component(s) will be conditionally nested within a template, it seems easiestto just hardcode your views. eg:

// within template used for individual result-list items
{{#if condition }}
   {{view reusableSubView}}
{{/if} 

What is the preferred ember way of doing things here? Is there any overhead to creating outlets that may not necessarily be connected at all times?

回答1:

Okay, after hours and hours of playing around with this, it seems best to conditionally include a named outlet in a level directly above my nested view.

This is because you need a single, reliable outlet to target within a template, and there doesn't seem to be a nice way to programmatically name an outlet at runtime.

Further, the outlet you hope to target needs to exist somewhere within a parent template in the nesting of routers, controllers, and rendered templates used to render the current state of the screen. If an outlet hasn't been put in place by a parent route object, you cannot hope to target it in your leaf-node route.

Let me explain with an example:

SInce I originally asked my question, our UI has changed from a list of devices to a list of people, but the principle remains the same.

I have a list of people with their photos, and they can be clicked to show some more info next to their result.

I have a people template that looks something like:

<script type="text/x-handlebars" data-template-name="people" >
    <h3>People in this group</h3>
    <div class="peeps">
        {{#each controller}}
             {{ view App.PersonView }}
             {{#if selected }}
                   <div class='too-personal'>
                   {{ outlet veryPersonalDetails }}
                   </div>
             {{/if}}
         {{/each}} 
    </div>
</script>

And a person template that's kinda like this:

<script type="text/x-handlebars" data-template-name="person" >
        {{#linkTo person this}}
            <div data-desc="person-item" class="item">
                <div class="portrait">
                    <img class="avatar" {{bindAttr src="gravatarUrl"}}/>
                </div>
                <div class="statuslabel"><span {{bindAttr class=":label statusLabel"}}>{{statusText}}</span></div>
                <div class="cTitle">{{fullName}}</div>
            </div>
        {{/linkTo}}     </script>

And the details template with the extra information and edit options

<script type="text/x-handlebars" data-template-name="person/details" > 
various edit options    
</script>

On clicking a person result, my router picks up the url update, and stubs in the details template into the parent people template. My router is set up something like this:

App.Router.map(function() {
...
    this.resource('people', { path: '/people'}, function() {
        this.resource('person', { path: '/:person_id' },
            function() {
                this.route('details');
            }
        );
    });

});

With the individual routes:

App.PeopleRoute = Ember.Route.extend({
    model: function() {
        return App.People.find();
    },
    setupController: function(controller, model) {
        controller.set('content', model);
    }
});

App.PersonRoute = Ember.Route.extend({
    model: function(params) {
        return App.People.peepsById[params.person_id];
    },

    setupController: function(controller, model) {
        this._super(controller, model);
        var person = model;
// this block ensures that only one person has a selected status at a time
// ignore the overly-verbose code. I'm cleaning it up tomorrow 
        var ls = App.People.lastSelected;
        if (ls) {
            ls.set('selected', false);
        }
        person.set('selected', true);
        App.People.lastSelected = person;
        controller.set('content', model);
    },

    renderTemplate: function() {
        // person is actually stored in router 'context' rather than 'model'
        // omg!
        var x = this.controllerFor('personDetails').set('content', this.get('context'));
        this.render('person/details', { // render person/details
            into:'people', // into the people template
            outlet: "veryPersonalDetails", // at the veryPersonalDetails outlet
            controller: x // using the personDetails controller for rendering
        });

// additional rendering in sidebar outlet for testing
        this.render('person/details',{
            into:'people',
            outlet: "personDetails",
            controller: x
        });

        console.log("@@@ we did it!");
    }
});

I originally tried to have the outlet within the PersonView, but this wouldn't work, as my person template was actually rendered within the people route. By the time the application reaches the person route, the controller has already rendered all the people in the list. The individual person I want to target has been passed by long ago.

By having the route act as an intermediary between the parent and desired child templates, I was able to make this happen.

Finally, regarding the question of whether to explicitly declare nested views within templates, or to just use outlets, I believe outlets are far cleaner. While this solution involved a bit of working around the routes, it's far preferable to having overly complicated template objects. Now I can hopefully achieve arbitrarily complex nestings of templates without touching anything but the routes involved in their rendering.

Further, this solution eliminates the overhead of unused outlets. I believe you should only have outlets you intend to use, rather than littering the 'dom' with a bunch of empty container objects.