Skip to content

Cryptic "TypeError: element2.setAttribute is not a function" when migrating component to Svelte 5 #14582

@hankertrix

Description

@hankertrix

Describe the bug

The video collapsible component below works just fine in Svelte 4, but completely breaks in Svelte 5. The only change to the component is the syntax to pass the props to the component. This is a major blocker as it completely breaks the page that the component is on, resulting in a blank page with no information at all.

Testing the component in the playground, the console says there is a double unmount of the component, but I have no idea why or how.

Reproduction

The svelte component in question:

<!-- The component to create the video collapsible -->
<script lang="ts">
  //

  // The interface for the props passed to the component
  interface Props {
    //

    // The variable to take in the list of videos
    videos:
      | [string, string][]
      | {
          [key: string]: string;
        };

    // The variable to take in the title for the video collapsible
    title?: string;
  }

  // The type of event for the loadEmbeds function
  type LoadEmbedsEvent = MouseEvent & { currentTarget: HTMLElement };

  // Get the video and the title from the props
  let { videos, title = "View the videos" }: Props = $props();

  // Initialise the list of videos
  const listOfVideos: [string, string][] = Array.isArray(videos)
    ? videos
    : Object.entries(videos);

  // The regex to remove everything but the YouTube ID
  const youtubeIdRegex = /^.*\/(?:watch\?v=)?|[?&].+$/g;

  // The regex to remove everything but the YouTube timestamp
  const youtubeTimestampRegex = /^.*t=|s+$/g;

  // The function to run a function once
  function once(fn: ((event: LoadEmbedsEvent) => void) | null) {
    return function (this: typeof fn, event: LoadEmbedsEvent) {
      if (fn) fn.call(this, event);
      fn = null;
    };
  }

  // Function to get the ID of a YouTube video
  function getYoutubeId(youtubeVideoUrl: string) {
    return youtubeVideoUrl.replace(youtubeIdRegex, "").trim();
  }

  // Function to get the timestamp of a YouTube video
  function getYoutubeTimestamp(youtubeVideoUrl: string): number {
    //

    // Gets the string of the timestamp for the YouTube video
    const timestampString = youtubeVideoUrl
      .replace(youtubeTimestampRegex, "")
      .trim();

    // Returns 0 if the timestamp string doesn't exist
    // Otherwise, returns the timestamp string converted to a number
    return timestampString === "" ? 0 : parseInt(timestampString);
  }

  // Function to load all the embeds inside the video collapsible
  // when the collapsible is opened
  function loadEmbeds(e: MouseEvent & { currentTarget: HTMLElement }) {
    //

    // Gets the parent element
    const parentElement = e.currentTarget.parentElement;

    // Exits the function if the parent element is not defined
    if (parentElement == null) return;

    // Gets all the iframe elements in the parent element
    const iframeElements = parentElement.getElementsByTagName("iframe");

    // Iterates over all the iframe elements and set their src attribute
    for (const iframeElement of iframeElements)
      iframeElement.src = iframeElement.dataset.src!;
  }
</script>

