commit 3fecb5697bd3988c693b0b67b4c72715fb32175e Author: DarkWiiPlayer Date: Mon Dec 11 16:53:32 2023 +0100 [WIP] Start working on website diff --git a/index.html b/index.html new file mode 100644 index 0000000..848729f --- /dev/null +++ b/index.html @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + +
+

Skooma

+ import { html } from "skooma/render.js" +

+ A new way of building + + HTML + + + + + + in vanilla JavaScript +

+
+ + + Get Started + +
+ +
+
+
+

Elevator Pitch

+ +

+ Skooma combines easy and powerful HTML generation with optional reactivity.
+ All in plain JavaScript. +

+ + +
return html.p( + "Text Node", + html.br(), + html.b("HTML Node"), + {style:{color:"salmon"}}, +)
+
+ + + +

+ Skooma is small enough that you can use it even if you only need it in a single function, + easy enough to figure out what's going on even if you've never used it, + powerful enough that you won't need another framework and + flexible enough to let you use one regardless. +

+ +

+ If that sounds good, it's time to back up those claims! +

+

+ Continue reading to get an overview of skooma's features.
+ Or jump down to the explainer for the hows and whys. +

+
+
+ +
+

Getting Started

+ +
+

+ Trying out skooma is super easy!
+ Just import the html export into your script and start generating DOM nodes. +

+ +
import {html} from "https://cdn.jsdelivr.net/npm/skooma@1.3.1/skooma.min.js"
+
+ +
+

+ Calling any method on this object will generate an HTML node. + Passing in arguments is how you add children, set attributes and even do more advanced things. +

+ + + +
return html.div()
+
+
+ +

+ Using the component helper from the sckooma/observable.js 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. +

+ +
import {component} from "https://cdn.jsdelivr.net/npm/skooma@1.3.1/state.min.js"
+const myComponent = () => html.div("Rendered Component")
+component(myComponent) //Registers it as <my-component>
+ +

+ Of course, you can also just call document.body.append(html.span("My Text")) in your script. +

+
+
+ +
+

Basic DOM generation

+ +
+

+ Content can be added to a node by simply passing it as arguments to the function. + This works for strings, HTML elements, functions and even observable state containers. +

+ + + +
return html.span( + "Skooma ", + html.u( + "is ", + html.b("cool") + ) +)
+
+
+
+ +
+

+ Adding attributes is just as easy: Just pass an object into the function. + If the value is an array, it'll get joined with spaces. +

+ + + +
return html.span({ + id: "warning", + class: ["danger", "bold"], +})
+
+
+
+ +
+

+ Event listeners work just like attribute, except the value is a function: +

+ + + +
return html.button("Click Me!", { + click: event => { + alert("Button clicked :3") + } +})
+
+
+ +
+

+ Some "attributes" are handled in special ways to make things easier: +

+ +

+ style sets the inline styles of the node. +

+

+ camelCase names will be converted to kebab-case and values + will be converted to strings. +

+ + + +
return html.div( + { style: { + color: "salmon" + }}, + html.span("Big Salmon", + { style: { + fontSize: "1.4em" + }} + ) +)
+
+
+ +

+ dataset converts its key-value pairs into data- attributes. +

+ + + +
return html.span( + { dataset: { + from: "Fire", + to: "Heat", + snakeCase: "converted" + } }, + "Heat from Fire" +)
+
+
+ +

+ shadowRoot will attach a shadow DOM to the newly created node. +

+ + + +
return html.div( + { shadowRoot: [ + html.p("Shadow DOM text"), + "Arrays work too :)" + ]}, + "Light DOM text" +)
+
+
+
+
+
+ +
+ +

It's just JavaScript

+ +

Skooma is entirely implemented as a light-weight JavaScript library.

+ +

+ What this means is that there's no new syntax for things you already know how to do. + Functions like filter or map 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, + and the syntax is just the boring old javascript that most tools already understand. +

+ + + +
return html.ul( + ["u", "b", "i"].map( + type => html.li(html[type]( + `<${type}> element` + )) + ) +)
+
+
+
+ +
+

State changes in Skooma

+ +
import {ObservableObject} from 'skooma/observable.js'
+ +

+ Skooma 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. +

+ +

+ The render.js 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. +

+ + + +
const counter = new State({value: 0}) + +counter.valueChanged = newValue => + console.log(`Value: ${newValue}`) + +return html.buttonGroup( + html.button( + "Click Me! ", + html.span(counter), + { + click() { + counter.value += 1 + } + } + ), + html.button( + "Clear", { + click() { + counter.value = 0 + } + } + ) +)
+
+
+
+ +
+ +

Obligatory Todo list

+ +

+ This simple ToDo component uses nothing more than Skooma and + the stylesheets already on this page to save some boilerplate. +

+ + + +
const task = value => +html.flexRow ( + {class: "todo", gap: 1}, + value, + html.span("[x]", { + style: { + color: "var(--primary-6)", + cursor: "pointer" + }, + click(event) { + event + .target + .closest(".todo") + .remove() + } + }) +) +let todo, input +return todo = html.flexColumn( + {gap: 1}, + input = html.input({ + type: "text", + placeholder: + "Do some stuff", + }), + html.button("Add Task", + { + click() { + todo.append(task( + input.value || + input.placeholder + )) + } + }) +)
+
+
+
+ +
+

Skooma Explained

+ +

A bit of boring history

+ +

