From d70554f8810aaee0c79cda5d853706573d52cf8b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sun, 27 Sep 2015 23:15:46 +0100 Subject: [PATCH] feat($compile): multiple transclusion via named slots --- src/ng/compile.js | 68 +++++++- src/ng/directive/ngTransclude.js | 73 +++++++- test/ng/compileSpec.js | 289 +++++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+), 9 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index 393ab6375a27..58308991f334 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1486,6 +1486,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; } @@ -1827,9 +1834,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) { + 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; } } @@ -2134,11 +2188,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; @@ -2150,6 +2204,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); } } diff --git a/src/ng/directive/ngTransclude.js b/src/ng/directive/ngTransclude.js index 36201a69ab73..ec31eaf5fc23 100644 --- a/src/ng/directive/ngTransclude.js +++ b/src/ng/directive/ngTransclude.js @@ -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 - + * ### Default transclusion + * This example demonstrates simple transclusion. +