2

Making a web component by scratch

 6 months ago
source link: https://willschenk.com/howto/2024/making_a_web_component_by_scratch/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

No tools, progressive enhancement

Web Components are built into all browsers, and are a way to encapulate functionality without using a specialized web frame work. They also fail gracefully when javascript is disabled or otherwise not running.

We can sprinkle on functionality as things get faster and work better. Here's what my test page looks like without any of the javascript loaded.

nojs_hu5fae0b9b45638eb1c73ba5b13ec8ad2c_172927_500x500_fit_box_3.png

Here are some simple examples of how to make that work.

Watching Media

Here's an element that switches styles based upon the size of the screen, in this case min-width: 768px.

The html for this looks like

  <media-watcher
    match="background: pink"
    unmatch="background: lightblue">
    This is the matcher
  </media-watcher>

This is what it looks like when the screen is big:

media_big_hu7e2832286a5353e12010ae813d60a87d_10343_500x500_fit_box_3.png

And small:

media_small_hu2bccf9cee1803a7268fedb04862cee25_10053_500x500_fit_box_3.png

  class MediaWatcher extends HTMLElement {
      constructor() {
          super();

          this.match = this.getAttribute( "match" );
          this.unmatch = this.getAttribute( "unmatch" );
      }
      
      connectedCallback() {
          this.min_width = window.matchMedia("(min-width: 768px)");
          this.min_width.addEventListener( "change",
                                           () => this.setStyle( this.min_width ) );
          this.setStyle( this.min_width );
      }

      setStyle(matcher) {
          this.style.cssText = matcher.matches ? this.match : this.unmatch;
      }
  }

  customElements.define( 'media-watcher', MediaWatcher )

Adding a tooltip to an image

This was inspired from HTML Web Components an example, which goes through the reasoning behind it. What we have here is just a way to wrap the html tags that you know and love but be able to add functionality around it. I also added styles, but that's not necessary.

  <avatar-image>
    <img
      style="width: 8rem;"
      src="https://willschenk.com/about/avatar.jpg"
      alt="this is an old photo">
  </avatar-image>

Non-hover:

avatar_hu95a54a9761c637d4ca5e16c1e7a4334d_121350_500x500_fit_box_3.png

With hover:

avatar_tip_hu50310a521e9e86dc6660416b6122f33e_142577_500x500_fit_box_3.png

We are also adding additional HTML to the dom here using insertAdjacentHTML.

  class AvatarImage extends HTMLElement {
      connectedCallback() {
          let image = this.querySelector('img')

          image.style.cssText = `
      width: 8rem;
      height: 8rem;
      box-shadow: 0 20px 25px -5px rgba(0,0,0,.1), 0 10px 10px -5px rgba(0,0,0,.04);
      border-width: 2px;
      border-radius: 9999px;
      --border-opacity: 1;
      border-color: #edf2f7;
      border-color: rgba(237, 242, 247, var(--border-opacity));
      max-width: 100%;
      display: block;
      vertical-align: middle;
      border-style: solid;
  `;

          this.insertAdjacentHTML('beforeend', `
  <div style="width: auto;
    display: none;
    max-width: 20%;
    height: auto;
    min-height: 25px;
    line-height: 25px;
    font-size: 1rem;
    background-color: rgba(0, 0, 0, 0.7);
    color: #ffffff;
    border-radius: 5px;
    margin-top: 10px;
    padding: 10px 15px;">${image.getAttribute('alt')}</div>
  `)

          let div = this.querySelector("div")

          image.addEventListener( "mouseenter", () => div.style.display = 'block' )
          image.addEventListener( "mouseout", () => div.style.display = 'none' )

      }
  }

  customElements.define( 'avatar-image', AvatarImage )

Floating Header

I wanted to try and recreate the header from minimalism.com using the simpliest HTML markup I could. There's plenty of tweaks to be done with the styling but I thought would be an interesting example.

Here's the markup:

  <floating-header>
    <a href="/">Name</a>
    <a href="/menu">Menu</a>
    <a href="/search">Search</a>
  </floating-header>

And it gives us:

Wide screen:

floating-header-big_hu402ff9234ef48e8734907f091cef9c8b_177917_500x500_fit_box_3.png

Smaller screen:

