Some ideas I had about composable/reusable components

Hi @machty

I saw your video in seattle on ember components and loved it. Great stuff!!

I’m struggling to implement transclusion in ember.

I’m building a reusable accordion ember component. I’ll just talk to the header, not the body right now.

I have an outer template:

{{#each accordionPaneObject in listOfAccordionPaneObjects}}
    {{#ember-accordion-header}}{{yield}}{{/ember-accordion-header}}
{{/each}}

a header template:

<div class="panel-heading">
    {{yield}}
</div>

and a body template:

<div class="panel-body">{{yield}}</div>

I’m calling it like this:

{{#ember-accordion listOfAccordionPaneObjects=model}}
    {{#ember-accordion-header}}{{accordionPaneObject.SomeProperty}}{{/ember-accordion-header}}
{{/ember-accordion}} 

I cannot figure out why accordionPaneObject is not available inside ember-accordion-header.

the javascript is simple:

Bootstrap.EmberAccordionPaneHeaderComponent = Ember.Component.extend();

Bootstrap.EmberAccordionComponent = Ember.Component.extend({
    listOfAccordionPaneObjects: []
});

this["Ember"] = this["Ember"] || {};
this["Ember"]["TEMPLATES"] = this["Ember"]["TEMPLATES"] || {};
this["Ember"]["TEMPLATES"]["components/ember-accordion"] = Ember.Handlebars.compile(EmberAccordionTemplate);
this["Ember"]["TEMPLATES"]["components/ember-accordion-header"] = Ember.Handlebars.compile(EmberAccordionHeaderTemplate);

This is super easy to do in angular. Do you know how to do it in ember? Is it even possible? Thanks!

There was a blog post precisely about this in this week’s Ember Weekly.

Hi @tarasm @machty

I did see that article last night. I’ve gone through it a number of times but it doesn’t really make sense to me given what I know (more like don’t know) about ember and handlebars.

so, I’m curious if this is even possible in ember. This is an easy thing to do in angular ( plunkr: Plunker - Untitled ):

Thanks so much for taking a look!

<!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>

@machty @tarasm @wycats

I added a stackoverflow question for this: api - how to access parent component scope from a child components scope in ember? - Stack Overflow

Thanks!

@machty has there been any progress on this? Keep running into situations where overriding parts of components/sub-components would be massive awesome!

Happy to work on a pull request to help get things going?

@rlivsey I would love to see a consolidated gist (or just put it in this thread) of what you had in mind, particularly lots and lots of use cases of things not presently possible/easy to do with the present API.

@machty I imagine it would be along the lines of what you proposed here & in your component / ruby block example: https://gist.github.com/machty/8276094

I’ll consolidate my thoughts and extract some examples from my app of how it would be nice to work vs what we’re currently having to do.

@machty I’ve written up some initial thoughts: https://gist.github.com/rlivsey/9410694

Will expand on it with some more examples over the course of the day but interested in your opinion if it’s at all feasible / similar to what you’ve been thinking?

I tried to stick to the terminology of web-components, hence the {{content select="foo"}} naming etc…

@rlivsey wow, your gist has very similar ideas to one I did recently whose intent was to be as web components-y as possible: https://gist.github.com/machty/b468f59076dc4241155a

We are almost certainly going to be doing something like this approach, particularly with some kind of content-for helper and named yield blocks with default content if no overriding content-for is provided at render time.

Not sure how I feel about passing in options like tagName or class to content-for to convert the passed-in content-for template block fragment into an element; you can always just pass in an element with that tagname and class in as the named template fragment that you supply in the content-for. Also, there might be use cases of using a non-block form of content-for, but pass in data to override the data that the default yield block uses to render its internals, and I wouldn’t want that clashing with the tagName/class namespace used by Ember.View / Component.

I also took a stab at another approach which optionally puts more control over how of things are rendered/bound in the component JS code, which can have its benefits over the template approach to specifying all this stuff, particularly for large components that can only have their internals reconfigured by copying and pasting the whole template and changing only the small part that needs to change. Worth a look: http://emberjs.jsbin.com/ucanam/3884/edit . If we have something like this, it shouldn’t replace the templates-based approach, but give you another way to override.

Pretty cool though that we’ve landed at a lot of the same ideas.

Cool, I hadn’t seen that gist! Some ideas came from your previous ones eg. https://gist.github.com/machty/8276094

The idea of being able to specify tag name etc… was if the content helper was on the element:

<div class="foo" {{content select="foo"}}></div>

Then you could modify that element by using {{content for="foo" class="bar" tagName="li"}} to get:

<li class="foo bar">...</div>

Just about to hit a conference call, but will digest the jsbin etc and mull over it all some more!

Heya @machty How is all this going? Quite interested in getting the ember-table (ie adepar) guys to incoporate some of these changes so it’s a bit easier to build composable tables with overridability.

I <3 your ideas about reusability in components, BTW.

It seems one would need the ability to have sub-components defined within a component, possibly. Registering all constituent components or elements would seem overkill. Reusability could be garnered by modules later, I’d guess (ie import ember-table-cell as cell from ember-table-component), etc.

@rlivsey: would you share the code? It would be most valuable for those of us who are trying to do the same kind of thing.

Here’s a gist of what I’m currently doing, plenty of potential for refactoring but hopefully you get the idea:

https://gist.github.com/rlivsey/f9c1ef500d37cdaf97cd

Here is a related discussion with implementation using pure components:

Hi! I too really needed more customizable component templates, and decided to see what I could do about it. It turns out that implementing overridable blocks in components is actually possible and quite simple with some custom handlebars helpers.

The controller templates will look something like this:

<h1>Hello from the index page!</h1>
 
{{#my-component parent=this}}
  
  {{#section 'header'}}
    <h2>Header inside my awesome component.</h2>
  {{/section}}
  
  <p>
    Here goes the main body that fits into the standard "yield" helper.
    That's because I'm not in a section.
  <p>
  
  {{#section 'footer'}}
    <p>Show me in the footer, please.<p>
  {{/section}}
  
{{/my-component}}

The template of the component itself:

<div class="my-component">
  
  <div class="component-header">
    {{yield-section 'header'}} {{-- Places header section in here --}}
  </div>
  
  {{yield}}
  
  {{#if parent.footerSection}} {{-- Overrides if footer section is specified --}}
    <div class="component-footer">
      {{yield-section 'footer'}} {{-- Places footer section in here --}}
    </div>
  {{else}}
    <p>Just some generic content in case nothing else was specified.</p>
  {{/if}}
  
</div>

Is this close to what’s suggested here? It’s no nested components, but at least it’s a start. I guess it would be fairly trivial to add a context flag to the section helper to make it use the component’s context as well.

You can check out the (very simple) source here: https://gist.github.com/johanobergman/60483a8f49c30f245eef

See you around!

Edit: I realize this actually looks quite a bit like what’s in OP:s jsbin link. Sorry about that.