31 |
32 | ## Features
33 | - **No transpilation** – It's all plain vanilla JavaScript
34 | - **Small size** – Weighing in at `3kb`, you'll barely notice it
35 | - **Minimal API** – Only a handfull functions to learn
36 | - **No magic** – Prototypal state and events
37 | - **It's fast** – Both on server and client
38 |
39 | ## Example
40 | ```js
41 | import { html, mount, use, Component } from 'https://cdn.skypack.dev/yeet@next'
42 |
43 | mount('body', Component(App))
44 |
45 | function App (state, emit) {
46 | use(store)
47 |
48 | return function () {
49 | return html`
50 |
51 |
Clicked ${state.count} times
52 |
53 |
54 | `
55 | }
56 | }
57 |
58 | function store (state, emitter) {
59 | state.count = 0
60 | emitter.on('increment', function () {
61 | state.count++
62 | emitter.emit('render')
63 | })
64 | }
65 | ```
66 |
67 | ## Why yeet?
68 | Building interactive and performant websites shouldn't require a whole lot of
69 | dependencies, a bundler, or even Node.js for that matter. The JavaScript
70 | language has all the capabilities required built right in, without sacrificing
71 | either developer or user experience.
72 |
73 | Frameworks are tools and tools should be interchangeable and easy to replace.
74 | That's why yeet rely on the lowest common denominator – the DOM. There are no
75 | unneccessary abstractions such as virtual DOM, synthetic events or template
76 | syntax to learn. Only functions and prototypes.
77 |
78 | If you know JavaScript you already know most there is to know about yeet. And
79 | anything new you learn from using yeet is directly benefitial to anything else
80 | you might want to use JavaScript for.
81 |
82 | ## Prototypal state
83 | The state object in yeet is shared between components using prototypes. You can
84 | think of the state object as a shared context which components can use to read
85 | from and write to.
86 |
87 | However, a component can only ever mutate its own state, it can only read from
88 | the parent state, yet they are the same object – what?! This is achieved using
89 | prototypes. The prototype of a component's state object is the parent
90 | component's state object.
91 |
92 |
93 | About prototypal inheritance
94 |
95 | JavaScript prototypes are the mechanism for inheriting properties and behavior
96 | from one object to another. What is facinating about prototypes is that they
97 | are live – meaning that any change made to an object is immediately made
98 | available to all other objects whose prototype chain includes said object.
99 |
100 | ```js
101 | const parent = {}
102 | const child = Object.create(parent)
103 |
104 | parent.name = 'world'
105 | console.log(`Hello ${parent.name}`) // Hello world
106 | console.log(`Hello ${child.name}`) // Hello world
107 |
108 | child.name = 'planet'
109 | console.log(`Hello ${parent.name}`) // Hello world
110 | console.log(`Hello ${child.name}`) // Hello planet
111 | ```
112 |
113 | Read more about [Object prototypes][Object prototypes].
114 |
115 |
116 |
117 | To modify a parent state object, one can use events to communicate up the
118 | component tree (or prototype chain, if you will).
119 |
120 | ## Events
121 | Events are the core mechanism for communication up the component tree. Yeet
122 | adhers to the dogma "data down, events up", which is to say that data should be
123 | passed down the component tree, either with state or as arguments. When
124 | something happens, e.g. the user clicks a button, an event should be emitted
125 | which bubbles up the component tree, notifying components which may then mutate
126 | their state and issue a re-render.
127 |
128 | ## Components
129 | Components can be usefull in situations when you need a locally contained state,
130 | want to use some third party library or want to know when components mount or
131 | unmout in the DOM.
132 |
133 | Components in yeet use [generator functions][generator functions] to control the
134 | component lifecycle. By using generators yeet can step through your component
135 | and pause execution until the appropiate time, e.g. when the component has
136 | updated or is removed from the DOM. This allows you to retain local variables
137 | which persist throughout the component lifespan without meddling with `this` or
138 | learning new state management techinques, they're just regular ol' variables.
139 |
140 | ```js
141 | import { html, ref, mount, Component } from 'https://cdn.skypack.dev/yeet@next'
142 | import mapboxgl from 'https://cdn.skypack.dev/mapbox-gl'
143 |
144 | const state = { center: [18.0704503, 59.3244897] }
145 |
146 | mount('#app', Component(Map), state)
147 |
148 | function * Map (state, emit) {
149 | const container = ref()
150 | let map
151 |
152 | yield function * () {
153 | yield html``
154 |
155 | map = map || new mapboxgl.Map({
156 | container: container.current,
157 | center: state.center
158 | })
159 | }
160 |
161 | map.destroy()
162 | }
163 | ```
164 |
165 | ### Generators
166 | Using generators allows you to keep local variables accessible throughout the
167 | component lifecycle. If you are already familiar with generators there's not
168 | really that much to learn.
169 |
170 | If you are new to generators, learning yeet will only further build your
171 | JavaScript toolset, there is nothing here which you cannot use in other
172 | contexts.
173 |
174 | A generator function is a special kind of function which can pause execution
175 | midway and allows us to inspect intermediate values before procceding with
176 | execution. A generator function has two caracteristics which set it appart from
177 | regular functions, and asterics (`*`) after the `function` keyword and the
178 | `yield` keyword.
179 |
180 |
181 | The anatomy of a generator function
182 |
183 | ```js
184 | // ↓ This thing makes it a generator function
185 | function * createGenerator (list) {
186 | for (const num of list) {
187 | yield num // ← Pause here
188 | }
189 | return 'finished!'
190 | }
191 |
192 | // ↓ Call it like any other function
193 | const generator = createGenerator([1, 2, 3])
194 |
195 | // We can now step through the generator
196 | generator.next() // { value: 1, done: false }
197 | generator.next() // { value: 2, done: false }
198 | generator.next() // { value: 3, done: false }
199 | generator.next() // { value: 'finished!', done: true }
200 | ```
201 |
202 |
203 |
204 | By yielding in a yeet component you are telling yeet to halt execution and save
205 | the rest of the function for later, e.g. when the component has updated or when
206 | it is removed from the DOM. A yeet component's lifecycle is thereby clearly laid
207 | out in chronological order, from top to bottom.
208 |
209 | #### Lifecycle
210 | Generators are used to declare the lifecycle of yeet components. Only functions,
211 | html partials (returned by the `html` and `svg` tags) and promises carry any
212 | special meaning when using `yield`. When a yeet component yields a function,
213 | that is the function which will be used for any consecutive re-renders. Anything
214 | that comes after `yield` will be executed once the components is removed from
215 | the DOM (e.g. replaced by another element).
216 |
217 | ```js
218 | function * MyComponent () {
219 | // Happens only once, during setup
220 | yield function () {
221 | // Happens every time the component updates
222 | }
223 | // Happens only once, when the component is removed/replaced
224 | }
225 | ```
226 |
227 | They yielded function may also be a generator function. This can be used to
228 | perform side effects such as setting up subscriptions, manually manipulating the
229 | DOM or initializing some third party library. This is handled asynchrounously,
230 | meaning the DOM will have updated and the changes may have been made visible to
231 | the user before the generator finishes.
232 |
233 | ```js
234 | function MyComponent () {
235 | return function * () {
236 | // Happens before every update
237 | yield html`
Hello planet!
`
238 | // Happens after every update
239 | }
240 | }
241 | ```
242 |
243 | If you require immediate access to the rendered element, e.g. to _synchronously_
244 | mutate or inspect the rendered element _before_ the page updates, you may yield
245 | yet another function.
246 |
247 | _Note: Use with causion, this may have a negative impact on performance._
248 |
249 | ```js
250 | function MyComponent () {
251 | return function () {
252 | return function * () {
253 | // Happens before every update
254 | yield html`
Hello planet!
`
255 | // Happens SYNCHRONOUSLY after every update
256 | }
257 | }
258 | }
259 | ```
260 |
261 | #### Arguments (a.k.a. `props`)
262 | Even though all components have access to the shared state, you'll probably need
263 | to supply your components with some arguments to configure behavior or forward
264 | particular properties. You can either provide extra arguments to the `Component`
265 | function or you can call the function returned by `Component` with any number of
266 | arguments.
267 |
268 | ```js
269 | function Reaction (state, emit) {
270 | // ↓ Arguments are provided to the inner function
271 | return function ({ emoji }) {
272 | return html``
273 | }
274 | }
275 |
276 | // ↓ Declare component on beforehand
277 | const ReactionComponent = Component(Reaction)
278 |
279 | // ↓ Declare component and arguments on beforehand
280 | const SadReaction = Component(Reaction, { emoji: '😢' })
281 |
282 | html`
283 |
288 | `
289 | ```
290 |
291 | ### Async components
292 | Components can yield any value but if you yield a Promise yeet will await the
293 | promise before it continues to render. On the server, rendering is asynchronous by
294 | design, this means that all promises are resolved as the component renders.
295 | Rendering in the browser behaves a little differently. While awaiting a promise
296 | nothing will be rendered in place of the component. Once all yielded promises
297 | have resolved (or rejected) the component will finish rendering and the element
298 | will appear on the page.
299 |
300 | Yeet does not make any difference between promises which resolve or reject, you
301 | will have to catch and handle rejections accordingly, yeet will just forward the
302 | resolved or rejected value.
303 |
304 | ```js
305 | import fetch from 'cross-fetch'
306 | import { html, use } from 'yeet'
307 |
308 | function User (state, emit) {
309 | const get = use(api) // ← Register api store with component
310 | return function () {
311 | // ↓ Expose the promise to yeet
312 | const user = yield get(`/users/${state.user.id}`)
313 | return html`
314 |
315 |
${user.name}
316 |
317 | `
318 | }
319 | }
320 |
321 | function api (state, emit) {
322 | if (!state.cache) state.cache = {} // ← Use existing cache if available
323 |
324 | // ↓ Return a function for lazily reading from the cache
325 | return function (url) {
326 | if (url in state.cache) return state.cache[url] // ← Read from cache
327 | return fetch(url).then(async function (res) {
328 | const data = await data.json()
329 | state.cache[url] = data // ← Store response in cache
330 | return data // ← Return repsonse
331 | })
332 | }
333 | }
334 | ```
335 |
336 | #### Lists and Keys
337 | In most situations yeet does an excellent job at keeping track of which
338 | component goes where. This is in part handled by identifying which template tags
339 | (the `html` and `svg` tag functions) are used. In JavaScript, template
340 | literals are unique and yeet leverages this to keep track of which template tag
341 | goes where.
342 |
343 | When it comes to components, yeet uses your component function as a unique key to
344 | keep track of which component is tied to which element in the DOM.
345 |
346 | When it comes to lists of identical components, this becomes difficult and yeet
347 | needs a helping hand in keeping track. In these situations, you can provide a
348 | unique `key` to each component which will be used to make sure that everything
349 | keeps running smoothly.
350 |
351 | ```js
352 | function Exponential (state, emit) {
353 | let exponent = 1
354 |
355 | function increment () {
356 | exponent++
357 | emit('render')
358 | }
359 |
360 | return function ({ num }) {
361 | return html`
362 |
363 |
364 |
365 | `
366 | }
367 | }
368 |
369 | const numbers = [1, 2, 3, 4, 5]
370 | return html`
371 |
372 | ${numbers.map((num) => Component(Exponential, { num, key: num }))}
373 |
374 | `
375 | ```
376 |
377 | ### Stores
378 | Stores are the mechanism for sharing behavior between components, or even apps.
379 | A store can subscribe to events, mutate the local state and issue re-renders.
380 |
381 | ```js
382 | import { html, use, Component } from 'https://cdn.skypack.dev/yeet@next'
383 |
384 | function Parent (state, emit) {
385 | use(counter) // ← Use the counter store with this component
386 |
387 | return function () {
388 | return html`
389 | ${Component(Increment)}
390 |
391 | ${Component(Decrement)}
392 | `
393 | }
394 | }
395 |
396 | function Increment (state, emit) {
397 | return html``
398 | }
399 |
400 | function Decrement (state, emit) {
401 | return html``
402 | }
403 |
404 | function counter (state, emitter) {
405 | state.count = 0 // ← Define some initial state
406 |
407 | emitter.on('increment', function () {
408 | state.count++
409 | emitter.emit('render')
410 | })
411 |
412 | emitter.on('decrement', function () {
413 | state.count--
414 | emitter.emit('render')
415 | })
416 | }
417 | ```
418 |
419 | #### Events
420 | How you choose to name your events is entirely up to you. There's only one
421 | exception: the `render` event has special meaning and will re-render the closest
422 | component in the component tree. The `render` event does not bubble.
423 |
424 | ## Server rendering (SSR)
425 | Yeet has first-class support for server rendering. There are plans to support
426 | server-rendered templates, meaning any backend could render the actual HTML and
427 | yeet would wire up functionality using the pre-existing markup.
428 |
429 | Rendering on the server supports fully asynchronous components. If a component
430 | yields promises, yeet will wait for these promises to resolve while rendering.
431 |
432 | ### Server rendered templates (non-Node.js)
433 | _Coming soon…_
434 |
435 | ## API
436 | The API is intentionally small.
437 |
438 | ### html
439 | Create html partials which can be rendered to DOM nodes (or strings in Node.js).
440 |
441 | ```js
442 | import { html } from 'https://cdn.skypack.dev/yeet@next'
443 |
444 | const name = 'planet'
445 | html`
Hello ${name}!
`
446 | ```
447 |
448 | #### Attributes
449 | Both literal attributes as well as dynamically "spread" attributes work. Arrays
450 | will be joined with an empty space (` `) to make it easier to work with many
451 | space separated attributes, e.g. `class`.
452 |
453 | ```js
454 | import { html } from 'https://cdn.skypack.dev/yeet@next'
455 |
456 | const attrs = { disabled: true, hidden: false, placeholder: null }
457 | html``
458 | // →
459 | ```
460 |
461 | ##### Events
462 | Events can be attached to elements using the standard `on`-prefix.
463 |
464 | ```js
465 | import { html } from 'https://cdn.skypack.dev/yeet@next'
466 |
467 | html``
468 | ```
469 |
470 | #### Arrays
471 | If you have lists of things you want to render as elements, interpolating arrays
472 | works just like you'd expect.
473 |
474 | ```js
475 | import { html } from 'https://cdn.skypack.dev/yeet@next'
476 |
477 | const list = [1, 2, 3]
478 | html`${list.map((num) => html`
${num}
`)}`
479 | ```
480 |
481 | #### Fragments
482 | It's not always that you can or need to have an outer containing element.
483 | Rendering fragments works just like single container elements.
484 |
485 | ```js
486 | import { html } from 'https://cdn.skypack.dev/yeet@next'
487 |
488 | html`
489 |
Hello world!
490 |
Lorem ipsum dolor sit amet…
491 | `
492 | ```
493 |
494 | ### svg
495 | The `svg` tag is required for rendering all kinds of SVG elements, such as
496 | `