Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Reactivity utilities
  2. sourceFromEvent

sourceFromEvent#

Creates a source$ from DOM events or event emitters.

Use it when the event's origin is an emitter or an element you already hold, rather than a target resolved lazily — for that, see fromEventToSource$.

Import#

typescript
import { sourceFromEvent } from '@craft-ts/core';

Basic Usage#

typescript
import { state, sourceFromEvent } from '@craft-ts/core';

const button = document.querySelector('button')!;

// Create source from button clicks
const clickSource = sourceFromEvent(
  button,
  'click',
  () => (count: number) => count + 1,
);

const { clickCount } = state('clickCount', 0, {
  sources: [clickSource],
});

API#

typescript
function sourceFromEvent<T, E extends Event>(
  target: EventTarget,
  eventName: string,
  mapper: (event: E) => (state: T) => T,
): Observable<(state: T) => T>;

Examples#

Inside a Craft component#

sourceFromEvent takes an EventTarget you already hold. Inside a component that usually means a document- or window-level target, since the component's own elements are better handled with a plain event prop:

typescript
import { craftComponent, p } from '@craft-ts/component';
import { sourceFromEvent, state } from '@craft-ts/core';

export const KeyCounter = craftComponent('KeyCounter', {}, function* () {
  const keySource = sourceFromEvent(
    document,
    'keydown',
    () => (count: number) => count + 1,
  );

  const keys = yield* state('keys', 0, { sources: [keySource] });

  return p(function* () {
    return `Keys pressed: ${yield* keys()}`;
  });
});

For the component's own elements, use an event prop

button({ click: counter.increment }, 'Click me') is simpler and needs no target. Reach for sourceFromEvent when the event comes from outside the component's own markup, or when several states must react to the same event.

Input Changes#

typescript
const input = document.querySelector('input')!;

const inputSource = sourceFromEvent(
  input,
  'input',
  (event: Event) => () => (event.target as HTMLInputElement).value,
);

const { inputValue } = state('inputValue', '', {
  sources: [inputSource],
});

Mouse Position#

typescript
interface Position {
  x: number;
  y: number;
}

const mouseMoveSource = sourceFromEvent(
  document,
  'mousemove',
  (event: MouseEvent) => () => ({
    x: event.clientX,
    y: event.clientY,
  }),
);

const { mousePosition } = state<Position>(
  'mousePosition',
  { x: 0, y: 0 },
  {
    sources: [mouseMoveSource],
  },
);

Keyboard Input#

typescript
const keySource = sourceFromEvent(
  document,
  'keydown',
  (event: KeyboardEvent) => (keys: string[]) => [...keys, event.key],
);

const { pressedKeys } = state<string[]>('pressedKeys', [], {
  sources: [keySource],
});

Scroll Position#

typescript
const scrollSource = sourceFromEvent(
  window,
  'scroll',
  () => () => window.scrollY,
);

const { scrollPosition } = state('scrollPosition', 0, {
  sources: [scrollSource],
});

Form Submit#

typescript
const form = document.querySelector('form')!;

interface FormData {
  name: string;
  email: string;
}

const submitSource = sourceFromEvent(form, 'submit', (event: Event) => {
  event.preventDefault();
  const form = event.target as HTMLFormElement;
  const formData = new FormData(form);
  return () => ({
    name: formData.get('name') as string,
    email: formData.get('email') as string,
  });
});

const { formState } = state<FormData>(
  'formState',
  { name: '', email: '' },
  {
    sources: [submitSource],
  },
);

Window Resize#

typescript
interface WindowSize {
  width: number;
  height: number;
}

const resizeSource = sourceFromEvent(window, 'resize', () => () => ({
  width: window.innerWidth,
  height: window.innerHeight,
}));

const { windowSize } = state<WindowSize>(
  'windowSize',
  { width: window.innerWidth, height: window.innerHeight },
  {
    sources: [resizeSource],
  },
);

With Operators#

craft-ts does not depend on RxJS; this example uses it as your own optional dependency.

typescript
import { debounceTime, map } from 'rxjs/operators';

const input = document.querySelector('input')!;

// Debounced input with RxJS operators
const debouncedInputSource = sourceFromEvent(
  input,
  'input',
  (event: Event) => () => (event.target as HTMLInputElement).value,
).pipe(debounceTime(300));

const { searchQuery } = state('searchQuery', '', {
  sources: [debouncedInputSource],
});

Best Practices#

✅ Cleanup automatically handled - Sources unsubscribe when state is destroyed ✅ Use with throttle/debounce - For high-frequency events ✅ Extract to reusable functions - Create helper functions for common patterns ✅ Type your events - Use specific event types (MouseEvent, KeyboardEvent)

See Also#

  • source$ - Create reactive sources

←fromEventToSource$afterRecomputation→

On this page

  • Import
  • Basic Usage
  • API
  • Examples
  • Inside a Craft component
  • Input Changes
  • Mouse Position
  • Keyboard Input
  • Scroll Position
  • Form Submit
  • Window Resize
  • With Operators
  • Best Practices
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search