How to access the reach of a parent component from the scope of child components in ember?

I am curious if this is possible in ember. This is easy to do in angular (plunkr: http://plnkr.co/edit/O2e0ukyXdKMs4FcgKGmX?p=preview ):

The goal is to make an easy to use, versatile, reusable api accordion for api users.

Api, I want the caller to be able to use this (like the angular api):

{{#ember-accordion listOfAccordionPaneObjects=model}}

  {{#ember-accordion-heading}}
     heading template html {{accordionPaneObject.firstName}}
  {{/ember-accordion-heading}}

  {{#ember-accordion-body}}
     this is the accordion body  {{accordionPaneObject.lastName}}
  {{/ember-accordion-body}}

{{/ember-accordion}}

Here is a working example that I wrote using angular:

<!doctype html>
<html ng-app="angular-accordion">
<head>
    <style>
        .angular-accordion-header {
            background-color: #999;
            color: #ffffff;
            padding: 10px;
            margin: 0;
            line-height: 14px;
            -webkit-border-top-left-radius: 5px;
            -webkit-border-top-right-radius: 5px;
            -moz-border-radius-topleft: 5px;
            -moz-border-radius-topright: 5px;
            border-top-left-radius: 5px;
            border-top-right-radius: 5px;
            cursor: pointer;
            text-decoration: none;
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
            font-size: 14px;
        }

        .angular-accordion-container {
            height: 100%;
            width: 100%;
        }

        .angular-accordion-pane {
            padding: 2px;
        }

        .angularaccordionheaderselected {
            background-color: #bbb;
            color: #333;
            font-weight: bold;
        }

        .angular-accordion-header:hover {
            text-decoration: underline !important;
        }

        .angularaccordionheaderselected:hover {
            text-decoration: underline !important;
        }

        .angular-accordion-pane-content {
            padding: 5px;
            overflow-y: auto;
            border-left: 1px solid #bbb;
            border-right: 1px solid #bbb;
            border-bottom: 1px solid #bbb;
            -webkit-border-bottom-left-radius: 5px;
            -webkit-border-bottom-right-radius: 5px;
            -moz-border-radius-bottomleft: 5px;
            -moz-border-radius-bottomright: 5px;
            border-bottom-left-radius: 5px;
            border-bottom-right-radius: 5px;
        }

        .angulardisabledpane {
            opacity: .2;
        }
    </style>
</head>
<body style="margin: 0;">


<div style="height: 90%; width: 100%; margin: 0;" ng-controller="outerController">

    <angular-accordion list-of-accordion-pane-objects="outerControllerData">
        <pane>
            <pane-header>Header {{accordionPaneObject}}</pane-header>
            <pane-content>Content {{accordionPaneObject}}</pane-content>
        </pane>
    </angular-accordion>

</div>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.js"></script>
    <script>
        angular.module('angular-accordion', [])
                .directive('angularAccordion', function() {
                    var template = '';

                    return {
                        restrict: 'E',
                        transclude: true,
                        replace: true,
                        template: '<div>' +
                                        '<div ng-transclude class="angular-accordion-container" ng-repeat="accordionPaneObject in listOfAccordionPaneObjects"></div>' +
                                  '</div>',
                        controller: ['$scope', function($scope) {
                            var panes = [];

                            this.addPane = function(pane) {
                                panes.push(pane);
                            };
                        }],
                        scope: {
                            listOfAccordionPaneObjects: '='
                        }
                    };
                })
                .directive('pane', function() {
                    return {
                        restrict: 'E',
                        transclude: true,
                        replace: true,
                        template: '<div ng-transclude class="angular-accordion-pane"></div>'
                    };
                })
                .directive('paneHeader', function() {
                    return {
                        restrict: 'E',
                        require: '^angularAccordion',
                        transclude: true,
                        replace: true,
                        link: function(scope, iElement, iAttrs, controller) {
                            controller.addPane(scope);

                            scope.toggle = function() {
                                scope.expanded = !scope.expanded;
                            };
                        },
                        template: '<div ng-transclude class="angular-accordion-header" ng-click="toggle()"></div>'
                    };
                })
                .directive('paneContent', function() {
                    return {
                        restrict: 'EA',
                        require: '^paneHeader',
                        transclude: true,
                        replace: true,
                        template: '<div ng-transclude class="angular-accordion-pane-content" ng-show="expanded"></div>'
                    };
                })
                .controller('outerController', ['$scope', function($scope) {
                    $scope.outerControllerData = [1, 2, 3];
                }]);
    </script>
</body>
</html>

here where I get stuck doing the same with ember:

index.html

<!DOCTYPE html>
<html>
    <body>
        <script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.9/require.js" data-main="main.js"></script>
    </body>
</html>

main.js

require.config({
    paths: {
        'ember': 'bower_components/ember/ember',
        'handlebars': 'bower_components/handlebars/handlebars',
        'jquery': 'bower_components/jquery/jquery',
        'text': 'bower_components/requirejs-text/text'
    },
    shim: {
        ember: {
            deps: ['jquery', 'handlebars'],
            exports: 'Ember'
        }
    }
});

define(function(require) {
    var Ember = require('ember'),
        EmberAccordionComponent = require('src/EmberAccordionComponent'),
        EmberAccordionTemplate = require('text!templates/ember-accordion.hbs'),
        EmberAccordionHeaderTemplate = require('text!templates/ember-accordion-header.hbs'),
        EmberAccordionBodyTemplate = require('text!templates/ember-accordion-body.hbs'),
        ApplicationTemplate = require('text!templates/application.hbs'),
        IndexTemplate = require('text!templates/index.hbs');

    var App = Ember.Application.create({
        LOG_STACKTRACE_ON_DEPRECATION : true,
        LOG_BINDINGS                  : true,
        LOG_TRANSITIONS               : true,
        LOG_TRANSITIONS_INTERNAL      : true,
        LOG_VIEW_LOOKUPS              : true,
        LOG_ACTIVE_GENERATION         : true
    });

    Ember.TEMPLATES = {};
    Ember.TEMPLATES['application'] = Ember.Handlebars.compile(ApplicationTemplate);
    Ember.TEMPLATES['index'] = Ember.Handlebars.compile(IndexTemplate);
    Ember.TEMPLATES['components/ember-accordion'] = Ember.Handlebars.compile(EmberAccordionTemplate);
    Ember.TEMPLATES['components/ember-accordion-header'] = Ember.Handlebars.compile(EmberAccordionHeaderTemplate);
    Ember.TEMPLATES['components/ember-accordion-body'] = Ember.Handlebars.compile(EmberAccordionBodyTemplate);

    App.EmberAccordionComponent = EmberAccordionComponent;

    App.IndexRoute = Ember.Route.extend({
        model: function() {
            return [
                {
                    name: 'Bob'
                },
                {
                    name: 'Jill'
                }]
        }
    })
});

EmberAccordionComponent.js

define(function(require) {
    require('ember');

    var EmberAccordionComponent = Ember.Component.extend({});

    return EmberAccordionComponent;
});

application.hbs

{{outlet}}

corner-accordion-header.hbs

<div style="color: blue;">
    {{yield}}
</div>

Ember-accordion-body.hbs

<div style="color: green;">
    {{yield}}
</div>

index.hbs

{{#ember-accordion listOfAccordionPaneObjects=model}}
    {{#ember-accordion-header}}
        {{log this.constructor}}
        {{log this}}
        Header {{accordionPaneObject.name}}
    {{/ember-accordion-header}}
    {{#ember-accordion-body}}
        Body {{accordionPaneObject.name}}
    {{/ember-accordion-body}}
{{/ember-accordion}}

Ember-accordion.hbs

{{#each accordionPaneObject in listOfAccordionPaneObjects}}
    {{yield}}
{{/each}}

-

This is hard to do for debugging. So insert:

{{log this.constructor}}

and:

{{log this}}

in:

{{#ember-accordion-header}}

outputs the following:

  • Class.model = undefined (why?)
  • Ember.ArrayController

private -yill Ember.Component, (http://www.thesoftwaresimpleton.com/blog/2013/11/21/component-block/):

var EmberAccordionHeaderComponent = Ember.Component.extend({
    _yield: function(context, options) {
        var get = Ember.get,
            view = options.data.view,
            parentView = this._parentView,
            template = get(this, 'template');

        if (template) {
            Ember.assert("A Component must have a parent view in order to yield.", parentView);
            view.appendChild(Ember.View, {
                isVirtual: true,
                tagName: '',
                _contextView: parentView,
                template: template,
                context: get(view, 'context'), // the default is get(parentView, 'context'),
                controller: get(view, 'controller'), // the default is get(parentView, 'context'),
                templateData: { keywords: parentView.cloneKeywords() }
            });
        }
    }
});

, accordionPaneObject , {{log this.constructor}} :.EmberAccordionHeaderComponent

, , - , .

EmberAccordionHeaderComponent.js:

var EmberAccordionHeaderComponent = Ember.Component.extend({
    _yield: function(context, options) {
        var get = Ember.get,
            view = options.data.view,
            parentView = this._parentView,
            grandParentView = this._parentView._parentView,
            template = get(this, 'template');

        if (template) {
            Ember.assert("A Component must have a parent view in order to yield.", parentView);
            view.appendChild(Ember.View, {
                isVirtual: true,
                tagName: '',
                _contextView: parentView,
                template: template,
                context: get(grandParentView, 'context'), // the default is get(parentView, 'context'),
                controller: get(grandParentView, 'controller'), // the default is get(parentView, 'context'),
                templateData: { keywords: parentView.cloneKeywords() }
            });
        }
    }
});

accordionPaneObject, {{log this.constructor}} .EmberAccordionComponent. , , , - .

, _yield, , , :

this._parentView._context.content
+4
3

, , http://emberjs.jsbin.com/ivOyiZa/1/edit.

Javascript

App = Ember.Application.create();

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return [
      { head: "foo head", body: "foo body " },
      { head: "bar head", body: "bar body " },
      { head: "ya head", body: "yo body " }
    ];
  }
});

App.EmberAccordionComponent = Ember.Component.extend({
  // each accordion header/body item, will have a instance of that view.
  // so we can isolate the expanded state for each accordion header/body
  emberAccordionItemView: Ember.View.extend({    
    expanded: false
  }),
  _yield: function(context, options) {
    var get = Ember.get, 
    view = options.data.view,
    parentView = this._parentView,
    template = get(this, 'template');

    if (template) {
      Ember.assert("A Component must have a parent view in order to yield.", parentView);      
      view.appendChild(Ember.View, {
        isVirtual: true,
        tagName: '',
        _contextView: parentView,
        template: template,
        context: get(view, 'context'), // the default is get(parentView, 'context'),
        controller: get(view, 'controller'), // the default is get(parentView, 'context'),
        templateData: { keywords: parentView.cloneKeywords() }
      });
    }
  }
});

App.EmberAccordionHeaderComponent = Ember.Component.extend({  
  classNames: ['ember-accordion-header'],  
  click: function() {
    // here we toggle the emberAccordionItemView.expanded property
    this.toggleProperty('parentView.expanded');  
  }
});

  <script type="text/x-handlebars" data-template-name="index">
    {{#ember-accordion listOfAccordionPaneObjects=model}}                        
          {{#ember-accordion-header}}
              {{head}} <!-- each object passed in listOfAccordionPaneObjects=model can be accessed here -->
          {{/ember-accordion-header}}
          {{#ember-accordion-body}}
              {{body}} <!-- each object passed in listOfAccordionPaneObjects=model can be accessed here -->
          {{/ember-accordion-body}}        
    {{/ember-accordion}}        
  </script>

  <script type="text/x-handlebars" data-template-name="components/ember-accordion">     
    {{#each listOfAccordionPaneObjects itemViewClass="view.emberAccordionItemView"}}            
      <div class="ember-accordion-container">
        <div class="ember-accordion-pane">          
            {{yield}}          
        </div>
      </div>      
    {{/each}}
  </script>

  <script type="text/x-handlebars" data-template-name="components/ember-accordion-header">        
    {{yield}}    
  </script>

  <script type="text/x-handlebars" data-template-name="components/ember-accordion-body">    
    <!-- when EmberAccordionHeaderComponent.click is called, the expanded property change and the content can be visible or not, based on expanded truth -->
    {{#if parentView.expanded}}
      <div class="ember-accordion-pane-content">
        {{yield}}
      </div>
    {{/if}}
  </script>

Css

.ember-accordion-header {
  background-color: #999;
  color: #ffffff;
  padding: 10px;
  margin: 0;
  line-height: 14px;
  -webkit-border-top-left-radius: 5px;
  -webkit-border-top-right-radius: 5px;
  -moz-border-radius-topleft: 5px;
  -moz-border-radius-topright: 5px;
  border-top-left-radius: 5px;
  border-top-right-radius: 5px;
  cursor: pointer;
  text-decoration: none;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 14px;
}

.ember-accordion-container {
  height: 100%;
  width: 100%;
}

.ember-accordion-pane {
  padding: 2px;
}

.emberaccordionheaderselected {
  background-color: #bbb;
  color: #333;
  font-weight: bold;
}

.ember-accordion-header:hover {
  text-decoration: underline !important;
}

.emberaccordionheaderselected:hover {
  text-decoration: underline !important;
}

.ember-accordion-pane-content {
  padding: 5px;
  overflow-y: auto;
  border-left: 1px solid #bbb;
  border-right: 1px solid #bbb;
  border-bottom: 1px solid #bbb;
  -webkit-border-bottom-left-radius: 5px;
  -webkit-border-bottom-right-radius: 5px;
  -moz-border-radius-bottomleft: 5px;
  -moz-border-radius-bottomright: 5px;
  border-bottom-left-radius: 5px;
  border-bottom-right-radius: 5px;
}

.emberdisabledpane {
  opacity: .2;
}
+6

, .

, , , , jsbin, , mouseenter/mouseleave.

http://emberjs.jsbin.com/ijEwItO/3/edit

<script type="text/x-handlebars" data-template-name="components/unicorn-accordian">
  <ul>
    {{#each item in content itemController='unicornItem' itemView='unicornItem'}}
      <li>{{item.title}}
      {{#if bodyVisible}}
         <br/>
         {{item.body}}
      {{/if}}
      </li>
    {{/each}}
  </ul>
 </script>


App.UnicornAccordianComponent = Em.Component.extend();

App.UnicornItemController = Em.ObjectController.extend({
  bodyVisible: false
});

App.UnicornItemView = Em.View.extend({
  mouseEnter: function(){
    this.set('controller.bodyVisible', true);
  },

  mouseLeave: function(){
    this.set('controller.bodyVisible', false); 
  }
});
0

Of course, it is much simpler to implement the solution - pass the view (or other parent) as an argument to the component. This will give you access to all the properties of the view, while preserving all the benefits of using the contained component. For example:

{{#ember-accordion listOfAccordionPaneObjects=model info=view}}{{!-- Pass view in here--}}

    {{log view.info}}{{!-- This will log what view.parentView would have done--}}

  {{ember-accordion-heading firstName=accordionPaneObject.firstName}}

  {{ember-accordion-body lastName=accordionPaneObject.lastName}}

{{/ember-accordion}}

Your header template will look something like this:

Header template html here {{firstName}}

And your body template will look something like this:

Body html here {{lastName}}
0
source

All Articles