简体   繁体   中英

Declarative ember component which passes in a list of objects to be rendered / bound, while passing state that nested components can access?

I'm having a hard time figuring out how to build an ember component which has nested components which are rendered as a list based on an input list, while also allowing state to be passed in which is accessible from the nested components.

This is easy in 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;
        }

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


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

    <div class="angular-accordion-header" ng-click="fakeXhrService.load()">
        Click here to simulate loading new data.
    </div>
    <angular-accordion list-of-accordion-pane-objects="outerControllerData" loading="fakeXhrService.isLoading()">
        <pane>
            <pane-header ng-class="{loading:loading}">{{accordionPaneObject.firstName}}</pane-header>
            <pane-content>{{accordionPaneObject.lastName}}</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', [])
            .factory('fakeXhrService', ['$timeout', function($timeout) {
                var loading = false,
                    fakeXhrService;

                fakeXhrService = {
                    load: function() {
                        loading = true;

                        $timeout(function() {
                            loading = false;
                        }, 2000);
                    },
                    isLoading: function() {
                        return loading;
                    }
                };

                return fakeXhrService;
            }])
            .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" ng-cloak></div>' +
                            '</div>',
                    controller: ['$scope', function($scope) {
                        var panes = [];

                        this.addPane = function(pane) {
                            panes.push(pane);
                        };
                    }],
                    scope: {
                        listOfAccordionPaneObjects: '=',
                        loading: '='
                    }
                };
            })
            .directive('pane', function() {
                return {
                    restrict: 'E',
                    transclude: true,
                    require: '^angularAccordion',
                    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', 'fakeXhrService', function($scope, fakeXhrService) {
                var people = [],
                    i = 0;

                for(i; i < 10; i++) {
                    people.push({
                        firstName: 'first ' + i.toString(),
                        lastName: 'last ' + i.toString()
                    });
                }

                $scope.outerControllerData = people;

                $scope.fakeXhrService = fakeXhrService;
            }]);
</script>
</body>
</html>

plunkr: http://plnkr.co/edit/NxXAgP8Ba7MK1cz2IGXg?p=preview

Here's my attempt so far doing it in ember:

<!doctype html>
<html>
<head>
    <style>
        .ember-accordion-header {
            background-color: #999;
            color: #fff;
            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-pane {
            padding: 2px;
        }

        .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;
        }

        .ember-accordion-container {}

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

<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">
    {{#if parentView.expanded}}
        <div class="ember-accordion-pane-content">
            {{yield}}
        </div>
    {{/if}}
</script>

<script type="text/x-handlebars" data-template-name="index">
    from outside the component: {{test}}
    {{#ember-accordion listOfAccordionPaneObjects=model test=test}}
        {{#ember-accordion-header class="header"}}
            {{firstName}}<br />
            child inner scope test defined directly on the view inside the component: {{view.parentView.specifiedInComponent}}<br />
            child inner scope test passed into the component: {{view.parentView.test}}
        {{/ember-accordion-header}}
        {{#ember-accordion-body class="body"}}
            {{lastName}}<br />
        {{/ember-accordion-body}}
    {{/ember-accordion}}

</script>

<script type="text/x-handlebars" data-template-name="application">
    {{outlet}}
</script>


<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.2.1/handlebars.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/ember.js/1.2.0/ember.debug.js"></script>
<script>
    var Person = Ember.Object.extend({
            firstName: '',
            lastName: '',
            fullName: function() {
                return this.get('firstName') + ' ' + this.get('lastName');
            }.property()
        }),
        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,
                specifiedInComponent: 'this works, but how to get it from the property passed to the component?'
            }),
            _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() }
                    });
                }
            }
        }),
        EmberAccordionHeaderComponent = Ember.Component.extend({
            classNames: ['ember-accordion-header'],
            classNameBindings: ['expanded'],
            expanded: false,
            click: function() {
                // here we toggle the emberAccordionItemView.expanded property
                this.toggleProperty('parentView.expanded');
                this.toggleProperty('expanded');
            }
        }),
        App = Ember.Application.create(),
        people = [],
        i = 0;

    App.EmberAccordionComponent = EmberAccordionComponent;
    App.EmberAccordionHeaderComponent = EmberAccordionHeaderComponent;

    for(i; i < 10; i++) {
        people.push(Person.create({
            firstName: 'first ' + i.toString(),
            lastName: 'last ' + i.toString()
        }));
    }

    App.IndexRoute = Ember.Route.extend({
        model: function() {
            return people;
        }
    });

    App.IndexController = Ember.Controller.extend({
        init: function() {
            this._super();

            this.set('test', 'TEST WORKED!');
        }
    });
</script>
</body>
</html>

jsbin: http://jsbin.com/apIYurEN/1/edit

Where I'm stuck is:

  1. Why isn't the parameter 'test' accessible inside the ember-accordion-header component? If I define it directly on the view inside the outer component I can access it.
  2. How can I avoid putting view.parentView in front of the parameter I want to access, ie: view.parentView.specifiedInComponent? This is not simple for api consumers.
  3. Why do I have to override a private method of ember to get this far ( _yield ). Overriding private members is a bad idea since they can change between versions of ember.

Thanks!

You had me at: "This is easy in angular" :P

It's not necessary to override _yield. The default behaviour allows you to access view properties defined in the parentView by using {{view.property}}. When nesting components this means that the properties are passed down recursively. Also you forgot to set test=test on the ember-accordion-header component.

Here is a working version: http://jsbin.com/apIYurEN/6/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM