Ponys 🦄

Declarative creation of browser-native web components.

Download (source, 2.1 kB) Download (minified, 1.1 kB)

CDN:

npm:

GitHub: https://github.com/jhuddle/ponys

Ponys simplifies the process of creating custom elements.

Making new browser-native web components typically comes with a lot of boilerplate:

when all you really wanted to do was write this once somewhere:

so that you can use this everywhere else:

everypony

Wouldn't it be cool if browsers let you do that? 🤔

This is what Ponys allows you to do - in three different ways!

That's correct: you can inline your templates server-side, create them dynamically, or import them as single-file components - each of these snippets results in the same custom element.

But what about interactivity, styling, etc?

Any class you can use with customElements.define(), you can use with Ponys. Just add a <script> tag with a setup attribute inside your template tag/string/file, and export the class that contains your properties and methods as default - no constructor required.

Likewise with <style>! Ponys will put your component's elements behind a shadow root wherever possible, so your CSS is fully encapsulated.

Here's an example of an inline template with a <script setup> tag, which extends the built-in <button> element (not supported in Safari):

Counter button

See the code
<template name="counter-button" extends="button">
Count: <b id="count"></b>
<script setup="">
export default class extends HTMLButtonElement {
count = 0
update() {
this.$('#count').textContent = this.count;
}
increment() {
this.count++;
this.update();
}
connectedCallback() {
this.onclick = event => this.increment();
this.update();
}
}
</script>
</template>
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Note also that this template uses a special convenience method on the 'host' element of our component: this.$('#count') is equivalent to this.shadowRoot.querySelector('#count'). And what's more, every element defined in the template also has access to this method - as well as $$ for querySelectorAll, plus a host property that points directly at the host element - making it really easy for elements to reference each other within the current component.

With this capability, you can do a lot with relatively little JavaScript. Here's another more complicated example, this time imported as a single-file component:

Modal

See the code

Try adding some of the other custom elements defined above inside the <modal-container> tag - you'll see they work perfectly alongside the other elements rendered in our template's <slot>.

How can I use this with other JS code though?

Just import it! The <script setup> tag in your template is treated as being of type="module", which means you can use any library that's written as an ESM module: of course, you can import modules directly from your site's static assets, but you could also fetch libraries as needed from a CDN such as Skypack or esm.run by jsDelivr.

That's the approach taken in the example below: here, we import an ESM build of PapaParse from a CDN, in order to parse our custom element's text content as a CSV string. The rest of our code builds an HTML table from the resulting data object... click on the headers to sort by column!

CSV table

See the code
Character;Kind;Element of Harmony Twilight Sparkle;Alicorn;Magic Pinkie Pie;Earth;Laughter Fluttershy;Pegasus;Kindness Rainbow Dash;Pegasus;Loyalty Rarity;Unicorn;Generosity Applejack;Earth;Honesty

Nice, but I'm more familiar with reactive components...

No worries: the progressive-enhancement ecosystem has got you covered. Here's the exact same CSV table, but written with petite-vue; if you know regular Vue, you'll already know how it works. You could do much the same thing with Alpine.js too, for example. Or perhaps my next side-project... 😉

CSV table using petite-vue

See the code
Character;Kind;Element of Harmony Twilight Sparkle;Alicorn;Magic Pinkie Pie;Earth;Laughter Fluttershy;Pegasus;Kindness Rainbow Dash;Pegasus;Loyalty Rarity;Unicorn;Generosity Applejack;Earth;Honesty

But how do I...

OK, I'm sold. But... why the ponies?

Why not ponies? Ponies are awesome. 🦄

But to answer your question fully, there are a few actual reasons also:

With this project, my aim was to create a tool that would let me write web components the way I wish browsers would let me write web components; as such, the Ponys API is deliberately close to existing web standards, and constitutes my proposal for the future syntax/behavior of declarative web components.

My wish, therefore, is that Ponys will one day act as the ponyfill for the native API it suggests; I hope you find it convenient and joyful to use in the meantime. 🌈

- jhuddle