@remix-run/cli 0.1.0 → 0.3.0
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/README.md +0 -3
- package/dist/lib/bootstrap-project.d.ts.map +1 -1
- package/dist/lib/bootstrap-project.js +32 -9
- package/dist/lib/cli.d.ts +29 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +33 -14
- package/dist/lib/commands/completion.d.ts.map +1 -1
- package/dist/lib/commands/completion.js +5 -1
- package/dist/lib/commands/doctor.js +18 -18
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +9 -33
- package/dist/lib/commands/routes.js +3 -3
- package/dist/lib/commands/test.d.ts +1 -1
- package/dist/lib/commands/test.d.ts.map +1 -1
- package/dist/lib/commands/test.js +8 -4
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +4 -106
- package/dist/lib/controller-files.d.ts +0 -1
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +3 -5
- package/dist/lib/controller-ownership.d.ts +9 -9
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +56 -91
- package/dist/lib/doctor/controller-findings.d.ts +1 -1
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +15 -87
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
- package/dist/lib/doctor/controller-fix-plans.js +13 -24
- package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
- package/dist/lib/doctor/controller-placeholders.js +18 -149
- package/dist/lib/doctor/controllers.js +1 -1
- package/dist/lib/doctor/project.js +60 -52
- package/dist/lib/doctor/types.d.ts +2 -2
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/errors.d.ts +0 -6
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +0 -11
- package/dist/lib/load-route-map-worker.js +17 -9
- package/dist/lib/route-map.d.ts +1 -1
- package/dist/lib/route-map.d.ts.map +1 -1
- package/dist/lib/route-map.js +29 -17
- package/package.json +4 -5
- package/src/lib/bootstrap-project.ts +39 -13
- package/src/lib/cli.ts +46 -15
- package/src/lib/commands/completion.ts +6 -1
- package/src/lib/commands/doctor.ts +18 -21
- package/src/lib/commands/help.ts +9 -43
- package/src/lib/commands/routes.ts +3 -3
- package/src/lib/commands/test.ts +10 -4
- package/src/lib/completion.ts +4 -151
- package/src/lib/controller-files.ts +4 -8
- package/src/lib/controller-ownership.ts +78 -141
- package/src/lib/doctor/controller-findings.ts +20 -97
- package/src/lib/doctor/controller-fix-plans.ts +13 -29
- package/src/lib/doctor/controller-placeholders.ts +17 -189
- package/src/lib/doctor/controllers.ts +1 -1
- package/src/lib/doctor/project.ts +60 -52
- package/src/lib/doctor/types.ts +1 -5
- package/src/lib/errors.ts +0 -12
- package/src/lib/load-route-map-worker.ts +19 -10
- package/src/lib/route-map.ts +61 -16
- package/template/.agents/skills/remix/SKILL.md +588 -0
- package/template/.agents/skills/remix/references/animate-elements.md +195 -0
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +130 -0
- package/template/.agents/skills/remix/references/auth-and-sessions.md +443 -0
- package/template/.agents/skills/remix/references/component-model.md +282 -0
- package/template/.agents/skills/remix/references/create-mixins.md +158 -0
- package/template/.agents/skills/remix/references/data-and-validation.md +379 -0
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
- package/template/.agents/skills/remix/references/middleware-and-server.md +233 -0
- package/template/.agents/skills/remix/references/mixins-styling-events.md +213 -0
- package/template/.agents/skills/remix/references/routing-and-controllers.md +391 -0
- package/template/.agents/skills/remix/references/testing-patterns.md +172 -0
- package/{bootstrap → template}/AGENTS.md +13 -8
- package/template/README.md +29 -0
- package/template/app/actions/controller.tsx +18 -0
- package/template/app/assets/entry.ts +8 -0
- package/template/app/assets/prompt-button.tsx +163 -0
- package/template/app/assets.ts +20 -0
- package/template/app/middleware/render.tsx +43 -0
- package/template/app/router.ts +20 -0
- package/template/app/routes.ts +6 -0
- package/template/app/ui/document.tsx +33 -0
- package/template/app/ui/scaffold-home-page.tsx +527 -0
- package/template/gitignore +4 -0
- package/{bootstrap → template}/package.json +5 -6
- package/template/public/favicon.svg +11 -0
- package/{bootstrap → template}/server.ts +4 -5
- package/{bootstrap → template}/tsconfig.json +3 -3
- package/bootstrap/README.md +0 -27
- package/bootstrap/app/controllers/auth.tsx +0 -21
- package/bootstrap/app/controllers/home.tsx +0 -26
- package/bootstrap/app/router.ts +0 -10
- package/bootstrap/app/routes.ts +0 -6
- package/bootstrap/app/ui/document.tsx +0 -21
- package/bootstrap/app/ui/layout.tsx +0 -22
- package/bootstrap/app/utils/render.tsx +0 -7
- package/dist/lib/commands/skills.d.ts +0 -6
- package/dist/lib/commands/skills.d.ts.map +0 -1
- package/dist/lib/commands/skills.js +0 -222
- package/dist/lib/skills-cache.d.ts +0 -19
- package/dist/lib/skills-cache.d.ts.map +0 -1
- package/dist/lib/skills-cache.js +0 -89
- package/dist/lib/skills.d.ts +0 -30
- package/dist/lib/skills.d.ts.map +0 -1
- package/dist/lib/skills.js +0 -441
- package/src/lib/commands/skills.ts +0 -306
- package/src/lib/skills-cache.ts +0 -140
- package/src/lib/skills.ts +0 -706
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Component Model
|
|
2
|
+
|
|
3
|
+
## What This Covers
|
|
4
|
+
|
|
5
|
+
How a Remix Component is shaped and how its state, lifecycle, and updates behave. Read this when
|
|
6
|
+
the task involves:
|
|
7
|
+
|
|
8
|
+
- Writing a component (`handle` plus render function)
|
|
9
|
+
- Managing component-local state, derived values, or post-render DOM work
|
|
10
|
+
- Using `handle.props`, `handle.update()`, `handle.queueTask()`, `handle.signal`, `handle.id`, or
|
|
11
|
+
`handle.context`
|
|
12
|
+
- Listening to global events with cleanup tied to the component lifecycle
|
|
13
|
+
|
|
14
|
+
For host-element behavior (event handlers, styles, refs, animations), see
|
|
15
|
+
`mixins-styling-events.md`. For browser hydration, frames, and navigation, see
|
|
16
|
+
`hydration-frames-navigation.md`.
|
|
17
|
+
|
|
18
|
+
## Phases
|
|
19
|
+
|
|
20
|
+
A component has two phases:
|
|
21
|
+
|
|
22
|
+
1. **Setup phase** — runs once when the component is created
|
|
23
|
+
2. **Render phase** — returned function runs on initial render and every update
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { on, type Handle } from 'remix/ui'
|
|
27
|
+
|
|
28
|
+
function Counter(handle: Handle<{ initialCount?: number; label: string }>) {
|
|
29
|
+
let count = handle.props.initialCount ?? 0
|
|
30
|
+
|
|
31
|
+
return () => (
|
|
32
|
+
<button
|
|
33
|
+
mix={on('click', () => {
|
|
34
|
+
count++
|
|
35
|
+
handle.update()
|
|
36
|
+
})}
|
|
37
|
+
>
|
|
38
|
+
{handle.props.label}: {count}
|
|
39
|
+
</button>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Props
|
|
45
|
+
|
|
46
|
+
Components receive all JSX props through `handle.props`. The object identity is stable for the
|
|
47
|
+
component lifetime, and its values are updated before each render. Put initialization inputs on
|
|
48
|
+
normal JSX props and read them from `handle.props`:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
function Timer(handle: Handle<{ initialSeconds: number; paused?: boolean }>) {
|
|
52
|
+
let seconds = handle.props.initialSeconds
|
|
53
|
+
|
|
54
|
+
return () => <div>Time remaining: {seconds}s</div>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Usage: <Timer initialSeconds={60} paused={false} />
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Because `handle.props` is stable, destructuring `let { props } = handle` is safe when helpers need
|
|
61
|
+
to read current values later. Destructuring individual prop values is only a snapshot; prefer
|
|
62
|
+
`handle.props.name` inside callbacks and render output when values can change.
|
|
63
|
+
|
|
64
|
+
## State Rules
|
|
65
|
+
|
|
66
|
+
- Keep state in setup scope as plain JavaScript variables.
|
|
67
|
+
- Store only what affects rendering. Derive computed values in render.
|
|
68
|
+
- Do not mirror input state unless you truly need controlled behavior.
|
|
69
|
+
- Do work in event handlers, not in render. Use the handler scope for transient state.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// Derive computed values in render
|
|
73
|
+
function TodoList(handle: Handle) {
|
|
74
|
+
let todos: Array<{ text: string; completed: boolean }> = []
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
let completedCount = todos.filter((t) => t.completed).length
|
|
78
|
+
return <div>Completed: {completedCount}</div>
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Handle API
|
|
84
|
+
|
|
85
|
+
### `handle.update()`
|
|
86
|
+
|
|
87
|
+
Schedules a rerender. Returns a promise that resolves with an `AbortSignal` after the update
|
|
88
|
+
completes. Await it when you need the updated DOM before follow-up work:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
on('click', async () => {
|
|
92
|
+
isPlaying = true
|
|
93
|
+
let signal = await handle.update()
|
|
94
|
+
// DOM is now updated, safe to focus or measure
|
|
95
|
+
stopButton.focus()
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `handle.queueTask(task)`
|
|
100
|
+
|
|
101
|
+
Schedules a task to run after the next update. The task receives an `AbortSignal` that aborts when
|
|
102
|
+
the component re-renders or is removed. Use for post-render DOM work, reactive data loading, or
|
|
103
|
+
hydration-sensitive setup:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
let data = null
|
|
107
|
+
let requestedUrl: string | null = null
|
|
108
|
+
|
|
109
|
+
// Post-render DOM work in an event handler
|
|
110
|
+
on('click', () => {
|
|
111
|
+
showDetails = true
|
|
112
|
+
handle.update()
|
|
113
|
+
handle.queueTask(() => {
|
|
114
|
+
detailsSection.scrollIntoView({ behavior: 'smooth' })
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Reactive data loading keyed by props.url
|
|
119
|
+
return () => {
|
|
120
|
+
if (requestedUrl !== handle.props.url) {
|
|
121
|
+
let nextUrl = handle.props.url
|
|
122
|
+
requestedUrl = nextUrl
|
|
123
|
+
data = null
|
|
124
|
+
|
|
125
|
+
handle.queueTask(async (signal) => {
|
|
126
|
+
let response = await fetch(nextUrl, { signal })
|
|
127
|
+
let json = await response.json()
|
|
128
|
+
if (signal.aborted || requestedUrl !== nextUrl) return
|
|
129
|
+
data = json
|
|
130
|
+
handle.update()
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return <div>{data ?? 'Loading...'}</div>
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Avoid creating intermediate state just to trigger `queueTask`. Do the work directly in the handler
|
|
139
|
+
or the queued task.
|
|
140
|
+
|
|
141
|
+
### `handle.signal`
|
|
142
|
+
|
|
143
|
+
An `AbortSignal` aborted when the component disconnects. Use for cleanup:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
function Clock(handle: Handle) {
|
|
147
|
+
let interval = setInterval(handle.update, 1000)
|
|
148
|
+
handle.signal.addEventListener('abort', () => clearInterval(interval))
|
|
149
|
+
|
|
150
|
+
return () => <span>{new Date().toString()}</span>
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `handle.id`
|
|
155
|
+
|
|
156
|
+
Stable identifier per component instance. Useful for `htmlFor`, `aria-owns`, etc.:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
function LabeledInput(handle: Handle) {
|
|
160
|
+
return () => (
|
|
161
|
+
<div>
|
|
162
|
+
<label htmlFor={handle.id}>Name</label>
|
|
163
|
+
<input id={handle.id} type="text" />
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `handle.frame` and `handle.frames`
|
|
170
|
+
|
|
171
|
+
Frame-aware behavior for client entries rendered inside frames:
|
|
172
|
+
|
|
173
|
+
- `handle.frame.reload()` — reload the containing frame
|
|
174
|
+
- `handle.frame.src` — the URL of the containing frame
|
|
175
|
+
- `handle.frames.top` — the root frame (the whole page)
|
|
176
|
+
- `handle.frames.top.reload()` — reload the entire page/frame tree
|
|
177
|
+
- `handle.frames.get(name)` — look up a named frame; returns `FrameHandle | undefined`
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
function RefreshButton(handle: Handle) {
|
|
181
|
+
return () => <button mix={on('click', () => handle.frame.reload())}>Refresh</button>
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `handle.context`
|
|
186
|
+
|
|
187
|
+
Context for ancestor/descendant communication. See the context section below.
|
|
188
|
+
|
|
189
|
+
## Context
|
|
190
|
+
|
|
191
|
+
Use `handle.context.set()` to provide values and `handle.context.get(Provider)` to consume them.
|
|
192
|
+
`set()` does **not** trigger updates — call `handle.update()` if the tree needs to rerender.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
function ThemeProvider(handle: Handle<{ children?: RemixNode }, { theme: 'light' | 'dark' }>) {
|
|
196
|
+
let theme: 'light' | 'dark' = 'light'
|
|
197
|
+
handle.context.set({ theme })
|
|
198
|
+
|
|
199
|
+
return () => (
|
|
200
|
+
<div>
|
|
201
|
+
<button
|
|
202
|
+
mix={on('click', () => {
|
|
203
|
+
theme = theme === 'light' ? 'dark' : 'light'
|
|
204
|
+
handle.context.set({ theme })
|
|
205
|
+
handle.update()
|
|
206
|
+
})}
|
|
207
|
+
>
|
|
208
|
+
Toggle
|
|
209
|
+
</button>
|
|
210
|
+
{handle.props.children}
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function ThemedContent(handle: Handle) {
|
|
216
|
+
let { theme } = handle.context.get(ThemeProvider)
|
|
217
|
+
return () => <div>Current theme: {theme}</div>
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
For granular updates without re-rendering the full subtree, use `TypedEventTarget`:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { TypedEventTarget, addEventListeners } from 'remix/ui'
|
|
225
|
+
|
|
226
|
+
class Theme extends TypedEventTarget<{ change: Event }> {
|
|
227
|
+
#value: 'light' | 'dark' = 'light'
|
|
228
|
+
get value() {
|
|
229
|
+
return this.#value
|
|
230
|
+
}
|
|
231
|
+
setValue(value: 'light' | 'dark') {
|
|
232
|
+
this.#value = value
|
|
233
|
+
this.dispatchEvent(new Event('change'))
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function ThemeProvider(handle: Handle<{ children?: RemixNode }, Theme>) {
|
|
238
|
+
let theme = new Theme()
|
|
239
|
+
handle.context.set(theme)
|
|
240
|
+
|
|
241
|
+
return () => (
|
|
242
|
+
<div>
|
|
243
|
+
<button mix={on('click', () => theme.setValue(theme.value === 'light' ? 'dark' : 'light'))}>
|
|
244
|
+
Toggle
|
|
245
|
+
</button>
|
|
246
|
+
{handle.props.children}
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function ThemedContent(handle: Handle) {
|
|
252
|
+
let theme = handle.context.get(ThemeProvider)
|
|
253
|
+
addEventListeners(theme, handle.signal, {
|
|
254
|
+
change() {
|
|
255
|
+
handle.update()
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
return () => <div>Theme: {theme.value}</div>
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Global Events
|
|
263
|
+
|
|
264
|
+
Use `addEventListeners(target, handle.signal, listeners)` to listen to global targets with
|
|
265
|
+
automatic cleanup when the component disconnects:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
import { addEventListeners, type Handle } from 'remix/ui'
|
|
269
|
+
|
|
270
|
+
function ResizeTracker(handle: Handle) {
|
|
271
|
+
let width = window.innerWidth
|
|
272
|
+
|
|
273
|
+
addEventListeners(window, handle.signal, {
|
|
274
|
+
resize() {
|
|
275
|
+
width = window.innerWidth
|
|
276
|
+
handle.update()
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
return () => <div>{width}</div>
|
|
281
|
+
}
|
|
282
|
+
```
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Creating Mixins
|
|
2
|
+
|
|
3
|
+
## What This Covers
|
|
4
|
+
|
|
5
|
+
How to author your own reusable host-element behavior with `createMixin`. Read this when the task
|
|
6
|
+
involves:
|
|
7
|
+
|
|
8
|
+
- Combining multiple low-level events or DOM hooks into one semantic mixin
|
|
9
|
+
- Dispatching custom DOM events from a host node
|
|
10
|
+
- Encapsulating imperative DOM setup that several components share
|
|
11
|
+
- Typing custom events on `HTMLElementEventMap` for use with `on(...)`
|
|
12
|
+
|
|
13
|
+
For the built-in mixins most code should use, see `mixins-styling-events.md`.
|
|
14
|
+
|
|
15
|
+
Use `createMixin` from `remix/ui` to author reusable host-element behavior.
|
|
16
|
+
|
|
17
|
+
Most app code should use built-in core mixins (`on`, `css`, `ref`, `link`, `attrs`) and animation
|
|
18
|
+
mixins from `remix/ui/animation`. Create custom mixins when combining multiple low-level events
|
|
19
|
+
into one semantic event, or when the pattern is reused across components.
|
|
20
|
+
|
|
21
|
+
## Core Semantics
|
|
22
|
+
|
|
23
|
+
1. A mixin handle is tied to one mounted host node lifecycle.
|
|
24
|
+
2. `insert` is the host-node availability point for imperative setup.
|
|
25
|
+
3. `remove` is teardown for that same lifecycle.
|
|
26
|
+
4. `queueTask` runs post-commit and receives `(node, signal)` for mixins.
|
|
27
|
+
5. Mixin render functions should stay pure; side effects belong in `insert`, `remove`, or queued
|
|
28
|
+
work.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { createMixin } from 'remix/ui'
|
|
32
|
+
|
|
33
|
+
let myMixin = createMixin<HTMLElement>((handle) => {
|
|
34
|
+
handle.addEventListener('insert', (event) => {
|
|
35
|
+
// event.node is the mounted host node
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
handle.addEventListener('remove', () => {
|
|
39
|
+
// Clean up listeners, timers, observers
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return (props) => {
|
|
43
|
+
handle.queueTask((node) => {
|
|
44
|
+
// Post-commit work that needs the concrete host node
|
|
45
|
+
})
|
|
46
|
+
return <handle.element {...props} />
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Patterns
|
|
52
|
+
|
|
53
|
+
### Pure prop transform
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (
|
|
57
|
+
<handle.element {...props} title={title} />
|
|
58
|
+
))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Lifecycle-managed imperative setup
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
let withFocus = createMixin<HTMLElement>((handle) => {
|
|
65
|
+
handle.addEventListener('insert', (event) => {
|
|
66
|
+
event.node.focus()
|
|
67
|
+
})
|
|
68
|
+
return (props) => <handle.element {...props} />
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Custom Event Mixins
|
|
73
|
+
|
|
74
|
+
Create event mixins when you combine multiple low-level events into one semantic custom event that
|
|
75
|
+
is reused across components.
|
|
76
|
+
|
|
77
|
+
1. Namespace custom event names (`myapp:*`) to avoid collisions.
|
|
78
|
+
2. Extend `Event` with the data consumers need.
|
|
79
|
+
3. Declare the event on `HTMLElementEventMap` for type safety with `on(...)`.
|
|
80
|
+
4. Dispatch from the host node inside the mixin.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { createMixin, on } from 'remix/ui'
|
|
84
|
+
|
|
85
|
+
export let dragReleaseType = 'myapp:drag-release' as const
|
|
86
|
+
|
|
87
|
+
declare global {
|
|
88
|
+
interface HTMLElementEventMap {
|
|
89
|
+
[dragReleaseType]: DragReleaseEvent
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class DragReleaseEvent extends Event {
|
|
94
|
+
velocityX: number
|
|
95
|
+
velocityY: number
|
|
96
|
+
constructor(init: { velocityX: number; velocityY: number }) {
|
|
97
|
+
super(dragReleaseType, { bubbles: true, cancelable: true })
|
|
98
|
+
this.velocityX = init.velocityX
|
|
99
|
+
this.velocityY = init.velocityY
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export let dragRelease = createMixin<HTMLElement>((handle) => {
|
|
104
|
+
let node: HTMLElement | undefined
|
|
105
|
+
let tracking = false
|
|
106
|
+
let velocityX = 0
|
|
107
|
+
let velocityY = 0
|
|
108
|
+
let lastX = 0
|
|
109
|
+
let lastY = 0
|
|
110
|
+
let lastT = 0
|
|
111
|
+
|
|
112
|
+
handle.addEventListener('insert', (event) => {
|
|
113
|
+
node = event.node
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return () => (
|
|
117
|
+
<handle.element
|
|
118
|
+
mix={[
|
|
119
|
+
on('pointerdown', (event) => {
|
|
120
|
+
if (!event.isPrimary) return
|
|
121
|
+
tracking = true
|
|
122
|
+
lastX = event.clientX
|
|
123
|
+
lastY = event.clientY
|
|
124
|
+
lastT = event.timeStamp
|
|
125
|
+
node?.setPointerCapture(event.pointerId)
|
|
126
|
+
}),
|
|
127
|
+
on('pointermove', (event) => {
|
|
128
|
+
if (!tracking) return
|
|
129
|
+
let dt = Math.max(1, event.timeStamp - lastT)
|
|
130
|
+
velocityX = (event.clientX - lastX) / dt
|
|
131
|
+
velocityY = (event.clientY - lastY) / dt
|
|
132
|
+
lastX = event.clientX
|
|
133
|
+
lastY = event.clientY
|
|
134
|
+
lastT = event.timeStamp
|
|
135
|
+
}),
|
|
136
|
+
on('pointerup', () => {
|
|
137
|
+
if (!tracking) return
|
|
138
|
+
tracking = false
|
|
139
|
+
node?.dispatchEvent(new DragReleaseEvent({ velocityX, velocityY }))
|
|
140
|
+
}),
|
|
141
|
+
]}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Consume it:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<div
|
|
151
|
+
mix={[
|
|
152
|
+
dragRelease(),
|
|
153
|
+
on(dragReleaseType, (event) => {
|
|
154
|
+
console.log('velocity:', event.velocityX, event.velocityY)
|
|
155
|
+
}),
|
|
156
|
+
]}
|
|
157
|
+
/>
|
|
158
|
+
```
|