Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/tornqvist/yeet
Teeny-weeny front end framework
https://github.com/tornqvist/yeet
app dom framework frontend html javascript template ui vanilla
Last synced: 2 months ago
JSON representation
Teeny-weeny front end framework
- Host: GitHub
- URL: https://github.com/tornqvist/yeet
- Owner: tornqvist
- Created: 2021-01-12T10:25:31.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2024-07-28T12:04:16.000Z (5 months ago)
- Last Synced: 2024-10-14T16:27:14.001Z (2 months ago)
- Topics: app, dom, framework, frontend, html, javascript, template, ui, vanilla
- Language: JavaScript
- Homepage:
- Size: 552 KB
- Stars: 24
- Watchers: 4
- Forks: 1
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
## Features
- **No transpilation** – It's all plain vanilla JavaScript
- **Small size** – Weighing in at `3kb`, you'll barely notice it
- **Minimal API** – Only a handfull functions to learn
- **No magic** – Prototypal state and events
- **It's fast** – Both on server and client## Example
```js
import { html, mount, use, Component } from 'https://cdn.skypack.dev/yeet@next'mount('body', Component(App))
function App (state, emit) {
use(store)return function () {
return html`
Clicked ${state.count} times
emit('increment'))}>Click me
`
}
}function store (state, emitter) {
state.count = 0
emitter.on('increment', function () {
state.count++
emitter.emit('render')
})
}
```## Why yeet?
Building interactive and performant websites shouldn't require a whole lot of
dependencies, a bundler, or even Node.js for that matter. The JavaScript
language has all the capabilities required built right in, without sacrificing
either developer or user experience.Frameworks are tools and tools should be interchangeable and easy to replace.
That's why yeet rely on the lowest common denominator – the DOM. There are no
unneccessary abstractions such as virtual DOM, synthetic events or template
syntax to learn. Only functions and prototypes.If you know JavaScript you already know most there is to know about yeet. And
anything new you learn from using yeet is directly benefitial to anything else
you might want to use JavaScript for.## Prototypal state
The state object in yeet is shared between components using prototypes. You can
think of the state object as a shared context which components can use to read
from and write to.However, a component can only ever mutate its own state, it can only read from
the parent state, yet they are the same object – what?! This is achieved using
prototypes. The prototype of a component's state object is the parent
component's state object.About prototypal inheritance
JavaScript prototypes are the mechanism for inheriting properties and behavior
from one object to another. What is facinating about prototypes is that they
are live – meaning that any change made to an object is immediately made
available to all other objects whose prototype chain includes said object.```js
const parent = {}
const child = Object.create(parent)parent.name = 'world'
console.log(`Hello ${parent.name}`) // Hello world
console.log(`Hello ${child.name}`) // Hello worldchild.name = 'planet'
console.log(`Hello ${parent.name}`) // Hello world
console.log(`Hello ${child.name}`) // Hello planet
```Read more about [Object prototypes][Object prototypes].
To modify a parent state object, one can use events to communicate up the
component tree (or prototype chain, if you will).## Events
Events are the core mechanism for communication up the component tree. Yeet
adhers to the dogma "data down, events up", which is to say that data should be
passed down the component tree, either with state or as arguments. When
something happens, e.g. the user clicks a button, an event should be emitted
which bubbles up the component tree, notifying components which may then mutate
their state and issue a re-render.## Components
Components can be usefull in situations when you need a locally contained state,
want to use some third party library or want to know when components mount or
unmout in the DOM.Components in yeet use [generator functions][generator functions] to control the
component lifecycle. By using generators yeet can step through your component
and pause execution until the appropiate time, e.g. when the component has
updated or is removed from the DOM. This allows you to retain local variables
which persist throughout the component lifespan without meddling with `this` or
learning new state management techinques, they're just regular ol' variables.```js
import { html, ref, mount, Component } from 'https://cdn.skypack.dev/yeet@next'
import mapboxgl from 'https://cdn.skypack.dev/mapbox-gl'const state = { center: [18.0704503, 59.3244897] }
mount('#app', Component(Map), state)
function * Map (state, emit) {
const container = ref()
let mapyield function * () {
`
yield html`map = map || new mapboxgl.Map({
container: container.current,
center: state.center
})
}map.destroy()
}
```### Generators
Using generators allows you to keep local variables accessible throughout the
component lifecycle. If you are already familiar with generators there's not
really that much to learn.If you are new to generators, learning yeet will only further build your
JavaScript toolset, there is nothing here which you cannot use in other
contexts.A generator function is a special kind of function which can pause execution
midway and allows us to inspect intermediate values before procceding with
execution. A generator function has two caracteristics which set it appart from
regular functions, and asterics (`*`) after the `function` keyword and the
`yield` keyword.The anatomy of a generator function
```js
// ↓ This thing makes it a generator function
function * createGenerator (list) {
for (const num of list) {
yield num // ← Pause here
}
return 'finished!'
}// ↓ Call it like any other function
const generator = createGenerator([1, 2, 3])// We can now step through the generator
generator.next() // { value: 1, done: false }
generator.next() // { value: 2, done: false }
generator.next() // { value: 3, done: false }
generator.next() // { value: 'finished!', done: true }
```By yielding in a yeet component you are telling yeet to halt execution and save
the rest of the function for later, e.g. when the component has updated or when
it is removed from the DOM. A yeet component's lifecycle is thereby clearly laid
out in chronological order, from top to bottom.#### Lifecycle
Generators are used to declare the lifecycle of yeet components. Only functions,
html partials (returned by the `html` and `svg` tags) and promises carry any
special meaning when using `yield`. When a yeet component yields a function,
that is the function which will be used for any consecutive re-renders. Anything
that comes after `yield` will be executed once the components is removed from
the DOM (e.g. replaced by another element).```js
function * MyComponent () {
// Happens only once, during setup
yield function () {
// Happens every time the component updates
}
// Happens only once, when the component is removed/replaced
}
```They yielded function may also be a generator function. This can be used to
perform side effects such as setting up subscriptions, manually manipulating the
DOM or initializing some third party library. This is handled asynchrounously,
meaning the DOM will have updated and the changes may have been made visible to
the user before the generator finishes.```js
function MyComponent () {
return function * () {
// Happens before every update
yield html`Hello planet!
`
// Happens after every update
}
}
```If you require immediate access to the rendered element, e.g. to _synchronously_
mutate or inspect the rendered element _before_ the page updates, you may yield
yet another function._Note: Use with causion, this may have a negative impact on performance._
```js
function MyComponent () {
return function () {
return function * () {
// Happens before every update
yield html`Hello planet!
`
// Happens SYNCHRONOUSLY after every update
}
}
}
```#### Arguments (a.k.a. `props`)
Even though all components have access to the shared state, you'll probably need
to supply your components with some arguments to configure behavior or forward
particular properties. You can either provide extra arguments to the `Component`
function or you can call the function returned by `Component` with any number of
arguments.```js
function Reaction (state, emit) {
// ↓ Arguments are provided to the inner function
return function ({ emoji }) {
return html` emit('reaction', emoji)}>${emoji}`
}
}// ↓ Declare component on beforehand
const ReactionComponent = Component(Reaction)// ↓ Declare component and arguments on beforehand
const SadReaction = Component(Reaction, { emoji: '😢' })html`
${Component(Reaction, { emoji: '😀' })}
${ReactionComponent({ emoji: '😐' })}
${SadReaction}
`
```### Async components
Components can yield any value but if you yield a Promise yeet will await the
promise before it continues to render. On the server, rendering is asynchronous by
design, this means that all promises are resolved as the component renders.
Rendering in the browser behaves a little differently. While awaiting a promise
nothing will be rendered in place of the component. Once all yielded promises
have resolved (or rejected) the component will finish rendering and the element
will appear on the page.Yeet does not make any difference between promises which resolve or reject, you
will have to catch and handle rejections accordingly, yeet will just forward the
resolved or rejected value.```js
import fetch from 'cross-fetch'
import { html, use } from 'yeet'function User (state, emit) {
const get = use(api) // ← Register api store with component
return function () {
// ↓ Expose the promise to yeet
const user = yield get(`/users/${state.user.id}`)
return html`
${user.name}
`
}
}function api (state, emit) {
if (!state.cache) state.cache = {} // ← Use existing cache if available// ↓ Return a function for lazily reading from the cache
return function (url) {
if (url in state.cache) return state.cache[url] // ← Read from cache
return fetch(url).then(async function (res) {
const data = await data.json()
state.cache[url] = data // ← Store response in cache
return data // ← Return repsonse
})
}
}
```#### Lists and Keys
In most situations yeet does an excellent job at keeping track of which
component goes where. This is in part handled by identifying which template tags
(the `html` and `svg` tag functions) are used. In JavaScript, template
literals are unique and yeet leverages this to keep track of which template tag
goes where.When it comes to components, yeet uses your component function as a unique key to
keep track of which component is tied to which element in the DOM.When it comes to lists of identical components, this becomes difficult and yeet
needs a helping hand in keeping track. In these situations, you can provide a
unique `key` to each component which will be used to make sure that everything
keeps running smoothly.```js
function Exponential (state, emit) {
let exponent = 1function increment () {
exponent++
emit('render')
}return function ({ num }) {
return html`
${Math.pow(num, exponent)}
`
}
}
const numbers = [1, 2, 3, 4, 5]
return html`
${numbers.map((num) => Component(Exponential, { num, key: num }))}
`
```
### Stores
Stores are the mechanism for sharing behavior between components, or even apps.
A store can subscribe to events, mutate the local state and issue re-renders.
```js
import { html, use, Component } from 'https://cdn.skypack.dev/yeet@next'
function Parent (state, emit) {
use(counter) // ← Use the counter store with this component
return function () {
return html`
${Component(Increment)}
${state.count}
${Component(Decrement)}
`
}
}
function Increment (state, emit) {
return html` emit('increment')}>+`
}
function Decrement (state, emit) {
return html` emit('decrement')}>-`
}
function counter (state, emitter) {
state.count = 0 // ← Define some initial state
emitter.on('increment', function () {
state.count++
emitter.emit('render')
})
emitter.on('decrement', function () {
state.count--
emitter.emit('render')
})
}
```
#### Events
How you choose to name your events is entirely up to you. There's only one
exception: the `render` event has special meaning and will re-render the closest
component in the component tree. The `render` event does not bubble.
## Server rendering (SSR)
Yeet has first-class support for server rendering. There are plans to support
server-rendered templates, meaning any backend could render the actual HTML and
yeet would wire up functionality using the pre-existing markup.
Rendering on the server supports fully asynchronous components. If a component
yields promises, yeet will wait for these promises to resolve while rendering.
### Server rendered templates (non-Node.js)
_Coming soon…_
## API
The API is intentionally small.
### html
Create html partials which can be rendered to DOM nodes (or strings in Node.js).
```js
import { html } from 'https://cdn.skypack.dev/yeet@next'
const name = 'planet'
html`
Hello ${name}!
````
#### Attributes
Both literal attributes as well as dynamically "spread" attributes work. Arrays
will be joined with an empty space (` `) to make it easier to work with many
space separated attributes, e.g. `class`.
```js
import { html } from 'https://cdn.skypack.dev/yeet@next'
const attrs = { disabled: true, hidden: false, placeholder: null }
html``
// →
```
##### Events
Events can be attached to elements using the standard `on`-prefix.
```js
import { html } from 'https://cdn.skypack.dev/yeet@next'
html` alert('You clicked me!')}>Click me!`
```
#### Arrays
If you have lists of things you want to render as elements, interpolating arrays
works just like you'd expect.
```js
import { html } from 'https://cdn.skypack.dev/yeet@next'
const list = [1, 2, 3]
html`
- ${list.map((num) => html`
- ${num} `)}
```
#### Fragments
It's not always that you can or need to have an outer containing element.
Rendering fragments works just like single container elements.
```js
import { html } from 'https://cdn.skypack.dev/yeet@next'
html`
Hello world!
Lorem ipsum dolor sit amet…
`
```
### svg
The `svg` tag is required for rendering all kinds of SVG elements, such as
``, ``, `` etc. All the same kinds of behaviors as described in
[`html`](#html) apply to `svg`.
```js
import { svg } from 'https://cdn.skypack.dev/yeet@next'
svg`
`
```
### raw
If you have preformatted html that you wish to render, just interpolating them
in the template won't work. Text that is interpolated in templates is
automatically escaped to avoid common [XXS attacks][xxs], e.g. injecting script
tags.
```js
import { html, raw } from 'https://cdn.skypack.dev/yeet@next'
const content = 'Hello world!'
html`
// →
html`
// →
```
### ref
It's common to want to access elements in the DOM to mutate or read properties.
For this there is the `ref` helper which, when called, will return an object
with the property `current` which will be the currently mounted DOM node it was
attached to.
_Note: This only works in the client, `current` will never be available while
server rendering._
```js
import { html, ref, render } from 'https://cdn.skypack.dev/yeet@next'
const div = ref()
render(html`
div.current // ← Reference to the rendered div element
```
### use
Register a store to use with component. Accepts a function which will be called
with `state` and `emitter` (an instance of [`EventEmitter`](#eventemitter)).
Whatever is returned by the supplied function is returned by `use`. You should
refrain from using `use` anywhere but during the component setup stage.
Stores are great for sharing functionality between components. A shared store
can be used to handle common operations on the shared state object or just to
avoid duplicating code between components.
```js
import { html, use, ref } from 'https://cdn.skypack.dev/yeet@next'
function * Video (state, emit) {
const video = ref()
const detach = use(pauser(video))
yield ({ src }) => html``
detach()
}
function pauser (video) {
return function (state, emitter) {
function onvisibilitychange () {
if (document.visibilityState === 'visible') {
video.current.play()
} else {
video.current.pause()
}
}
document.addEventListener('visibilitychange', onvisibilitychange)
return function () {
document.removeEventListener('visibilitychange', onvisibilitychange)
}
}
}
```
### mount
Mount a given html partial on a DOM node. Accepts a html partial, a DOM node or
selector and optionally a root state object.
```js
import { html, mount } from 'https://cdn.skypack.dev/yeet@next'
mount('body', html`
Hello planet!
`)
```
```js
import { html, mount, Component } from 'https://cdn.skypack.dev/yeet@next'
mount(document.getElementById('app'), Component(Main), { name: 'world' })
function Main (state, emit) {
return html`
Hello ${state.name}!
`
}
```
### render
Render a partial to element (browser) or string (server). On the client, render
is synchronous and the resulting DOM node is returned. On the server `render`
always returns a promise which resolves to a string. Accepts an optional root
state object.
```js
import { html, render } from 'https://cdn.skypack.dev/yeet@next'
const h1 = render(html`
Hello planet!
`))document.body.appendChild(h1)
```
```js
import { html, render } from 'yeet'
import { createServer } from 'http'
createServer(async function (req, res) {
const body = await render(html`Hello world!`)
res.end(body)
}).listen(8080)
```
```js
import { Readable } from 'stream'
import { html, render } from 'yeet'
import { createServer } from 'http'
createServer(async function (req, res) {
Readable.from(html`Hello world!`).pipe(res)
}).listen(8080)
```
### Component
The Component function accepts a function as its first argument and any number
of additional arguments. The additional arguments will be forwarded to the inner
render function. The Component function returns a function which may be called
with any number of arguments, these arguments will override whichever arguments
were supplied prior.
It is best practice to provide an object as the first render argument since the
optional `key` property is extracted from the first render argument.
```js
import { html, render, Component } from 'https://cdn.skypack.dev/yeet@next'
function Greeting () {
return function (props, name = 'world') {
return html`
${props?.phrase || 'Hello'} ${name}!
`}
}
render(Component(Greeting))
// →
Hello world!
render(Component(Greeting, { phrase: 'Hi' }))
// →
Hi world!
render(Component(Greeting, { phrase: 'Howdy' }, 'planet'))
// →
Howdy planet!
const Greeter = Component(Greeting)
render(Greeter({ phrase: 'Nice to meet you,' }))
// →
Nice to meet you, world!
```
### EventEmitter
Stores are called with state and an event emitter. The event emitter can be used
to act on events submitted from e.g. user actions. All events except the
`render` event bubbles up the component tree.
You can register a catch-all event listener by attaching a listener for the `*`
event. The first argument to catch-all listeners is the event name followed by
the event arguments.
```js
emitter.on('*', function (event, ...args) {
console.log(`Emitted event "${event}" with arguments:`, ...args)
})
```
#### `emitter.on(string, Function)`
Attach listener for the specified event name.
#### `emitter.removeListener(string, Function)`
Remove the event listener for the specified event name.
#### `emitter.emit(string, ...any)`
Emit an event of the specified name accompanied by any number of arguments.
## Attribution
There wouldn't be a yeet if there hadn't been a [choo][choo]. Yeet borrows a lot
of the core concepts such as a shared state and event emitter from choo. The
idea of performant DOM updates based on template literals was born from proof
of concept work done by [Renée Kooi][goto-bus-stop].
## TODO
[ ] Server-rendered templates (non-Node.js)
[choo]: https://github.com/choojs/choo
[goto-bus-stop]: https://github.com/goto-bus-stop
[Object prototypes]: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes
[generator functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
[xxs]: https://en.wikipedia.org/wiki/Cross-site_scripting