2

I'm trying to build a video player Web Component, but I can't seem to get the video and source elements to render properly.

customElements.define('video-player',
  class extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById('video-player-template').content;
      console.log(template)
      const shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(template.cloneNode(true));
    }
  }
);
<template id="video-player-template">
    <video controls width="720" height="380" muted autoplay>
        <slot name="video-src" />
    </video>
    <slot></slot>
</template>


<video-player>
    <h1>Video player web component</h1>
    <source slot="video-src" src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" type="video/webm" />
</video-player>

Why doesn't the video element render?

Dan Gayle
  • 2,277
  • 1
  • 24
  • 38

2 Answers2

2

see the documentation: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video

<video> wants a <source> element as immediate child,
thus you can't use a <slot> there.
(the same is true for <table>, which can't have <slot>)

You extract the src from your <video-player src="..."> Web Component,
then create that <source> tag.

I have added all shadowDOM styling options for a complete example

customElements.define('video-player',
  class extends HTMLElement {
    constructor() {
      super()
        .attachShadow({mode:'open'})
        .append(document.getElementById(this.nodeName).content.cloneNode(true));
    }
    connectedCallback() {
      let src = this.getAttribute("source");
      let ext = src.split(".").slice(-1)[0];
      this.shadowRoot
          .querySelector("video")
          .innerHTML = `<source src="${src}" type="video/${ext}">`;
    }
  }
);
<video-player source="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm">
  <span slot="title">A beautiful video</span>
  <div class="desc">My video description</div>
</video-player>
<video-player source="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm">
  <span slot="title">Another beautiful video</span>
  <div class="desc">And more description</div>
</video-player>

<template id="VIDEO-PLAYER">
  <style>
    :host { display:inline-block }
    h1 { margin:0px;background:var(--bgcolor,green);text-align:center }
  </style>
  <div part="videoContainer">
    <h1><slot name="title"></slot></h1>
    <video controls width="100%" muted></video>
    <div><slot><!-- all non-slot labeled content goes here --></slot></div>
  </div>
</template>

<style>
  video-player {
    font: 10px Arial; /* Inheritable styles style shadowDOM */
    width: 240px;
    --bgcolor: gold; /* CSS properties can style shadowDOM */
  }
  .desc { /* container/global CSS styles slotted content!!!!  */
    width: 100%;
    background: beige;
  }
  ::part(videoContainer){ /* shadowParts style all usages in shadowDOM */
    border: 5px solid grey;
  }
</style>

ShadowDOM is styled by:

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • So we **do** need to use `.innerHTML`? I've never used the "web components" technique before and I solved the issue earlier by using **innerHTML** but I thought it was hacky so was waiting for a different Answer. At some point I was even thinking _"Instead of Components, why not just use a Class for this task?"_ (then I remembered Classes are a pain in JS). – VC.One Feb 24 '22 at 17:46
  • 1
    go for ``document.createElement("source")`` if some guru tells you ``innerHTML`` is not to be used. Or put the ```` **inside** the TEMPLATE and change its ``src`` and ``type`` attributes. You are the programmer, there are plenty of ways to skin this cat. – Danny '365CSI' Engelman Feb 24 '22 at 17:52
  • I hear you about _"engineering"_ our own working solutions even if they don't follow the accepted rule. What got me was that I was able to access the **HTMLMediaElement** part and could even check its width or height, but even after setting the `.src=` (and the "src" URL was confirmed in console) it still would not play (not even trusty `.load()` would work), that's when I went dynamic ... So **you're right about ** tag being needed (but only because `document.createElement("video")` wasn't used, which can accept a `.src` value without needing a `` tag).... – VC.One Feb 24 '22 at 18:20
  • I've seen examples using UL>LI, with the LI being slots. Were those examples incorrect, since the same semantics apply? – Dan Gayle Feb 28 '22 at 20:28
  • I would have to see those examples to judge what pattern is used. – Danny '365CSI' Engelman Feb 28 '22 at 20:32
  • @Danny'365CSI'Engelman https://javascript.info/slots-composition#menu-example – Dan Gayle Mar 01 '22 at 23:56
1

A slightly different approach using observedAttributes to extract setup values...

The logic is like this:

  • Create a basic <template id="video-player-template"> </template> tag
  • Re-use the component each time as a <video-player> tag
  • The video tag values are extracted from the <video-player>'s tag setup code.
  • Dynamically create a <video> tag object with extracted values from <video-player>.

Here is some testable code:

<html>
<head>
<style>

</style>
</head>
<body>

<!-- 1) create template -->
<template id="video-player-template">

<slot></slot>

</template>

<!-- 2) test as Component -->
<!-- test Component #1 with video loop -->
<video-player id="vidplayer1" width="400" height="300" muted autoplay controls loop type="video/webm" 
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm" >
</video-player>

<!-- test Component #2  -->
<video-player id="vidplayer2" width="200" height="120" muted autoplay controls type="video/webm" 
src="https://www.w3schools.com/tags/movie.mp4" >
</video-player>

<!-- controller scripts -->

<script type="text/javascript">

/*
test files
> MP4:  https://www.w3schools.com/tags/movie.mp4
> WEBM: https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
*/

var template;
var nodes;
var shadowRoot;

customElements.define(
    'video-player',
    class extends HTMLElement 
    {
        constructor() 
        {
            super();
            template = document.getElementById('video-player-template').content;

            console.log(template)

            shadowRoot = this.attachShadow({mode: 'open'});
            shadowRoot.appendChild(template.cloneNode(true));
        }
        
        //# get component attributes (from template tag setup)
        static get observedAttributes() 
        {
            //# extract values from video tag template to use in output video tag
            //# eg:  controls id width height muted loop autoplay ... etc
            return ['src', 'id', 'width', 'height', 'controls', 'muted', 'autoplay', 'loop'];
        }

        //# attribute change
        attributeChangedCallback(property, oldValue, newValue) 
        {
            if (oldValue === newValue) { return; }
            else { this[ property ] = newValue; }
        }

        //# connect component
        connectedCallback() 
        {

            //# component is ready to be accessed

            //# generate dynamic video tag
            let player_code = "";
            player_code += `<video `;

            if( `${ this.id }` != "undefined")
            { player_code += `id="${ this.id }" `}

            if( `${ this.width }` != "undefined")
            { player_code += `width="${ this.width }" `; }

            if( `${ this.height }` != "undefined")
            { player_code += `height="${ this.height }" `; }

            if( `${ this.controls }` != "undefined")
            { player_code += `controls `; }

            if( `${ this.muted }` != "undefined")
            { player_code += `muted `; }

            if( `${ this.autoplay }` != "undefined")
            { player_code += `autoplay `; }

            if( `${ this.loop }` != "undefined")
            { player_code += `loop `; }
            
            player_code += `<source src="${ this.src }" `;
            
            //# get TYPE for video ( because ".type" is a reserved keyword )
            if( String((`${ this.src }`).indexOf(".webm")) != -1)
            { player_code += `type="video/webm" `; }
            else if( String((`${ this.src }`).indexOf(".mp4")) != -1)
            { player_code += `type="video/mp4" `; }

            player_code += `/> </video> `;

            //# apply code of dynamic video tag (add to page)...
            this.innerHTML = player_code;
            
    }
});

</script>
</body>
</html>
VC.One
  • 14,790
  • 4
  • 25
  • 57