> Snowblade is currently under active initial development and available only for preview in its compiled "dist" state. As such, pull requests are not being considered at this time. However, bug reports, feedback, and feature requests are encouraged — the goal of this repository is to create an open discussion. Please submit any considerations of this nature as issues here on GitHub.
Why?
Snowblade was inspired by Alpine.js, which offers developers the ability to leverage a fully-reactive framework via attributes sprinkled into your existing markup like x-for, x-text, or x-on:click. Using Alpine in-tandem with utility frameworks like Tailwind CSS, developers can rapidly build complete app frontends with little overhead and often without ever writing more than a single .html file.
This ease of use has the potential to come at a cost however, as the HTML source starts to grow very rapidly and with considerable redundancy — especially for SPA-type applications.
The goal of Snowblade is to break-down your app's HTML into smaller and reusable components without forcing you into adopting a new syntax like Vue or React's JSX. Already have most of your frontend built? Great! Snowblade works with the HTML you already have. All you have to do is extract the components that you want to reuse and organize.
> #### Why not use React or Vue? > As developers, we each typically have a framework of choice when it comes to creating application views. Each framework comes with its own idiosyncracies; workflows; dependencies; and, often times, a learning curve. Tools like Alpine and Tailwind CSS leverage the universal familiarity of HTML and enable developers to accomplish a bulk of their frontend development in one place, the DOM — an approach that is both rapid and increasingly "instinctive" in its execution. > > Snowblade aims to build on that universal familiarity and seeks to make mangement of frontend components syntactically-natural, central, and accessible to everyone.
Install
From npm: Install the CLI tool from npm.
``sh npm i snowblade --save-dev `
Config
Inside of a Node environment, Snowblade is a command line utility accessible using the npm command snowblade with a configuration file that you specify. Create a snowblade.config.js file in your project's root:
snowblade.config.js
`js // REQUIRED : Object | Array`
$3
To provision a basic configuration file, with optional sample components, you can use the --init option in your project's directory:
`sh npx snowblade --init `
$3
By using a JS file as the configuration object, Snowblade enables developers the ability to leverage the object-oriented nature of JavaScript. In this way, you can declare things like include, props, or pipes as constants, and then pass them into as many or as few config objects that need them:
snowblade.config.js
`js const include = [ 'resources/snowblade/shared/*/.html' ];
export default [ { input: 'resources/snowblade/views/app.html', include, // assigned from const output: { file: 'resources/views/app.html', props, // assigned from const pipes, // assigned from const } },
/ ... /
{ input: 'resources/snowblade/views/dashboard.html', include, // assigned from const output: { file: 'resources/views/dashboard.html', props, // assigned from const pipes, // assigned from const } } ]
`
Usage
When run on its own, the snowblade command, by default, will look in your project's root directory for a snowblade.config.js file. If you wish to specify a different file for various build types, you may use the --config or -c switch to specify a different config:
Eventually, the intention is to use implement the Chokidar file-watcher library to allow for a single command, snowblade --watch, to run in the background during component development. In this way, as each component is modified and saved, Snowblade will recompile to reflect changes.
For now, your development workspace can be configured to work with Nodemon watching your Snowblade directory:
Expression of components in Snowblade starts by declaring a component definition with native HTML, and then expressing that component in your markup through use of a syntactically-natural custom tag that you define. Starting from the input document in your config, your markup will be compiled as each component is referenced, expressed, and rendered.
$3
Snowblade output begins with one item, an input document specified as the input property of a config object in snowblade.config.js. From this input document, Snowblade will cascade through any component expressions and render them as HTML where they are written. A sample input document might look something like this:
`html
`
As promised, the syntax is plain old HTML. The only difference is that we've included components for Snowblade to reference and render. This is first done by referencing the components using standard tags with an empty snowblade attribute:
`html
`
Think of these tags like ES6 import statements. Next, the components are told where to render within the document by expressing each component's custom tag:
`html
`
> #### Why use a generic tag instead of a custom tag? > To take advantage of IDE and editor logic/formatting. Most editors' language servers will offer auto-completion of paths expressed on the href attribute of a tag. This makes it easier for you to ensure that you've typed the correct path to your imported component. > > Eventually, I'd like to extend the existing HTML LSP-compliant language server in VSCode to help with auto-completion of component names and, where necessary, rewriting of component imports when project assets are reorganized. If anyone reading this has experience with LSP in VSCode, and would like to help, please contact me via DM on Twitter.
#### Config-provided Components
If you've defined a string or array of strings on the include property of snowblade.config.js, you don't need to express your component imports using tags for those component documents which match your given Glob patterns. In the case that a config-provided component has a name conflict with an import-provided component expressed via tag, Snowblade will use the import-provided component when resolving a component expression.
> #### Unique Component Names > > While making use of import-provided components allows for duplicating component names, this is a practice against which I strongly recommend in most cases. However, an exception to this could be if you were defining a custom table component. In such an instance, you may want to have Data expressed differently under than you would if it were expressed under .
---
$3
The remainder of this README will be written under the assumption that all components are import-provided, but it should be noted that this is not necessary, and you can provide all of your components using the include property on your config object, making things much easier. Use Snowblade how you want to use it, in whatever way makes your project easier.
---
$3
The input document example included a link to the component
./app.html and then expressed it as within the markup. For this to render correctly, there needs to be a component file present at ./app.html, relative to the index document:
app.html
`html
`
A lot is happening here, but let's start from the top to understand what's being expressed. Most critically, the component document starts with a
tag that provides a handle for the component we're defining — in this case, we're defining an app component that will be expressed as , so the name attribute of our tag is set to "App". The full tag is expressed as:`html
`
For each component name, use of PascalCase (like ES6 classes) is recommended to aid in syntactic visibility, but this is optional and completely up to you. Snowblade component tags are case-sensitive, so if you declare a component as
App, it must be expressed as , and not as .
> #### Case-insensitive Code Formatters > > Some code formatters will drop capital letters in HTML tag names, especially those names which match existing standards-compliant tags. While Snowblade recognizes the difference between
and or
and
, you may find that your code formatter replaces upper-cased tag names with lower-cased equivalents. > > If you're using Prettier, you will not experience an issue with casing unless you attempt to express a component using an upper-case name that matches an existing standard HTML tag.
#### Component Nesting
Below the
tag, several tags have been specified. Each of these will correspond to a component document that expresses its export via a , similar to the one defined for the App component.
Within the body of the
App component, there's a mix of native markup as well as Snowblade component expressions. In this way, a component can yield its HTML wherever it's needed in a document — nested inside of as many or as few expressions as is desired.
$3
In the example
App component document, notice that the Sidebar and Timeline expressions have class attributes applied to them. In this case, we're making use of Tailwind CSS to define the width of each component inside of their parent
. This is a critical aspect of Snowblade and one that makes component reusability so versatile. To take a look at how this is applied, let's examine the content of an example sidebar.html component:`html
All Notes
x-for="note in notes" $$wrap="template" ::showPreview ::size="large" />
`
If you're unfamiliar with Tailwind CSS, the classes we've applied to the inner
in App containing our Sidebar and Editor components provide for a space that occupies the entirety of the browser below the Navigation component — we'll call this space the "content" area. Then, the classes applied to the wrapping
in Sidebar provide for a space that occupies the entire height and width of the content area.
> *The
Modal component is position: absolute; and does not count against our app's vertical space.
However, the sidebar component is just that, a sidebar. It shouldn't occupy the entire content area – only a space to the side of it. By passing the
w-32 class in the class attribute for Sidebar in App, Snowblade will add w-32 to the class attribute of the wrapping
in Sidebar when it renders its HTML for App. The Tailwind CSS class w-32 will limit the width of Sidebar to 8rem, leaving the remaining space in the content area to be occupied by Editor, on which we've applied a Tailwind CSS class of flex-1.
In this way, our sidebar component can be reused anywhere throughout our app. Attributes that need to be modified based on context can be applied onto the component expression itself rather than hard-coded into the component definition.
$3
Looking at the component document for
Sidebar, you might have noticed that the component expression for NoteThumbnail is looking a little busy. In addition to applying an attribute for Alpine, x-for, the component expression also has a few attributes specific to Snowblade. To understand what each of these attributes does, let's look at the component document that the NoteThumbnail expression references:
App, we already know that any attributes we apply to a component expression will be applied to the root element within that component. However, those familiar with Alpine know that the x-for attribute can only be applied to a element, and the component definition for NoteThumbnail uses a
as its root.
We could just wrap the root
inside of a within the component definition, but suppose we want to use our thumbnail component elsewhere within our app? With it wrapped inside of a tag, we might lose the ability to reuse it. Instead, the expression for NoteThumbnail in Sidebar applies the Snowblade $$wrap attribute, which will wrap the component definition in a specified tag (in this case, a tag) before applying any additional attribute logic. In this way, when the component's HTML is rendered, it will look like this:`html
type="checkbox" class="p-2 mx-auto" />
`
Notice that Snowblade applied the attribute
x-for to the specified wrapper, and not directly onto the
inside the component definition. This is critical to note, as the $$wrap attribute, is the first thing to evaluate when parsing a component expression.
$3
Examining that output, did you notice anything else? The
class attribute for the last
contained some mustache syntax as {{ showPreview }} which was rendered as: `html
`
This is because a property was passed in the component expression for
NoteThumbnail as ::showPreview. Snowblade components can receive properties using the syntax ::propName="value" on a component expression. Within the component, these property values are yielded using property tokens:`html
{{ propName }}
{{ propName | pipeName }}
`
Within the
of the component definition, property defaults can be given that are used when the component expression is absent of said property:`html snowblade name="NoteThumbnail" ::showPreview="hidden" ::showCheck="hidden" ::size="medium" > `
For the property
showPreview, a default value of "hidden" is given (display: none; in Tailwind CSS), but on the component expression for NoteThumbnail, this is overriden to an empty value as ::showPreview. In this way, you can make use of "boolean-style" properties for your components, where a truthy or falsy value is the property default that gets overriden by an empty value on a component expression.
$3
Reviewing the component expression for
NoteThumbnail you'll notice that the ::size property was passed as "large", but was rendered as:`html
`
In this case, before
size was expressed, it was processed by a pipe — a function accepting a single argument and returning a string:`js function size(arg) { return { large: 'h-16', medium: 'h-8', small: 'h-4', }[arg]; } `
Pipes can be useful where you'd like to use more "natural" language to express complex strings of data. In
NoteThumbnail it's being used to convert values of large, medium, and small into Tailwind CSS height classes. Back in the example snowblade.config.js, the pipe TailwindMxAlign is defined to convert left, right, and center into their margin equivalents — useful when making use of flexbox:`js TailwindMxAlign(arg) { return { left: 'mr-auto', center: 'mx-auto', right: 'ml-auto' }[arg === '' ? 'center' : arg]; } `
As you've probably noticed, there's two places where pipes can be defined. Where a pipe is only needed for a single component, it can be declared in a
.
Slots
Component slots in Snowblade work exactly as you'd expect. As of this writing, Snowblade supports one type of slot — the default slot. Named slots are under consideration, so if you'd like to see more development on this, please suggest an implementation strategy by raising an issue.
Within a component document, a default slot can be expressed using the
tag with an empty snowblade attribute:`html
Some default content goes here.
`
When using a component expression, the
will be fulfilled by including markup between the expression's tags:`html
This is my slot content.
`
If no content is supplied, Snowblade will render the default content expressed between the
tags in the component definition.
$3
Suppose you've defined a button component that you want to use with a slot:
basicbutton.html
`html
type="button" class="py-2 px-4 bg-{{ fill }}-500 border border-{{ fill }}-500 hover:bg-{{ fill }}-400 text-white transition ease-in duration-200 text-center text-base font-semibold rounded-lg w-{{ width }}" > {{ label }}
`
When you use the
BasicButton expression, you could fulfill the button's label in one of two ways:`html
Click Me!
`
This works because slot processing is the second step that takes place when parsing a component expression — right after the
$$wrap attribute is evaluated. Because the default slot content is the property token {{ label }}, Snowblade will have access to substitute a given value when evaluating the expression-assigned property ::label.
> #### Why would you want to do this? > > Using a property token as the default slot content can be helpful if you want the ability to globally-manipulate a component's slotted content. Through the use of config-provided property values, you can manage slot content in one location. > > This is also really useful for when using magic properties with Alpine, so please keep reading!
Magic Properties
Saving the best for last, magic properties are what make Snowblade the perfect companion for Alpine developers. Consider the
BasicButton component definition described above. If you wanted to make it more "Alpine-friendly," you could make some changes:`html
x-for, your component expression would look something like this:`html
`
That works, but it's less than ideal. In the component definition, you've had to do a lot of escaping and now, if you want to reuse the button outside of an Alpine context, you can't because
class is declared as x-bind:class and the button's label is expressed as x-text rather than native HTML. Additionally, you now have to surround static property values with single quotes, like the default values for fill and width.
Is there a better way? Of course there is.
$3
Let's restore the original version of the
BasicButton definition:`html
type="button" class="py-2 px-4 bg-{{ fill }}-500 border border-{{ fill }}-500 hover:bg-{{ fill }}-400 text-white transition ease-in duration-200 text-center text-base font-semibold rounded-lg w-{{ width }}" > {{ label }}
`
Now, instead of mangling the component definition, let's instead change the way that we write the component expression:
`html
`
Take note of the
$ chars added to the expression properties label and fill. When Snowblade compiles, this is what you'll see:`html
At first glance, there doesn't seem to be anything wrong, but within the
innerHTML of there's some interesting considerations for Snowblade:
* A property token,
{{ label }}, is inline with static HTML. * Within that static HTML next to a property token, another property token, {{ icon }}, is inside of an attribute.
For standard Snowblade expression properties, this is a simple case of substitution, and nothing about which to worry. With Alpine, however, there's more to consider. If you want to dynamically set the property of
labelandicon, you'd need to wrap {{ label }} inside of a
or , so that it can be assigned x-html, right?
Not with magic properties! Leaving things as they are, let's write our component expression:
x-html: * Like before, the value is wrapped as a template literal with ` chars. * Where a dynamic expression is required, it's wrapped as a template literal placeholder ${...}. * Most importantly, the entire content of the innerHTML of has been escaped into HTML-safe chars that will render exactly as you require at runtime. Alpine writes each element to the DOM at runtime, and the browser doesn't know any differently.
> #### Just because you can, should you? > > Probably not. > > I wrote this feature into Snowblade because it felt like something that should be there, but when debugging an app, the less complexity there is, the better. If you don't mind taking the extra 1.2 seconds required to wrap your property token inside a
element, you may save yourself some time if you ever need to dive into the browser inspector.
Attribute Coalescence Control
You've already seen that Snowblade can coalesce attributes onto the root elements of a component definition. This makes it easy to leverage utility frameworks like Tailwind CSS to create variants of existing components. To even further reduce redundancy, we can extend this behavior using action attributes.
$3
As you begin breaking-down your application's markup into recyclable components, you're likely to find that you have a need to begin nesting fundamental components within others to create variations without redundancy. Consider an input-based example where we have two components,
LabeledInput establishes an expression for BasicInput, and explicitly re-declares all of the properties that could be passed onto it. There's nothing wrong with this, and it would indeed work, but it also creates redundancy and requires you to update two component definitions if you decide to change the property declarations on one.
Instead of explicitly re-declaring, you can make use of any combination of Snowblade action attributes to do the heavy lifting for you, let's rewrite our definition for
LabeledInput, using the action attributes $$provides, $$reserves, $$accepts, and $$declared:
With our new action attributes in-place, we can now write our component expression for
LabeledInput as:`html `
Let's break down what each action attribute does for us:
* The
$$provides attribute, declared with no value, tells Snowblade that any attributes (property, magic, or plain-old HTML) should not be applied to the markup for LabeledInput, but should be held for distribution to elements contained within. * If we wanted to be specific, we could write the value of $$provides as $$provides="::placeholder ::value ::color ::width", which would explicitly pass-on only those attributes — retaining the rest for property expression or coalescence. * The $$reserves attribute, with a value of $$declared, tells Snowblade that while we're passing-on any attribute given on an expression of LabeledInput, we still want to keep to value of any declared properties so that they can be expressed in the markup for LabeledInput. * Like $$provides, giving an empty value for $$reserves acts as a wildcard — retaining all attributes given on LabeledInput for property expression or coalescence. * The $$declared keyword can only be used in the value for $$reserves or $$accepts. It has no effect in $$provides. * The $$accepts attribute, declared with no value, tells Snowblade that any attributes collected by $$provides should be coalesced onto the expression for BasicInput. Again, this works for both properties, magic properties, and plain-old HTML. * Like $$provides and $$reserves, we can also be explicit in the way that we use $$accepts. If desired, we could write $$accepts="::placeholder ::value ::color ::width" * Expression of the $$accepts attribute can be done multiple times throughout a component definition, and on both component expressions as well as standard HTML elements. * The $$accepts attribute, declared as $$accepts="class" ensure's that any declaration for class on LabeledInput is passed on to our wrapping
. In this way, we can express with of our component using Tailwind's w-XX classes. * The $$rejects property, declared as $$rejects="class" ensures that we don't pass on the class declarations we intend to use on the wrapping
only.
In this way, whether you provide an attribute using property syntax, magic syntax, or plain-old HTML, you can tell Snowblade where you want your declarations to go.
$3
Using
$$provides and $$accepts is easy enough, but it still exposes us to the potential for added redundancy. Suppose we intend to use BasicInput in more than one component. We'd have to write a $$provides and $$accepts attribute for each occurence — leading to fragmented control.
Instead, where we can anticipate desired attribute coalescence, we can make use of the
$$utilizes attribute to keep things terse. Let's rewrite the definition for BasicInput and assume we have another component, BasicButton that we're going to use in a new definition, OneButtonInput:
type="button" class="py-2 px-4 bg-{{ color }}-500 border border-{{ color }}-500 hover:bg-{{ fill }}-400 text-{{ text }} transition ease-in duration-200 text-center text-base font-semibold rounded-lg w-24" onclick="{{ click }}" > {{ label }}
`
Notice that in both definitions, we've assigned
$$accepts in the tag. This indicates to Snowblade that every expression of our component will acquire provided attributes according to the value for $$accepts:
*
BasicInput * Acquires provided attributes ::placeholder, ::value, ::color, and ::width, because they are implied by $$declared. * Acquires provided attributes readonly and required, because they are explicitly stated. * BasicButton * Acquires ::click, ::label, ::color, and ::text because they are implied by $$declared.
Let's write our component definition for
OneButtonInput with these characteristics in-mind:
OneButtonInput, we can express the properties for BasicInput and BasicButton like this:`html `
In the
tag for our component definition, we've established $$utilizes and referenced both BasicButton and BasicInput. This tells Snowblade to refer to the value of $$accepts in each given component definition. Any attributes found there will be treated as if we'd established $$provides on OneButtonInput and $$accepts on each component expression.
$3
Looking at the expression for
OneButtonInput, we can see that we've left-out a few declared properties and, if we look closely look at the definitions for BasicButton and BasicInput, we can see that the property color is declared in both components with very different implementations. In BasicButton, color refers to the fill colour of the button, while in BasicInput, it refers to the border colour of the input field. If we tried to establish ::color on , Snowblade would pass on the property to both components, and we may not like what we see.
Instead, we can use the
$$exposes attribute to designate a prefix controlling attribute coalescence. Consider an example that uses both BasicInput and two expressions of BasicButton:
BasicButton and BasicInput, the $$exposes attribute provides a moniker through which each individual expression can be addressed. On the component expression for TwoButtonInput, this is done by concatenating the value of each $$exposes with a pipe | char, and then the attribute we want to pass. As we're able to address each element directly, the attributes passed can be properties, magic properties, or plain-old HTML attributes.
While it's common that you'll want to pass attributes onto component expressions, you can also use
$$exposes to expose standard elements as well:`html
My faxed joke won a pager in the cable TV quiz show.
My faxed joke won a pager in the cable TV quiz show.
`
$3
If you prefer a more succinct approach to component exposition, you can also establish the
$$exposes attribute in the tag of a component definition:
BasicButton within a component definition, you can access button|attribute="" on your expression without providing $$exposes directly. If you need multiple occurrences of BasicButton, Snowblade will use numeric incrementation to address each occurrence, starting from the top of your component's markup. In this way, TwoButtonInput could be written as:
twobuttoninput.html
`html
`
Then, assuming you also established
$$exposes="input" in the definition for BasicInput, your expression of TwoButtonInput could look like this:`html required ::$value="username" ::placeholder="@johndoe" input|::color="blue" button|::click="validateUsername()" button|::color="gray" button2|::click="submitUsername()" button2|::color="blue" /> `
Indexing for incremented exposure begins at
1. If you don't provide a numeric value (like the example above), Snowblade assumes a value of 1 unless another element in the component definition is assigned $$exposes directly as $$exposes="button". In cases such as these, Snowblade would pass anything prefixed with button| onto that element, and anything prefixed with button1| onto the first instance of BasicButton.
$3
#### Declared Only on Component Metadata
Attribute | Function :--- | :---
$$provides | Attributes matching this space-delimited list will be distributed to elements or component expressions and not coalesced onto the root component element or expressed as properties. $$reserves | Attributes matching this space-delimited list will be coalesced onto the root component element or expressed as properties, and not distributed to elements or component expressions. $$utilizes | The component will implement the $$provides, $$accepts, $$reserves, and $$exposes rules of component names matching this space-delimited list. $$declared (value) | This is a value that can be assigned to $$reserves or $$accepts, only when expressed on component metadata.
#### Declared on Either Component Metadata or Elements/Component Expressions
Attribute | Function :-- | :--
$$exposes | The given value is used as a prefix in the pattern prefix\|attribute="value" to edit the element or component's attributes directly. $$accepts | Attributes matching this space-delimited list are received for coalescence where given by a $$providesdeclaration.
#### Declared Only on Elements/Component Expresssions Attribute | Function — Must be used with
$$accepts :--- | :--- $$rejects | Attributes matching this space-delimited list will be rejected for coalescence where given by a $$provides declaration (useful when using wildcard $$accepts). $$revises | Attributes matching this space-delimited list will replace (instead of coalesce with) their existing values (useful for type on , etc.).
Licensed under the MIT license, see LICENSE for details.
About the Project
Hi, I'm Stephan, and I'm the developer of Snowblade.
> #### TL;DR > > * Snowblade was built after I got tired of trying to learn Vue and React. > * Please be my friend. > * If you enjoy using Snowblade, please consider sponsoring the project.
I started work on this project in mid-December and took almost two months off of my primary job as a freelance software developer to bring the concept to life. My work on it began after a single-page report for a client's project ballooned from 50 lines of code to +1200. The report, driven by Alpine, worked flawlessly, but was severely lacking with regards to organization. After taking a phone call in which I tried to debug a discrepancy with the client in realtime, struggling to sift through a mess of
#region tags and arrowhead comments just wasn't doing it for me. I wanted a better way to work.
The obvious answer was to switch to React or Vue, but both of them felt like overkill for the use case, and there was the little matter of me not knowing how to use either. After watching several Vue tutorials at Laracasts, one video's comment stuck with me:
"This is so much easier to do in Alpine."
That was the straw that broke the camel's back. He wasn't wrong. Getting up and running with Vue, especially for something as nominal as a single-page report, required a ton of overhead; a steep learning curve; and felt less than intuitive when compared with Alpine. I had everything I needed in my Alpine model — all I wanted was a little organization.
For about three nights, I scoured the Internet searching for existing solutions. Things came close, but not close enough. The goal I had in-mind was pretty basic: I didn't want to write any JavaScript — everything had to be HTML-based. No template literals, no JSX, no pragma... just pure HTML. I didn't find anything.
To me, it seemed crazy that things like Babel and WebPack existed, but nothing filled-in the transpiler gap for HTML. In reality, though, the need for something like this is a relatively fresh concept. When Caleb Porzio wrote Alpine, he introduced a new way to build reactive frontends that kept developers' work in one place: the DOM. With that in-mind, I got to work. Building something like this seemed like a great way to give back to the open-source community and an opportunity to make new friends. No, seriously, I only know like two other developers — please be my friend.
Snowblade is definitely still in its early stages of development. When I started the project, I wasn't familiar with TypeScript, so there's a considerable amount of refactoring to be done. As I needed Snowblade to work for my job, lots of "hacks" have been applied with
// FIXME:` comments. With a reasonably-working preview, I'll be doing what I can in the coming weeks to burn through these, and promise to open the source up for pull requests once this is completed.
In the interim, please submit ideas, feedback, bug reports and any other considerations as issues right here on GitHub. As with any open-source project, Snowblade is built as my work allows for it. If you enjoy using Snowblade, and want to see further development, please consider sponsoring the project.