Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat($compile): multiple transclusion via named slots
Browse files Browse the repository at this point in the history
Now you can efficiently split up and transclude content into specified
places in a component's template.

```html
<pane>
  <pane-title>Some content for slot A</pane-title>
  <pane-content>Some content for slot A</pane-content>
</component>
```

```js
mod.directive('pane', function() {
  return {
    restrict: 'E',
    transclude: { paneTitle: '?titleSlot', paneContent: 'contentSlot' },
    template:
    '<div class="pane">' +
      '<h1 ng-transclude="titleSlot"></h1>' +
      '<div ng-transclude="contentSlot"></div>' +
    '</div>' +
  };
});
```

Closes #4357
Closes #12742
Closes #11736
Closes #12934
  • Loading branch information
petebacondarwin committed Oct 12, 2015
1 parent 40c974a commit a4ada8b
Show file tree
Hide file tree
Showing 3 changed files with 421 additions and 9 deletions.
68 changes: 66 additions & 2 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
});
};

// We need to attach the transclusion slots onto the `boundTranscludeFn`
// so that they are available inside the `controllersBoundTransclude` function
var boundSlots = boundTranscludeFn.$$slots = createMap();
for (var slotName in transcludeFn.$$slots) {
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
}

return boundTranscludeFn;
}

Expand Down Expand Up @@ -1821,9 +1828,56 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nonTlbTranscludeDirective: nonTlbTranscludeDirective
});
} else {

var slots = createMap();
$template = jqLite(jqLiteClone(compileNode)).contents();

if (isObject(directiveValue)) {

// We have transclusion slots - collect them up and compile them and store their
// transclusion functions
$template = [];
var slotNames = createMap();
var filledSlots = createMap();

// Parse the slot names: if they start with a ? then they are optional
forEach(directiveValue, function(slotName, key) {
var optional = (slotName.charAt(0) === '?');
slotName = optional ? slotName.substring(1) : slotName;
slotNames[key] = slotName;
slots[slotName] = [];
// filledSlots contains `true` for all slots that are either optional or have been
// filled. This is used to check that we have not missed any required slots
filledSlots[slotName] = optional;
});

// Add the matching elements into their slot
forEach($compileNode.children(), function(node) {

This comment has been minimized.

Copy link
@ggoodman

ggoodman Dec 2, 2015

Contributor

The use of .children() appears to have the side-effect that any non-HTMLElement nodes are skipped and therefore dropped from the default transclusion slot. Attn: @petebacondarwin.

This comment has been minimized.

Copy link
@ggoodman

ggoodman Dec 2, 2015

Contributor

I changed .children() to .contents() instead and the non-element nodes are properly transcluded and no errors are reported. I have not run tests.

This comment has been minimized.

Copy link
@petebacondarwin

petebacondarwin Dec 2, 2015

Author Contributor

Yes, let's fix that

var slotName = slotNames[directiveNormalize(nodeName_(node))];
var slot = $template;
if (slotName) {
filledSlots[slotName] = true;
slots[slotName].push(node);
} else {
$template.push(node);
}
});

// Check for required slots that were not filled
forEach(filledSlots, function(filled, slotName) {
if (!filled) {
throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName);
}
});

forEach(Object.keys(slots), function(slotName) {
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
});
}

$compileNode.empty(); // clear contents
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn);
childTranscludeFn.$$slots = slots;
}
}

