Files
dotfiles/dot_agents/skills/remix/references/create-mixins.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

154 lines
4.2 KiB
Markdown

# Creating Mixins
## What This Covers
How to author your own reusable host-element behavior with `createMixin`. Read this when the task involves:
- Combining multiple low-level events or DOM hooks into one semantic mixin
- Dispatching custom DOM events from a host node
- Encapsulating imperative DOM setup that several components share
- Typing custom events on `HTMLElementEventMap` for use with `on(...)`
For the built-in mixins most code should use, see `mixins-styling-events.md`.
Use `createMixin` from `remix/ui` to author reusable host-element behavior.
Most app code should use built-in core mixins (`on`, `css`, `ref`, `link`, `attrs`) and animation mixins from `remix/ui/animation`. Create custom mixins when combining multiple low-level events into one semantic event, or when the pattern is reused across components.
## Core Semantics
1. A mixin handle is tied to one mounted host node lifecycle.
2. `insert` is the host-node availability point for imperative setup.
3. `remove` is teardown for that same lifecycle.
4. `queueTask` runs post-commit and receives `(node, signal)` for mixins.
5. Mixin render functions should stay pure; side effects belong in `insert`, `remove`, or queued work.
```tsx
import { createMixin } from 'remix/ui'
let myMixin = createMixin<HTMLElement>((handle) => {
handle.addEventListener('insert', (event) => {
// event.node is the mounted host node
})
handle.addEventListener('remove', () => {
// Clean up listeners, timers, observers
})
return (props) => {
handle.queueTask((node) => {
// Post-commit work that needs the concrete host node
})
return <handle.element {...props} />
}
})
```
## Patterns
### Pure prop transform
```tsx
let withTitle = createMixin((handle) => (title: string, props: { title?: string }) => (
<handle.element {...props} title={title} />
))
```
### Lifecycle-managed imperative setup
```tsx
let withFocus = createMixin<HTMLElement>((handle) => {
handle.addEventListener('insert', (event) => {
event.node.focus()
})
return (props) => <handle.element {...props} />
})
```
## Custom Event Mixins
Create event mixins when you combine multiple low-level events into one semantic custom event that is reused across components.
1. Namespace custom event names (`myapp:*`) to avoid collisions.
2. Extend `Event` with the data consumers need.
3. Declare the event on `HTMLElementEventMap` for type safety with `on(...)`.
4. Dispatch from the host node inside the mixin.
```tsx
import { createMixin, on } from 'remix/ui'
export let dragReleaseType = 'myapp:drag-release' as const
declare global {
interface HTMLElementEventMap {
[dragReleaseType]: DragReleaseEvent
}
}
export class DragReleaseEvent extends Event {
velocityX: number
velocityY: number
constructor(init: { velocityX: number; velocityY: number }) {
super(dragReleaseType, { bubbles: true, cancelable: true })
this.velocityX = init.velocityX
this.velocityY = init.velocityY
}
}
export let dragRelease = createMixin<HTMLElement>((handle) => {
let node: HTMLElement | undefined
let tracking = false
let velocityX = 0
let velocityY = 0
let lastX = 0
let lastY = 0
let lastT = 0
handle.addEventListener('insert', (event) => {
node = event.node
})
return () => (
<handle.element
mix={[
on('pointerdown', (event) => {
if (!event.isPrimary) return
tracking = true
lastX = event.clientX
lastY = event.clientY
lastT = event.timeStamp
node?.setPointerCapture(event.pointerId)
}),
on('pointermove', (event) => {
if (!tracking) return
let dt = Math.max(1, event.timeStamp - lastT)
velocityX = (event.clientX - lastX) / dt
velocityY = (event.clientY - lastY) / dt
lastX = event.clientX
lastY = event.clientY
lastT = event.timeStamp
}),
on('pointerup', () => {
if (!tracking) return
tracking = false
node?.dispatchEvent(new DragReleaseEvent({ velocityX, velocityY }))
}),
]}
/>
)
})
```
Consume it:
```tsx
<div
mix={[
dragRelease(),
on(dragReleaseType, (event) => {
console.log('velocity:', event.velocityX, event.velocityY)
}),
]}
/>
```