javascripthtmlcssember.jsjavascriptmvc

Rendering ember component outside parent template


Imagine the following situation. I have a sidebar positioned absolutely and then some more stuff positioned absolutely within that sidebar. In that sidebar I have a button, which shows a menu, the template looks like this:

<button>Click me</button>
{{#if shouldDisplayMenu}}
    {{view App.MyMenu}}
{{/if}}

What I like about this solution and want to keep is that the menu in the template is defined right next to the button, making for nice maintainability.

The problem is that I want the menu to be positioned relative to the viewport, i.e. rendered to document.body and positioned using position absolute. This way it will render into the sidebar and cause a scrollbar to be visible because I have already reset positioning context via position relative in one of the parent elements.

Is there any way I can keep {{view App.MyMenu}} where it is now, but have it render to body (perhaps using outlets or some other mechanism)? What is the recommended pattern for a situation like this?


Solution

  • There are plenty of ways to render views in ember with related context. I present four approaches (i think the first is more likely what you are looking for) :

    1. If the view is required to be placed somewhere out of a handlebars template, it is possible to use jQuery to place the rendered content wherever it is required to be.

    (since the replacement of the view element is happening after didInsertElement it is probably required to also add styles to not show the element eg display:none , and show it after it has been repositioned)

    Example http://emberjs.jsbin.com/gotabore/1/edit

    2. Use {{render}} helper to render the view in place if that place is somewhere in another template. Although i'm not certain about the context requirements, this helper is pretty flexible (http://emberjs.com/guides/templates/rendering-with-helpers/#toc_the-code-render-code-helper).

    Example http://emberjs.jsbin.com/xibayava/1/edit

    3. Use {{outlet}} helper to render the view based on what is specified within the visited route. (http://emberjs.com/guides/routing/rendering-a-template/)

    Example http://emberjs.jsbin.com/tiyuqenu/1/edit

    4. Use a ContainerView to push a view programmatically anywhere it is required to be.

    Example http://emberjs.jsbin.com/yoyujeqi/1/edit


    UPDATE for approach 4: for versions equal or greater than 1.8.x , to avoid the error mentioned in http://emberjs.com/guides/deprecations/#toc_global-lookup-of-views , it is possible to specify a property associated to the ContainerView as shown in the following example

    http://emberjs.jsbin.com/difayakeki/1/edit?html,js,output


    related code

    approach 1

    hbs

    <script type="text/x-handlebars">
        <h2> Welcome to Ember.js</h2>
    
        {{outlet}}
      </script>
    
      <script type="text/x-handlebars" data-template-name="index">
        This is index<br/>
        {{view App.TestView}}
      </script>
      <script type="text/x-handlebars" data-template-name="test">
        This is the test view<br/>
        <b>{{myProp}}</b>
      </script>
    
      <div id="a-div" style="background-color:lightgray">
        This is a div somewhere,<br/>
      </div>
    

    js

    App = Ember.Application.create();
    
    App.Router.map(function() {
      // put your routes here
    });
    
    App.IndexController = Ember.Controller.extend({
      myProp:"index controller prop"
    });
    
    App.IndexView = Ember.View.extend({
      placeTestViewSomewhere:function(){
    
        var theTestView = this.$(".my-test-view").detach();
        $("#a-div").append(theTestView);
    
      }.on("didInsertElement")
    });
    
    App.TestView = Ember.View.extend({
      templateName:"test",
      classNames:["my-test-view"]
    });
    

    approach 2

    hbs

    <script type="text/x-handlebars">
        <h2> Welcome to Ember.js</h2>
    
        {{outlet}}
      </script>
    
      <script type="text/x-handlebars" data-template-name="index">
        This is index<br/>
    
        <div style="background-color:lightgray">
        This is a div in index template with render helper,<br/>
        {{render "test"}}
      </div>
      </script>
      <script type="text/x-handlebars" data-template-name="test">
        This is the test view<br/>
        <b>{{myProp}}</b>
      </script>
    

    js

    App.IndexView = Ember.View.extend({
      placeTestViewSomewhere:function(){
    
    //     var theTestView = this.$(".my-test-view").detach();
    //     $("#a-div").append(theTestView);
    
      }.on("didInsertElement")
    });
    
    App.TestController = Ember.Controller.extend({
      myProp:"test controller prop"
    });
    
    App.TestView = Ember.View.extend({
      templateName:"test"
    });
    

    approach 3

    hbs

    <script type="text/x-handlebars">
        <h2> Welcome to Ember.js</h2>
    
        {{outlet}}
        <div style="background-color:lightgray">
        This is a div in application template with outlet helper,<br/>
        {{outlet "test-outlet"}}
      </div>
      </script>
    
      <script type="text/x-handlebars" data-template-name="index">
        This is index<br/>
    
      </script>
      <script type="text/x-handlebars" data-template-name="test">
        This is the test view<br/>
        <b>{{myProp}}</b>
      </script>
    

    js

    App.IndexRoute = Ember.Route.extend({
      renderTemplate: function() {
        this.render("index");
        this.render('test', {   // the template to render
          into: 'application',                // the template to render into
          outlet: 'test-outlet'             // the name of the outlet in that template
        });
      }
    });
    
    App.IndexView = Ember.View.extend({
      placeTestViewSomewhere:function(){
    
    //     var theTestView = this.$(".my-test-view").detach();
    //     $("#a-div").append(theTestView);
    
      }.on("didInsertElement")
    });
    
    App.TestController = Ember.Controller.extend({
      myProp:"test controller prop"
    });
    
    App.TestView = Ember.View.extend({
      templateName:"test"
    });
    

    approach 4

    hbs

    <script type="text/x-handlebars">
        <h2> Welcome to Ember.js</h2>
    
        {{outlet}}
        <div style="background-color:lightgray">
        This is a div in application template with view helper and a view container,<br/>
        {{view Ember.ContainerView viewName="my-menu-container"}}
      </div>
    
      </div>
      </script>
    
      <script type="text/x-handlebars" data-template-name="index">
        This is index<br/>
      </script>
      <script type="text/x-handlebars" data-template-name="test">
        This is the test view<br/>
        <b>{{myProp}}</b>
      </script>
    

    js

    App.IndexController = Ember.Controller.extend({
      myProp:"index controller prop"
    });
    
    App.IndexView = Ember.View.extend({
      placeTestViewSomewhere:function(){
    
        var theTestView = App.TestView.create({});
        theTestView.set("context",this.get("context"));
        this.get("parentView.my-menu-container").pushObject(theTestView);
    
      }.on("didInsertElement")
    });
    
    
    App.TestView = Ember.View.extend({
      templateName:"test"
    });
    

    UPDATE for approach 4:

    js

    App.ApplicationView = Em.View.extend({
      containerView:Em.ContainerView
    });
    

    hbs

    {{view view.containerView viewName="my-menu-container"}}