10

I would like to use an anchor-element with a hash-URL inside of a custom element that uses shadow DOM. I would expect, that the browser scrolls down to that anchor, but it does not do it (at least Chrome).

Detail:

I have an index.html like this:

...
<a href="#destinationInsideShadowDOM">Jump</a>
...
<my-custom-element></my-custom-element>
...

And another html-file for the custom-element, which contains the anchor:

<template id="my-custom-element">
   ...
   <a id="destinationInsideShadowDOM"></a>
   ...
</template>

I want the browser to scroll down to that anchor when I click on the link in index.html.

When I move the anchor into index.html, outside of the custom-element, it works, but not inside.

treeno
  • 2,510
  • 1
  • 20
  • 36
  • 3
    By definition Shadow Dom isolates it's content from the main Dom tree for all selections (that is: for CSS but also for querySelector and links). So it' the normal, expected behaviour. You'll have to handle this by JS – Supersharp Apr 15 '17 at 14:38
  • 2
    I understand. I solved it using `scrollIntoView()` – treeno Apr 16 '17 at 15:05
  • I'm still having problems when opening the hashed link in a new browser tab. I'm currently using a scrollIntoView() in a setTimeout inside Polymer callbacks (either ready or connectedCallback), but it only works around half the times. It seems that the template just isn't stamped in the DOM by the time the callback fires. One idea I've had to solve this is to use the MutationObserver API to scroll once the content has loaded, but it seems like overkill. – Hubert Feb 08 '18 at 07:59
  • I use vanilla custom Elements without polymer, so maybe there are some differences that I don't know, but using a timeout should't be nessecary. Did you try to use the WebComponentsReady-Event? That is fired by the polyfills that are used by polymer. See https://stackoverflow.com/questions/21763690/polymer-and-webcomponentsready-event – treeno Feb 08 '18 at 10:55

2 Answers2

1

Go into the console and type this:

document.getElementById('destinationInsideShadowDOM')

The result will be null because the element with that ID does not belong to the document. It belongs to the shadowRoot of the custom element.

As @treeno mentioned in a comment, you can use Element.scrollIntoView() to get the behavior you want.

Patrick McElhaney
  • 57,901
  • 40
  • 134
  • 167
  • Could you elaborate on how to use `Element.scrollIntoView` in this precise example? This means digging through the shadow DOM in the link "onclick" event handler to access the element that must be scrolled into view? – Eric Burel Apr 04 '23 at 09:13
  • Yes, something like `document.querySelector(‘my-custom-element’).shadowRoot.getElementById(‘destinationInsideShadowDom’).scrollIntoView()` – Patrick McElhaney Apr 05 '23 at 12:12
  • 1
    In practice if I know I’m going to want to move some particular tag in my custom element into view, I’d probably create a method on the custom element to do that. In other words, make it part of the element’s public API and preserve encapsulation so I don’t have to dig into the shadow DOM from outside. And I don’t have to worry that if I refactor I may break someone else’s code. – Patrick McElhaney Apr 05 '23 at 12:18
  • Very interesting, do you have any example/pointer to some docs? I am not sure how to create such a public API, to me the custom element is mostly HTML from an external standpoint, not sure how to add a new executable method to it, apart from triggering custom events. – Eric Burel Apr 06 '23 at 07:02
  • A custom element is defined by a class that extends HTMLElement. You can add methods and properties to that class. It will be easier to answer if you post a new question with a snippet of your code and ask “how can I add methods to the custom element in this code”. – Patrick McElhaney Apr 06 '23 at 13:56
  • I feel like the current question is pretty relevant as a use case actually, and it's not properly answered. I get how to add a method to a custom element, but I don't get how you would properly implement scrolling this way, because the component that receive the click might be a parent of the element that should be scrolled, or they might be totally unrelated. So who calls what when? Currently I dig the shadow DOM and call scrollIntoView but it feels bristle. – Eric Burel Apr 24 '23 at 06:46
  • Yes, linking to the inside of an element's shadow DOM is inherently brittle. You could add a public method to your element like `scrollToMe() { this.shadowRoot.querySelector('#innerElement').scrollIntoView() }`. I could elaborate more in an answer instead of a comment, especially if I knew more details about your specific use case. Why not ask another question? Questions are free. – Patrick McElhaney Apr 26 '23 at 14:24
  • What I don't get is where/how would you call "theElement.scrollToMe()"? In the example provided by OP, you would replace the "a" tag by a button with an "click" handler, but it then how it gets the right element to scroll to? I've posted an answer using Lit below, tracking the element via a ref, but I don't think this would allow to call a method internal to this element (or not sure how if it's actually possible). – Eric Burel Apr 28 '23 at 08:50
  • 1
    Ok after more research, you can just call the element custom methods, so `document.querySelector("my-component").scrollToMe()` would work for instance. – Eric Burel May 16 '23 at 09:12
  • Yes, exactly. I still think it makes sense to ask as a separate question. Even if you answer it yourself. Surface that knowledge to help the next person. :) – Patrick McElhaney May 17 '23 at 15:31
  • Trying not to create dupes, I've searched with a few keywords, I think this one could be relevant: https://stackoverflow.com/questions/73714284/litelement-call-a-method-from-external-web-component I am also trying to look for a place to document this, after a few months journey in the web components world I barely found docs recalling those simple facts or pointings to docs that describe them – Eric Burel May 23 '23 at 08:01
0

Using Lit, a ref allows to keep track of the elements.

// rough pseudo-code
class Parent extends LitElement {

this.refs: Map<Ref>

constructor() {
  this.refs = new Map()
}

render() {
  const elems = this.ids.map((id) => {
    const elemRef = createRef<HTMLElement>()
    return = html`<div id=${id} ${ref(elemRef)}`
  })
  return html`<div>
<button @click=${() => this.refs.get("1").scrollIntoView()}>Scroll<button>
${elems}
</div>`
}

}
  • It's using Lit, not raw web components
  • The parent has to know about the child you want to scroll to. You may end up needing some additional logic if the parent is not aware of those child.
Eric Burel
  • 3,790
  • 2
  • 35
  • 56