+ The skooma/render.js module traces its basic idea back to a Lua module. + The basic mechanism is the exact same, as the languages are very similar; + with the noteable difference that skooma.lua outputs text and uses a very + lightweight DOM-like data-structure as an intermediary representation. +

+ +

+ 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. +

+ +

+ One design goal while further expanding the capabilities of skooma has been to preserve this original use-case. + The library is small enough 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 render module does not depend on the observable + module, and has as little code as it can get away with to enable interoperability. +

+ +

+ The later addition of the Obervable module, and the ability of skooma 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. +
+ And with most of this extra complexity existing in a separate file, + the size impact on the core skooma library is very minimal, when + reactivity is not wanted, or an alternative mechanism for reactivity + is preferred. +

+ +

Implementation

+ +

+ Skooma is based on a convenient wrapper around document.createElement + made even more convenient by a Proxy to generate & populate DOM nodes. +

+ +

Rationale & design principles

+ +

+ The design of skooma is based on the following assumption about web applications: +

+
+ Most web application code can be modeled with simple abstractions. + The few cases that can't tend to be too complex for any general-purpose abstraction. +
+

+ 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. +

+ +

+ One consideration that sets skooma apart from other alternatives, + is the emphasis on being both easy to adopt, integrate, and even abandon. +

+

+ While skooma provides some level of abstraction over native browser APIs, everything done via skooma 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. +

+

+ This, in practice, means that adding some code that uses skooma 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. +

+

+ Even taking out certain aspects like reactivity, while still using skooma 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. +

+

+ Where other frameworks suck in their stomach and do their best to fit every use-case you could have, + skooma is happy to just step aside and make space for more specialised tools. + +

+
+
+ + + diff --git a/js/SkoomaShowcase.js b/js/SkoomaShowcase.js new file mode 100644 index 0000000..51a4337 --- /dev/null +++ b/js/SkoomaShowcase.js @@ -0,0 +1,124 @@ +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 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" + +hljs.registerLanguage("html", lang_html) + +const capture = event => event.stopPropagation() + +const css = ` +:host { + display: grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + gap: var(--padding, 1em); + padding: var(--padding, 1em); + position: relative; +} +@media (max-width: 60em) { + :host { + display: flex; + flex-flow: column; + } +} +.error { + font-family: "Courier New", sans-serif; + grid-column: span 2; +} +.error:empty { display: none; } +.error:not(:empty)~* { display: none; } +.hidden { display: none; } + +html-preview { + contain: inline-size layout style paint; +} + +.edit { + left: -1.4em; + z-index: 10; + position: absolute; + display: block; + content: '🖉'; + line-height: 100%; + opacity: .2; + font-size: 2em; + cursor: pointer; +} +.edit.editing { + opacity: .6; +} +` +const theme = html.link({ + rel: "stylesheet", + href: "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css" +}) + +element(class SkoomaShowcase extends HTMLElement { + constructor() { + super() + this.attachShadow({mode: "open"}) + this.addEventListener("input", _ => { + this.render() + }) + } + + get editable() { + return this.querySelector('[contenteditable="true"]') + } + + format() { + if (!this.editable) { + const code = this.querySelector('[contenteditable]') + code.innerHTML = hljs.highlight("javascript", code.innerText).value + } + } + + connectedCallback() { + 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), + this.error = html.div({class: ["error"]}), + this.output = html.code(), + this.preview = html.htmlPreview({input: capture}), + ) + this.format() + this.render() + } + + render() { + const code = this.querySelector("code").innerText + try { + const fn = new Function("html", "empty", "State", code) + const result = fn(html, empty, State) + this.error.replaceChildren() + this.output.innerHTML = hljs.highlight("html", result.outerHTML).value + this.preview.classList.toggle("hidden", this.getAttribute("preview") === "false") + this.output.classList.toggle("hidden", this.getAttribute("code") === "false") + this.preview.replaceChildren(result) + } catch (error) { + console.error(error.stack) + this.error.innerText = error.stack + } + } +}) diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..8ab84fa --- /dev/null +++ b/styles.css @@ -0,0 +1,66 @@ +:root { + --corner-radius: 2px; +} + +[contenteditable] { white-space: pre } + +#elevator-pitch { + & .big { + font-size: 1.4em; + } + + & em { + font-style: unset; + color: var(--secondary-5); + } +} + +p.important { + font-size: 1.2em; +} + +page-hero { + background-image: + radial-gradient(ellipse at 100% -40%, #fff0, #ccaaff06 70%, #fff0 70%), + radial-gradient(ellipse at -40% -20%, #fff0, #bb88ff02 60%, #fff0 60%), + radial-gradient(ellipse at 180% 180%, #fff0, #bb88ff02 60%, #fff0 60%), + linear-gradient(to top left, #a2f1, transparent), + linear-gradient(to top right, #c2d1, transparent); + &::after { + content: ''; + display: block; + position: absolute; + bottom: 0; + height: .2rem; + left: 0; + right: 0; + background-image: linear-gradient(to right, #0660, #0f6, #0660); + background-image: linear-gradient(to right in hsl, #6060, #f0f, #6060); + filter: saturate(.4) opacity(.2); + } +} + +skooma-showcase { + --padding: .6em; + border-radius: var(--corner-radius); + padding: var(--padding); + background-color: #fff1; + --hr-color: var(--primary-4); + tab-size: 3; +} + +type-writer { + font-weight: bold; + &:defined::after { + content: "|"; + opacity: 0.5; + } +} + +main { + line-height: 1.2em; + + p { + line-height: 1.6em; + } +}