snuggsi ツ - Easy Custom Elements in ~1kiloByte
All you need is a browser and basic understanding of HTML, CSS, & JavaScript Classes to be productive!
Performance is the art of avoiding work
- #FreeJewelry 💍 💎
-
You prefer to be D.R.Y. and build reusable web components on a gradual learning curve.
-
You prefer convention over configuration.
-
Web Components are ready for production & Custom Elements v1 has support for every modern 🍃 greenfield browser.
Made with 💖 Vanilla JS™ No need to learn Node.js, React, Webpack, Babel, or Gulp. (You can if ya want to use snuggsiツ with those tools. But you don't need to!)
#UseThePlatform
snuggsiツ works in a plain 'ol HTML file! Simply place the following <script> within your webpage:
<script src=https://unpkg.com/snuggsi></script>
Et Voila (that's it!) ツ
snuggsiツ provides a prolyfill for the following native web platform features:
Support | Edge* | Chrome* | Firefox* | Safari 9+ | Android* | iOS Safari* |
---|---|---|---|---|---|---|
Templates | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Custom Elements | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Slot Replacement | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
*Indicates the current version of the browser
snuggsiツ encourages convention over configuration using familiar techniques that are native to all browsers.
Gone are the sleepless nights where your code suffers from <div>
itus, or need to install packages on a terminal just to write HTML
. People who are more comfortable with HTML
should be able to start marking up their ideas immediately! You shouldn't have to know CSS or JavaScript! (But it definitely helps if you need styling and functionality).
snuggsiツ believes in using Progressive Enhancement. Not just with your code, but also with your Developer eXperience (DX). We will start from the beginning with a simple Custom Element and gradually enhance functionality step-by-step.
When picking a name for your custom element there are a few naming conventions you must be aware of. We can simply use hello-world
for now.
HTML
has always been a declarative language. Why not just use it to declare Custom Elements?
If you know how to write HTML
you can start using snuggsiツ. Sometimes you need to sandbox a section of your page for styling. Other times you need a custom container of complex functionality. Either way you usually start with a plain ole' HTML
element declaration:
Not all HTML tags are created equal!
A "valid HTML
Element" has always allowed non-standard tag names (as long as you remember to provide a closing tag). In the bad old days of the web, HTML5
elements were once "non-standard" to HTML 4.0
. However, these days we have far more flexibility in our markup:
<a>…</a> <!-- valid (hyperlink) -->
<b>…</b> <!-- valid (HTML4.01)-->
<c>… <!-- invalid (no closing tag) -->
<c>…</c> <!-- valid (HTMLUnknownElement) -->
<p>…</p> <!-- valid (with closing tag) -->
<p>… <!-- valid (with optional closing tag) -->
<img /> <!-- valid (x-HTML self closing tag) -->
<hr> <!-- valid (no content tag) -->
👍 Rule of thumb: Close all non-standard HTML
Element tags!
As you learned earlier there are a few conventions to adhere to be considered a "valid Custom Element" you will need an alpha-numeric character followed by a hyphen in the tag name (at minimum):
<foo></foo> <!-- invalid (CUSTOM element but valid HTML element) -->
<foo-bar> <!-- invalid (no closing tag) -->
<a-></a-> <!-- valid (you only need a character and a hyphen) -->
<foo-bar></foo-bar> <!-- valid -->
👍 Rule of thumb: Use kebab case (with hyphens) for tag names.
We now know enough to be dangerous and make your own Custom Element tag:
<hello-world></hello-world>
Et Voila ツ (No really … That's it!)
At this point your custom element can be styled using CSS just like any other element.
<style>
hello-world {
background: #bada55
}
</style>
<hello-world>
Hello
</hello-world>
See A JavaScript-free custom element implementation
And Building a <tool-tip>
component for more (sans JavaScript) custom elements CSS fun!
The {token}
is simply a well named dynamic variable you will Describe later. {token}
s are placeholders which watch for changes to your custom element's class
property of the same name. Since they are "placeholders" and not live code, Front-end designers are no longer blocked by needing to install a JavaScript framework just to write CSS
!
<foo-bar> This is a token 👉 {baz} and {bat} is another! </foo-bar>
👍 Rule of thumb: If the {token}
name is not in a thesaurus then I probably shouldn't use it.
A "live token" is a declarative way to bind data values to your Custom Element. A nice convention to a real historical P.I.T.A. of keeping values updated. Live {token}
s are also "✨ automagically" updated each time the element re-renders.
Let's add a {token}
to <hello-world>
:
<hello-world>
Hello {planet}
</hello-world>
👍 Rule of thumb: A {token}
is not "live" until there is a class
description for its functionality.
Lastly, we can visually enhance your <hello-world>
Custom Element by making it "block level" with CSS display: block
. This way your element plays nicely in your layout:
<hello-world>
Hello {planet}
<style>
hello-world { display: block }
</style>
</hello-world>
We have finished your Custom Element Declaration using HTML
, & CSS
!🌟 Now on to your Definition.
Every Custom Element
MUST be Defined within the CustomElementsRegistry
. This is simple with snuggsiツ
Let's define
your element using the Element
interface:
// <hello-world> … </hello-world>
Element `hello-world`
👍 Rule of thumb: Use backticks around tag names (``).
This syntax is not JSX. It's actually called tagged template literals and is native to the platform.
Custom elements use the native Element
interface definition strategy for two reasons:
- To prevent you from worrying about browser inconsistencies as the technology matures.
- Prevent global namespace pollution. (
Element
has been native to the web platform for decades!)
Classic JavaScript syntax may also be used. However this should be the job of a transpiler not the developer. Transpilers take care of normalizing Modern JavaScript to a specific retrograde.
Element ('hello-world') // classic javascript definition
Great so far!🎉 Although your Element behaves like any other HTMLElement
, we should add some functionality custom to your needs.
Next, we need to pass a class
description to the function returned by your Element
definition.
// <hello-world> … </hello-world>
Element `hello-world`
( class HelloWorld extends HTMLElement { … } )
👍 Rule of thumb: MUST define a class
which extends HTMLElement
Let's shorten your description up a bit by using an anonymous class expression to describe the class
. This convention is preferred as using an explicit class
declaration name can potentially pollute the global namespace:
// <hello-world> … </hello-world>
Element `hello-world`
( class extends HTMLElement { … } )
👍 Rule of thumb: Use enclosing parenthesis around (class …)
definitions.
Since we previously declared a {planet}
token within your <hello-world>
element we need to also define a class property
of the same name to replace the {planet}
token with a value.
Class properties may look like typical JavaScript Functions. However, they are treated as properties. (called without parenthesis). class
properties are described by using the get
ter and set
ter annotations before the name.
Let's add a property to your class
definition and give {planet}
some life:
// <hello-world> … {planet} … </hello-world>
Element `hello-world`
(class extends HTMLElement {
get planet () // used for {planet} token
// "✨ automagic" token binding
{ return 'world 🌎' }
})
👍 Rule of thumb: class
properties are functions begining with the keywords get
& set
.
👍 Rule of thumb: {tokens}
will use the class
property value of the same name by default.
{token}
value is updated after each re-render but it beyond the scope of this simple example.
Since your hello-world
Custom Element is an HTMLElement
at its core, we can access your property directly from the DOM!
// <hello-world> … </hello-world>
document.querySelector
('hello-world').planet // world 🌎
👍 Rule of thumb: Do not use parenthesis ()
when calling get
ters & set
ters.
event
handlers can be any method function which can be placed on any child elements and also onto the custom element itself (i.e.onclick=eatBacon
). However, you will not have to explicitly set the handler in HTML when you follow native naming conventions. This is the magic behind snuggsiツ Global event
Listeners. They register themselves onto the custom element and "listen" for you! As a convenience, your new custom element uses Event Delegation to capture all its children's event bubbles of the same name.
// <hello-world>
// `onclick` is "automagically" attached
//
// <a onclick=onsneeze> ACHOO! </a>
// </hello-world>
Element `hello-world`
(class extends HTMLElement {
// native event handler names
// are "✨automagically" attached to `hello-world`
onclick (event) {
// prevent event from bubbling
// Custom Element will re-render
// after event unless prevented
event.preventDefault ()
event.target // element which triggered event
this // is ALWAYS bound to the Custom Element container 🎉
}
onsneeze (event) {
/* must be declared in HTML
<button onclick=onsneeze>
*/
}
})
👍 Rule of thumb: Use native GlobalEventHandlers
names if you don't want to explicitly set listeners in HTML.
See full list of Global Event Handlers on MDN
Lastly, all event
handlers (and global event
listeners) are passed a native event
object.
P.S. YES the event handler will autobind
this
to the current custom element instance! 🎉
…or just copy & 🍝pasta into a new HTML file and have at it!
<!-- 👇 Declaration --------------------------->
<hello-world>
Hello {planet}
<button onclick=speak>Greet</button>
</hello-world>
<script src=//unpkg.com/snuggsi></script>
<script>
// 👇 Definition -------------------------------
Element `hello-world`
// 👇 Description ------------------------------
(class extends HTMLElement {
get planet () // property token
// "✨ automagic" token binding
{ return 'world 🌎' }
onclick (e) { // event handler
// "✨ automagic" event registration
alert(`You clicked on ${e.target.tagName}`)
}
speak(e) { // bound event handler
e.preventDefault()
alert('One small step...')
}
})
</script>
…or just copy & 🍝pasta into a new HTML file and have at it!
<hello-kitty icon=😻 >
<header>{greeting}</header>
<figure>
<figcaption>
<button onclick=meow>Hello new kitty!</button>
</figcaption>
<img alt='Random kitty cat' src={url} onclick=pet >
</figure>
<style>
hello-kitty *
{ margin: 1em; text-align: center }
</style>
</hello-kitty>
<script src=//unpkg.com/snuggsi></script>
<script>
Element `hello-kitty`
(class extends HTMLElement {
// CONSTRUCTOR ---------------------------------------
initialize ()
// see `meow` event handler
{ this.url = 'https://placekitten.com/400/400?image=' }
// PROPERTIES ----------------------------------------
set icon // on element
// default to html attribute
( value = this.getAttribute `icon` )
// set html attribute to new value
{ this.setAttribute (`icon`, value) }
get icon () // from element
{ return this.getAttribute `icon` }
get greeting () // "✨ automagic" token binding
{ return `<hello-kitty> Carousel ${ this.icon }` }
// METHODS -------------------------------------------
random () {
return Math.round
( Math.random `` * 16 )
}
// EVENT HANDLERS ------------------------------------
onclick (e) {
// "✨ automagic" global event handler registration
alert (`You clicked on ${e.target.tagName} ${ this.icon }`)
}
pet ()
{ alert `Puuuuuurrrrrrrrrrrr!!!` }
meow (e) { // custom handler
e.preventDefault ``
this.querySelector `img`
.setAttribute (`src`, this.url + this.random () )
// element will "✨ automagically" re-render !!!
}
})
</script>
The <template>
is used to define custom element content for use within multiple elements.
Useful when we need to:
- Separate a custom element definition into a Web Component
- Bind a context to the template using An
Array
or POJO (Plain Ol' JavaScriptObject
) - Append rendered template to the document:
- If
context
is an objectbind
a single<template>
- If
context
is a collection (i.e. anArray
)bind
a sequential<template>
DocumentFragment
per item
- If
<template name=developer>
<!-- `{nickname}` will bind to `context` property `nickname` -->
<h1>{nickname}</h1>
</template>
<script src=//unpkg.com/snuggsi></script>
<script>
const
template = Template `developer`
, context = { nickname: 'That Beast' }
template.bind (context)
</script>
<template name="developer">
<!-- invisible
<h1>{name}</h1>
-->
</template>
<h1>That Beast</h1><!-- template is used as head for tail insertion -->
Each <template name>
will be mapped over each
context item within the array. When the array items
are objects each property will map to a respective
{token}
of the same name.
Note: The #
symbol is used to retrieve the collection's current index of iteration.
<ul>
<template name=item>
<li>Hello {name}! Your index # is {#}
</template>
</ul>
<script src=//unpkg.com/snuggsi></script>
<script>
// when context is a collection
const
template = Template `item`
, context = [ {name: 'DevPunk'}, {name: 'Snuggsi'} ]
// internal template render for each item in context
template.bind (context)
</script>
<ul>
<template name="item">
<!-- invisible
<li>Hello {name}! Your index # is {#}
-->
</template>
<li>Hello DevPunk! Your number index # is 0</li>
<li>Hello Snuggsi! Your number index # is 1</li>
</ul>
Each <template name>
will be mapped over each
context item within the array. When the array items
are scalar (i.e. strings, numbers, booleans)
each item will map to a {self}
helper token.
Note: The #
symbol is used to retrieve the collection's current index of iteration.
<dl>
<template name=recipe>
<dt> Step {#}.
<dd> {self}.
</template>
</dl>
<script src=//unpkg.com/snuggsi></script>
<script>
// when context is a collection of scalar variables (i.e. Strings)
const
template = Template `recipe`
, context = [
"Preheat oven"
, "Place pre-made cake in oven"
, "Don't burn the cake"
, "Nom Nom"
]
// internal template render for each item in context
template.bind (context)
</script>
<dl>
<template name="recipe"> … </template>
<dt> Step 1.</dt>
<dd> Preheat oven.</dd>
<dt> Step 2.</dt>
<dd> Place pre-made cake in oven.</dd>
<dt> Step 3.</dt>
<dd> Don't burn the cake!</dd>
<dt> Step 4.</dt>
<dd> Nom Nom!</dd>
</dl>
snuggsiツ is able to use modern compression algorithms to create bundles as small as ~1500 OCTETS (or one 1500byte Ethernet packet frame)
A chronological CalVer strategy is used instead of SemVer. Read More
Please read CONTRIBUTING.md before contributing.
Contributing while using Visual Studio Code is simple! Read More