@e280/sly 0.2.4 → 0.3.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +302 -614
- package/package.json +6 -8
- package/s/_archive/README.md +1221 -0
- package/s/{base → _archive/base}/element.ts +5 -2
- package/s/_archive/view/index.ts +7 -0
- package/s/_archive/view/types.ts +45 -0
- package/s/{view → _archive/view}/utils/parts/capsule.ts +9 -2
- package/s/demo/demo.bundle.ts +2 -9
- package/s/demo/views/counter-light.ts +13 -0
- package/s/demo/views/counter-shadow.ts +16 -0
- package/s/demo/views/demo.ts +24 -18
- package/s/demo/views/loaders.ts +7 -7
- package/s/index.html.ts +30 -33
- package/s/index.ts +0 -2
- package/s/loaders/make.ts +1 -1
- package/s/loaders/parts/ascii-anim.ts +6 -8
- package/s/loaders/parts/error-display.ts +9 -9
- package/s/tests.test.ts +1 -4
- package/s/view/common/css-reset.ts +19 -0
- package/s/view/hooks/plumbing/hooks.ts +28 -0
- package/s/view/hooks/plumbing/hookscope.ts +12 -0
- package/s/view/hooks/use-css.ts +14 -0
- package/s/view/hooks/use-cx.ts +41 -0
- package/s/view/hooks/use-life.ts +17 -0
- package/s/view/hooks/use-mount.ts +30 -0
- package/s/view/hooks/use-name.ts +10 -0
- package/s/view/hooks/use-once.ts +9 -0
- package/s/view/hooks/use-op.ts +12 -0
- package/s/view/hooks/use-ref.ts +11 -0
- package/s/view/hooks/use-signal.ts +16 -0
- package/s/view/hooks/use-state.ts +20 -0
- package/s/view/hooks/use-wake.ts +8 -0
- package/s/view/index.ts +17 -4
- package/s/view/light.ts +50 -0
- package/s/view/parts/apply-attrs.ts +22 -0
- package/s/view/parts/apply-styles.ts +21 -0
- package/s/view/parts/cx.ts +26 -0
- package/s/view/parts/reactivity.ts +22 -0
- package/s/view/parts/sly-shadow.ts +8 -0
- package/s/view/shadow.ts +93 -0
- package/s/view/types.ts +15 -34
- package/x/demo/demo.bundle.js +2 -8
- package/x/demo/demo.bundle.js.map +1 -1
- package/x/demo/demo.bundle.min.js +45 -58
- package/x/demo/demo.bundle.min.js.map +4 -4
- package/x/demo/views/counter-light.d.ts +1 -0
- package/x/demo/views/counter-light.js +10 -0
- package/x/demo/views/counter-light.js.map +1 -0
- package/x/demo/views/counter-shadow.d.ts +1 -0
- package/x/demo/views/counter-shadow.js +12 -0
- package/x/demo/views/counter-shadow.js.map +1 -0
- package/x/demo/views/demo.d.ts +1 -4
- package/x/demo/views/demo.js +23 -18
- package/x/demo/views/demo.js.map +1 -1
- package/x/demo/views/loaders.d.ts +1 -1
- package/x/demo/views/loaders.js +7 -7
- package/x/demo/views/loaders.js.map +1 -1
- package/x/index.d.ts +0 -2
- package/x/index.html +30 -140
- package/x/index.html.js +31 -31
- package/x/index.html.js.map +1 -1
- package/x/index.js +0 -2
- package/x/index.js.map +1 -1
- package/x/loaders/make.d.ts +1 -1
- package/x/loaders/parts/ascii-anim.d.ts +1 -1
- package/x/loaders/parts/ascii-anim.js +6 -7
- package/x/loaders/parts/ascii-anim.js.map +1 -1
- package/x/loaders/parts/error-display.d.ts +1 -1
- package/x/loaders/parts/error-display.js +9 -9
- package/x/loaders/parts/error-display.js.map +1 -1
- package/x/tests.test.js +1 -4
- package/x/tests.test.js.map +1 -1
- package/x/view/common/css-reset.js +17 -0
- package/x/view/common/css-reset.js.map +1 -0
- package/x/view/hooks/plumbing/hooks.d.ts +11 -0
- package/x/view/hooks/plumbing/hooks.js +26 -0
- package/x/view/hooks/plumbing/hooks.js.map +1 -0
- package/x/view/hooks/plumbing/hookscope.d.ts +10 -0
- package/x/view/hooks/plumbing/hookscope.js +12 -0
- package/x/view/hooks/plumbing/hookscope.js.map +1 -0
- package/x/view/hooks/use-css.d.ts +4 -0
- package/x/view/hooks/use-css.js +10 -0
- package/x/view/hooks/use-css.js.map +1 -0
- package/x/view/hooks/use-cx.d.ts +10 -0
- package/x/view/hooks/use-cx.js +33 -0
- package/x/view/hooks/use-cx.js.map +1 -0
- package/x/view/hooks/use-life.d.ts +2 -0
- package/x/view/hooks/use-life.js +13 -0
- package/x/view/hooks/use-life.js.map +1 -0
- package/x/{base/utils/mounts.d.ts → view/hooks/use-mount.d.ts} +1 -0
- package/x/{base/utils/mounts.js → view/hooks/use-mount.js} +7 -1
- package/x/view/hooks/use-mount.js.map +1 -0
- package/x/view/hooks/use-name.d.ts +2 -0
- package/x/view/hooks/use-name.js +8 -0
- package/x/view/hooks/use-name.js.map +1 -0
- package/x/view/hooks/use-once.d.ts +2 -0
- package/x/view/hooks/use-once.js +7 -0
- package/x/view/hooks/use-once.js.map +1 -0
- package/x/view/hooks/use-op.d.ts +3 -0
- package/x/view/hooks/use-op.js +9 -0
- package/x/view/hooks/use-op.js.map +1 -0
- package/x/view/hooks/use-ref.d.ts +5 -0
- package/x/view/hooks/use-ref.js +11 -0
- package/x/view/hooks/use-ref.js.map +1 -0
- package/x/view/hooks/use-signal.d.ts +3 -0
- package/x/view/hooks/use-signal.js +12 -0
- package/x/view/hooks/use-signal.js.map +1 -0
- package/x/view/hooks/use-state.d.ts +1 -0
- package/x/view/hooks/use-state.js +17 -0
- package/x/view/hooks/use-state.js.map +1 -0
- package/x/view/hooks/use-wake.d.ts +2 -0
- package/x/view/hooks/use-wake.js +6 -0
- package/x/view/hooks/use-wake.js.map +1 -0
- package/x/view/index.d.ts +15 -4
- package/x/view/index.js +15 -4
- package/x/view/index.js.map +1 -1
- package/x/view/light.d.ts +2 -0
- package/x/view/light.js +41 -0
- package/x/view/light.js.map +1 -0
- package/x/view/parts/apply-attrs.d.ts +2 -0
- package/x/view/parts/apply-attrs.js +22 -0
- package/x/view/parts/apply-attrs.js.map +1 -0
- package/x/{base/utils → view/parts}/apply-styles.js.map +1 -1
- package/x/view/parts/cx.d.ts +12 -0
- package/x/view/parts/cx.js +24 -0
- package/x/view/parts/cx.js.map +1 -0
- package/x/view/parts/reactivity.d.ts +5 -0
- package/x/view/parts/reactivity.js +18 -0
- package/x/view/parts/reactivity.js.map +1 -0
- package/x/view/parts/sly-shadow.d.ts +3 -0
- package/x/view/parts/sly-shadow.js +7 -0
- package/x/view/parts/sly-shadow.js.map +1 -0
- package/x/view/shadow.d.ts +6 -0
- package/x/view/shadow.js +72 -0
- package/x/view/shadow.js.map +1 -0
- package/x/view/types.d.ts +13 -21
- package/s/demo/views/counter.ts +0 -50
- package/s/demo/views/fastcount.ts +0 -29
- package/x/base/css-reset.js +0 -19
- package/x/base/css-reset.js.map +0 -1
- package/x/base/element.d.ts +0 -19
- package/x/base/element.js +0 -52
- package/x/base/element.js.map +0 -1
- package/x/base/index.d.ts +0 -5
- package/x/base/index.js +0 -6
- package/x/base/index.js.map +0 -1
- package/x/base/types.d.ts +0 -3
- package/x/base/types.js +0 -3
- package/x/base/types.js.map +0 -1
- package/x/base/use.d.ts +0 -59
- package/x/base/use.js +0 -129
- package/x/base/use.js.map +0 -1
- package/x/base/utils/attr-watcher.d.ts +0 -8
- package/x/base/utils/attr-watcher.js +0 -20
- package/x/base/utils/attr-watcher.js.map +0 -1
- package/x/base/utils/mounts.js.map +0 -1
- package/x/base/utils/reactor.d.ts +0 -5
- package/x/base/utils/reactor.js +0 -25
- package/x/base/utils/reactor.js.map +0 -1
- package/x/base/utils/states.d.ts +0 -13
- package/x/base/utils/states.js +0 -41
- package/x/base/utils/states.js.map +0 -1
- package/x/base/utils/use-attrs.d.ts +0 -11
- package/x/base/utils/use-attrs.js +0 -18
- package/x/base/utils/use-attrs.js.map +0 -1
- package/x/demo/views/counter.d.ts +0 -374
- package/x/demo/views/counter.js +0 -42
- package/x/demo/views/counter.js.map +0 -1
- package/x/demo/views/fastcount.d.ts +0 -12
- package/x/demo/views/fastcount.js +0 -21
- package/x/demo/views/fastcount.js.map +0 -1
- package/x/spa/index.barrel.d.ts +0 -4
- package/x/spa/index.barrel.js +0 -3
- package/x/spa/index.barrel.js.map +0 -1
- package/x/spa/index.d.ts +0 -2
- package/x/spa/index.js +0 -2
- package/x/spa/index.js.map +0 -1
- package/x/spa/plumbing/braces.d.ts +0 -12
- package/x/spa/plumbing/braces.js +0 -55
- package/x/spa/plumbing/braces.js.map +0 -1
- package/x/spa/plumbing/primitives.d.ts +0 -22
- package/x/spa/plumbing/primitives.js +0 -65
- package/x/spa/plumbing/primitives.js.map +0 -1
- package/x/spa/plumbing/router-core.d.ts +0 -13
- package/x/spa/plumbing/router-core.js +0 -38
- package/x/spa/plumbing/router-core.js.map +0 -1
- package/x/spa/plumbing/types.d.ts +0 -35
- package/x/spa/plumbing/types.js +0 -2
- package/x/spa/plumbing/types.js.map +0 -1
- package/x/spa/router.d.ts +0 -13
- package/x/spa/router.js +0 -39
- package/x/spa/router.js.map +0 -1
- package/x/spa/spa.test.d.ts +0 -15
- package/x/spa/spa.test.js +0 -78
- package/x/spa/spa.test.js.map +0 -1
- package/x/view/utils/contextualize.d.ts +0 -13
- package/x/view/utils/contextualize.js +0 -18
- package/x/view/utils/contextualize.js.map +0 -1
- package/x/view/utils/make-component.d.ts +0 -5
- package/x/view/utils/make-component.js +0 -17
- package/x/view/utils/make-component.js.map +0 -1
- package/x/view/utils/make-view.d.ts +0 -2
- package/x/view/utils/make-view.js +0 -32
- package/x/view/utils/make-view.js.map +0 -1
- package/x/view/utils/parts/capsule.d.ts +0 -12
- package/x/view/utils/parts/capsule.js +0 -50
- package/x/view/utils/parts/capsule.js.map +0 -1
- package/x/view/utils/parts/chain.d.ts +0 -13
- package/x/view/utils/parts/chain.js +0 -26
- package/x/view/utils/parts/chain.js.map +0 -1
- package/x/view/utils/parts/context.d.ts +0 -9
- package/x/view/utils/parts/context.js +0 -10
- package/x/view/utils/parts/context.js.map +0 -1
- package/x/view/utils/parts/directive.d.ts +0 -5
- package/x/view/utils/parts/directive.js +0 -20
- package/x/view/utils/parts/directive.js.map +0 -1
- package/x/view/utils/parts/naked.d.ts +0 -18
- package/x/view/utils/parts/naked.js +0 -57
- package/x/view/utils/parts/naked.js.map +0 -1
- package/x/view/utils/parts/sly-view.d.ts +0 -6
- package/x/view/utils/parts/sly-view.js +0 -16
- package/x/view/utils/parts/sly-view.js.map +0 -1
- package/x/view/view.d.ts +0 -11
- package/x/view/view.js +0 -15
- package/x/view/view.js.map +0 -1
- /package/s/{base → _archive/base}/css-reset.ts +0 -0
- /package/s/{base → _archive/base}/index.ts +0 -0
- /package/s/{base → _archive/base}/types.ts +0 -0
- /package/s/{base → _archive/base}/use.ts +0 -0
- /package/s/{base → _archive/base}/utils/apply-styles.ts +0 -0
- /package/s/{base → _archive/base}/utils/attr-watcher.ts +0 -0
- /package/s/{base → _archive/base}/utils/mounts.ts +0 -0
- /package/s/{base → _archive/base}/utils/reactor.ts +0 -0
- /package/s/{base → _archive/base}/utils/states.ts +0 -0
- /package/s/{base → _archive/base}/utils/use-attrs.ts +0 -0
- /package/s/{spa → _archive/spa}/index.barrel.ts +0 -0
- /package/s/{spa → _archive/spa}/index.ts +0 -0
- /package/s/{spa → _archive/spa}/plumbing/braces.ts +0 -0
- /package/s/{spa → _archive/spa}/plumbing/primitives.ts +0 -0
- /package/s/{spa → _archive/spa}/plumbing/router-core.ts +0 -0
- /package/s/{spa → _archive/spa}/plumbing/types.ts +0 -0
- /package/s/{spa → _archive/spa}/router.ts +0 -0
- /package/s/{spa → _archive/spa}/spa.test.ts +0 -0
- /package/s/{view → _archive/view}/utils/contextualize.ts +0 -0
- /package/s/{view → _archive/view}/utils/make-component.ts +0 -0
- /package/s/{view → _archive/view}/utils/make-view.ts +0 -0
- /package/s/{view → _archive/view}/utils/parts/chain.ts +0 -0
- /package/s/{view → _archive/view}/utils/parts/context.ts +0 -0
- /package/s/{view → _archive/view}/utils/parts/directive.ts +0 -0
- /package/s/{view → _archive/view}/utils/parts/naked.ts +0 -0
- /package/s/{view → _archive/view}/utils/parts/sly-view.ts +0 -0
- /package/s/{view → _archive/view}/view.ts +0 -0
- /package/x/{base → view/common}/css-reset.d.ts +0 -0
- /package/x/{base/utils → view/parts}/apply-styles.d.ts +0 -0
- /package/x/{base/utils → view/parts}/apply-styles.js +0 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
|
|
2
|
+
<div align="center"><img alt="" width="256" src="./assets/favicon.png"/></div>
|
|
3
|
+
|
|
4
|
+
# 🦝 sly
|
|
5
|
+
> *mischievous shadow views*
|
|
6
|
+
|
|
7
|
+
[@e280](https://e280.org/)'s new [lit](https://lit.dev/)-based frontend webdev library.
|
|
8
|
+
|
|
9
|
+
- 🍋 [**#views**](#views) — hooks-based, shadow-dom'd, template-literal'd
|
|
10
|
+
- 🪵 [**#base-element**](#base-element) — for a more classical experience
|
|
11
|
+
- 🪄 [**#dom**](#dom) — the "it's not jquery" multitool
|
|
12
|
+
- 🫛 [**#ops**](#ops) — reactive tooling for async operations
|
|
13
|
+
- ⏳ [**#loaders**](#loaders) — animated loading spinners for rendering ops
|
|
14
|
+
- 💅 [**#spa**](#spa) — hash routing for your spa-day
|
|
15
|
+
- 🪙 [**#loot**](#loot) — drag-and-drop facilities
|
|
16
|
+
- 🧪 https://sly.e280.org/ — our testing page
|
|
17
|
+
- **✨[shiny](https://shiny.e280.org/)✨** — our wip component library
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
<br/><br/>
|
|
22
|
+
|
|
23
|
+
## 🦝 sly and friends
|
|
24
|
+
> `@e280/sly`
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install @e280/sly lit @e280/strata @e280/stz
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> [!NOTE]
|
|
31
|
+
> - 🔥 [lit](https://lit.dev/), for html rendering
|
|
32
|
+
> - ⛏️ [@e280/strata](https://github.com/e280/strata), for state management (signals, state trees)
|
|
33
|
+
> - 🏂 [@e280/stz](https://github.com/e280/stz), our ts standard library
|
|
34
|
+
> - 🐢 [@e280/scute](https://github.com/e280/scute), our buildy-bundly-buddy
|
|
35
|
+
|
|
36
|
+
> [!TIP]
|
|
37
|
+
> you can import everything in sly from `@e280/sly`,
|
|
38
|
+
> or from specific subpackages like `@e280/sly/view`, `@e280/sly/dom`, etc...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<br/><br/>
|
|
43
|
+
<a id="views"></a>
|
|
44
|
+
|
|
45
|
+
## 🍋🦝 sly views
|
|
46
|
+
> *modern views, in lightness, or darkness...*
|
|
47
|
+
|
|
48
|
+
- 🪶 **no compile step** — just god's honest javascript, via [lit](https://lit.dev/)-html tagged-template-literals
|
|
49
|
+
- 🪝 **hooks-based** — declarative rendering with [modern hooks](#hooks) familiar to react devs
|
|
50
|
+
- ⚡ **reactive** — views auto-rerender whenever any [strata](https://github.com/e280/strata)-compatible state changes
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import {html} from "lit"
|
|
54
|
+
import {light, shadow, dom} from "@e280/sly"
|
|
55
|
+
|
|
56
|
+
export const MyLightView = light(() => html`<p>blinded by the light</p>`)
|
|
57
|
+
|
|
58
|
+
export const MyShadowView = shadow(() => html`<p>shrouded in darkness</p>`)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 🍋 light views
|
|
62
|
+
> *just pretend it's react*
|
|
63
|
+
|
|
64
|
+
- **define a light view**
|
|
65
|
+
```ts
|
|
66
|
+
import {html} from "lit"
|
|
67
|
+
import {light, useSignal} from "@e280/sly"
|
|
68
|
+
|
|
69
|
+
export const MyCounter = light((start: number) => {
|
|
70
|
+
const $count = useSignal(start)
|
|
71
|
+
const increment = () => $count.value++
|
|
72
|
+
|
|
73
|
+
return html`
|
|
74
|
+
<button @click="${increment}">${$count.value}</button>
|
|
75
|
+
`
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
- **render it into the dom**
|
|
79
|
+
```ts
|
|
80
|
+
dom.in(".demo").render(html`
|
|
81
|
+
<h1>my cool counter demo</h1>
|
|
82
|
+
${MyCounter(123)}
|
|
83
|
+
`)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 🍋 shadow views
|
|
87
|
+
> *each shadow view gets its own cozy [shadow-dom](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) bubble and supports [slots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)*
|
|
88
|
+
|
|
89
|
+
- **define a shadow view**
|
|
90
|
+
```ts
|
|
91
|
+
import {css, html} from "lit"
|
|
92
|
+
import {shadow, useName, useCss, useSignal} from "@e280/sly"
|
|
93
|
+
|
|
94
|
+
export const MyShadowCounter = shadow((start: number) => {
|
|
95
|
+
useName("shadow-counter")
|
|
96
|
+
useCss(css`button { color: cyan }`)
|
|
97
|
+
|
|
98
|
+
const $count = useSignal(start)
|
|
99
|
+
const increment = () => $count.value++
|
|
100
|
+
|
|
101
|
+
return html`
|
|
102
|
+
<button @click="${increment}">${$count()}</button>
|
|
103
|
+
<slot></slot>
|
|
104
|
+
`
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
- **render it into the dom**
|
|
108
|
+
```ts
|
|
109
|
+
dom.in(".demo").render(html`
|
|
110
|
+
<h1>my cool counter demo</h1>
|
|
111
|
+
${MyShadowCounter(234)}
|
|
112
|
+
`)
|
|
113
|
+
```
|
|
114
|
+
- **.with to nest children or set attrs**
|
|
115
|
+
```ts
|
|
116
|
+
dom.in(".demo").render(html`
|
|
117
|
+
<h1>my cool counter demo</h1>
|
|
118
|
+
|
|
119
|
+
${MyShadowCounter.with({
|
|
120
|
+
props: [234],
|
|
121
|
+
attrs: {"data-whatever": 555},
|
|
122
|
+
children: html`
|
|
123
|
+
<p>woah, slotting support!</p>
|
|
124
|
+
`,
|
|
125
|
+
})}
|
|
126
|
+
`)
|
|
127
|
+
```
|
|
128
|
+
- **oh, you can do custom shadow config if needed**
|
|
129
|
+
```ts
|
|
130
|
+
const MyCustomShadow = shadow.config(() => {
|
|
131
|
+
const host = document.createElement("div")
|
|
132
|
+
const shadow = host.attachShadow({mode: "open"})
|
|
133
|
+
return {host, shadow}
|
|
134
|
+
})(() => html`<p>shrouded in darkness</p>`)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
<a id="hooks"></a>
|
|
138
|
+
|
|
139
|
+
### 🍋 hooks reference
|
|
140
|
+
|
|
141
|
+
#### 👮 follow the hooks rules
|
|
142
|
+
> just like [react hooks](https://react.dev/warnings/invalid-hook-call-warning), the execution order of sly's `use` hooks actually matters..
|
|
143
|
+
> you must not call these hooks under `if` conditionals, or `for` loops, or in callbacks, or after a conditional `return` statement, or anything like that.. *otherwise, heed my warning: weird bad stuff will happen..*
|
|
144
|
+
|
|
145
|
+
#### 🌚 shadow-only hooks
|
|
146
|
+
- **useName** — *(shadow only)* — set the "data-view" attr value
|
|
147
|
+
```ts
|
|
148
|
+
useName("squarepants")
|
|
149
|
+
// <div data-view="squarepants">
|
|
150
|
+
```
|
|
151
|
+
- **useCss** — *(shadow only)* — attach stylesheets (use lit's `css`!) to the shadow root
|
|
152
|
+
```ts
|
|
153
|
+
useCss(css1, css2, css3)
|
|
154
|
+
```
|
|
155
|
+
- **useHost** — *(shadow only)* — get the host element
|
|
156
|
+
```ts
|
|
157
|
+
const host = useHost()
|
|
158
|
+
```
|
|
159
|
+
- **useShadow** — *(shadow only)* — get the shadow root
|
|
160
|
+
```ts
|
|
161
|
+
const shadow = useShadow()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### 🌞 universal hooks
|
|
165
|
+
- **useState** — react-like hook to create some reactive state (we prefer signals)
|
|
166
|
+
```ts
|
|
167
|
+
const [count, setCount] = useState(0)
|
|
168
|
+
|
|
169
|
+
const increment = () => setCount(n => n + 1)
|
|
170
|
+
```
|
|
171
|
+
- **useRef** — react-like hook to make a non-reactive box for a value
|
|
172
|
+
```ts
|
|
173
|
+
const ref = useRef(0)
|
|
174
|
+
|
|
175
|
+
ref.current // 0
|
|
176
|
+
ref.current = 1 // does not trigger rerender
|
|
177
|
+
```
|
|
178
|
+
- **useSignal** — create a [strata](https://github.com/e280/strata) signal
|
|
179
|
+
```ts
|
|
180
|
+
const $count = useSignal(1)
|
|
181
|
+
|
|
182
|
+
// read the signal
|
|
183
|
+
$count()
|
|
184
|
+
|
|
185
|
+
// write the signal
|
|
186
|
+
$count(2)
|
|
187
|
+
```
|
|
188
|
+
- see [strata readme](https://github.com/e280/strata)
|
|
189
|
+
- **useDerived** — create a [strata](https://github.com/e280/strata) derived signal
|
|
190
|
+
```ts
|
|
191
|
+
const $product = useDerived(() => $count() * $whatever())
|
|
192
|
+
```
|
|
193
|
+
- see [strata readme](https://github.com/e280/strata)
|
|
194
|
+
- **useOnce** — run fn at initialization, and return a value
|
|
195
|
+
```ts
|
|
196
|
+
const whatever = use.once(() => {
|
|
197
|
+
console.log("happens one time")
|
|
198
|
+
return 123
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
whatever // 123
|
|
202
|
+
```
|
|
203
|
+
- **useMount** — setup mount/unmount lifecycle
|
|
204
|
+
```ts
|
|
205
|
+
useMount(() => {
|
|
206
|
+
console.log("mounted")
|
|
207
|
+
return () => console.log("unmounted")
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
- **useWake** — run fn each time mounted, and return value
|
|
211
|
+
```ts
|
|
212
|
+
const whatever = use.wake(() => {
|
|
213
|
+
console.log("mounted")
|
|
214
|
+
return 123
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
whatever // 123
|
|
218
|
+
```
|
|
219
|
+
- **useLife** — mount/unmount lifecycle, but also return a value
|
|
220
|
+
```ts
|
|
221
|
+
const whatever = use.life(() => {
|
|
222
|
+
console.log("mounted")
|
|
223
|
+
const value = 123
|
|
224
|
+
return [value, () => console.log("unmounted")]
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
whatever // 123
|
|
228
|
+
```
|
|
229
|
+
- **useRender** — returns a fn to rerender the view (debounced)
|
|
230
|
+
```ts
|
|
231
|
+
const render = useRender()
|
|
232
|
+
|
|
233
|
+
render().then(() => console.log("render done"))
|
|
234
|
+
```
|
|
235
|
+
- **useRendered** — get a promise that resolves *after* the next render
|
|
236
|
+
```ts
|
|
237
|
+
useRendered().then(() => console.log("rendered"))
|
|
238
|
+
```
|
|
239
|
+
- **useOp** — start loading an op based on an async fn
|
|
240
|
+
```ts
|
|
241
|
+
const op = useOp(async() => {
|
|
242
|
+
await nap(5000)
|
|
243
|
+
return 123
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
- **useOpPromise** — start loading an op based on a promise
|
|
247
|
+
```ts
|
|
248
|
+
const op = use.op.promise(doAsyncWork())
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 🍋 happy hooks recipes
|
|
252
|
+
- make a ticker — mount, cycle, and nap
|
|
253
|
+
```ts
|
|
254
|
+
import {cycle, nap} from "@e280/stz"
|
|
255
|
+
```
|
|
256
|
+
```ts
|
|
257
|
+
const $seconds = useSignal(0)
|
|
258
|
+
|
|
259
|
+
useMount(() => cycle(async() => {
|
|
260
|
+
await nap(1000)
|
|
261
|
+
$seconds.value++
|
|
262
|
+
}))
|
|
263
|
+
```
|
|
264
|
+
- wake + rendered, to do something after each mount's first render
|
|
265
|
+
```ts
|
|
266
|
+
useWake(() => useRendered.then(() => {
|
|
267
|
+
console.log("after first render")
|
|
268
|
+
}))
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
<br/><br/>
|
|
274
|
+
<a id="legacy-views"></a>
|
|
275
|
+
|
|
276
|
+
## 🍋🦝 LEGACY sly views
|
|
277
|
+
> `@e280/sly/view`
|
|
278
|
+
> *the crown jewel of sly*
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
view(use => () => html`<p>hello world</p>`)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
- 🪶 **no compile step** — just god's honest javascript, via [lit](https://lit.dev/)-html tagged-template-literals
|
|
285
|
+
- 🥷 **shadow dom'd** — each view gets its own cozy [shadow](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) bubble, and supports [slots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
|
|
286
|
+
- 🪝 **hooks-based** — declarative rendering with the [`use`](#use) family of ergonomic hooks
|
|
287
|
+
- ⚡ **reactive** — they auto-rerender whenever any [strata](https://github.com/e280/strata)-compatible state changes
|
|
288
|
+
- 🧐 **not components, per se** — they're comfy typescript-native ui building blocks [(technically, lit directives)](https://lit.dev/docs/templates/custom-directives/)
|
|
289
|
+
- 🧩 **componentizable** — any view can be magically converted into a proper [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)
|
|
290
|
+
|
|
291
|
+
### 🍋 view example
|
|
292
|
+
```ts
|
|
293
|
+
import {view, dom, BaseElement} from "@e280/sly"
|
|
294
|
+
import {html, css} from "lit"
|
|
295
|
+
```
|
|
296
|
+
- **declare view**
|
|
297
|
+
```ts
|
|
298
|
+
export const CounterView = view(use => (start: number) => {
|
|
299
|
+
use.styles(css`p {color: green}`)
|
|
300
|
+
|
|
301
|
+
const $count = use.signal(start)
|
|
302
|
+
const increment = () => $count.value++
|
|
303
|
+
|
|
304
|
+
return html`
|
|
305
|
+
<button @click="${increment}">
|
|
306
|
+
${$count.value}
|
|
307
|
+
</button>
|
|
308
|
+
`
|
|
309
|
+
})
|
|
310
|
+
```
|
|
311
|
+
- `$count` is a [strata signal](https://github.com/e280/strata#readme) *(we like those)*
|
|
312
|
+
- **inject view into dom**
|
|
313
|
+
```ts
|
|
314
|
+
dom.in(".app").render(html`
|
|
315
|
+
<h1>cool counter demo</h1>
|
|
316
|
+
${CounterView(1)}
|
|
317
|
+
`)
|
|
318
|
+
```
|
|
319
|
+
- 🤯 **register view as web component**
|
|
320
|
+
```ts
|
|
321
|
+
dom.register({
|
|
322
|
+
MyCounter: CounterView
|
|
323
|
+
.component()
|
|
324
|
+
.props(() => [1]),
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
```html
|
|
328
|
+
<my-counter></my-counter>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### 🍋 view settings
|
|
332
|
+
- optional settings for views you should know about
|
|
333
|
+
```ts
|
|
334
|
+
export const CoolView = view
|
|
335
|
+
.settings({mode: "open", delegatesFocus: true})
|
|
336
|
+
.render(use => (greeting: string) => html`😎 ${greeting} <slot></slot>`)
|
|
337
|
+
```
|
|
338
|
+
- all [attachShadow params](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters) (like `mode` and `delegatesFocus`) are valid `settings`
|
|
339
|
+
- note the `<slot></slot>` we'll use in the next example lol
|
|
340
|
+
|
|
341
|
+
### 🍋 view chains
|
|
342
|
+
- views have this sick chaining syntax for supplying more stuff at the template injection site
|
|
343
|
+
```ts
|
|
344
|
+
dom.in(".app").render(html`
|
|
345
|
+
<h2>cool example</h2>
|
|
346
|
+
${CoolView
|
|
347
|
+
.props("hello")
|
|
348
|
+
.attr("class", "hero")
|
|
349
|
+
.children(html`<em>spongebob</em>`)
|
|
350
|
+
.render()}
|
|
351
|
+
`)
|
|
352
|
+
```
|
|
353
|
+
- `props` — provide props and start a view chain
|
|
354
|
+
- `attr` — set html attributes on the `<sly-view>` host element
|
|
355
|
+
- `children` — add nested [slottable](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots) content
|
|
356
|
+
- `render` — end the view chain and render the lit directive
|
|
357
|
+
|
|
358
|
+
### 🍋 view/component universality
|
|
359
|
+
- **you can start with a view,**
|
|
360
|
+
```ts
|
|
361
|
+
export const GreeterView = view(use => (name: string) => {
|
|
362
|
+
return html`<p>hello ${name}</p>`
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
- view usage
|
|
366
|
+
```ts
|
|
367
|
+
GreeterView("pimsley")
|
|
368
|
+
```
|
|
369
|
+
**then you can convert it to a component.**
|
|
370
|
+
```ts
|
|
371
|
+
export class GreeterComponent extends (
|
|
372
|
+
GreeterView
|
|
373
|
+
.component()
|
|
374
|
+
.props(component => [component.getAttribute("name") ?? "unknown"])
|
|
375
|
+
) {}
|
|
376
|
+
```
|
|
377
|
+
- html usage
|
|
378
|
+
```html
|
|
379
|
+
<greeter-component name="pimsley"></greeter-component>
|
|
380
|
+
```
|
|
381
|
+
- **you can start with a component,**
|
|
382
|
+
```ts
|
|
383
|
+
export class GreeterComponent extends (
|
|
384
|
+
view(use => (name: string) => {
|
|
385
|
+
return html`<p>hello ${name}</p>`
|
|
386
|
+
})
|
|
387
|
+
.component()
|
|
388
|
+
.props(component => [component.getAttribute("name") ?? "unknown"])
|
|
389
|
+
) {}
|
|
390
|
+
```
|
|
391
|
+
- html usage
|
|
392
|
+
```html
|
|
393
|
+
<greeter-component name="pimsley"></greeter-component>
|
|
394
|
+
```
|
|
395
|
+
**and it already has `.view` ready for you.**
|
|
396
|
+
- view usage
|
|
397
|
+
```ts
|
|
398
|
+
GreeterComponent.view("pimsley")
|
|
399
|
+
```
|
|
400
|
+
- **understanding `.component(BaseElement)` and `.props(fn)`**
|
|
401
|
+
- `.props` takes a fn that is called every render, which returns the props given to the view
|
|
402
|
+
```ts
|
|
403
|
+
.props(() => ["pimsley"])
|
|
404
|
+
```
|
|
405
|
+
the props fn receives the component instance, so you can query html attributes or instance properties
|
|
406
|
+
```ts
|
|
407
|
+
.props(component => [component.getAttribute("name") ?? "unknown"])
|
|
408
|
+
```
|
|
409
|
+
- `.component` accepts a subclass of `BaseElement`, so you can define your own properties and methods for your component class
|
|
410
|
+
```ts
|
|
411
|
+
const GreeterComponent = GreeterView
|
|
412
|
+
|
|
413
|
+
// declare your own custom class
|
|
414
|
+
.component(class extends BaseElement {
|
|
415
|
+
$name = signal("jim raynor")
|
|
416
|
+
updateName(name: string) {
|
|
417
|
+
this.$name.value = name
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// props gets the right types on 'component'
|
|
422
|
+
.props(component => [component.$name.value])
|
|
423
|
+
```
|
|
424
|
+
- `.component` provides the devs interacting with your component, with noice typings
|
|
425
|
+
```ts
|
|
426
|
+
dom<GreeterComponent>("greeter-component").updateName("mortimer")
|
|
427
|
+
```
|
|
428
|
+
- typescript class wizardry
|
|
429
|
+
- ❌ smol-brain approach exports class value, but NOT the typings
|
|
430
|
+
```ts
|
|
431
|
+
export const GreeterComponent = (...)
|
|
432
|
+
```
|
|
433
|
+
- ✅ giga-brain approach exports class value AND the typings
|
|
434
|
+
```ts
|
|
435
|
+
export class GreeterComponent extends (...) {}
|
|
436
|
+
```
|
|
437
|
+
- **register web components to the dom**
|
|
438
|
+
```ts
|
|
439
|
+
dom.register({GreeterComponent})
|
|
440
|
+
```
|
|
441
|
+
- **oh and don't miss out on the insta-component shorthand**
|
|
442
|
+
```ts
|
|
443
|
+
dom.register({
|
|
444
|
+
QuickComponent: view.component(use => html`⚡ incredi`),
|
|
445
|
+
})
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
<a id="use"></a>
|
|
449
|
+
|
|
450
|
+
### 🍋 "use" hooks reference
|
|
451
|
+
- 👮 **follow the hooks rules**
|
|
452
|
+
> just like [react hooks](https://react.dev/warnings/invalid-hook-call-warning), the execution order of sly's `use` hooks actually matters..
|
|
453
|
+
> you must not call these hooks under `if` conditionals, or `for` loops, or in callbacks, or after a conditional `return` statement, or anything like that.. *otherwise, heed my warning: weird bad stuff will happen..*
|
|
454
|
+
- **use.name** — set the "view" attr value, eg `<sly-view view="squarepants">`
|
|
455
|
+
```ts
|
|
456
|
+
use.name("squarepants")
|
|
457
|
+
```
|
|
458
|
+
- **use.styles** — attach stylesheets into the view's shadow dom
|
|
459
|
+
```ts
|
|
460
|
+
use.styles(css1, css2, css3)
|
|
461
|
+
```
|
|
462
|
+
*(alias `use.css`)*
|
|
463
|
+
- **use.signal** — create a [strata signal](https://github.com/e280/strata)
|
|
464
|
+
```ts
|
|
465
|
+
const $count = use.signal(1)
|
|
466
|
+
|
|
467
|
+
// read the signal
|
|
468
|
+
$count()
|
|
469
|
+
|
|
470
|
+
// write the signal
|
|
471
|
+
$count(2)
|
|
472
|
+
```
|
|
473
|
+
- `derived` signals
|
|
474
|
+
```ts
|
|
475
|
+
const $product = use.derived(() => $count() * $whatever())
|
|
476
|
+
```
|
|
477
|
+
- `lazy` signals
|
|
478
|
+
```ts
|
|
479
|
+
const $product = use.lazy(() => $count() * $whatever())
|
|
480
|
+
```
|
|
481
|
+
- go read the [strata readme](https://github.com/e280/strata) about this stuff
|
|
482
|
+
- **use.once** — run fn at initialization, and return a value
|
|
483
|
+
```ts
|
|
484
|
+
const whatever = use.once(() => {
|
|
485
|
+
console.log("happens only once")
|
|
486
|
+
return 123
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
whatever // 123
|
|
490
|
+
```
|
|
491
|
+
- **use.mount** — setup mount/unmount lifecycle
|
|
492
|
+
```ts
|
|
493
|
+
use.mount(() => {
|
|
494
|
+
console.log("view mounted")
|
|
495
|
+
|
|
496
|
+
return () => {
|
|
497
|
+
console.log("view unmounted")
|
|
498
|
+
}
|
|
499
|
+
})
|
|
500
|
+
```
|
|
501
|
+
- **use.wake** — run fn each time mounted, and return value
|
|
502
|
+
```ts
|
|
503
|
+
const whatever = use.wake(() => {
|
|
504
|
+
console.log("view mounted")
|
|
505
|
+
return 123
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
whatever // 123
|
|
509
|
+
```
|
|
510
|
+
- **use.life** — mount/unmount lifecycle, but also return a value
|
|
511
|
+
```ts
|
|
512
|
+
const v = use.life(() => {
|
|
513
|
+
console.log("mounted")
|
|
514
|
+
const value = 123
|
|
515
|
+
return [value, () => console.log("unmounted")]
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
v // 123
|
|
519
|
+
```
|
|
520
|
+
- **use.events** — attach event listeners to the element (auto-cleaned up)
|
|
521
|
+
```ts
|
|
522
|
+
use.events({
|
|
523
|
+
keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
|
|
524
|
+
keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
|
|
525
|
+
})
|
|
526
|
+
```
|
|
527
|
+
- **use.states** — [internal states](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states) helper
|
|
528
|
+
```ts
|
|
529
|
+
const states = use.states()
|
|
530
|
+
states.assign("active", "cool")
|
|
531
|
+
```
|
|
532
|
+
```css
|
|
533
|
+
[view="my-view"]::state(active) { color: yellow; }
|
|
534
|
+
[view="my-view"]::state(cool) { outline: 1px solid cyan; }
|
|
535
|
+
```
|
|
536
|
+
- **use.attrs** — ergonomic typed html attribute access
|
|
537
|
+
- `use.attrs` is similar to [#dom.attrs](#dom.attrs)
|
|
538
|
+
```ts
|
|
539
|
+
const attrs = use.attrs({
|
|
540
|
+
name: String,
|
|
541
|
+
count: Number,
|
|
542
|
+
active: Boolean,
|
|
543
|
+
})
|
|
544
|
+
```
|
|
545
|
+
```ts
|
|
546
|
+
attrs.name // "chase"
|
|
547
|
+
attrs.count // 123
|
|
548
|
+
attrs.active // true
|
|
549
|
+
```
|
|
550
|
+
- use.attrs.{strings/numbers/booleans}
|
|
551
|
+
```ts
|
|
552
|
+
use.attrs.strings.name // "chase"
|
|
553
|
+
use.attrs.numbers.count // 123
|
|
554
|
+
use.attrs.booleans.active // true
|
|
555
|
+
```
|
|
556
|
+
- use.attrs.on
|
|
557
|
+
```ts
|
|
558
|
+
use.attrs.on(() => console.log("an attribute changed"))
|
|
559
|
+
```
|
|
560
|
+
- **use.render** — rerender the view (debounced)
|
|
561
|
+
```ts
|
|
562
|
+
use.render()
|
|
563
|
+
```
|
|
564
|
+
- **use.renderNow** — rerender the view instantly (not debounced)
|
|
565
|
+
```ts
|
|
566
|
+
use.renderNow()
|
|
567
|
+
```
|
|
568
|
+
- **use.rendered** — promise that resolves *after* the next render
|
|
569
|
+
```ts
|
|
570
|
+
use.rendered.then(() => {
|
|
571
|
+
const slot = use.shadow.querySelector("slot")
|
|
572
|
+
console.log(slot)
|
|
573
|
+
})
|
|
574
|
+
```
|
|
575
|
+
- **use.op** — start with an op based on an async fn
|
|
576
|
+
```ts
|
|
577
|
+
const op = use.op(async() => {
|
|
578
|
+
await nap(5000)
|
|
579
|
+
return 123
|
|
580
|
+
})
|
|
581
|
+
```
|
|
582
|
+
- **use.op.promise** — start with an op based on a promise
|
|
583
|
+
```ts
|
|
584
|
+
const op = use.op.promise(doAsyncWork())
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### 🍋 "use" recipes
|
|
588
|
+
- make a ticker — mount, cycle, and nap
|
|
589
|
+
```ts
|
|
590
|
+
import {cycle, nap} from "@e280/stz"
|
|
591
|
+
```
|
|
592
|
+
```ts
|
|
593
|
+
const $seconds = use.signal(0)
|
|
594
|
+
|
|
595
|
+
use.mount(() => cycle(async() => {
|
|
596
|
+
await nap(1000)
|
|
597
|
+
$seconds.value++
|
|
598
|
+
}))
|
|
599
|
+
```
|
|
600
|
+
- wake + rendered, to do something after each mount's first render
|
|
601
|
+
```ts
|
|
602
|
+
use.wake(() => use.rendered.then(() => {
|
|
603
|
+
console.log("after first render")
|
|
604
|
+
}))
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
<br/><br/>
|
|
610
|
+
<a id="base-element"></a>
|
|
611
|
+
|
|
612
|
+
## 🪵🦝 sly base element
|
|
613
|
+
> `@e280/sly/base`
|
|
614
|
+
> *the classic experience*
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
import {BaseElement, Use, dom} from "@e280/sly"
|
|
618
|
+
import {html, css} from "lit"
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`BaseElement` is more of an old-timey class-based "boomer" approach to making web components, but with a millennial twist — its `render` method gives you the same `use` hooks that views enjoy.
|
|
622
|
+
|
|
623
|
+
👮 a *BaseElement* is not a *View*, and cannot be converted into a *View*.
|
|
624
|
+
|
|
625
|
+
### 🪵 let's clarify some sly terminology
|
|
626
|
+
- "Element"
|
|
627
|
+
- an html element; any subclass of the browser's HTMLElement
|
|
628
|
+
- all genuine ["web components"](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) are elements
|
|
629
|
+
- "BaseElement"
|
|
630
|
+
- sly's own subclass of the browser-native HTMLElement
|
|
631
|
+
- is a true element and web component (can be registered to the dom)
|
|
632
|
+
- "View"
|
|
633
|
+
- sly's own magic concept that uses a lit-directive to render stuff
|
|
634
|
+
- NOT an element or web component (can NOT be registered to the dom)
|
|
635
|
+
- NOT related to BaseElement
|
|
636
|
+
- can be converted into a Component via `view.component().props(() => [])`
|
|
637
|
+
- "Component"
|
|
638
|
+
- a sly view that has been converted into an element
|
|
639
|
+
- is a true element and web component (can be registered to the dom)
|
|
640
|
+
- actually a subclass of BaseElement
|
|
641
|
+
- actually contains the view on `Component.view`
|
|
642
|
+
|
|
643
|
+
### 🪵 base element setup
|
|
644
|
+
- **declare your element class**
|
|
645
|
+
```ts
|
|
646
|
+
export class MyElement extends BaseElement {
|
|
647
|
+
static styles = css`span{color:orange}`
|
|
648
|
+
|
|
649
|
+
// custom property
|
|
650
|
+
$start = signal(10)
|
|
651
|
+
|
|
652
|
+
// custom attributes
|
|
653
|
+
attrs = dom.attrs(this).spec({
|
|
654
|
+
multiply: Number,
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
// custom methods
|
|
658
|
+
hello() {
|
|
659
|
+
return "world"
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
render(use: Use) {
|
|
663
|
+
const $count = use.signal(1)
|
|
664
|
+
const increment = () => $count.value++
|
|
665
|
+
|
|
666
|
+
const {$start} = this
|
|
667
|
+
const {multiply = 1} = this.attrs
|
|
668
|
+
const result = $start() + (multiply * $count())
|
|
669
|
+
|
|
670
|
+
return html`
|
|
671
|
+
<span>${result}</span>
|
|
672
|
+
<button @click="${increment}">+</button>
|
|
673
|
+
`
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
- **register your element to the dom**
|
|
678
|
+
```ts
|
|
679
|
+
dom.register({MyElement})
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### 🪵 base element usage
|
|
683
|
+
- **place the element in your html body**
|
|
684
|
+
```html
|
|
685
|
+
<body>
|
|
686
|
+
<my-element></my-element>
|
|
687
|
+
</body>
|
|
688
|
+
```
|
|
689
|
+
- **now you can interact with it**
|
|
690
|
+
```ts
|
|
691
|
+
const myElement = dom<MyElement>("my-element")
|
|
692
|
+
|
|
693
|
+
// js property
|
|
694
|
+
myElement.$start(100)
|
|
695
|
+
|
|
696
|
+
// html attributes
|
|
697
|
+
myElement.attrs.multiply = 2
|
|
698
|
+
|
|
699
|
+
// methods
|
|
700
|
+
myElement.hello()
|
|
701
|
+
// "world"
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
<br/><br/>
|
|
707
|
+
<a id="dom"></a>
|
|
708
|
+
|
|
709
|
+
## 🪄🦝 sly dom
|
|
710
|
+
> `@e280/sly/dom`
|
|
711
|
+
> *the "it's not jquery!" multitool*
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
import {dom} from "@e280/sly"
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### 🪄 dom queries
|
|
718
|
+
- `require` an element
|
|
719
|
+
```ts
|
|
720
|
+
dom(".demo")
|
|
721
|
+
// HTMLElement (or throws)
|
|
722
|
+
```
|
|
723
|
+
```ts
|
|
724
|
+
// alias
|
|
725
|
+
dom.require(".demo")
|
|
726
|
+
// HTMLElement (or throws)
|
|
727
|
+
```
|
|
728
|
+
- `maybe` get an element
|
|
729
|
+
```ts
|
|
730
|
+
dom.maybe(".demo")
|
|
731
|
+
// HTMLElement | undefined
|
|
732
|
+
```
|
|
733
|
+
- `all` matching elements in an array
|
|
734
|
+
```ts
|
|
735
|
+
dom.all(".demo ul li")
|
|
736
|
+
// HTMLElement[]
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### 🪄 dom.in scope
|
|
740
|
+
- make a scope
|
|
741
|
+
```ts
|
|
742
|
+
dom.in(".demo") // selector
|
|
743
|
+
// Dom instance
|
|
744
|
+
```
|
|
745
|
+
```ts
|
|
746
|
+
dom.in(demoElement) // element
|
|
747
|
+
// Dom instance
|
|
748
|
+
```
|
|
749
|
+
- run queries in that scope
|
|
750
|
+
```ts
|
|
751
|
+
dom.in(demoElement).require(".button")
|
|
752
|
+
```
|
|
753
|
+
```ts
|
|
754
|
+
dom.in(demoElement).maybe(".button")
|
|
755
|
+
```
|
|
756
|
+
```ts
|
|
757
|
+
dom.in(demoElement).all("ol li")
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### 🪄 dom utilities
|
|
761
|
+
- `dom.register` web components
|
|
762
|
+
```ts
|
|
763
|
+
dom.register({MyComponent, AnotherCoolComponent})
|
|
764
|
+
// <my-component>
|
|
765
|
+
// <another-cool-component>
|
|
766
|
+
```
|
|
767
|
+
- `dom.register` automatically dashes the tag names (`MyComponent` becomes `<my-component>`)
|
|
768
|
+
- `dom.render` content into an element
|
|
769
|
+
```ts
|
|
770
|
+
dom.render(element, html`<p>hello world</p>`)
|
|
771
|
+
```
|
|
772
|
+
```ts
|
|
773
|
+
dom.in(".demo").render(html`<p>hello world</p>`)
|
|
774
|
+
```
|
|
775
|
+
- `dom.el` little element builder
|
|
776
|
+
```ts
|
|
777
|
+
const div = dom.el("div", {"data-whatever": 123, "data-active": true})
|
|
778
|
+
// <div data-whatever="123" data-active></div>
|
|
779
|
+
```
|
|
780
|
+
- `dom.elmer` make an element with a fluent chain
|
|
781
|
+
```ts
|
|
782
|
+
const div = dom.elmer("div")
|
|
783
|
+
.attr("data-whatever", 123)
|
|
784
|
+
.attr("data-active")
|
|
785
|
+
.children("hello world")
|
|
786
|
+
.done()
|
|
787
|
+
// HTMLElement
|
|
788
|
+
```
|
|
789
|
+
- `dom.mk` make an element with a lit template (returns the first)
|
|
790
|
+
```ts
|
|
791
|
+
const div = dom.mk(html`
|
|
792
|
+
<div data-whatever="123" data-active>
|
|
793
|
+
hello world
|
|
794
|
+
</div>
|
|
795
|
+
`) // HTMLElement
|
|
796
|
+
```
|
|
797
|
+
- `dom.events` <a id="dom.events"></a> to attach event listeners
|
|
798
|
+
```ts
|
|
799
|
+
const detach = dom.events(element, {
|
|
800
|
+
keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
|
|
801
|
+
keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
|
|
802
|
+
})
|
|
803
|
+
```
|
|
804
|
+
```ts
|
|
805
|
+
const detach = dom.in(".demo").events({
|
|
806
|
+
keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
|
|
807
|
+
keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
|
|
808
|
+
})
|
|
809
|
+
```
|
|
810
|
+
```ts
|
|
811
|
+
// unattach those event listeners when you're done
|
|
812
|
+
detach()
|
|
813
|
+
```
|
|
814
|
+
- `dom.attrs` <a id="dom.attrs"></a> to setup a type-happy html attribute helper
|
|
815
|
+
```ts
|
|
816
|
+
const attrs = dom.attrs(element).spec({
|
|
817
|
+
name: String,
|
|
818
|
+
count: Number,
|
|
819
|
+
active: Boolean,
|
|
820
|
+
})
|
|
821
|
+
```
|
|
822
|
+
```ts
|
|
823
|
+
const attrs = dom.in(".demo").attrs.spec({
|
|
824
|
+
name: String,
|
|
825
|
+
count: Number,
|
|
826
|
+
active: Boolean,
|
|
827
|
+
})
|
|
828
|
+
```
|
|
829
|
+
```ts
|
|
830
|
+
attrs.name // "chase"
|
|
831
|
+
attrs.count // 123
|
|
832
|
+
attrs.active // true
|
|
833
|
+
```
|
|
834
|
+
```ts
|
|
835
|
+
attrs.name = "zenky"
|
|
836
|
+
attrs.count = 124
|
|
837
|
+
attrs.active = false // removes html attr
|
|
838
|
+
```
|
|
839
|
+
```ts
|
|
840
|
+
attrs.name = undefined // removes the attr
|
|
841
|
+
attrs.count = undefined // removes the attr
|
|
842
|
+
```
|
|
843
|
+
or if you wanna be more loosey-goosey, skip the spec
|
|
844
|
+
```ts
|
|
845
|
+
const a = dom.in(".demo").attrs
|
|
846
|
+
a.strings.name = "pimsley"
|
|
847
|
+
a.numbers.count = 125
|
|
848
|
+
a.booleans.active = true
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
<br/><br/>
|
|
854
|
+
<a id="ops"></a>
|
|
855
|
+
|
|
856
|
+
## 🫛🦝 sly ops
|
|
857
|
+
> `@e280/sly/ops`
|
|
858
|
+
> *tools for async operations and loading spinners*
|
|
859
|
+
|
|
860
|
+
```ts
|
|
861
|
+
import {nap} from "@e280/stz"
|
|
862
|
+
import {Pod, podium, Op, loaders} from "@e280/sly"
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
### 🫛 pods: loading/ready/error
|
|
866
|
+
- a pod represents an async operation in terms of json-serializable data
|
|
867
|
+
- there are three kinds of `Pod<V>`
|
|
868
|
+
```ts
|
|
869
|
+
// loading pod
|
|
870
|
+
["loading"]
|
|
871
|
+
|
|
872
|
+
// ready pod contains value 123
|
|
873
|
+
["ready", 123]
|
|
874
|
+
|
|
875
|
+
// error pod contains an error
|
|
876
|
+
["error", new Error()]
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### 🫛 podium: helps you work with pods
|
|
880
|
+
- get pod status
|
|
881
|
+
```ts
|
|
882
|
+
podium.status(["ready", 123])
|
|
883
|
+
// "ready"
|
|
884
|
+
```
|
|
885
|
+
- get pod ready value (or undefined)
|
|
886
|
+
```ts
|
|
887
|
+
podium.value(["loading"])
|
|
888
|
+
// undefined
|
|
889
|
+
|
|
890
|
+
podium.value(["ready", 123])
|
|
891
|
+
// 123
|
|
892
|
+
```
|
|
893
|
+
- see more at [podium.ts](./s/ops/podium.ts)
|
|
894
|
+
|
|
895
|
+
### 🫛 ops: nice pod ergonomics
|
|
896
|
+
- an `Op<V>` wraps a pod with a signal for reactivity
|
|
897
|
+
- create an op
|
|
898
|
+
```ts
|
|
899
|
+
const op = new Op<number>() // loading status by default
|
|
900
|
+
```
|
|
901
|
+
```ts
|
|
902
|
+
const op = Op.loading<number>()
|
|
903
|
+
```
|
|
904
|
+
```ts
|
|
905
|
+
const op = Op.ready<number>(123)
|
|
906
|
+
```
|
|
907
|
+
```ts
|
|
908
|
+
const op = Op.error<number>(new Error())
|
|
909
|
+
```
|
|
910
|
+
- 🔥 create an op that calls and tracks an async fn
|
|
911
|
+
```ts
|
|
912
|
+
const op = Op.load(async() => {
|
|
913
|
+
await nap(4000)
|
|
914
|
+
return 123
|
|
915
|
+
})
|
|
916
|
+
```
|
|
917
|
+
- await for the next ready value (or thrown error)
|
|
918
|
+
```ts
|
|
919
|
+
await op // 123
|
|
920
|
+
```
|
|
921
|
+
- get pod info
|
|
922
|
+
```ts
|
|
923
|
+
op.pod // ["loading"]
|
|
924
|
+
op.status // "loading"
|
|
925
|
+
op.value // undefined (or value if ready)
|
|
926
|
+
```
|
|
927
|
+
```ts
|
|
928
|
+
op.isLoading // true
|
|
929
|
+
op.isReady // false
|
|
930
|
+
op.isError // false
|
|
931
|
+
```
|
|
932
|
+
- select executes a fn based on the status
|
|
933
|
+
```ts
|
|
934
|
+
const result = op.select({
|
|
935
|
+
loading: () => "it's loading...",
|
|
936
|
+
ready: value => `dude, it's ready! ${value}`,
|
|
937
|
+
error: err => `dude, there's an error!`,
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
result
|
|
941
|
+
// "dude, it's ready! 123"
|
|
942
|
+
```
|
|
943
|
+
- morph returns a new pod, transforming the value if ready
|
|
944
|
+
```ts
|
|
945
|
+
op.morph(n => n + 1)
|
|
946
|
+
// ["ready", 124]
|
|
947
|
+
```
|
|
948
|
+
- you can combine a number of ops into a single pod like this
|
|
949
|
+
```ts
|
|
950
|
+
Op.all(Op.ready(123), Op.loading())
|
|
951
|
+
// ["loading"]
|
|
952
|
+
```
|
|
953
|
+
```ts
|
|
954
|
+
Op.all(Op.ready(1), Op.ready(2), Op.ready(3))
|
|
955
|
+
// ["ready", [1, 2, 3]]
|
|
956
|
+
```
|
|
957
|
+
- error if any ops are in error, otherwise
|
|
958
|
+
- loading if any ops are in loading, otherwise
|
|
959
|
+
- ready if all the ops are ready
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
<br/><br/>
|
|
964
|
+
<a id="loaders"></a>
|
|
965
|
+
|
|
966
|
+
## ⏳🦝 sly loaders
|
|
967
|
+
> `@e280/sly/loaders`
|
|
968
|
+
> *animated loading spinners for ops*
|
|
969
|
+
|
|
970
|
+
```ts
|
|
971
|
+
import {loaders} from "@e280/sly"
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
### ⏳ make a loader, choose an anim
|
|
975
|
+
- create a loader fn
|
|
976
|
+
```ts
|
|
977
|
+
const loader = loaders.make(loaders.anims.dots)
|
|
978
|
+
```
|
|
979
|
+
- see all the anims available on the testing page https://sly.e280.org/
|
|
980
|
+
- ngl, i made too many.. *i was having fun, okay?*
|
|
981
|
+
|
|
982
|
+
### ⏳ render an op with it
|
|
983
|
+
- use your loader to render an op
|
|
984
|
+
```ts
|
|
985
|
+
return html`
|
|
986
|
+
<h2>cool stuff</h2>
|
|
987
|
+
|
|
988
|
+
${loader(op, value => html`
|
|
989
|
+
<div>${value}</div>
|
|
990
|
+
`)}
|
|
991
|
+
`
|
|
992
|
+
```
|
|
993
|
+
- when the op is loading, the loading spinner will animate
|
|
994
|
+
- when the op is in error, the error will be displayed
|
|
995
|
+
- when the op is ready, your fn is called and given the value
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
<br/><br/>
|
|
1000
|
+
<a id="spa"></a>
|
|
1001
|
+
|
|
1002
|
+
## 💅🦝 sly spa
|
|
1003
|
+
> `@e280/sly/spa`
|
|
1004
|
+
> *hash router for single-page-apps*
|
|
1005
|
+
|
|
1006
|
+
```ts
|
|
1007
|
+
import {spa, html} from "@e280/sly"
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
### 💅 spa.Router basics
|
|
1011
|
+
- **make a spa router**
|
|
1012
|
+
```ts
|
|
1013
|
+
const router = new spa.Router({
|
|
1014
|
+
routes: {
|
|
1015
|
+
home: spa.route("#/", async() => html`home`),
|
|
1016
|
+
settings: spa.route("#/settings", async() => html`settings`),
|
|
1017
|
+
user: spa.route("#/user/{userId}", async({userId}) => html`user ${userId}`),
|
|
1018
|
+
},
|
|
1019
|
+
})
|
|
1020
|
+
```
|
|
1021
|
+
- all route strings must start with `#/`
|
|
1022
|
+
- use braces like `{userId}` to accept string params
|
|
1023
|
+
- home-equivalent hashes like `""` and `"#"` are normalized to `"#/"`
|
|
1024
|
+
- the router has an effect on the appearance of the url in the browser address bar -- the home `#/` is removed, aesthetically, eg, `e280.org/#/` is rewritten to `e280.org` using *history.replaceState*
|
|
1025
|
+
- you can provide `loader` option if you want to specify the loading spinner (defaults to `loaders.make()`)
|
|
1026
|
+
- you can provide `notFound` option, if you want to specify what is shown on invalid routes (defaults to `() => null`)
|
|
1027
|
+
- when `auto` is true (default), the router calls `.refresh()` and `.listen()` in the constructor.. set it to `false` if you want manual control
|
|
1028
|
+
- you can set `auto` option false if you want to omit the default initial refresh and listen calls
|
|
1029
|
+
- **render your current page**
|
|
1030
|
+
```ts
|
|
1031
|
+
return html`
|
|
1032
|
+
<div class="my-page">
|
|
1033
|
+
${router.render()}
|
|
1034
|
+
</div>
|
|
1035
|
+
`
|
|
1036
|
+
```
|
|
1037
|
+
- returns lit content
|
|
1038
|
+
- shows a loading spinner when pages are loading
|
|
1039
|
+
- will display the notFound content for invalid routes (defaults to null)
|
|
1040
|
+
- **perform navigations**
|
|
1041
|
+
- go to settings page
|
|
1042
|
+
```ts
|
|
1043
|
+
await router.nav.settings.go()
|
|
1044
|
+
// goes to "#/settings"
|
|
1045
|
+
```
|
|
1046
|
+
- go to user page
|
|
1047
|
+
```ts
|
|
1048
|
+
await router.nav.user.go("123")
|
|
1049
|
+
// goes to "#/user/123"
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
### 💅 spa.Router advanced
|
|
1053
|
+
- **generate a route's hash string**
|
|
1054
|
+
```ts
|
|
1055
|
+
const hash = router.nav.user.hash("123")
|
|
1056
|
+
// "#/user/123"
|
|
1057
|
+
|
|
1058
|
+
html`<a href="${hash}">user 123</a>`
|
|
1059
|
+
```
|
|
1060
|
+
- **check if a route is the currently-active one**
|
|
1061
|
+
```ts
|
|
1062
|
+
const hash = router.nav.user.active
|
|
1063
|
+
// true
|
|
1064
|
+
```
|
|
1065
|
+
- **force-refresh the router**
|
|
1066
|
+
```ts
|
|
1067
|
+
await router.refresh()
|
|
1068
|
+
```
|
|
1069
|
+
- **force-navigate the router by hash**
|
|
1070
|
+
```ts
|
|
1071
|
+
await router.refresh("#/user/123")
|
|
1072
|
+
```
|
|
1073
|
+
- **get the current hash string (normalized)**
|
|
1074
|
+
```ts
|
|
1075
|
+
router.hash
|
|
1076
|
+
// "#/user/123"
|
|
1077
|
+
```
|
|
1078
|
+
- **the `route(...)` helper fn enables the braces-params syntax**
|
|
1079
|
+
- but, if you wanna do it differently, you *can* implement your own hash parser to do your own funky syntax
|
|
1080
|
+
- **dispose the router when you're done with it**
|
|
1081
|
+
```ts
|
|
1082
|
+
router.dispose()
|
|
1083
|
+
// stop listening to hashchange events
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
<br/><br/>
|
|
1089
|
+
<a id="loot"></a>
|
|
1090
|
+
|
|
1091
|
+
## 🪙🦝 loot
|
|
1092
|
+
> `@e280/sly/loot`
|
|
1093
|
+
> *drag-and-drop facilities*
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
import {loot, view, dom} from "@e280/sly"
|
|
1097
|
+
import {ev} from "@e280/stz"
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
### 🪙 `loot.Drops`
|
|
1101
|
+
> *accept the user dropping stuff like files onto the page*
|
|
1102
|
+
- **setup drops**
|
|
1103
|
+
```ts
|
|
1104
|
+
const drops = new loot.Drops({
|
|
1105
|
+
predicate: loot.hasFiles,
|
|
1106
|
+
acceptDrop: event => {
|
|
1107
|
+
const files = loot.files(event)
|
|
1108
|
+
console.log("files dropped", files)
|
|
1109
|
+
},
|
|
1110
|
+
})
|
|
1111
|
+
```
|
|
1112
|
+
- **attach event listeners to your dropzone,** one of these ways:
|
|
1113
|
+
- **view example**
|
|
1114
|
+
```ts
|
|
1115
|
+
view(() => () => html`
|
|
1116
|
+
<div
|
|
1117
|
+
?data-indicator="${drops.$indicator()}"
|
|
1118
|
+
@dragover="${drops.dragover}"
|
|
1119
|
+
@dragleave="${drops.dragleave}"
|
|
1120
|
+
@drop="${drops.drop}">
|
|
1121
|
+
my dropzone
|
|
1122
|
+
</div>
|
|
1123
|
+
`)
|
|
1124
|
+
```
|
|
1125
|
+
- **vanilla-js whole-page example**
|
|
1126
|
+
```ts
|
|
1127
|
+
// attach listeners to the body
|
|
1128
|
+
ev(document.body, {
|
|
1129
|
+
dragover: drops.dragover,
|
|
1130
|
+
dragleave: drops.dragleave,
|
|
1131
|
+
drop: drops.drop,
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
// sly attribute handler for the body
|
|
1135
|
+
const attrs = dom.attrs(document.body).spec({
|
|
1136
|
+
"data-indicator": Boolean,
|
|
1137
|
+
})
|
|
1138
|
+
|
|
1139
|
+
// sync the data-indicator attribute
|
|
1140
|
+
drops.$indicator.on(bool => attrs["data-indicator"] = bool)
|
|
1141
|
+
```
|
|
1142
|
+
- **flashy css indicator for the dropzone,** so the user knows your app is eager to accept the drop
|
|
1143
|
+
```css
|
|
1144
|
+
[data-indicator] {
|
|
1145
|
+
border: 0.5em dashed cyan;
|
|
1146
|
+
}
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### 🪙 `loot.DragAndDrops`
|
|
1150
|
+
> *setup drag-and-drops between items within your page*
|
|
1151
|
+
- **declare types for your draggy and droppy things**
|
|
1152
|
+
```ts
|
|
1153
|
+
// money that can be picked up and dragged
|
|
1154
|
+
type Money = {value: number}
|
|
1155
|
+
// dnd will call this a "draggy"
|
|
1156
|
+
|
|
1157
|
+
// bag that money can be dropped into
|
|
1158
|
+
type Bag = {id: number}
|
|
1159
|
+
// dnd will call this a "droppy"
|
|
1160
|
+
```
|
|
1161
|
+
- **make your dnd**
|
|
1162
|
+
```ts
|
|
1163
|
+
const dnd = new loot.DragAndDrops<Money, Bag>({
|
|
1164
|
+
acceptDrop: (event, money, bag) => {
|
|
1165
|
+
console.log("drop!", {money, bag})
|
|
1166
|
+
},
|
|
1167
|
+
})
|
|
1168
|
+
```
|
|
1169
|
+
- **attach dragzone listeners** (there can be many dragzones...)
|
|
1170
|
+
```ts
|
|
1171
|
+
view(use => () => {
|
|
1172
|
+
const money = use.once((): Money => ({value: 280}))
|
|
1173
|
+
const dragzone = use.once(() => dnd.dragzone(() => money))
|
|
1174
|
+
|
|
1175
|
+
return html`
|
|
1176
|
+
<div
|
|
1177
|
+
draggable="${dragzone.draggable}"
|
|
1178
|
+
@dragstart="${dragzone.dragstart}"
|
|
1179
|
+
@dragend="${dragzone.dragend}">
|
|
1180
|
+
money ${money.value}
|
|
1181
|
+
</div>
|
|
1182
|
+
`
|
|
1183
|
+
})
|
|
1184
|
+
```
|
|
1185
|
+
- **attach dropzone listeners** (there can be many dropzones...)
|
|
1186
|
+
```ts
|
|
1187
|
+
view(use => () => {
|
|
1188
|
+
const bag = use.once((): Bag => ({id: 1}))
|
|
1189
|
+
const dropzone = use.once(() => dnd.dropzone(() => bag))
|
|
1190
|
+
const indicator = !!(dnd.dragging && dnd.hovering === bag)
|
|
1191
|
+
|
|
1192
|
+
return html`
|
|
1193
|
+
<div
|
|
1194
|
+
?data-indicator="${indicator}"
|
|
1195
|
+
@dragenter="${dropzone.dragenter}"
|
|
1196
|
+
@dragleave="${dropzone.dragleave}"
|
|
1197
|
+
@dragover="${dropzone.dragover}"
|
|
1198
|
+
@drop="${dropzone.drop}">
|
|
1199
|
+
bag ${bag.id}
|
|
1200
|
+
</div>
|
|
1201
|
+
`
|
|
1202
|
+
})
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
### 🪙 loot helpers
|
|
1206
|
+
- **`loot.hasFiles(event)`** — return true if `DragEvent` contains any files (useful in `predicate`)
|
|
1207
|
+
- **`loot.files(event)`** — returns an array of files in a drop's `DragEvent` (useful in `acceptDrop`)
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
<br/><br/>
|
|
1212
|
+
<a id="e280"></a>
|
|
1213
|
+
|
|
1214
|
+
## 🧑💻🦝 sly is by e280
|
|
1215
|
+
reward us with github stars
|
|
1216
|
+
build with us at https://e280.org/ but only if you're cool
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
<br/><br/>
|
|
1221
|
+
|