Build for the web using zero JavaScript.
Basically, ZeroScript is just vanilla HTML with a few extra tags. Its purpose is to reduce the web's dependency on JavaScript by opening the door for other languages to compete.
Making JavaScript interchangeable is unrealistic unless other languages can both generate HTML AND manipulate the DOM using one common approach. The idea is to use an HTML-first strategy. Instead of putting HTML inside your logic, ZeroScript puts your logic inside your HTML.
In many ways, ZeroScript is like Markdown. It's not an implementation but rather a small set of rules for outputting predictable results for the web regardless of language choice. Like Markdown, there's also room for various "flavors" to bring their own special embellishments.
ZeroScript's purpose is grounded in the desire for the web to remain THE melting pot of human ideas and progress. The best way to prevent stagnation is to open the floodgates for other languages to compete.
Important
The examples in this README use a fictional language called Any++
in order to provide concrete examples without any favoritism. Conceptually, any imperative language could be substituted.
- Components
- Attributes
- Children
- Control Flow
- Styles
- Routing
- Bring Your Own Language (BYOL)
- State Management
- Ecosystem
Every .html
file is automatically a component. No need to register or import anything. Just use its file name. File names are limited to alpha-numerics, hyphens, underscores, and cannot begin with a number.
my-button.html | index.html |
<button>
Click me
</button> |
<html>
<body>
<my-button />
</body>
</html> |
Note
Markdown files (.md
) make great components too.
HTML has a spirit of being forgiving, therefore naming collisions must not result in an error. Since two files can have the same name if located in different directories precedence is determined as follows:
- Prefer the file that is in the same directory as the referencing component.
- Prefer any files that are in a descendant directories over any files located in ancestor paths (i.e. great-great-grandchildren before cousins).
- Next, prefer the file with a shorter directory depth
- Finally, prefer the file whose path sorts alphanumerically first.
To explicitly reference components with naming collisions:
- Use
/
notation + the names of directories. (Capitalization matters.) - Use
//
as shorthand notation to skip any number of directories - There is an implied
//
at the beginning of every component
📦 my-project
└─ 📂 ui
├─ 📜 index.html
├─ 📂 music
│ └─ 📂 components
│ └─ 📜 score.html 1️⃣
└─ 📂 sports
├─ 📂 components
│ └─ 📜 score.html 2️⃣
└─ 📂 music
└─ 📜 score.html 3️⃣
index.html
<html>
<body>
1️⃣ <score />
2️⃣ <sports//score />
3️⃣ <music/score />
</body>
</html>
HTML attributes are an easy way to pass inputs into a component thus boosting reusability.
You'll notice the use of the {{ }}
escape sequence in the component file. You'll learn more about this in the Hole Punch section below.
index.html | my-button.html |
<html>
<body>
<my-button name="Rylan" />
</body>
</html> |
<button>
Hello {{name}}
</button> |
Note
A component's attributes are optional by default. The ability to denote some attributes as "required" is outside the scope of this document but is a highly recommended "flavor" to be provided by the guest language.
In your attributes, when using data types other than strings, use JSON-like syntax where the double-quotes are excluded. Non-primitives, such as objects and arrays, must be handled inside a hole-punch {{ }}
so that the specific syntax can vary by language.
index.html
<html>
<body>
<my-button text="Hello" count=1 weight=3.14 cta=true time={{date}} />
</body>
</html>
Any file in the same directory with the same filename but different extension must be conceptually treated as a part of the same component. Rules for handling may vary according to file type.
For example, including a .css
file makes it trivial to include styles with your component. However, should that component ever repeat its HTML, its CSS would only ever be included once.
Sibling files can also make use of hole punches {{ }}
.
File Structure | index.html | Output |
|
<html>
<body>
<component />
<component />
<component />
</body>
</html> |
<html>
<body>
<style>
...CSS content...
</style>
<div>
...HTML content...
</div>
<div>
...HTML content (repeated)...
</div>
<div>
...HTML content (repeated)...
</div>
</body>
</html> |
Note
While sibling files can technically work with .js
files too, that's not the recommended approach. This might be useful if you are certain your website will only ever be statically generated. But it's important to note that ZeroScript has a different language-agnostic approach for building dynamic features that offers a gradual migration path from a fully static website to a fully dynamic web app. You'll learn more about this in the BYOL section below.
To simplify composability, any child-tags to a component can be accessed using {{content}}
. This is intentionally similar to attribute-usage. This makes content
a reserved keyword and therefore cannot be used as a component's attribute.
index.html | my-button.html |
<html>
<body>
<my-button>
<b>Please</b> click me
</my-button>
</body>
</html> |
<button>
{{content}}
</button> |
It is possible to provide outside HTML as inputs to a component. However this isn't done through its attribute
s but using slot
s instead. Similar to Web Components, child-tags can be used as HTML-based inputs using the slot
keyword.
Any tags without a slot
attribute must default to the reserved slot content
. Slot order does not matter and any missing slots are treated as null
.
index.html | my-button.html |
<html>
<body>
<my-button>
<img slot="icon" src="..." />
<span slot="text">
I am <i>more</i> than a <b>string</b>.
</span>
</my-button>
</body>
</html> |
<button>
{{icon}} • {{text}}
</button> |
Note
Slots are optional and if not supplied, the component must still function. Similar to attributes, though, it possible and recommended for a guest language to support "required inputs" much like a class with a non-nullable property.
ZeroScript uses a few tags for control flow: <if>
, <else>
, <else-if>
and <foreach>
. These are reserved keywords so custom components by those names are not allowed. While the content inside these tags might be included, excluded or repeated, the enclosing tag itself is never included in the generated HTML. (The browser wouldn't know what to do with an <if>
tag anyway right?)
Most other frameworks choose to use their natural syntax instead of extending HTML to handle control flow. For them, this is a sensible choice since it allows for greater flexibility. Since the purpose of ZeroScript is to be language-agnostic, it takes a more generic approach by simply extending HTML and to lean into its hole-punching approach for extending functionality.
The <if>
tag conditionally excludes its content. It uses a valueless attribute as the condition for terseness.
<if {{user == null}}>
<sign-in-form />
</if>
Use <else>
and <else-if>
tags in conjunction with <if>
. These are sibling tags and do not require a parent container as you see with <ul>
and <li>
.
<if {{loginState == .LOGGED_OUT}}>
<sign-in-form />
</if>
<else-if {{loginState == .LOGGED_IN}}>
<sign-out-button />
</else-if>
<else>
<circular-progress />
</else>
The if-let
syntax lets you handle your dynamic content in a less verbose way. If the expression evaluates to null
, all child content is excluded from the generated HTML. Otherwise, the compiler can safely assume non-nullability for the provided attribute name.
In this example, if any of user
, profile
, name
, or first
are null then the entire child contents are skipped, including the "Welcome back " text.
<if firstName={{user?.profile?.name?.first}}>
Welcome back {{firstName}}.
</if>
Multiple null-checks are allowed as well as chaining with else
and else-if
.
<if first={{user?.firstName}} last={{user?.lastName}}>
Welcome back {{first}} {{last}}.
</if>
<else>
<h2>Loading...</h2>
</else>
Use a <foreach>
tag to repeat its contents in a declarative way.
<ul>
<foreach fruit in={{allFruits}}>
<li>{{fruit}}</li>
</foreach>
</ul>
CSS can be used normally, including embedded <style>
tags and externally referenced .css
files.
However, any .css
file that shares the same filename and parent directory as an .html
file is considered a sibling file and must automatically have its styles included before any HTML to prevent any dreaded FOUC. Inclusion must occur only once per HTML document, since its .html
counterpart might be repeated multiple times.
Component-level styles must be scoped to its component. It is not required to use a Shadow DOM like Web Components do.
my-button.css | Output |
p {
font-size: 18pt;
}
em {
color: orange;
} |
<html>
<body>
<style>
.my-button p {
font-size: 18pt;
}
.my-button em {
color: orange;
}
</style>
<button>
<p>This button has <em>style</em>!</p>
</button>
</body>
</html>
|
ZeroScript uses file-based routing as a language-agnostic way to define your URL routing patterns. ZeroScript does encourage any "flavor" to embellish additional features in their own native language syntax.
The only files publicly accessible by an HTTP route are index.html
files. This allows you to organize your components in any location without the concern for them being accessed directly.
The name of each directory represents a route segment for the URL. For example, if there were an index.html
file located at myproject/blog/about/index.html
it could be accessed at https://example.com/blog/about
. (Trailing slashes are intentionally excluded.)
If a directory does not contain an index.html
file, its corresponding HTTP route results in a 404.
A dynamic route segment can be used by wrapping the directory name in square brackets. Accessing the values of the dynamic routes will vary by language.
File System | HTTP |
---|---|
myproject/blog/[slug] |
example.com/blog/first-post |
myproject/products/[id] |
example.com/products/1234 |
Note
Directory-based routing is intentionally designed to only handle very basic use cases. It's encouraged that each guest language support more sophisticated routing in their own unique ways that cater to their each of their strengths.
For example query string parameters could be accessed from from a request object like {{req.slug}}
or like {{request.Routes["slug"]}}
, whatever fits most naturally in to the language.
The same non-prescriptive stance applies for supporting HTTP methods beyond simple GET
s such as POST
.
Displaying errors can be handled with a file by the name of the HTTP status code. For example the output of 404.html
would be used for any not-found routes. Use *
after the first digit as a wildcard. For example, 5**.html
would cover all server errors, negating the need to create a unique file for each error code. Using both 404.html
and 4**.html
is valid and the most specific pattern should be used.
Astute readers might notice that, at this point, we now have all the tools necessary to host or generate a static website composed of reusable components using only .html
and .css
files and nothing else.
This is a good thing. It ties the durability of your frontend to the longevity of the web itself. The more you can "embrace the platform" the less susceptible your codebase is to software rot. It shouldn't be an unreasonable expectation to return to a 10 year old project and have everything function exactly as before.
The sections that follow cover a common approach for progressively enhancing your website using your language of choice.
Use the DOM's regular events for event handling but instead of specifying JavaScript inside a string, reference your own method using a hole-punch escape sequence {{ }}
. This method can optionally choose to take the event as an argument.
<button onclick={{handleClick}}>
Click me
</button>
Using closures can be a valid option too, if your language supports it.
<button onclick={{e => count++}}>
Clicks: {{count}}
</button>
In the spirit of "embracing the platform" you can repurpose the <script>
tag for use by any language using the (to-spec) language
attribute.
When using the <script>
tag for any language besides JavaScript, be sure to never include it as a part of any generated HTML. (The browser wouldn't know how to execute it anyway right?) Execution can be handled server-side or client-side via WebAssembly. More on that in the Server-Side or Client-Side section.
my-button.html
<button onClick={{handleClick}}>
Clicks: {{count}}
</button>
<script lang="any++">
count = 0
void handleClick(event) {
count++
}
</script>
Tip
Whatever value is specified in the lang
attribute is technically moot since it's never included in the HTML. Therefore, whatever value is specified is more for human consumption than for the machine.
The sibling file approach works great for other languages. Any file in the same directory with the same filename but different file extension shall be treated as part of the same component. Rules regarding variable scoping and code-importing are intentionally undefined so that they can vary by language.
my-button.html | my-button.any |
<button onClick={{handleClick}}>
Clicks: {{count}}
</button> |
count = 0
void handleClick(event) {
count++
} |
The "hole punch" pattern {{ }}
is familiar since it appears like a regular string interpolation. However, in order to excel at both HTML-generation and DOM-manipulation there is one important distinction. Instead of simply returning a final string, it returns a "composition object" which simply just hangs onto the inputs for later use. Once a composition is built, it becomes trivial to either lazily generate the HTML in full or to compare its input values with an older composition's input values for anything that might have changed so that it may generate instructions needed for updating the DOM.
Contents inside the hole punch must be expressions, not statements.
The advantages of this approach are many:
- Simplicity - state changes don't require scope tracking
- Derived data - compositions compare the inline expression values, not the state itself
- Portability - works equally well in WASM as server-side since it does not depend on a DOM tree to re-compose
- Event Handling - DOM events are natural fit for marshalling
- Precision - can modify the values of individual DOM nodes instead of only brute-forcing large HTML fragments at a time
- Speed - no virtual DOM necessary
- Memory - no server-side node tree construction necessary
Why not both?
ZeroScript follows a Unidirectional data flow design pattern.
A unidirectional data flow (UDF) is a design pattern where state flows down and events flow up. By following unidirectional data flow, you can decouple composables that display state in the UI from the parts of your app that store and change state.
Thankfully the DOM's event-handling model encapsulates events as objects. This makes them a natural fit for marshalling across the boundary where your language of choice is running whether that be remotely on the server or locally as WebAssembly.
Additionally, if your transport supports bi-directional communication, like WebSockets or even WASM-marshalling, this opens up your web app for realtime functionality - specifically, DOM updates that aren't user-initiated.
Since ZeroScript calls for DOM events to be serialized, they can easily be transported to other runtimes for handling whether that be over WebAssembly interop, or through the network to the server. It can function across a number of different transports, each with their own benefits and tradeoffs.
Transport | Browser event fired | DOM mutation instructions |
---|---|---|
HTTP(s) | Event POSTed to server | Included in POST response |
Server-sent events | Event POSTed to server | Pushed via SSE |
WebSockets | Event sent via WebSocket | Pushed via WebSocket |
WebAssembly | Event marshalled over interop | Marshalled over interop |
Important
All transports support realtime features through bi-directional communication except for HTTP(s).
There's nothing negative about using an API. It's just not necessary with the ZeroScript programming model. To compare, classical SPAs might respond to a DOM event in the browser by fetching data from an API and updating the DOM accordingly. With ZeroScript frameworks, since the event is automatically sent to the server for handling, there's no need to call a separate API since the data can be accessed directly.
Note
This only applies to server-side runtimes. WebAssembly runtimes will likely still prefer using an API.
Use <state>
for values that change. <state>
can be shared with and changed by other components.
<state score=0 />
<MyButton count={{score}} />
<h1>The score is {{score}}</h1>
When <state>
is assigned to an attribute, any changes to the attribute's value also changes the <state>
. Here, every keypress will update the <h1>
tag.
<state name="World" />
<input value={{name}} />
<h1>Hello {{name}}!</h1>
In the same manner as GitHub flavored Markdown, it is encouraged that guest languages bring their own embellishments that take advantage of their syntax's unique features. These deviations can borrow the "flavor" terminology in the same way, for example, "Acme flavored Zero."
Below is a running list of known guest languages and the various features they support.
Features | xUI Tags |
---|---|
Dynamic hosting | ✅ |
SSG (static site generation) | coming soon |
AoT (Ahead of time compilation) | coming soon |
WASM (WebAssembly) | coming soon |
WebSockets | ✅ |
SSE (server-side events) | coming soon |
Event-POSTing | coming soon |
Local caching | coming soon |
Optimistic writes | coming soon |
Animations | coming soon |
Global & session state | coming soon |
Branch preview | coming soon |