Expand Down Expand Up @@ -2130,11 +2184,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {

// This is the function that is injected as `$transclude`.
// Note: all arguments are optional!
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) {
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
var transcludeControllers;

// No scope passed in:
if (!isScope(scope)) {
slotName = futureParentElement;
futureParentElement = cloneAttachFn;
cloneAttachFn = scope;
scope = undefined;
Expand All @@ -2146,6 +2200,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!futureParentElement) {
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
}
if (slotName) {
var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
if (!slotTranscludeFn) {
throw $compileMinErr('noslot',
'No parent directive that requires a transclusion with slot name "{0}". ' +
'Element: {1}',
slotName, startingTag($element));
}
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
}
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
}
}
Expand Down
73 changes: 66 additions & 7 deletions src/ng/directive/ngTransclude.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@
* @description
* Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
*
* You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name
* as the value of the `ng-transclude` or `ng-transclude-slot` attribute.
*
* Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.
*
* @element ANY
*
* @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided or empty then
* the default slot is used.
*
* @example
<example module="transcludeExample">
* ### Default transclusion
* This example demonstrates simple transclusion.
<example name="simpleTranscludeExample" module="transcludeExample">
<file name="index.html">
<script>
angular.module('transcludeExample', [])
Expand Down Expand Up @@ -53,21 +61,72 @@
</file>
</example>
*
*/
* @example
* ### Multi-slot transclusion
<example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample">
<file name="index.html">
<div ng-controller="ExampleController">
<input ng-model="title" aria-label="title"> <br/>
<textarea ng-model="text" aria-label="text"></textarea> <br/>
<pane>
<pane-title><a ng-href="{{link}}">{{title}}</a></pane-title>
<pane-body><p>{{text}}</p></pane-body>
</pane>
</div>
</file>
<file name="app.js">
angular.module('multiSlotTranscludeExample', [])
.directive('pane', function(){
return {
restrict: 'E',
transclude: {
'paneTitle': '?title',
'paneBody': 'body'
},
template: '<div style="border: 1px solid black;">' +
'<div ng-transclude="title" style="background-color: gray"></div>' +
'<div ng-transclude="body"></div>' +
'</div>'
};
})
.controller('ExampleController', ['$scope', function($scope) {
$scope.title = 'Lorem Ipsum';
$scope.link = "https://google.com";
$scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
}]);
</file>
<file name="protractor.js" type="protractor">
it('should have transcluded the title and the body', function() {
var titleElement = element(by.model('title'));
titleElement.clear();
titleElement.sendKeys('TITLE');
var textElement = element(by.model('text'));
textElement.clear();
textElement.sendKeys('TEXT');
expect(element(by.binding('title')).getText()).toEqual('TITLE');
expect(element(by.binding('text')).getText()).toEqual('TEXT');
});
</file>
</example> */
var ngTranscludeMinErr = minErr('ngTransclude');
var ngTranscludeDirective = ngDirective({
restrict: 'EAC',
link: function($scope, $element, $attrs, controller, $transclude) {

function ngTranscludeCloneAttachFn(clone) {
$element.empty();
$element.append(clone);
}

if (!$transclude) {
throw minErr('ngTransclude')('orphan',
throw ngTranscludeMinErr('orphan',
'Illegal use of ngTransclude directive in the template! ' +
'No parent directive that requires a transclusion found. ' +
'Element: {0}',
startingTag($element));
}

$transclude(function(clone) {
$element.empty();
$element.append(clone);
});
$transclude(ngTranscludeCloneAttachFn, null, $attrs.ngTransclude || $attrs.ngTranscludeSlot);
}
});

Loading

11 comments on commit a4ada8b

@campersau
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want to note that there are two mistakes in the commit message which might get into the changelog:

  • </component> should be </pane>
<pane>
  <pane-title>Some content for slot A</pane-title>
  <pane-content>Some content for slot A</pane-content>
</component> <!-- here -->
  • Trailing + after </div>
mod.directive('pane', function() {
  return {
    restrict: 'E',
    transclude: { paneTitle: '?titleSlot', paneContent: 'contentSlot' },
    template:
    '<div class="pane">' +
      '<h1 ng-transclude="titleSlot"></h1>' +
      '<div ng-transclude="contentSlot"></div>' +
    '</div>' + // here
  };
});

@petebacondarwin
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! @campersau - I will make sure we catch those on the changelog.

@schmod
Copy link
Contributor

@schmod schmod commented on a4ada8b Oct 20, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 This has been a long time coming. Awesome work!

@filipbech
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cant get this to work with current 1.5.0 beta1... https://code.angularjs.org/1.5.0-beta.1/angular.js
(maybe I should make a seperate issue? - or maybe its just not in yet?)

Made a simple codepen to show off: http://codepen.io/filipbech/pen/pjVvNM

It just transcludes both slot-contents into both slots...

@petebacondarwin
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature is not in 1.5.0-beta.1.
See http://codepen.io/anon/pen/NGMGYY, which uses the master build

@filipbech
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect. Thanks!
Any guess how far away 1.5 is?

@petebacondarwin
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Days or weeks at most

@igorhart
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petebacondarwin Amazing work! It covers every use case I could come up with!
Also, I really love the fact that it's very easy to add new slots on top of existing directives without creating "default" slots.

@wesleycho
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally! This solves one of our biggest pains in UI Bootstrap, not being able to pass through multiple content ala Web Components!

@guygit
Copy link

@guygit guygit commented on a4ada8b Dec 29, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,

using "Multi-slot" transclusion and the new "component()" function (Ng version 1.5.0-rc.0)

there seems to be no way for the inner component to use the API of the outer component:

<outer>
   <inner on-close="outerclose()"></inner>
</outer>

... where "outer" component (NG 1.5 component) exposes an API "outerclose()" to it's nested directive.

The only way I could coax it into work was something like that:

<outer vm="vm">
   <inner on-close="vm.outer.outerclose"></inner>
</outer>
function OuterCtrl () {
   var outer = this;
   // exposing the API
   outer.outerclose = function () { blah };
   // Trick, extending the surrounding scope (vm) of <outer> with outer's API
   outer.vm.outer = outer;
}

But is not the right approach I guess - however how is supposed to access API then?
Thank you!
guygit

@petebacondarwin
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@guygit - could you create an issue to discuss this and reference me in it so that I notice it.

Please sign in to comment.