Skip to content

Instantly share code, notes, and snippets.

@TotallyInformation
Created May 25, 2022 17:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TotallyInformation/ebdd7bbf70042f4f899a5a03761a71e6 to your computer and use it in GitHub Desktop.
Save TotallyInformation/ebdd7bbf70042f4f899a5a03761a71e6 to your computer and use it in GitHub Desktop.
Test web component
/** Define a new zero dependency custom web component ECMA module that can be used as an HTML tag
*
* TO USE THIS TEMPLATE: CHANGE ALL INSTANCES OF 'NavBar' and 'nav-bar' & change version below
*
* @version 0.1 2022-05-25 Pre-release
*
* See: https://web.dev/custom-elements-v1/, https://web.dev/shadowdom-v1/
*
* See https://github.com/runem/web-component-analyzer#-how-to-document-your-components-using-jsdoc on how to document
* Use `npx web-component-analyzer ./components/button-send.js` to create/update the documentation
* or paste into https://runem.github.io/web-component-analyzer/
* Use `npx web-component-analyzer ./components/*.js --format vscode --outFile ./vscode-descriptors/ti-web-components.html-data.json`
* to generate/update vscode custom data files. See https://github.com/microsoft/vscode-custom-data/tree/main/samples/webcomponents
*
**/
/**
* Copyright (c) 2022 Julian Knight (Totally Information)
* https://it.knightnet.org.uk, https://github.com/TotallyInformation
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const componentName = 'nav-bar'
const className = 'NavBar'
// just for syntax highlighting in VSCode
function html(strings, ...keys) {
return strings.map( (s, i) => {
return s + (keys[i] || '')
}).join('')
}
// https://www.makeuseof.com/responsive-navigation-bar-using-html-and-css/
const template = document.createElement('template')
template.innerHTML = html`
<style>
:host {
/* display: block; default is inline */
/* contain: content; performance boost */
box-sizing: border-box;
/* position: relative; */
width: 100%;
background-color: var(--surface1);
color: var(--text1);
padding: 0;
margin: 0 0 0.8rem 0;
/* border: 2px solid silver; */
}
nav {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
width: 100%;
overflow-x: auto;
}
nav * {
/* display: block; */
margin: .1rem;
}
ul {
list-style: none;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
position: relative;
}
li {
display: inline-block;
}
li:hover a {
color: var(--text3);
}
a {
text-decoration: none;
color: var(--text1);
}
ul ul {
display: none;
}
.logo {
order: 0;
flex: 0 1 auto;
align-self: auto;
color: var(--primary-bg);
}
.hamburger {
order: 0;
flex: 0 1 auto;
align-self: auto;
color: var(--primary-bg);
}
.site-menu {
order: 0;
flex: 1 1 auto;
align-self: auto;
}
.has-sub-menu:hover {
position: relative;
z-index: 10;
}
.has-sub-menu:hover ul {
/* display: initial; */
display: block;
background-color: blue;
position: absolute;
top: 1em;
left: 1em;
z-index: 20;
}
.has-sub-menu:hover ul li {
display: block;
z-index: 30;
}
#checkbox_toggle {
display: none;
}
</style>
<nav>
<div class="hamburger">
<!-- USING CHECKBOX HACK -->
<input type="checkbox" id="checkbox_toggle" />
<label for="checkbox_toggle" class="hamburger">&#9776;</label>
</div>
<div class="logo">TI</div>
<ul class="site-menu">
<li><a href="/">Home</a></li>
<li><a href="/">About</a></li>
<li class="has-sub-menu">
<a href="/">Services</a>
<ul class="site-sub-menu">
<li><a href="/">Dropdown 1 </a></li>
<li><a href="/">Dropdown 2</a></li>
<li><a href="/">Dropdown 2</a></li>
<li><a href="/">Dropdown 3</a></li>
<li><a href="/">Dropdown 4</a></li>
</ul>
</li>
<li><a href="/">Pricing</a></li>
<li class="button"><a href="#">Log In</a></li>
<li class="button secondary"><a href="#">Sign Up</a></li>
</ul>
</nav>
`
// Define the class and make it the default export
/**
* @element nav-bar
*
* @fires nav-bar:construction - Document object event. evt.details contains the data
* @fires nav-bar:connected - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
* @fires nav-bar:disconnected - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
* @fires nav-bar:attribChanged - When a watched attribute changes. `evt.details` contains the details of the change.
* NOTE that listeners can be attached either to the `document` or to the specific element instance.
*
* @attr {string} name - Optional. Will be used to synthesize an ID if no ID is provided.
* attr {string} data-* - Optional. All data-* attributes are returned in the _meta prop as a _meta.data object.
*
* @prop {string} name - Sync'd from name attribute
*
* @slot Container contents
*
* @csspart ??? - Uses the uib-styles.css uibuilder master for variables where available.
*/
export default class NavBar extends HTMLElement {
//#region ---- Class Variables ----
/** Standard _ui object to include in msgs */
_ui = {
type: componentName,
event: undefined,
id: undefined,
name: undefined,
data: undefined, // All of the data-* attributes as an object
}
/** Mini jQuery-like shadow dom selector (see constructor) */
$
/** Holds the name for this instance of the component */
name = undefined
/** Holds a count of how many instances of this component are on the page */
static _iCount = 0
//#endregion ---- ---- ---- ----
//#region ---- Utility Functions ----
//#endregion ---- ---- ---- ----
//#region ---- Event Handlers ----
/** Handle a `uibuilder:msg:_ui:update:${this.id}` custom event
* @param {CustomEvent} evt uibuilder `uibuilder:msg:_ui:update:${this.id}` custom event evt.details contains the data
*/
_uibMsgHandler(evt) {
// If there is a payload, we want to replace the slot - easiest done from the light DOM
// if ( evt['detail'].payload ) {
// const el = document.getElementById(this.id)
// el.innerHTML = evt['detail'].payload
// }
// If there is a payload, we want to replace the VALUE
// if ( evt['detail'].payload ) {
// const el = this.shadowRoot.getElementById('value')
// el.innerHTML = evt['detail'].payload
// }
}
//#endregion ---- ---- ---- ----
constructor() {
super()
this.attachShadow({ mode: 'open', delegatesFocus: true })
.append(template.content.cloneNode(true))
// this.append(template.content.cloneNode(true))
// this.$ = this.shadowRoot.querySelector.bind(this.shadowRoot)
this.dispatchEvent(new Event(`${componentName}:construction`, { bubbles: true, composed: true }))
} // ---- end of constructor ----
// List all attribs we want to observe
static get observedAttributes() { return [
'name'
] }
// Runs when an observed attribute changes - Note: values are always strings
attributeChangedCallback(name, oldVal, newVal) {
// Don't bother if the new value same as old
if ( oldVal === newVal ) return
// Create a property from the value - WARN: Be careful with name clashes
this[name] = newVal
this.dispatchEvent(new CustomEvent(`${componentName}:attribChanged`, {
bubbles: true,
composed: true,
detail: {
id: this.id,
name: this.name,
attribute: name,
newVal: newVal,
oldVal: oldVal,
}
} ) )
} // --- end of attributeChangedCallback --- //
// Runs when an instance is added to the DOM
connectedCallback() {
++NavBar._iCount // increment total instance count
// Create an id from name or calculation if needed
this.name = this.getAttribute('name')
if (!this.id) {
if (this.name) this.id = this.name.toLowerCase().replace(/\s/g, '_')
else this.id = `sc-${NavBar._iCount}`
}
// Listen for a uibuilder msg that is targetted at this instance of the component
document.addEventListener(`uibuilder:msg:_ui:update:${this.id}`, this._uibMsgHandler.bind(this) )
this.dispatchEvent(new CustomEvent(`${componentName}:connected`, {
bubbles: true,
composed: true,
detail: {
id: this.id,
name: this.name
},
} ) )
} // ---- end of connectedCallback ---- //
// Runs when an instance is removed from the DOM
disconnectedCallback() {
// NB: Dont decrement SimpleCard._iCount because that could lead to id nameclashes
// @ts-ignore
document.removeEventListener(`uibuilder:msg:_ui:update:${this.id}`, this._uibMsgHandler )
this.dispatchEvent(new CustomEvent(`${componentName}:disconnected`, {
bubbles: true,
composed: true,
detail: {
id: this.id,
name: this.name
},
} ) )
} // ---- end of disconnectedCallback ---- //
} // ---- end of Class ---- //
/** Self register the class to global
* Enables new data lists to be dynamically added via JS
* and lets the static methods be called
*/
window[className] = NavBar
// Self-register the HTML tag
customElements.define(componentName, NavBar)
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<title>Web Component Test: nav-bar</title>
<script type="module" async >
import './nav-bar.js'
</script>
</head><body>
<nav-bar></nav-bar>
<h1>Demonstrating Web Components</h1>
<p>
This is some demo text.
</p>
</body></html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment