Portals and options.parent
If the LocationStack is missing some elements, although they have been correctly tagged, the most likely cause is the use of Portal-like methods to render their contents, e.g. they render part of their templates in an arbitrary location of the DOM.
Because the Tracker follows the DOM upwards, to reconstruct where an Interaction occurred, this will lead to incorrect Locations.
Example scenario
Here is a simple example:
<div [tagContent]="{ id: 'search' }" class="search">
Search: <input [tagInput]="{ id: 'search-input' }" type="text"/>
<search-results
[tagOverlay]="{ id: 'search-results' }"
class="search-results"
>
<search-result *ngFor="let searchResult of searchResults">
<div
[tagPressable]="{ id: 'search-result-' + searchResult.id }"
(click)="selectResult(searchResult, $event)"
class="search-result"
>
{{ searchResult.text }}
</div>
</search-result>
</search-results>
</div>
DOM without portals
If search-results
did not use portals, this would be the final DOM after running a search:
<html> Location Tree
<body> RootLocationContext: home
<div class="search"> └─ ContentContext: search
Search: <input type="text"/> ├─ InputContext: search-input
<div class="search-results"> └─ OverlayContext: search-results
<div class="search-result"> ├─ PressableContext: search-result-1
search result 1 │
</div> │
<div class="search-result"> ├─ PressableContext: search-result-2
search result 2 │
</div> │
<div class="search-result"> └─ PressableContext: search-result-3
search result 3
</div>
</div>
</div>
</body>
</html>
Everything works fine: the Tracker can easily navigate from a result upwards and collect the correct Location, e.g. search-results
is a child of search
and a sibling of search-input
.
DOM with portals
Let's assume that search-results
, instead, uses a Portal to render its results.
This is how the above template may result in the DOM (oversimplified):
<html> Location Tree
<body> RootLocationContext: home
<div class="search"> ├─ ContentContext: search
Search: <input type="text"/> │ └─ InputContext: search-input
</div> │
<div class="overlay-container"> │
<div id="search-overlay-container"> │
<div class="search-results"> └─ OverlayContext: search-results
<div class="search-result"> ├─ PressableContext: search-result-1
search result 1 │
</div> │
<div class="search-result"> ├─ PressableContext: search-result-2
search result 2 │
</div> │
<div class="search-result"> └─ PressableContext: search-result-3
search result 3
</div>
</div>
</div>
</div>
</body>
</html>
As you can see search-results
is not a child of the search <div>
anymore.
It has been rendered somewhere else and actually, from a DOM point of view, it's now a sibling of the search div.
When the Tracker attempts to track a click on one of the results, it will produce an incorrect LocationStack.
options.parent
to cross portals
All location taggers allow overriding the parent Tagged Element via the parent
attribute of their options.
This basically tells the Tracker to ignore the DOM tree and instead follow the Location Tagging Attributes we specify.
The whole process revolves around three steps:
- Factor Tagging Attributes for the parent
applyTaggingAttributes
on parent- Set
options.parent
of the child
Factor Tagging Attributes for the parent
Instead of simply tagging the parent Element in the template, we have to go back to the Component and pre-factor its Location Tagging Attributes.
There's no strict way of doing so, in the following example we are going to use a public variable and ngInit
.
@Component({
selector: 'search-component',
templateUrl: './component.html'
})
export class SearchComponent implements OnInit {
public searchContentTag: TagLocationReturnValue;
public ngOnInit(): void {
this.searchContentTag = tagContent({
id: 'search'
});
}
}
applyTaggingAttributes
on parent
In our example template we had instrumented the search-result overlay like so:
<div [tagContent]="{ id: 'search' }" class="search">
Now that we have pre-factored its Tagging Attributes, we can instead apply them like this:
<div [applyTaggingAttributes]="searchContentTag" class="search">
This achieves the same exact result, but now we have as set of TaggingAttributes referring to the search
div.
Set options.parent
of the child
Last step: we have to link the portaled search-result
div to the search
div we just instrumented.
To do we are going to modify the overlay tagging from this:
<search-results
[tagOverlay]="{ id: 'search-results' }"
class="search-results"
>
To this:
<search-results
[tagOverlay]="{ id: 'search-results', options: { parent: searchContentTag } }"
class="search-results"
>
Angular Tracker now knows that search-results
has search
as a parent, regardless of where it resides in the DOM.
Here is the final template:
<div [applyTaggingAttributes]="searchContentTag" class="search">
Search: <input [tagInput]="{ id: 'search-input' }" type="text"/>
<search-results
[tagOverlay]="{ id: 'search-results', options: { parent: searchContentTag } }"
class="search-results"
>
<search-result *ngFor="let searchResult of searchResults">
<div
[tagPressable]="{ id: 'search-result-' + searchResult.id }"
(click)="selectResult(searchResult, $event)"
class="search-result"
>
{{ searchResult.text }}
</div>
</search-result>
</search-results>
</div>