Making a web component by scratch
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.
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.
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:
And small:
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:
With hover:
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:
Smaller screen:
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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK