From aa39f829c67898c649643a295b45b803797daa77 Mon Sep 17 00:00:00 2001 From: themaker Date: Thu, 2 Apr 2026 14:04:48 +0100 Subject: [PATCH] feat(.changeset/*.md) context support in vanjs package added to changeset --- .changeset/eager-lies-behave.md | 43 ++++++++++ .changeset/funky-humans-open.md | 5 ++ packages/vanjs/src/utils/context.ts | 35 ++++++++- packages/vanjs/src/van.ts | 117 ++++++++++++++-------------- 4 files changed, 140 insertions(+), 60 deletions(-) create mode 100644 .changeset/eager-lies-behave.md create mode 100644 .changeset/funky-humans-open.md diff --git a/.changeset/eager-lies-behave.md b/.changeset/eager-lies-behave.md new file mode 100644 index 0000000..aa02563 --- /dev/null +++ b/.changeset/eager-lies-behave.md @@ -0,0 +1,43 @@ +--- +"@michthemaker/vanjs": minor +--- + +Add Context API for logical state sharing across component trees + +Introduces `createContext` and `useContext` functions that enable sharing reactive state without prop drilling. Context uses logical scoping (not DOM-based) and integrates seamlessly with VanJS's reactive state system. + +**New exports:** + +- `createContext()`: Creates a new context object with a Provider method +- `useContext(context)`: Retrieves the current context value (must be called within a Provider) + +**Example usage:** + +```typescript +import van, { createContext, useContext } from "@michthemaker/vanjs"; + +const { div, button } = van.tags; +const ThemeContext = createContext<{ color: string }>(); + +const theme = van.state({ color: "blue" }); + +ThemeContext.Provider(theme, () => { + const currentTheme = useContext(ThemeContext); + return div( + () => `Theme: ${currentTheme.val.color}`, + button( + { + onclick: () => (theme.val = { color: "red" }), + }, + "Change Theme" + ) + ); +}); +``` + +**Features:** + +- Shallow reactivity: context values must be VanJS state objects +- Supports nested providers of the same context +- Type-safe with TypeScript generics +- Throws helpful errors when used incorrectly diff --git a/.changeset/funky-humans-open.md b/.changeset/funky-humans-open.md new file mode 100644 index 0000000..bbf69e9 --- /dev/null +++ b/.changeset/funky-humans-open.md @@ -0,0 +1,5 @@ +--- +"@michthemaker/vanjs": patch +--- + +Add JSDoc documentation for Context API diff --git a/packages/vanjs/src/utils/context.ts b/packages/vanjs/src/utils/context.ts index 2b6fc2f..83bdcdb 100644 --- a/packages/vanjs/src/utils/context.ts +++ b/packages/vanjs/src/utils/context.ts @@ -2,6 +2,11 @@ import type { ChildDom, State, StateView } from "../van.ts"; const IS_CONTEXT_OBJECT = Symbol("vanjs_is_context_object"); +/** + * A context object for sharing reactive state across the component tree. + * + * @template T The type of the context's value. + */ export type Context = { Provider: ( stateValue: State, @@ -11,12 +16,28 @@ export type Context = { const contextStacks = new Map, StateView[]>(); +/** + * Creates a context object for sharing state without prop drilling. + * + * @template T The type of value this context will hold + * + * @example + * + * ```ts + * const ThemeContext = createContext<{ color: string }>(); + * const theme = van.state({ color: "blue" }); + * + * ThemeContext.Provider(theme, () => { + * const currentTheme = useContext(ThemeContext); + * return div(() => `Color: ${currentTheme.val.color}`); + * }); + * ``` + */ export let createContext = (): Context => { return { // @ts-ignore [IS_CONTEXT_OBJECT]: true, Provider: function provider(stateValue, childrenFn) { - // 1. Get or create stack for this context if (!contextStacks.has(this)) { contextStacks.set(this, []); } @@ -29,6 +50,18 @@ export let createContext = (): Context => { }; }; +/** + * Returns the current context value from the nearest Provider. + * + * @template T The type of the context's value + * + * @example + * + * ```ts + * const theme = useContext(ThemeContext); + * div(() => `Color: ${theme.val.color}`); + * ``` + */ export let useContext = (context: Context): State => { // @ts-ignore if (!context[IS_CONTEXT_OBJECT]) throw new Error("Object is not a `Context`"); diff --git a/packages/vanjs/src/van.ts b/packages/vanjs/src/van.ts index 9e187d7..fe7bf9b 100644 --- a/packages/vanjs/src/van.ts +++ b/packages/vanjs/src/van.ts @@ -1,47 +1,5 @@ import type { ElementEventHandlers } from "./event-handlers.ts"; -/* -Examples of React jsdoc comment types that clearly states what the API does -/** - * Created by {@link createRef}, or {@link useRef} when passed `null`. - * - * @template T The type of the ref's value. - * - * @example - * - * ```tsx - * const ref = createRef(); - * - * ref.current = document.createElement('div'); // Error - * ``` - */ -// interface RefObject { -// /** -// * The current value of the ref. -// -// current: T; -// } -// */ -// -// -// // This will technically work if you give a Consumer or Provider but it's deprecated and warns -// /** -// * Accepts a context object (the value returned from `React.createContext`) and returns the current -// * context value, as given by the nearest context provider for the given context. -// * -// * @version 16.8.0 -// * @see {@link https://react.dev/reference/react/useContext} -// */ -// function useContext(context: Context /*, (not public API) observedBits?: number|boolean */): T; -// /** -// * Returns a stateful value, and a function to update it. -// * -// * @version 16.8.0 -// * @see {@link https://react.dev/reference/react/useState} -// */ -// function useState(initialState: S | (() => S)): [S, Dispatch>]; -// - export type { ElementEventHandlers, ReactiveEventHandler, @@ -56,34 +14,29 @@ export type { * * ```ts * const count = van.state(0); - * - * count.val; // read — tracked as dependency - * count.val = 5; // write — triggers reactive updates - * count.oldVal; // previous value before last update - * count.rawVal; // raw value, no dependency tracking + * count.val = 5; // triggers reactive updates * ``` */ export interface State { /** - * The current value of the state. Reading this property inside a binding or derive - * tracks it as a dependency. Writing to this property triggers reactive updates. + * The current value of the state. Reading tracks it as a dependency, writing triggers updates. */ val: T; /** - * The previous value before the last update. Reading this property tracks it as a dependency. + * The previous value before the last update. */ readonly oldVal: T; /** - * The raw value without dependency tracking. Use this to read the value without - * creating a reactive dependency. + * The raw value without dependency tracking. */ readonly rawVal: T; } -// Defining readonly view of State for covariance. -// Basically we want StateView to implement StateView +/** + * Readonly view of State for covariance. + */ export type StateView = Readonly>; export type Val = State | T; @@ -110,6 +63,11 @@ export type PropsWithKnownKeys = Partial<{ : K]: PropValueOrDerived; }>; +/** + * A mutable ref object that holds a current value. + * + * @template T The type of the ref's value. + */ export type Ref = { current: T | null }; export type RefProp = { ref?: Ref }; @@ -137,43 +95,84 @@ export type TagFunc = ( ...rest: readonly ChildDom[] ) => Result; -// HTML Tags - typed for all known HTML elements +/** + * HTML tag functions typed for all known HTML elements. + */ type HTMLTags = Readonly>> & { [K in keyof HTMLElementTagNameMap]: TagFunc; }; -// SVG Tags - typed for all known SVG elements +/** + * SVG tag functions typed for all known SVG elements. + */ type SVGTags = Readonly>> & { [K in keyof SVGElementTagNameMap]: TagFunc; }; -// MathML Tags - typed for all known MathML elements +/** + * MathML tag functions typed for all known MathML elements. + */ type MathMLTags = Readonly>> & { [K in keyof MathMLElementTagNameMap]: TagFunc; }; -// Namespace URIs type SVGNamespaceURI = "http://www.w3.org/2000/svg"; type MathMLNamespaceURI = "http://www.w3.org/1998/Math/MathML"; -// Namespace function overloads +/** + * Creates tag functions for elements in a specific namespace. + */ type NamespacedTags = { (namespaceURI: SVGNamespaceURI): SVGTags; (namespaceURI: MathMLNamespaceURI): MathMLTags; (namespaceURI: string): Readonly>>; }; +/** + * Creates a reactive state object. + * + * @template T The type of the state's value. + * + * @example + * + * ```ts + * const count = van.state(0); + * const name = van.state("Alice"); + * ``` + */ declare function state(): State; declare function state(initVal: T): State; +/** + * The main VanJS interface. + */ export interface Van { + /** + * Creates a reactive state object. + */ readonly state: typeof state; + + /** + * Creates derived state from a function. + */ readonly derive: (f: () => T) => State; + + /** + * Adds child elements to a DOM node. + */ readonly add: ( dom: Element | DocumentFragment, ...children: readonly ChildDom[] ) => Element; + + /** + * Tag functions for creating HTML, SVG, and MathML elements. + */ readonly tags: HTMLTags & NamespacedTags; + + /** + * Hydrates existing DOM with VanJS reactivity. + */ readonly hydrate: ( dom: T, f: (dom: T) => T | null | undefined