Transclusion doesn't handle changes to the content and doesn't allow CSS selectors #15330
Description
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:
directive('myDirective', function () {
return {
restrict: 'EA',
scope: {},
bindToController: {
...
},
controllerAs: 'ctrl',
templateUrl: ...,
link: linkfn,
controller: ctrlfn,
transclude: {
'slotA': 'sectionA',
'slotB': 'sectionB'
}
}
function linkfn(scope, elem, attrs, ctrl) {
...
}
function ctrlfn($scope, $element) {
...
}
})
Template for main directive:
...
<section class="..." my-directive-transclude="slotA"></section>
<section class="..." my-directive-transclude="slotB"></section>
...
Subsidiary directive:
directive('myDirectiveTransclude', function () {
return {
restrict: 'A',
require: '^myDirective',
scope: false,
link: function (scope, elem, attrs, ctrl, transclude) {
var slot = attrs.myDirectiveTransclude
transclude(function (clone) {
elem.empty()
elem.append(clone.children())
}, elem, slot)
}
}
})
The page:
...
<my-directive>
<section-a>
content
</section-a>
<section-b>
content
</section-b>
</my-directive>
...
causes
TypeError: Cannot read property 'childNodes' of undefined
at nodeLinkFn (http://.../angular.js:9330:58)
at compositeLinkFn (http://.../angular.js:8620:13)
at compositeLinkFn (http://.../angular.js:8623:13)
at compositeLinkFn (http://.../angular.js:8623:13)
at publicLinkFn (http://.../angular.js:8500:30)
at lazyCompilation (http://.../angular.js:8844:25)
at boundTranscludeFn (http://.../angular.js:8637:16)
at controllersBoundTransclude (http://.../angular.js:9377:22)
at Object.link (http://.../my-script.js:##:##)
at http://.../angular.js:1247:18
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).
- Angular version: 1.5.8
- Browser: IE 11, Chrome 52 (I don't support anything else).
- Language: ES5
Activity
gkalpak commentedon Oct 29, 2016
Can you create a live reproduction (e.g. using CodePen, Plnkr etc), so we can investigate this further?
firelizzard18 commentedon Oct 29, 2016
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 anyelementSelector
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.gkalpak commentedon Oct 30, 2016
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.: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()
orElement#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 asjQuery#is()
orElement#matches()
) that they know are available on the environements they support.Example:
firelizzard18 commentedon Oct 31, 2016
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 afterslotMap
's initialization.I made the following patch to
forEach($compileNode.contents(), function(node) {
:And in
forEach(directiveValue, function(elementSelector, slotName) {
, I replaced this:With this:
Here's the full diff:
gkalpak commentedon Nov 1, 2016
Glad "async append" works for you 😃
Would you like to open a PR with the matcher/selector function feature and continue the discussion there?