angular / angular.js

AngularJS - HTML enhanced for web apps!
https://angularjs.org
MIT License
58.84k stars 27.51k forks source link

Muli-slot with transclude: 'element' behavior #14244

Open JordanFriendshuh opened 8 years ago

JordanFriendshuh commented 8 years ago

I was wondering if there was any support for having element style transclusion for the multi-slot. What I mean by that is when you do a singular transclusion you can do "translcude: 'element'" instead of "transclude: true" to get different non-wrapping behavior. Is there any support for the element behavior with multi-slot?

Narretz commented 8 years ago

Hmm, I'm not sure how this would work. transclude: element takes the whole element and makes it available to the template. How would that work with slots? Because you have different elements if you have different slots. And transcluding the whole slot doesn't make much sense to me, as it's only used to idenfify the slot.

devinlyons commented 8 years ago

@JordanFriendshuh Are you suggesting that they re-introduce their replace feature in transclusion? If so, I agree. I tried to wrap bootstrap's form-group with feedback into a component with multi-slot transclusion and it won't work because the ng-transclude wrapper breaks the expected HTML structure for the CSS.

Here's the code I'm working with:

feedbackInput.html:

<div class="form-group has-feedback" data-ng-class="{'has-success': (form.$submitted || form[field].$dirty) && form[field].$valid, 'has-error': (form.$submitted || form[field].$dirty) && form[field].$invalid}">
    <ng-transclude ng-transclude-slot="label"></ng-transclude>
    <ng-transclude ng-transclude-slot="input"></ng-transclude>
    <span class="glyphicon glyphicon-remove form-control-feedback" data-ng-show="(form.$submitted || form[field].$dirty) && form[field].$invalid"></span>
    <span class="glyphicon glyphicon-ok form-control-feedback" data-ng-show="(form.$submitted || form[field].$dirty) && form[field].$valid"></span>
    <div ng-transclude data-ng-messages="form[field].$error" data-ng-show="form[field].$dirty || form.$submitted" role="alert"></div>
</div>
angular.module('Forms', ['ngMessages'])
    .component('feedbackInput', {
        templateUrl: '/app/templates/feedbackInput.html',
        require: {
            formCtrl: '^form'
        },
        transclude: {
            'input': 'input',
            'label': '?label'
        },
        //replace: true,
        controller: ['$scope', '$element', '$timeout', function ($scope, $element, $timeout) {
            $scope.field = '';

            this.$onInit = function () {
                $scope.form = this.formCtrl;
            }

            $timeout(function () {
                $scope.field = $element.find('input').attr('name');
            });
        }]
    });

And here's the component in action:

<feedback-input>
    <label for="orderNumber" class="required"> Order #</label>
    <input required type="number" class="form-control" id="orderNumber" name="orderNumber" data-ng-model="form.orderNumber" />
    <div data-ng-message="required" class="text-danger slide-appear">This field is required.</div>
</feedback-input>

But is doesn't work because bootstrap expects the elements with the "form-control-feedback" class to be at the same level as the input. It can't be at that level because the span with the "ng-transclude" attribute remains. The "glyphicon-remove" images ends up appearing above the textbox. I would need AngularJS to remove the span and replace it with the input from the component.

tilwinjoy commented 8 years ago

Looks like this is what I'm trying to do as well... And I'm also trying to create directives for bootstrap components.

What I'm trying to do is, let the user provide optional transclude slot contents, and if the user provides content of a slot, then it replaces that part of the template. This is much required because otherwise it breaks the CSS.

For example, suppose I have the following template:

<div class="my-custom-container">
   <label class="my-custom-label">Fallback</label>
   <input class="my-custom-input"/>
</div>

for <my-custom-directive></<my-custom-directive> directive,

If the user uses it like

<my-custom-directive>
   <my-custom-label class="users-custom-class"><span>Custom content</span><my-custom-label>
</<my-custom-directive>

I want it to expand as:

<div class="my-custom-container">
   <label class="my-custom-label users-custom-class"><span>Custom content</span></label>
  <!--  <my-custom-label> merged with it's tranclude slot parent like a replace:true directive does. Good :) -->
   <input class="my-custom-input"/>
