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

feat(ngTransclude & $compile): fix treatment of unfilled optional slots #13431

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 76 additions & 30 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,18 +226,21 @@
* * `$element` - Current element
* * `$attrs` - Current attributes object for the element
* * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
* `function([scope], cloneLinkingFn, futureParentElement)`.
* * `scope`: optional argument to override the scope.
* * `cloneLinkingFn`: optional argument to create clones of the original transcluded content.
* * `futureParentElement`:
* `function([scope], cloneLinkingFn, futureParentElement, slotName)`:
* * `scope`: (optional) override the scope.
* * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content.
* * `futureParentElement` (optional):
* * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
* * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
* * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
* and when the `cloneLinkinFn` is passed,
* as those elements need to created and cloned in a special way when they are defined outside their
* usual containers (e.g. like `<svg>`).
* * See also the `directive.templateNamespace` property.
*
* * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`)
* then the default translusion is provided.
* The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns
* `true` if the specified slot contains content (i.e. one or more DOM nodes).
*
* #### `require`
* Require another directive and inject its controller as the fourth argument to the linking function. The
Expand Down Expand Up @@ -337,14 +340,6 @@
* The contents are compiled and provided to the directive as a **transclusion function**. See the
* {@link $compile#transclusion Transclusion} section below.
*
* There are two kinds of transclusion depending upon whether you want to transclude just the contents of the
* directive's element or the entire element:
*
* * `true` - transclude the content (i.e. the child nodes) of the directive's element.
* * `'element'` - transclude the whole of the directive's element including any directives on this
* element that defined at a lower priority than this directive. When used, the `template`
* property is ignored.
*
*
* #### `compile`
*
Expand Down Expand Up @@ -474,6 +469,30 @@
* Testing Transclusion Directives}.
* </div>
*
* There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
* directive's element, the entire element or multiple parts of the element contents:
*
* * `true` - transclude the content (i.e. the child nodes) of the directive's element.
* * `'element'` - transclude the whole of the directive's element including any directives on this
* element that defined at a lower priority than this directive. When used, the `template`
* property is ignored.
* * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
*
* **Mult-slot transclusion** is declared by providing an object for the `transclude` property.
*
* This object is a map where the keys are the name of the slot to fill and the value is the element selector
* used to match the HTML to the slot. Only element names are supported for matching. If the element selector
* is prefixed with a `?` then that slot is optional.
*
* For example, the transclude object `{ slotA: '?my-custom-element' }` maps `<my-custom-element>` elements to
* the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive.
*
* Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements
* in the transclude content. If you wish to know if an optional slot was filled with content, then you can call
* `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and
* injectable into the directive's controller.
*
*
* #### Transclusion Functions
*
* When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion
Expand Down Expand Up @@ -1511,7 +1530,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// 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);
if (transcludeFn.$$slots[slotName]) {
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
} else {
boundSlots[slotName] = null;
}
}

return boundTranscludeFn;
Expand Down Expand Up @@ -1855,32 +1878,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} 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
// We have transclusion slots,
// collect them up, compile them and store their transclusion functions
$template = [];
var slotNames = createMap();

var slotMap = 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] = [];
// Parse the element selectors
forEach(directiveValue, function(elementSelector, slotName) {
// If an element selector starts with a ? then it is optional
var optional = (elementSelector.charAt(0) === '?');
elementSelector = optional ? elementSelector.substring(1) : elementSelector;

slotMap[elementSelector] = slotName;

// We explicitly assign `null` since this implies that a slot was defined but not filled.
// Later when calling boundTransclusion functions with a slot name we only error if the
// slot is `undefined`
slots[slotName] = null;

// 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.contents(), function(node) {
var slotName = slotNames[directiveNormalize(nodeName_(node))];
var slotName = slotMap[nodeName_(node)];
if (slotName) {
filledSlots[slotName] = true;
slots[slotName] = slots[slotName] || [];
slots[slotName].push(node);
} else {
$template.push(node);
Expand All @@ -1894,9 +1927,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});

forEach(Object.keys(slots), function(slotName) {
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
});
for (var slotName in slots) {
if (slots[slotName]) {
// Only define a transclusion function if the slot was filled
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
}
}
}

$compileNode.empty(); // clear contents
Expand Down Expand Up @@ -2125,6 +2161,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
transcludeFn = controllersBoundTransclude;
transcludeFn.$$boundTransclude = boundTranscludeFn;
// expose the slots on the `$transclude` function
transcludeFn.isSlotFilled = function(slotName) {
return !!boundTranscludeFn.$$slots[slotName];
};
}

if (controllerDirectives) {
Expand Down Expand Up @@ -2221,16 +2261,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
}
if (slotName) {
// slotTranscludeFn can be one of three things:
// * a transclude function - a filled slot
// * `null` - an optional slot that was not filled
// * `undefined` - a slot that was not declared (i.e. invalid)
var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
if (!slotTranscludeFn) {
if (slotTranscludeFn) {
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
} else if (isUndefined(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);
} else {
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
}
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
}
}
}
Expand Down
Loading