<!-- The HTML for the video collapsible -->
<details>
  <summary {title} onclick={once(loadEmbeds)}>
    <div class="text">{listOfVideos.length} videos</div>
    <div class="icon-wrapper">
      <div class="plus-icon">
        <div class="vertical-bar"></div>
        <div class="horizontal-bar"></div>
      </div>
    </div>
  </summary>

  <section class="video-collapsible">
    <!-->

    <!-- Iterates over the list of videos -->
    {#each listOfVideos as [videoInfo, url]}
      {@const youtubeId = getYoutubeId(url)}
      {@const youtubeTimestamp = getYoutubeTimestamp(url)}

      <!-- Display the YouTube embed -->
      <iframe
        data-src={`https://www.youtube-nocookie.com/embed/${youtubeId}?start=${youtubeTimestamp}`}
        src=""
        title={videoInfo}
        frameborder="0"
        allow="clipboard-write; encrypted-media; picture-in-picture; web-share"
        allowfullscreen
      >
        <a href={url} target="_blank" title={videoInfo}>{videoInfo}</a>
      </iframe>
    {/each}
  </section>
</details>

<!-- The styles for the video collapsible -->
<style>
  details {
    --collapsible-label-padding: 1em;
  }

  summary {
    display: flex;
    flex-direction: row;
    align-items: center;
    padding: var(--collapsible-label-padding);
    cursor: pointer;
    background-color: var(--accent-colour);
  }

  summary:hover {
    background-color: var(--accent-hover-colour);
  }

  .icon-wrapper {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: end;
  }

  /* The "+" icon at the end of the video collapsible */
  /* Reference: https://jsfiddle.net/psullivan6/0eL3jezk/ */
  .plus-icon {
    --icon-size: 1.2em;
    --icon-margin: 0.25em;
    --bar-width: 0.25em;

    position: relative;
    width: var(--icon-size);
    height: var(--icon-size);
    justify-self: end;
    margin-right: var(--icon-margin);
    margin-bottom: var(--icon-margin);
  }

  /* Vertical line of the "+" symbol */
  .vertical-bar {
    top: 0;
    left: 50%;
    width: var(--bar-width);
    height: 100%;
    margin-top: 2px;
  }

  /* Horizontal line of the "+" symbol */
  .horizontal-bar {
    top: 50%;
    left: 0;
    height: var(--bar-width);
    width: 100%;
    margin-left: 2px;
  }

  .vertical-bar,
  .horizontal-bar {
    position: absolute;
    background-color: var(--icon-colour);
    transition: rotate var(--animation-timing);
  }

  .video-collapsible {
    padding: 0 var(--collapsible-label-padding);
    background-color: var(--collapsible-background-colour);
  }

  iframe {
    width: 100%;
    height: 100%;
    aspect-ratio: 16/9;
    margin: 2em 0;
  }

  /* Styles for when the collapsible is open */

  details[open] > summary {
    background-color: var(--accent-active-colour);
  }

  details[open] .vertical-bar {
    rotate: 90deg;
  }

  details[open] .horizontal-bar {
    rotate: 180deg;
  }

  /* The styles for mobile devices */
  @media only screen and (max-width: 700px) {
    iframe {
      margin: 1.25em 0;
    }
  }
</style>

Some test data to pass to the component:

let videos = ["4K Sample Video | Alpha 6700 | Sony | α", "https://youtu.be/O5O3yK8DJCc"];

Full repository (navigate to the skate recommendations page):
https://github.com/hankertrix/Inline-Skate-Info/tree/migrate-to-svelte-5

Logs

Uncaught TypeError: element2.setAttribute is not a function

  in {expression}
  in VideoCollapsible.svelte
  in TricksSection.svelte
  in TricksPage.svelte
  in +page.svelte
  in PagefindHighlightLoader.svelte
  in +layout.svelte
  in root.svelte
    set_attribute http://localhost:5173/node_modules/.vite/deps/chunk-PGMNWDSX.js?v=8f47ecb8:1452
    VideoCollapsible http://localhost:5173/src/lib/components/general/VideoCollapsible.svelte:135
    update_reaction http://localhost:5173/node_modules/.vite/deps/chunk-IXLKCLCE.js?v=8f47ecb8:1830
    update_effect http://localhost:5173/node_modules/.vite/deps/chunk-IXLKCLCE.js?v=8f47ecb8:1921
    create_effect http://localhost:5173/node_modules/.vite/deps/chunk-IXLKCLCE.js?v=8f47ecb8:1242
    block http://localhost:5173/node_modules/.vite/deps/chunk-IXLKCLCE.js?v=8f47ecb8:1387
    template_effect http://localhost:5173/node_modules/.vite/deps/chunk-IXLKCLCE.js?v=8f47ecb8:1384
    VideoCollapsible http://localhost:5173/src/lib/components/general/VideoCollapsible.svelte:132
    e http://localhost:5173/node_modules/.vite/deps/chunk-PGMNWDSX.js?v=8f47ecb8:975
    update_reaction http://localhost:5173/node_modules/.vite/deps/chunk-IXLKCLCE.js?v=8f47ecb8:1830

System Info

System:
    OS: Linux 6.12 EndeavourOS
    CPU: (16) x64 AMD Ryzen 9 7940HS w/ Radeon 780M Graphics
    Memory: 10.61 GB / 14.85 GB
    Container: Yes
    Shell: 5.2.37 - /bin/bash
  Binaries:
    Node: 23.1.0 - /usr/bin/node
    npm: 10.9.0 - /usr/bin/npm
    pnpm: 9.14.4 - /usr/bin/pnpm
  Browsers:
    Chromium: 131.0.6778.85
  npmPackages:
    svelte: ^4.2.19 => 5.7.1

Severity

blocking an upgrade

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions