Compare commits

...

No commits in common. "53e19602f23f77315b552776d9dd8d083abd10af" and "8624d80eb8a62760af600bfe173764e57cdbaaa1" have entirely different histories.

3 changed files with 197 additions and 168 deletions

View file

@ -2,15 +2,23 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<script type="importmap">
{
"imports": {
"nyooom/render": "https://cdn.jsdelivr.net/gh/darkwiiplayer/nyooom@d457c38/render.js",
"nyooom/observable": "https://cdn.jsdelivr.net/gh/darkwiiplayer/nyooom@d457c38/observable.js",
"nyooom/showcase": "./js/nyooom-showcase.js"
}
}
</script>
<script type="module" src="https://cdn.jsdelivr.net/gh/darkwiiplayer/components@master/TypeWriter.js"></script>
<style>
@import
/* url('https://cdn.jsdelivr.net/gh/darkwiiplayer/css@main/all.css') */
url('https://darkwiiplayer.github.io/css/all.css') layer(framework);
@import
/* url('https://cdn.jsdelivr.net/gh/darkwiiplayer/css@main/schemes/talia.css') */
url('https://darkwiiplayer.github.io/css/schemes/talia.css') layer(theme);
@import url('styles.css') layer(site);
.jsdelivr-badge {
@ -45,7 +53,7 @@
</script>
<script type="module">
import "./js/NyooomShowcase.js"
import "nyooom/showcase"
</script>
<a name=top></a>
@ -54,8 +62,8 @@
<nav class="bar">
<ul>
<li><a href="#top">Top</a></li>
<li><a href="#getting-started">Getting Started</a></li>
<li><a href="https://github.com/darkwiiplayer/nyooom">GitHub</a></li>
<li><a href="https://git.but.gay/darkwiiplayer/nyooom">Git</a></li>
<li><a href="https://github.com/darkwiiplayer/nyooom">Github</a></li>
<li><a href="https://www.npmjs.com/package/nyooom">npm</a></li>
</ul>
</nav>
@ -66,7 +74,7 @@
<!-- <img src="https://picsum.photos/1920/1080"> -->
<hgroup>
<h1 style="font-size: 6vw">Nyooom</span></h1>
<code lang="js">import { html } from "nyooom.js"</code>
<code lang="js">import { html } from "nyooom/render"</code>
<p>
A new way of building
<type-writer loop>
@ -85,85 +93,74 @@
</flex-row>
</page-hero>
<vertical-spacer style="height: 6rem"></vertical-spacer>
<main style="margin-top: 0">
<section id="elevator-pitch" class="box">
<div class="content-width" style="padding: 2em 0">
<h2>Elevator Pitch</h2>
<main>
<section id="elevator-pitch" class="content-width">
<h2>Elevator Pitch</h2>
<p class="big">
Nyooom combines <em>easy</em> and <em>powerful</em> HTML generation with optional <em>reactivity</em>, all in plain JavaScript.
</p>
<p class="big">
With nyooom <em>Nyooom</em> you can elegantly express nested DOM structures in <em>plain JavaScript</em>.
</p>
<p class="big">
At its core, it is a library to <em>generate HTML elements</em>.
</p>
<p class="big">
Together with its utility modules like <code>Observable</code>, it turns into a powerful <em>front end micro framework</em>.
</p>
<nyooom-showcase>
<nyooom-showcase preview=false>
<code><div contenteditable="false">return html.p(
"Text Node",
html.br(),
html.b("HTML Node"),
{style:{color:"salmon"}},
)</div></code>
</nyooom-showcase>
</nyooom-showcase>
<vertical-spacer></vertical-spacer>
<vertical-spacer></vertical-spacer>
<p>
Nyooom aims to be <em>small</em> enough that you can use it even if you only need it in a single function,
<em>easy</em> enough that you can figure out what's going on even if you've never used it before and
<em>powerful</em> enough that you won't <i>need</i> another framework, without preventing you from using one if you prefer.
</p>
<p>
Nyooom is <em>small</em> enough that you can use it even if you only need it in a single function,
<em>obvious</em> enough to figure out what's going on even if you've never used it,
<em>powerful</em> enough that you won't <i>need</i> another framework and
<em>flexible</em> enough to let you use one regardless.
</p>
<p>
If that sounds good, it's time to back up those claims!
<p>
</p>
Continue reading to get an overview of how nyooom works and what it can do.<br>
Or jump down to the <a href="#nyooom-explained">explainer</a> to get a more detailed explanation of what nyooom is good at and why.
</p>
<p>
If that sounds good, it's time to back up those claims!
</p>
</div>
</section>
<section id="getting-started" class="content-width">
<h2>Getting Started</h2>
<section>
<p class="important">
Trying out nyooom is super easy!<br>
Just import the <code>html</code> export into your script and start generating DOM nodes.
<p>
Trying out nyooom is super easy:
</p>
<pre><code lang=js>import {html} from "https://cdn.jsdelivr.net/npm/nyooom@1.3.1/nyooom.min.js"</code></pre>
<pre class="box"><code lang=js>import {html} from "https://cdn.jsdelivr.net/npm/nyooom/nyooom.js"</code></pre>
</section>
<section>
<p>
Indexing this object with any value will return a node factory function.<br>
The type of node is decided by the property name: calling <code>html.div()</code> will create a <code>&lt;div&gt;</code> node.
Calling any method on the <code>html</code> export will generate an HTML node.<br>
Passing in arguments is how you add children, set attributes and even do more advanced things.
</p>
<nyooom-showcase preview="false">
<code>
<div contenteditable="false">return html.div()</div>
<div contenteditable="false">return html.div("content")</div>
</code>
</nyooom-showcase>
<p>
If you quickly want to render your HTML to a website to see the result,
the component helper found in <code>sckooma/observable.js</code> makes this a lot easier.<br>
Using the <code>component</code> helper from the <code>nyooom/observable</code> module
makes it easier to see results on your page:
Just pass it a function that returns some DOM nodes, and it'll register a custom element for you.
</p>
<pre><code lang=js>import {component} from "https://cdn.jsdelivr.net/npm/nyooom@1.3.1/state.min.js"
<pre class="box"><code lang=js>import {component} from "https://cdn.jsdelivr.net/npm/nyooom/observable.js"
const myComponent = () =&gt; html.div("Rendered Component")
component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
<p>
Of course you can also just call <code lang=js>document.body.append(html.span("My Text"))</code> in your script.
Of course, you can also just call <code lang=js>document.body.append(html.span("My Text"))</code> in your script.
</p>
</section>
</section>
@ -174,10 +171,7 @@ component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
<section>
<p>
Content can be added to a node by simply passing it as arguments to the function.
String arguments get inserted as text nodes, and DOM nodes are simply appended to the child list.
<br>
This way, your JS code can start looking almost like a proper templating language,
albeit with some extra quotes and braces here and there.
This works for strings, HTML elements, functions and even observable state containers.
</p>
<nyooom-showcase preview="false">
@ -211,7 +205,7 @@ component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
<section>
<p>
If the value is a function, it'll be added as an event listener instead.
Event listeners work just like attribute, except the value is a function:
</p>
<nyooom-showcase code="false">
@ -225,8 +219,16 @@ component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
</nyooom-showcase>
<section>
<p class="big">
Some "attributes" are handled in special ways to make things easier:
</p>
<p>
Setting inline styles is just as easy:
<code>style</code> sets the inline styles of the node.
</p>
<p>
<code>camelCase</code> names will be converted to <code>kebab-case</code> and values
will be converted to strings.
</p>
<nyooom-showcase>
@ -243,13 +245,9 @@ component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
)</div>
</code>
</nyooom-showcase>
<p>
<code>camelCase</code> names will be converted to <code>kebab-case</code> and values
will be converted to strings.
</p>
<p>
And the same goes for the <code>dataset</code> property.
<code>dataset</code> converts its key-value pairs into <code>data-</code> attributes.
</p>
<nyooom-showcase preview=false>
@ -266,15 +264,16 @@ component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
</nyooom-showcase>
<p>
And setting <code>shadowRoot</code> to a DOM node or an array of such will attach a shadow DOM to the newly created node.
<code>shadowRoot</code> will attach a shadow DOM to the newly created node.
</p>
<nyooom-showcase code=false>
<code>
<div contenteditable="false">return html.div(
{ shadowRoot:
html.p("Shadow DOM text")
},
{ shadowRoot: [
html.p("Shadow DOM text"),
"Arrays work too :)"
]},
"Light DOM text"
)</div>
</code>
@ -285,12 +284,12 @@ component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
<section class="content-width">
<h2>It's all just JavaScript</h2>
<h2>It's just JavaScript</h2>
<p>When generating HTML with nyooom, you never stop writing vanilla JavaScript that runs directly in your browser.</p>
<p>Nyooom is entirely implemented as a light-weight JavaScript library.</p>
<p>
What this means is that there's no new syntax for things you already know how to do.
This means that there is no new syntax for things you already know how to do.
Functions like <code>filter</code> or <code>map</code> can be applied directly to your data,
nodes can be assigned to variables right as you create them,
common structures can be extracted into function and even passed around as arguments,
@ -308,131 +307,91 @@ component(myComponent) //Registers it as &lt;my-component&gt;</code></pre>
)</div>
</code>
</nyooom-showcase>
</section>
<section class="content-width">
<h2>State changes in Nyooom</h2>
<pre><code lang=js>import {ObservableObject} from 'nyooom/observable.js'</code></pre>
<h3>nyooom.js</h3>
<pre class="box"><code lang=js>import {ObservableObject} from 'nyooom/observable'</code></pre>
<p>
Nyooom considers object with a truthy <code>observable</code> property to be Observables.
These objects are assumed to have a <code>value</code> property representing their current
value, and to emit a <code>"change"</code> event when their value changes.
Nyooom offers a series of state management classes it calls "observables".
Their purpose is to store one or more values and emit events when they are changed.
</p>
<p>
Passing an observable to a generator function where an attribute would be expected
will bind the attribute to the observable.
The attribute value will be set to the initial value of the observable,
and future changes of the observable will update the value of the attribute.
</p>
<p>
Passing an observable where a child element would be expected
will insert a reactive child element by inserting the current value
of the observable and replacing it whenever the value gets updated.<br>
For primitives, this means they will first be converted to strings and then into a text node.
</p>
<p>
Special attributes are also supported. For example, setting an attribute to
an observable with a function value, the function will be registered as an
event listener. Changing the value of the observable to a different function
will unregister the current listener and register the new one instead.
</p>
<h3>observable.js</h3>
<p>
This module exports several classes that store state and emit events whenever the state changes.
</p>
<p>
The most generic one of these would be <code>ObservableObject</code>,
which in its <code>values</code> property exposes a Proxy to a plain JavaScript object.
Changing any value on this proxy will emit an event on the associated <code>ObservableObject</code>.
By default, changes on all Observables are enqueued and an event is dispatched in a microtask.
The <code>render.js</code> library handles observables by setting DOM nodes up to
get updated when the observable emits a change event. Neihter module depends on the other
and they use a common protocol to work together.
</p>
<nyooom-showcase code="false">
<code>
<div contenteditable="false">const counter = new State({value: 0})
<div contenteditable="false">const counter = new State(0)
counter.valueChanged = newValue =&gt;
console.log(`Value: ${newValue}`)
return html.flexColumn(
{gap: 1},
return html.buttonGroup(
html.button(
"Click Me! ",
html.span(counter),
{ click: () =&gt; {
counter.value += 1
}}
html.span(
counter.compose(val =&gt; val ? val : "")
),
{
click() {
counter.value += 1
}
}
),
html.button(
"Clear",
{ click: () =&gt; {
counter.value = 0
}}
"Clear", {
click() {
counter.value = 0
}
}
)
)</div>
</code>
</nyooom-showcase>
<!-- TODO: Describe constructor options -->
<p>
The basic <code>State</code> object is backed by a plain JS object, so their attributes are unique and do not persist page reload.<br>
By contrast, the <code>StoredState</code> class, which extends the core <code>State</code> class, is backed by a <code>Storage</code> object like
<code>window.localStorage</code> or <code>window.sessionStorage</code>, meaning that they persist page reloads.<br>
Additionally, they detect changes both from the current as well as from other browser tabs/windows, so any updates of the state
get propagated automatically to all states backed by the same storage.
</p>
</section>
<section class="content-width">
<h2>A simple Todo list</h2>
<h2>Obligatory Todo list</h2>
<p>
A simple, but not completely bare-bones todo list application,
using nothing more than Nyooom and the CSS already present
on this page to save some code.
This simple ToDo component uses nothing more than Nyooom and
the stylesheets already on this page to save some boilerplate.
</p>
<nyooom-showcase code="false">
<code>
<div contenteditable="false">let todo, input
const task = value =>
<div contenteditable="false">const task = value =>
html.flexRow (
{class: ["todo"], gap: 1},
{class: "todo", gap: 1},
value,
html.span("[x]", {
style: {
color: "var(--primary-6)",
cursor: "pointer"
},
click: event =&gt; {
event
.target
.closest(".todo")
.remove()
click(event) {
event
.target
.closest(".todo")
.remove()
}
})
)
return todo =
html.flexColumn(
let todo, input
return todo = html.flexColumn(
{gap: 1},
input=html.input({
input = html.input({
type: "text",
placeholder:
"Do some stuff",
}),
html.button("Add Task",
{
click: event =&gt; {
click() {
todo.append(task(
input.value ||
input.placeholder
@ -447,17 +406,100 @@ html.flexColumn(
<section id="nyooom-explained" class="content-width">
<h2>Nyooom Explained</h2>
<h3>A bit of boring history</h3>
<p>
TODO: Write
The <code>nyooom/render.js</code> module traces its basic idea back to a <a href="https://github.com/darkwiiplayer/skooma">Lua module</a>.
The basic mechanism is the exact same, as the languages are very similar;
with the noteable difference that the Lua version outputs text and uses a very
lightweight DOM-like data-structure as an intermediary representation.
</p>
<p>
Porting the library code to JavaScript was an obvious choice, given how similar both
languages are in how they handle functions and metaprogramming.
The concept worked well to generate HTML to send to the client,
so it would work equally well to generate DOM nodes directly in the browser.
And the obvious advantages over a server-side implementation is that things like event listeners could be attached directly.
</p>
<p>
One design goal while further expanding the capabilities of nyooom has been to preserve this original use-case.
The library is <i>small enough</i> that pulling it into a project just to generate a few nested DOM structures
here and there isn't an unjustifiable waste of application size.
This is also why the <code>render</code> module does not depend on the <code>observable</code>
module, and has as little code as it can get away with to enable interoperability.
</p>
<p>
The later addition of the <code>Obervable</code> module, and the ability of nyooom to detect state objects and
handle them specially to enable reactivity is what truly enables the library to be used on its own to build
simple and even moderately complex interactive components.
<br>
And with most of this extra complexity existing in a separate file,
the size impact on the core nyooom library is very minimal, when
reactivity is not wanted, or an alternative mechanism for reactivity
is preferred.
</p>
<h3>Implementation</h3>
<p>
Nyooom is based on a convenient wrapper around <code>document.createElement</code>
made even more convenient by a <code>Proxy</code> to generate & populate DOM nodes.
</p>
<h3>Rationale & design principles</h3>
<p>
The design of nyooom is based on the following assumption about web applications:
</p>
<blockquote>
Most web application code can be modeled with simple abstractions.<br>
The few cases that can't tend to be too complex for any general-purpose abstraction anyway.
</blockquote>
<p>
Leading to a framework that focuses on the former group,
while giving developers the tools to handle the tricky bits without having to hack themselves out of the framework first.
</p>
<p>
One consideration that sets nyooom apart from other alternatives,
is the emphasis on being both easy to adopt, integrate, and even abandon.
</p>
<p>
While nyooom provides some level of abstraction over native browser APIs, everything done via nyooom is
still easy to mentally map onto the underlying APIs. This reduces the mental load of translating what the
code says, and what is happening to the website, creating a common mental model with vanilla javascript,
as well as other non-magic libraries and micro-frameworks.
</p>
<p>
This, in practice, means that adding some code that uses nyooom to an existing project, integrating it with
other technologies, and even removing it in parts or in whole from a project that has outgrown it, or from performance-critical
parts of an application that need even the last drop of performance squeezed out of the browser are all very
simple to achieve.
</p>
<p>
Even taking out certain aspects like reactivity, while still using nyooom to generate DOM nodes that get manipulated
manually in response to complex state-changes is as easy as replacing the state objects with primitive values
and writing the custom code to take their place.
</p>
<p>
Where other frameworks suck in their stomach and do their best to fit every use-case you could have,
nyooom is happy to just step aside and make space for more specialised tools.
<!-- nyooom: the framework that promotes body-acceptance -->
</p>
</section>
</main>
<footer class="inset box">
<flex-row gap=1>
<div>
Nyooom is great!
</div>
<flex-row gap=3>
<table>
<caption style="padding: 0">Feedback</caption>
<tr>
<th scope="col">Fediverse</th>
<td><a href="https://tech.lgbt/@darkwiiplayer">@darkwiiplayer@tech.lgbt</a></td></tr>
</table>
</flex-row>
</footer>
</html>
</htmlt>

View file

@ -1,5 +1,5 @@
import {html,empty} from "https://cdn.jsdelivr.net/gh/darkwiiplayer/skooma-js@f79e7a9/skooma.js"
import {State} from "https://cdn.jsdelivr.net/gh/darkwiiplayer/skooma-js@f79e7a9/state.js"
import {html,nothing} from "nyooom/render"
import {ObservableValue,ObservableObject} from "nyooom/observable"
import element from "https://darkwiiplayer.github.io/easier-elements-js/easier-elements.js"
import hljs from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/highlight.min.js';
import lang_html from "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/es/languages/xml.min.js"
@ -76,25 +76,9 @@ element(class NyooomShowcase extends HTMLElement {
}
connectedCallback() {
this.classList.add("box")
this.shadowRoot.replaceChildren(
html.slot(),
html.span("🖉", {
class: "edit",
click: ({target: button}) => {
this.querySelectorAll("[contenteditable]")
.forEach(item => {
if (item.contentEditable == "true") {
item.contentEditable = "false"
button.classList.remove("editing")
this.format()
} else {
item.contentEditable = "true"
button.classList.add("editing")
item.innerText = item.innerText
}
})
}
}),
html.style(css),
...Array.from(document.styleSheets).map(sheet => sheet.ownerNode.cloneNode(true)),
theme.cloneNode(true),
@ -110,7 +94,7 @@ element(class NyooomShowcase extends HTMLElement {
const code = this.querySelector("code").innerText
try {
const fn = new Function("html", "empty", "State", code)
const result = fn(html, empty, State)
const result = fn(html, nothing, ObservableValue)
this.error.replaceChildren()
this.output.innerHTML = hljs.highlight("html", result.outerHTML).value
this.preview.classList.toggle("hidden", this.getAttribute("preview") === "false")

View file

@ -11,7 +11,10 @@
& em {
font-style: unset;
color: var(--secondary-5);
color: var(--secondary-4);
@container style(--color-scheme: dark) {
color: var(--secondary-7);
}
}
}