astro-xelement
v3.2.0
Published
XElement is a powerful Astro Web Component generator. Create your own Astro compliant Web Components using only HTML Elements with additional Client-Side JS/TS interactivity sprinkled into the Element.
Downloads
115
Readme
XElement
is an Astro only, HTML Web Component Generator.
It lets you to create any type of Custom HTML Web Components to use within Astro, from a single interface.
XElement
lets you choose when to run JS on the client-side, whether it be when the document is ready or on any given event, or on any type of observer.
Supports all browser native features such as; Browser fetch
API, and Web animation
API. If its on the window
you can use it by calling either window || document
within the Element.
You can even directly render content within the Shadow DOM and more.
Respecting Astro's unique approach to Island's Architecture, XElement
expands this concept in some innovative ways to work with your collection of web components, with some really surprising results.
Project Status
⚠️ This project is under active development and can be considered stable as of
v2
.
We will constantly be seeking to make XElement
better with new features and improvements in due course. And will always work alongside Astro as that framework matures in time.
v2
comes of the release of Astro v0.21
using their new GO compiler. Previous version of XElement
that was supported on Astro versions older than this has now been archived.
Compatability
XElement
is supported on all versions of Astro >v0.21
This particular version of XElement will not be supported on Astro versions <0.20.12
.
Getting Started
Import XElement
from from npm:
npm i astro-xelement -D
Import into your Astro file:
---
import XElement from 'astro-xelement'
---
All XElement
components are polymorphic in the way that they can be implemented.
This means that you can specify in a number of different ways how to you wish to use XElement
for creating and designing your own interactive HTML web components.
How to use
XElement
lets you generate simple HTML Elements and complex Web-Components with JS from a single place. This provides the component with a form of Element encapsulation for fine-grained interactivity and dynamism.
It'll let you choose how to tie in JS into your HTML Element.
And it is really simple to use, take a look over some of these examples.
---
import XElement from 'astro-xelement'
---
<XElement @is="h1" class="joy">Here Comes A Title</XElement>
<!-- renders as -->
<h1 class="joy">Here Comes A Title<h1>
In this example, we are representing an article heading as a page title via JS.
---
import XElement from 'astro-xelement'
---
<XElement @is="h1"
@do={element => {
document.title = `${element.textContent} - Pushed to the Page Title via JS`}
}
>Some Article Title</XElement>
<!-- renders as -->
<h1>Some Article Title</h1>
<!-- js changes the title to -->
<title>Some Article Title - Pushed to the Page Title via JS</title>
Another example, here it can be used to handle a click event in JS on the Element.
---
import XElement from 'astro-xelement'
---
<XElement.button
@click={element => {
console.log('clicked')
}}
>Button: Clickable</XElement.button>
<!-- renders as -->
<button>Button: Clickable</button>
<!-- logs "clicked" to the console when clicked -->
The next example is the pinnacle of all framework examples, setting up a custom 'Counter' HTML Web Component.
---
import XElement from 'astro-xelement'
const { Button, CounterComponent } = XElement
---
<CounterComponent>
<Button
@do={() => {
let count = 0;
this.onclick = () => {
counter_output.textContent = ++count
}
}}
>Increment</Button> <span id="counter_output">0</span>
</CounterComponent>
<!-- renders as -->
<counter-component>
<button>Increment</button> <span id="counter_output">0</span>
</counter-component>
<!-- increments `counter_output` when clicked -->
👆 A Working HTML Counter Web Component 🤯
Properties
This is the shape of the XElement
props:
/** @typedef Tag - Valid HTML Tag names */
type Tag = keyof HTMLElementTagNameMap | (string & {})
export interface Props {
'@is': Tag,
shadowroot,
...attrs?: any
}
type Tag
: HTMLElementTagName | string
Here we allow you to generate spec-compliant semantic HTML tag names for your Web Element. This is provided from TS index.d.ts
type bindings. A full list can be found here
The Tag
can be extended to incorporate custom Tags, allowing you to specify and create HTML compliant Web Components, that render to the DOM.
type @is
: Tag
The @is
property accepts a string indicating the type of element being created. By default, it is a span
.
@is = "div" | "p" | "a" | "audio" | "img" | "video" ...
This is a necessary property to allow XElement
to generate the HTML Element that you wish to consume for you component.
Since XElement
is polymorphic in its nature, it does need to know what type of Element it is to generate. There are three ways to inform XElement
of the type of element its to create.
<!-- (1) using `@is`-->
<XElement @is="div"></XElement>
<!-- (2) using `.` notation-->
<XElement.button></XElement.button>
---
// (3) using a `named` reference
const { Section } = XElement
---
<Section></Section>
For further information on HTML Elements and their representations, please visit MDN-Element
type attrs
: HTMLAttributes
Can utilize as many of the HTML Element Attributes that are registered for the element specified.
You can use the full compliment of associated HTML attributes such as; aria-
attributes, access and modify the data-
attributes, specify the elements id
and apply class
and inline style
attributes.
XElement
Client-Side JS
Now the purpose of XElement
is to help facilitate generating and consuming Astro compliant, Web standard HTML Elements, that need JS without the use of an external framework.
In order to respect Astro's Island's ethos, and hydration policies XElement
has been structured in such a way that the JS sent to the client is packaged as independent modules, attached only to the element in question.
Since the JS is scoped directly to each HTMLElement
in a way not previously possible it allows you to specify when you wish to run your script, and how.
This can be done either on an event handler or by providing a payload to run on certain conditions.
These are the following methods to apply client-side JS using XElement
:
@do
: void
The @do
property accepts a function which runs when the element has loaded and document is ready.
@do={(element) => {
console.log(element)
}}
This is the equivalent to using the Astro client:load
hydration selector.
@visible
: void
The @visible
property indicates that the given function should run when the element is visible to the viewport.
@visible={() => {
console.log('Im Visible and Active')
}}
This is equivalent to Astro's client:visible
hydration selector.
@visible:once
: void
This @visible:once
property only runs once when it becomes visible on the viewport, it then removes and disconnects itself from the Element.
@visible:once={() => {
console.log('See me Once, run me Once')
}}
@resize
: void
The @resize
property fires a callback whenever there is a change to XElement
's dimensions, either its content or border box.
@resize={() => {
console.log("I've changed size!")
}}
This is equivalent to Astro's client:media
hydration selector.
@resize:once
: void
The @resize:once
property only runs once when the element has been resized only once, it then would remove and disconnect itself from the Element.
@resize:once={() => {
console.log("I've only changed size Once!!")
}}
@observe
: void
The @observe
property runs whenever there is a DOM Mutation change to the Element or its sub-components, such as: Attributes, Children, Modifications made to the Components Subtree and also its data. By default it would observe all the aforementioned attributes unless specified.
@observe={() => {
console.log("Something's Changed with the element's properties")
}}
@observe:attr
: void
This allows to observe any changes that are only made to the Elements Attributes
@observe:attr={()=>{
console.log("Something's Changed with the element's attributes")
}}
@observe:children
: void
This allows to observe any changes that are only made to the Elements children
@observe:children={()=>{
console.log("Something's Changed with the element's children")
}}
@observe:sub
: void
This allows to observe any changes that are only made to the Elements subtree
@observe:sub={()=>{
console.log("Something's Changed with the element's subtree")
}}
@observe:data
: void
This allows to observe any changes that are only made to the Elements character data
@observe:data={()=>{
console.log("Something's Changed with the element's character data")
}}
@ANY_EVENT
: EventTarget< void >
The @event
property followed by an event name indicates that the given function should listen to the given event name.
@click | @fullscreenchange | @mouseenter ...
@ANY_EVENT:remove
: void
The @event:remove
property is the removal of event listeners of a given type from an element.
@click:remove={() => console.log("Removed the click event!")}
@ANY_EVENT:once
: void
The @event:once
property that the given function should listen to the given event name and fire only once, removing itself when done.
@click:once={() => console.log('Im a one time deal')}
@ANY_EVENT:prevent
: void
The @event:prevent
property followed by an event name indicates that the given function should prevent the default behaviour of that particular event listeners effects.
@click:prevent={() => console.log('Prevent default behaviour in full effect')}
@ANY_EVENT:useCapture
: void
The @event:useCapture
property followed by an event name indicates that the given function should listen to the given event name, capturing the bubbling behaviour of that event to the element.
@click:useCapture={() => console.log('Initiate Capture of the Event')}
@animate
&& @timings
: < object[ ] | object >
XElement
allows you to animate the element directly by specifying your animations and key-frames as normal when utilising the standard Web Animation API
Use the @animate
to provide a list of keyframes to animate over. @timings
is an Object which contains the timing options for the animation. These two are used in concert with each other.
<XElement @is="p"
@animate={[
// keyframes
{ transform: 'translateX(0px)' },
{ transform: 'translateX(300px)' }
]}
@timings={
{
// timing options
duration: 1000,
iterations: 'Infinity'
}
}
>
See, I'm a Text in motion
</XElement>
Using fetch
XElement also supports client-side's native fetch()
API. This allows you to GET
data from the internet, POST
your form's and data back to your servers, everything you can normally do with fetch()
all directly from within the scope of the Element itself.
<XElement
@is="button"
@click={async ()=>{
await fetch('https://jsonplaceholder.typicode.com/todos/1').then(response => response.json())
console.log("Here is the Fetched Data", json)
}}
>
Dynamic Imports
XElement allows you to dynamically import scripts into your Element.Either from the file system using Astro.resolve('path/to/file.mjs)
, or via a URL http://packageRegistry.com/somePackage.{browser | esm}.js
.
Since XElement
utilises ES Modules
, it lets you use other module script files .mjs
. This allows you to export
the Elements logic from one place and dynamically import
them to work within the browser, for more information on .js
vs .mjs
.
You can also import third party modules from around the ecosystem providing they are esm
compliant, this allows you to have a form of package-manager-less type of development.
Not needing to install packages to use with your XElement
, instead just pulled using an URL from sources such as: Skypack, jspm.io, jsDelivr or esm.sh..
This way you can load and consume any from of packages or scripts from npm sources without installing them to node_modules
first.
Confetti Example
This example encapsulates XElement
within a Confetti Web Component, here we are calling the canvas-confetti
module via a URL import. We then can call the confetti()
directly within the @click
event handler.
---
import XElement from 'astro-xelement'
const { button: ConfettiButton, div: Confetti } = XElement
---
<Confetti class="confetti">
<ConfettiButton
class="btn"
@click={() => {
const confetti = await import('https://cdn.skypack.dev/canvas-confetti').then(exports => exports.default)
confetti()
}}
>Confetti</ConfettiButton>
</Confetti>
Normally you would import the modules that you need directly into the Element that is using it. Leveraging the browser cache, multiple requests to the same export would only result in the one file being sent.
define:vars=
: Object
With Astro v0.21
you are able to send props
from inside the Astro Codefence, into the <script>
tag, using Astro's define:vars
directive.
This has also been replicated with XElement
to allow you to have the same ability to send data from Astro into your client-side JS, letting you leverage Astro compliment of tools and abilities to its full power.
---
import XElement from 'astro-xelement'
const {Title} = XElement
const fadeIn = [
{
transform: 'translateY(50px)',
opacity: 0
},
{
transform: 'translateY(0)',
opacity: 1
}
];
const timing = {
duration: 1000,
easing: 'cubic-bezier(0.390, 0.575, 0.565, 1.000)',
fill: 'both'
};
---
<Title
define:vars={
{
fadeIn,
timing
}
}
@visible={(event)=> {
event.target.animate(fadeIn,timing)
}}
>
See me Fade In
</Title>
Credits
This project owes a tremendous amount of gratitude and thanks to jonathantneal for his continuous support and hacking away, helping to guide this whimsical fantasy into creation.
Special acknowledgement to the Astro Core team for their dedication and hard work towards building Astro as a fantastic framework for Frontend development.