floating-header-small_hue694c7e3fa9cf110983b5cc93d71e5a8_159253_500x500_fit_box_3.png
  class FloatingHeader extends HTMLElement {
  	connectedCallback () {
          // Create a MediaQueryList object
          const min_width = window.matchMedia("(min-width: 768px)")
          
          // Call listener function at run time
          this.setStyle(min_width);

          min_width.addEventListener("change", () => this.setStyle(min_width) )
      }

      setStyle(matcher) {
          let style = `
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  --un-bg-opacity: 1;
  background-color: rgb(255 255 255 / var(--un-bg-opacity));
  margin-left: auto;
  margin-right: auto;
  display: grid;
  top: 0;
  right: 0;
  left: 0;
  `
          // @media (min-width: 768px) {
          if( matcher.matches ) {
              style += `
  padding-left: 1rem;
  padding-right: 1rem;
  border-width: 1px;
  max-width: 1024px;
  margin-top: 1rem;
  margin-bottom: 1rem;
  grid-auto-flow: column;
  position: fixed;
  border-style: solid;
  border-radius: 0.75rem;
  --un-border-opacity: 1;
  border-color: rgb(226 232 240 / var(--un-border-opacity));
  `
          }

          this.style.cssText = style;

          // Set the alignment of the children
          for (let i = 0; i < this.children.length; i++) {
              let align = 'start';
              if( matcher.matches ) {
                  
                  if( i == 0 ) {
                      align = 'start';
                  } else if ( i == this.children.length - 1 ) {
                      align = 'end';
                  } else {
                      align = 'center';
                  }
              }
              this.children[i].style['justify-self'] = align;
          }
  	}
  }

  customElements.define('floating-header', FloatingHeader );

Making a map

First we install leaflet:

  npm i leaflet
  <map-component id="my_map" lat="51.505" lon="-0.09"></map-component>

This is more of a proof of concept, but you can encapsulate some functionality in a way that's easy to contain.

  import 'leaflet/dist/leaflet.css';
  import L from 'leaflet';

  class MapComponent extends HTMLElement {
      connectedCallback() {

          this.insertAdjacentHTML( "beforeend", "<div id='map'></div>" );

          let mapdiv = this.querySelector( "#map" );
          mapdiv.style.cssText = `
  height: 400px;
  width: 600%;
  z-index:0;
  max-width: 100%;
  max-height: 100%;
  `
          
          let lat = this.getAttribute( "lat" )
          let lon = this.getAttribute( "lon" )

          console.log( "lat", lat );
          console.log( "lon", lon );

          let map = L.map('map').setView([lat, lon], 13);

          L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
              maxZoom: 19,
              attribution: '© OpenStreetMap'
          }).addTo(map);
      }
  }

  customElements.define( 'map-component', MapComponent )

Boiler plate

Here is some html that shows how to use it all, and the one liner to get this static site up and running.

    <html>
    <head>
      <title>Hello</title>
      <script src="media-watcher.js" type="module"></script>
      <script src="floating-header.js" type="module"></script>
      <script src="avatar-image.js" type="module"></script>
      <script src="map.js" type="module"></script>
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <style>
        floating-header a {
          color: black;
          text-transform: uppercase;
          font-size: 0.75rem;
          line-height: 1rem;
          text-decoration: none;
        }
        .leaflet-container {
    		height: 400px;
    		width: 600px;
    		max-width: 100%;
    		max-height: 100%;
    	  }
        </style>
    </head>
    <body>
      <media-watcher match="padding-top: 3rem;display: block;"></media-watcher>
      <floating-header>
        <a style="font-weight: bold" href="/">Name</a>
        <a href="/menu">Menu</a>
        <a href="/search">Search</a>
      </floating-header>

      <p>
        <media-watcher
          match="background: pink"
          unmatch="background: lightblue">
          This is the matcher
        </media-watcher>
      </p>

      <h1>Main title</h1>
      <p>Paragraph of text</p>

      <div>
        <avatar-image>
          <img
            style="width: 8rem;"
            src="https://willschenk.com/about/avatar.jpg"
            alt="this is an old photo">
        </avatar-image>
      </div>

      <div style="height: 300px; width: 100%; z-index: 0">
        <map-component lat="51.505" lon="-0.09"></map-component>
      </div>
    </body>
  </html>

Now we can start it up, and see how the page loads.

  npx vite

And, of course if you want to publish it all:

  npx vite deploy

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK