UNPKG

woby

Version:

A high-performance framework with fine-grained observable/signal-based reactivity for building rich applications.

340 lines (273 loc) 11.3 kB
# Counter Demo: Complete Guide to Custom Elements in Woby This document provides a comprehensive walkthrough of creating custom elements in Woby using the Counter demo as a practical example. It covers all aspects of custom element creation, from basic setup to advanced features like context propagation and serialization. ## Overview The Counter demo showcases several important concepts: 1. Custom element creation with proper defaults 2. HTML attribute serialization using `toHtml` and `fromHtml` options 3. Function storage in observables using array notation 4. Object and Date serialization 5. Context usage in custom elements 6. Differences between HTML and JSX usage of custom elements ## Complete Example Breakdown ### Component Definition ```typescript import { $, $$, useMemo, render, customElement, isObservable, createContext, useContext, useEffect, defaults, SYMBOL_DEFAULT, useMountedContext, type ElementAttributes } from 'woby' import type { ObservableReadonly } from 'soby' // Create a context for sharing values between parent and child elements const CounterContext = createContext(null) const useCounterContext = () => useContext(CounterContext) // Define default props using the def function pattern const def = () => { const value = $(0, { type: 'number' } as const) return { title: $('Counter'), // Store function in observable array to hide it from HTML attributes increment: $([() => { value($$(value) + 10) }], { toHtml: o => undefined }), //hide this from html attributes value, nested: { nested: { text: $('abc') } }, // Object with custom serialization obj: $({ nested: { text: 'abc' } }, { toHtml: o => JSON.stringify(o), fromHtml: o => JSON.parse(o) }), // Date with custom serialization date: $(new Date(), { toHtml: o => o.toISOString(), fromHtml: o => new Date(o) }), disabled: $(false, { type: 'boolean' } as const), children: undefined } } // Create the component using defaults for proper two-way synchronization const Counter = defaults(def, (props) => { const { title, increment: inc, // decrement, value, nested, disabled, obj, date, children, ...restProps } = props // Use mounted context for proper context propagation in custom elements const context = useMountedContext(CounterContext) // Access the function from the observable array const increment = $$(inc)[0] ?? (() => { value($$(value) + 1) }) const decrement = () => { value($$(value) - 1) } const v = useMemo(() => $$($$($$(nested)?.nested)?.text)) const m = useMemo(() => { return $$(value) + '' + $$(v) }) return ( <div {...restProps} style={{ border: '1px solid red' }}> <h1>{title}</h1> <p>Value: <b>{value}</b></p> <p>Memo: <b>{m}</b></p> <p>Parent Context (TSX): <b>{context}</b></p> <p>Object: {() => JSON.stringify($$(obj))}</p> <p>Date: {() => $$(date).toString()}</p> <button disabled={disabled} onClick={increment}>+</button> <button disabled={disabled} onClick={decrement}>-</button> {() => $$(children) ? <div style={{ border: '1px solid gray', padding: '10px' }}> <CounterContext.Provider value={value}> {children} </CounterContext.Provider> </div> : null} <p>------------{title} compoent end-------------</p> </div> ) }) ``` ### Context Value Component ```typescript // Simple context value display component const ContextValue = defaults(() => ({}), (props) => { const context = useMountedContext(CounterContext) //direct use return <span >(Context Value = <b>{context}</b>)</span> }) // Processed context value component const ProcessedContextValue = defaults(() => ({}), (props) => { const [context, m] = useMountedContext(CounterContext) return <span >(Pcocessed Context Value = <b>{useMemo(() => $$($$(context)) + ' Processed')}</b>){m}</span> }) ``` ### Custom Element Registration ```typescript // Register components as custom elements customElement('counter-element', Counter) customElement('context-value', ContextValue) customElement('my-上下문-값', ContextValue) customElement('processed-context-value', ProcessedContextValue) ``` ### JSX Namespace Extension ```typescript // Extend JSX namespace to include custom elements for TypeScript support declare module 'woby' { namespace JSX { interface IntrinsicElements { 'counter-element': ElementAttributes<typeof Counter> 'context-value': ElementAttributes<typeof ContextValue> 'my-上下문-값': ElementAttributes<typeof ContextValue> 'processed-context-value': ElementAttributes<typeof ProcessedContextValue> } } } ``` ## HTML Usage Custom elements can be used directly in HTML with automatic attribute synchronization: ```html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Counter</title> </head> <body> <div id="app"></div> <script type="module" src="./index.tsx"></script> <h1>HTML-created Custom Elements with Context</h1> <!-- Parent counter element with a value --> <counter-element title="Parent Counter" value="5" style.color="blue" style.font-size="1.5em" class="border-2 border-blue-500 border-solid bg-amber-400 p-4 m-2"> <p>Parent Counter Content</p> <p>Parent Context Value: <context-value /></p> <p>Parent my-上下문-값: <my-上下문-값 /></p> Child counter element that should inherit context <counter-element title="Child Counter" value="10" style$color="green" style$font-size="1em" obj='{"name": "John", "age": 30}' class="border-2 border-green-500 border-solid bg-yellow-200 p-2 m-2"> <p>Child Context Value: <context-value /></p> <p>Child my-上下문-값: <my-上下문-값 /></p> </counter-element> </counter-element> </body> </html> ``` ## JSX/TSX Usage Custom elements can also be used in JSX/TSX with full TypeScript support: ```typescript const App = () => { const value = $(0) const increment = () => value(prev => prev + 1) const decrement = () => value(prev => prev - 1) return <> <h1>Custom element<br /></h1> <h1>&lt;counter-element&gt; - &lt;counter-element&gt;:<br /></h1> <counter-element title={'Custom element in TSX'} style$color={'red'} style$font-size='1.1em' nested$nested$text='xyz' nested={{ nested: { text: $('abc') } }} obj={$({ nested: { text: 'this obj will be serialized and deserialized to html attribute' } }, { toHtml: obj => JSON.stringify(obj), fromHtml: obj => JSON.parse(obj) })} class={'border-2 border-black border-solid bg-amber-400'}> <context-value /> <ContextValue /> <ProcessedContextValue /> <processed-context-value /> <h2>Nested Custom &lt;counter-element&gt;:<br /></h2> <counter-element title={'counter-element Nested'} style$color={'orange'} style$font-size='1em' nested$nested$text=' nested context' nested={{ nested: { text: $(' nested context') } }} class={'border-2 border-black border-solid bg-amber-400 m-10'}> <context-value /> <ContextValue /> </counter-element> </counter-element> </> } ``` ## Key Concepts Explained ### 1. Defaults Pattern The `defaults` function is essential for custom elements because it: - Enables two-way synchronization between HTML attributes and component props - Provides default values for all props - Handles proper merging of props from different sources ```typescript const Counter = defaults(def, (props) => { // Component implementation }) ``` ### 2. Function Storage in Observables Functions cannot be directly stored in observables for HTML usage. Instead, they are stored in arrays: ```typescript increment: $([() => { value($$(value) + 10) }], { toHtml: o => undefined }) ``` The `toHtml: o => undefined` option prevents the function from appearing as an HTML attribute. ### 3. Object and Date Serialization Complex objects and dates require custom serialization: ```typescript // Object serialization obj: $({ nested: { text: 'abc' } }, { toHtml: o => JSON.stringify(o), fromHtml: o => JSON.parse(o) }) // Date serialization date: $(new Date(), { toHtml: o => o.toISOString(), fromHtml: o => new Date(o) }) ``` ### 4. Context Propagation Custom elements automatically propagate context to child elements: ```typescript const context = useMountedContext(CounterContext) ``` ### 5. Nested Properties Nested properties can be accessed using dot notation in HTML and dash notation in JSX: ```html <!-- HTML usage --> <counter-element nested.nested.text="value"></counter-element> ``` ```tsx // JSX usage <counter-element nested-nested-text="value"></counter-element> ``` ### 6. Style Properties Style properties can be set using dot notation in HTML and dash notation in JSX: ```html <!-- HTML usage --> <counter-element style.color="red" style.font-size="1.5em"></counter-element> ``` ```tsx // JSX usage <counter-element style$color="red" style$font-size="1.5em"></counter-element> ``` ## Best Practices 1. **Always use `defaults`** for custom elements to enable proper synchronization 2. **Store functions in arrays** with `toHtml: o => undefined` to hide them from HTML 3. **Use `useMountedContext`** for context in custom elements 4. **Provide custom serialization** for complex objects and dates 5. **Extend JSX namespace** for TypeScript support 6. **Use proper type annotations** for all props ## Common Patterns ### Simple Counter Component ```typescript function def() { const value = $(0, { type: 'number' } as const) return { value, increment: $([() => { value($$(value) + 1) }], { toHtml: o => undefined }) } } const SimpleCounter = defaults(def, (props) => { const { value, increment } = props return ( <div> <span>{value}</span> <button onClick={() => increment[0]()}>+</button> </div> ) }) customElement('simple-counter', SimpleCounter) ``` ### Component with Context ```typescript const MyContext = createContext(null) const ContextDisplay = defaults(() => ({}), (props) => { const context = useMountedContext(MyContext) return <span>Context Value: {context}</span> }) customElement('context-display', ContextDisplay) ``` This comprehensive guide demonstrates how to create powerful, flexible custom elements in Woby that work seamlessly in both HTML and JSX contexts.