</div>

After playing around with the available options I was only able to create something like

<div class="my-custom-container">
   <label class="my-custom-label">
       <label  class="users-custom-class"><span>Custom content</span></label>
    </label>
   <input class="my-custom-input">
        <input class="users-custom-class"/> <!--  everything goes inside the tranclude holder, bad :( -->
   </input>
</div>

here's a stackoverflow question I asked regarding this

Narretz commented 8 years ago

I'm not a fan of adding a replace functionality to the slot transclusion. It would introduced the same problems we have with normal "replace" and transclude: element. I also think that bootstrap needs to relax its styling rules. But you can actually do this manually if your really need it: http://plnkr.co/edit/vBEmbo65jB3Jug2L4Ynj?p=preview

tilwinjoy commented 8 years ago

@Narretz Yeah I figured I could do that and had posted that as an answer to my own question a while ago. It includes replace:true as well so that it copies all the attributes.

I'm not sure what all are the problems you're referring to, still it'll be great to have these features built in. But if that is not at all possible and replace:true option is going to be deprecated in future, it'll be nice to at least have the services that copes the attributes etc that the developer can use if the problems you were referring to doesn't affect his use case at his own risk.

Narretz commented 8 years ago

Most problems would exactly come from merging attributes, especially angular expressions. This can get complex very fast. We currently don't special case ngClass / ngStyle / ngEvent directives, and they can break easily.

adgoncal commented 8 years ago

I think support for "element" style transclusion on multi-slot transclusion should be available.

Suppose the template for a component <list></list> looks like this:

<ul ng-transclude="some-items"></ul>
<hr/>
<ul ng-transclude="other-items"></ul>

Currently, there is no way to pass <li></li> elements directly to the <ul></ul>. For example:

<list>
  <some-items>
    <li>item1</li>
    <li>item2</li>
  </some-items>
  <other-items>
    <li>item1</li>
    <li>item2</li>
  </other-items>
</list>

Would become:

<ul ng-transclude="some-items">
  <some-items>
    <li>item1</li>
    <li>item2</li>
  </some-items>
</ul>
<hr/>
<ul ng-transclude="other-items">
  <other-items>
    <li>item1</li>
    <li>item2</li>
  </other-items>
</ul>

But the desired outcome is:

<ul ng-transclude="some-items">
  <li>item1</li>
  <li>item2</li>
</ul>
<hr/>
<ul ng-transclude="other-items">
  <li>item1</li>
  <li>item2</li>
</ul>

One way to solve this is to allow transclusion based on attributes. For example:

<list>
  <ul>
    <li some-items>item1</li>
    <li some-items>item2</li>
  </ul>

  <ul>
    <li other-items>item1</li>
    <li other-items>item2</li>
  </ul>
</list>

Alternatively, an attribute to indicate that only the children should be transcluded (transclude-contents)

<list>
  <some-items transclude-contents>
    <li>item1</li>
    <li>item2</li>
  </some-items>
  <other-items transclude-contents>
    <li>item1</li>
    <li>item2</li>
  </other-items>
</list>

This would allow easier interaction with bootstrap as well. Suppose there is a need to transclude dropdown elements in a <main-navbar></main-navbar> component:

Template:

<nav class="navbar navbar-fixed-top">
  <div class="container-fluid">
    <header class="navbar-header pull-left">
        <div uib-dropdown="" is-open="$ctrl.isOpen" class="pull-left">
          <button type="button" class="btn btn-default navbar-btn" uib-dropdown-toggle>
            <span class="sr-only">Toggle dropdown menu</span>
            <i class="fa fa-lg fa-fw fa-bars"></i>
          </button>
          <ul class="uib-dropdown-menu" role="menu" ng-transclude="dropdown"></ul>
        </div>
    </header>
  </div>
</nav>

Using it:

<main-navbar>
  <dropdown transclude-contents>
    <li>item1</li>
    <li>item2</li>
  </dropdown>
</main-navbar>