Open firelizzard18 opened 7 years ago
Can you create a live reproduction (e.g. using CodePen, Plnkr etc), so we can investigate this further?
http://plnkr.co/edit/xPQG5YJvT5JhHtZUvTTY?p=preview
I made a patch to $compile on my server. If there's no slotName
that matches the normalized tag name, then I check for any elementSelector
s that start with $
. I treat everything after the $
as a CSS selector and check if $(node).is(elementSelector.substring(1))
. This is not exactly what I want, because I still have an extra element in my DOM, but at least I don't have any bogus non-html elements present.
I see. The problem is that linking makes certain assumptions regarding the structure of the compiled DOM. What will work (afaict) is appending clone.children()
asynchronously; i.e. after the linking phase. E.g.:
// Change:
elem.append(clone.children());
// To:
scope.$evalAsync(function() { elem.append(clone.children()); });
Here is a simplified demo, but it should work on your demo too.
There is one minor caveat though: Breaking the synchronous nature of linking might break other directives' assumptions. This is an edge-case and unlikely to affect any real usecases (especially on directives using templateUrl
, which are already asynchronous), but it is not impossible.
Does this work for you?
BTW, the subject of allowing more flexibility in matching elements to be transcluded (e.g. matching against attributes or classes as well) has been discussed when the multi-slot transclusion was implemented. Since we can't rely on features like jQuery#is()
or Element#matches()
, adding more flexibility would also increase the complexity. Therefore, we had decided to implement the simple, less flexible nodeName matching only and consider adding more flexibility when/if specific usecases arise.
Off the top of head, trying to avoid adding too much complexity and loc for a feature that will only benefit few people, the following might be a good compromize:
Allow the values of the transclude
property to also be "matcher" functions (apart from a normalized nodeName). Each matcher would be called for every node transcluded node (that isn't matched against another slot) and deciding whether the node is a match for the corresponding slot (see example below). This way, the user would be able to use any criterion they wish and also use features (such as jQuery#is()
or Element#matches()
) that they know are available on the environements they support.
Example:
// Definition of `myComponent`
.component('myComponent', {
template: `
<section ng-transclude="slotA"></section>
<section ng-transclude="slotB"></section>
`,
transclude: {
slotA: node => node.hasAttribute('slot-a'),
slotB: node => node.classList.contains('slot-b')
}
})
<!-- Usage of `myComponent` -->
<my-component>
<header slot-a>Slot A</header>
<header class="slot-b">Slot B</header>
<footer slot-a>...A...</footer>
<footer class="slot-b">...B...</footer>
</my-component>
<!-- Resulting HTML -->
<my-component>
<section ng-transclude="slotA">
<header slot-a>Slot A</header>
<footer slot-a>...A...</footer>
</section>
<section ng-transclude="slotB">
<header class="slot-b">Slot B</header>
<footer class="slot-b">...B...</footer>
</section>
</my-component>
Awesome, thanks! The async append works perfectly, and I think the idea of using a function selector is great! And it certainly covers my usage.
I added var selectorMap = createMap();
immediately after slotMap
's initialization.
I made the following patch to forEach($compileNode.contents(), function(node) {
:
for (var slotName in selectorMap) {
var elementSelector = selectorMap[slotName];
if (!angular.isFunction(elementSelector))
continue;
if (!elementSelector(node))
continue;
filledSlots[slotName] = true;
slots[slotName] = slots[slotName] || [];
slots[slotName].push(node);
}
And in forEach(directiveValue, function(elementSelector, slotName) {
, I replaced this:
var optional = (elementSelector.charAt(0) === '?');
elementSelector = optional ? elementSelector.substring(1) : elementSelector;
slotMap[elementSelector] = slotName;
With this:
var optional = false;
if (angular.isString(elementSelector)) {
optional = (elementSelector.charAt(0) === '?');
elementSelector = optional ? elementSelector.substring(1) : elementSelector;
slotMap[elementSelector] = slotName;
} else if (angular.isFunction(elementSelector)) {
optional = !!elementSelector.optional;
selectorMap[slotName] = elementSelector;
}
Here's the full diff:
diff --git a/.../angular.js b/.../angular.js
index abcdef..012346 100644
--- a/.../angular.js
+++ b/.../angular.js
@@ -9017,15 +9017,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
$template = [];
var slotMap = createMap();
+ var selectorMap = createMap();
var filledSlots = createMap();
// 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;
+ // PATCH START
+ var optional = false
+ if (angular.isString(elementSelector)) {
+ optional = (elementSelector.charAt(0) === '?');
+ elementSelector = optional ? elementSelector.substring(1) : elementSelector;
- slotMap[elementSelector] = slotName;
+ slotMap[elementSelector] = slotName;
+ } else if (angular.isFunction(elementSelector)) {
+ optional = !!elementSelector.optional;
+
+ selectorMap[slotName] = elementSelector;
+ }
+ // PATCH END
// 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
@@ -9039,6 +9049,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// Add the matching elements into their slot
forEach($compileNode.contents(), function(node) {
+ // PATCH START
+ for (var slotName in selectorMap) {
+ var elementSelector = selectorMap[slotName];
+ if (!angular.isFunction(elementSelector))
+ continue;
+ if (!elementSelector(node))
+ continue;
+ filledSlots[slotName] = true;
+ slots[slotName] = slots[slotName] || [];
+ slots[slotName].push(node);
+ }
+ // PATCH END
var slotName = slotMap[directiveNormalize(nodeName_(node))];
if (slotName) {
filledSlots[slotName] = true;
Glad "async append" works for you :smiley: Would you like to open a PR with the matcher/selector function feature and continue the discussion there?
Do you want to request a feature or report a bug? Mostly a feature, kind of a bug
What is the current behavior? Content is transcluded in the way I expect, but an error is thrown and angular doesn't properly bind to the transcluded content.
Minimal reproduction of the problem with instructions Main directive:
Template for main directive:
Subsidiary directive:
The page:
causes
What is the expected behavior? Content is transcluded in the way I expect and everything works.
What is the motivation / use case for changing the behavior? I want to be able to transclude section-a and section-b into my-directive, but I want real tags and I don't want things nested obnoxiously.
Please tell us about your environment: Visual Studio 2013, Nuget, Windows 10, IIS 7.5 (or whatever comes with win 10).