IntroduzionePanoramica
NUCLO / 12

भाषा

दस्तावेज़

Nuclo दस्तावेज़

Nuclo इंस्टॉल करने, स्पष्ट अपडेट के साथ UI बनाने, टाइपयुक्त CSS से स्टाइल करने और सर्वर पर HTML रेंडर करने के लिए व्यावहारिक संदर्भ।

त्वरित शुरुआतnpm create nuclo@latest
त्वरित शुरुआत
v0.3.30
मौजूदा रिलीज़
175
टैग बिल्डर
~18.4 KB
कोर, gzip संपीड़ित
स्पष्ट
रनटाइम मॉडल

अवलोकन

#

Nuclo TypeScript के लिए एक छोटी DOM लाइब्रेरी है। आप पेज को div() और button() जैसे साधारण फ़ंक्शन के साथ बनाते हैं, स्टेट को सामान्य वेरिएबल में रखते हैं, और जब पेज अपडेट करना हो तो update() कॉल करते हैं।

Nuclo में कोई वर्चुअल DOM, कोई कंपाइलर, कोई प्रॉक्सी और कोई सिग्नल नहीं है।

main.ts
import 'nuclo'

let count = 0

const App = () =>
  div(
    h1(() => `Count: ${count}`),
    button('Add one', { onClick: () => { count++; update() } }),
  )

render(App, document.getElementById('app')!)

मानसिक मॉडल

  1. टैग बिल्डर के साथ UI बनाएँ।
  2. स्टेट पर निर्भर प्रत्येक मान के लिए एक फ़ंक्शन पास करें।
  3. साधारण JavaScript से स्टेट बदलें।
  4. update() कॉल करें। Nuclo फिर से फ़ंक्शन चलाता है और DOM को पैच करता है।

किसी वेरिएबल को बदलने से DOM पर असर नहीं पड़ता है। पेज तभी बदलता है जब आप update() कॉल करते हैं।

त्वरित शुरुआत

#

create-nuclo Nuclo के लिए तैयार Vite और TypeScript प्रोजेक्ट बनाता है:

$npm create nuclo@latest
$bun create nuclo
$pnpm create nuclo

यह प्रोजेक्ट का नाम पूछता है। प्रॉम्प्ट को छोड़ने के लिए, नाम और फ़्लैग पास करें। npm के साथ, फ़्लैग से पहले -- लगाएँ:

terminal
$ npm create nuclo@latest my-app -- --template basic --yes

फिर डिपेंडेंसी इंस्टॉल करें और डेव सर्वर शुरू करें:

terminal
$ cd my-app
$ npm install
$ npm run dev

विकल्प

  • [project-name]: प्रोजेक्ट का फ़ोल्डर। डिफ़ॉल्ट nuclo-app है।
  • -t, --template <name>: कॉपी करने के लिए टेम्पलेट। फ़िलहाल केवल basic मौजूद है।
  • -y, --yes: कभी प्रॉम्प्ट न दिखाएँ, डिफ़ॉल्ट का उपयोग करें।
  • -f, --force: ऐसे फ़ोल्डर में लिखें जो खाली न हो।
  • -h, --help: सहायता प्रिंट करें।

इंस्टॉलेशन और इम्पोर्ट

#

किसी मौजूदा प्रोजेक्ट में Nuclo जोड़ने के लिए:

terminal
$ npm install nuclo

या pnpm add nuclo, yarn add nuclo या bun add nuclo का उपयोग करें। Nuclo की कोई निर्भरता नहीं है।

रूटिंग कोर पैकेज का हिस्सा नहीं है। यह अलग nuclo-router पैकेज में मिलता है, जिसे ज़रूरत पड़ने पर nuclo के साथ इंस्टॉल करें।

ग्लोबल्स

import 'nuclo' इन्हें globalThis पर रखता है:

  • 175 टैग बिल्डर: HTML के लिए 112 (div, span, button, ...) और SVG के लिए 63, Svg सफ़िक्स के साथ (svgSvg, pathSvg,…)। टैग बिल्डर देखें।
  • 16 हेल्पर: update, scope, when, list, region, into, on, render, hydrate, forceUpdate, css, cx, createCss, variants, keyframes और globalStyle।

इम्पोर्ट किसी भी कोड से पहले चलना चाहिए जो बिल्डर को कॉल करता है। ES मॉड्यूल इम्पोर्ट के क्रम में चलते हैं, इसलिए यह विफल रहता है:

main.ts
import { App } from './app' // app.ts calls div() → ReferenceError: div is not defined
import 'nuclo'

अपनी एंट्री फ़ाइल में import 'nuclo' को पहले रखें, या इसे ग्लोबल्स का उपयोग करने वाली प्रत्येक फ़ाइल में इम्पोर्ट करें।

नेम्ड इम्पोर्ट

हेल्पर नेम्ड एक्सपोर्ट के रूप में भी उपलब्ध हैं। ये वही फ़ंक्शन हैं जो ग्लोबल्स में हैं। getCssText() और resetStyles() केवल नेम्ड एक्सपोर्ट के रूप में मिलते हैं। टैग बिल्डर केवल ग्लोबल हैं, इसलिए import { div } from 'nuclo' काम नहीं करता है।

main.ts
import { render, update, getCssText } from 'nuclo'

एंट्री पॉइंट

  • nuclo: ग्लोबल्स को पंजीकृत करता है और 16 हेल्पर के साथ getCssText और resetStyles को एक्सपोर्ट करता है।
  • nuclo/ssr: renderToString, renderManyToString, renderToStringWithContainer और getCssText। सर्वर रेंडरिंग देखें।
  • nuclo/polyfill: Node और Bun के लिए एक छोटा DOM। nuclo/polyfill देखें।
  • nuclo/types: केवल टाइप। TypeScript देखें।

TypeScript

#

Nuclo अपने टाइप के साथ आता है। आपको दो चीज़ें चाहिए:

  • "moduleResolution": "bundler". node16 और nodenext अभी समर्थित नहीं हैं।
  • DOM lib. जब आप lib सेट नहीं करते हैं तो TypeScript इसे डिफ़ॉल्ट रूप से शामिल करता है।

यदि प्रोजेक्ट में कोई फ़ाइल 'nuclo' इम्पोर्ट करती है, तो प्रत्येक फ़ाइल ग्लोबल टाइप देखती है। इसके अलावा कुछ नहीं चाहिए। यदि कोई फ़ाइल इसे इम्पोर्ट नहीं करती है, उदाहरण के लिए परीक्षणों के लिए एक अलग tsconfig में, टाइप खुद जोड़ें:

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["nuclo/types"]
  }
}

या .d.ts फ़ाइल में /// <reference types="nuclo/types" /> जोड़ें।

उपयोगी टाइप

ये टाइप ग्लोबल हैं, इसलिए आप इन्हें इम्पोर्ट के बिना उपयोग करते हैं:

  • NodeModFn<Tag>: टैग बिल्डर का रिटर्न टाइप। इसे एक कंपोनेंट के रिटर्न टाइप के रूप में उपयोग करें।
  • NodeModLike<Tag>: टैग बिल्डर आर्ग्युमेंट के रूप में जो कुछ स्वीकार करता है।
  • ExpandedElement<Tag>: render() और hydrate() क्या लौटाते हैं।
  • ElementTagName: कोई भी HTML टैग नाम।
  • ExpandedElementAttributes<Tag>: वह एट्रिब्यूट ऑब्जेक्ट जिसे टैग बिल्डर स्वीकार करता है, जिसमें on* हैंडलर, onMount और onDestroy शामिल हैं। इसका उपयोग उन कंपोनेंट के लिए करें जो एट्रिब्यूट आगे पास करते हैं।
  • MountCallback और DestroyCallback: onMount और onDestroy हैंडलर।
  • WhenBuilder, ListModifier: when() और list() क्या रिटर्न देते हैं।
  • RegionOptions, RegionType: region() के विकल्प।
  • StyleResult, Style, ThemeConfig, CssInstance और अन्य स्टाइल टाइप। आप इन्हें भी इम्पोर्ट कर सकते हैं: import type { StyleResult } from 'nuclo'.
badge.ts
function Badge(text: string): NodeModFn<'span'> {
  return span(text)
}

ExpandedElement<Tag> प्रत्येक DOM सदस्य को वैकल्पिक के रूप में चिह्नित करता है। जब आपको पूर्ण एलिमेंट टाइप की आवश्यकता हो तो इसे कास्ट करें: render(App, root) as HTMLDivElement।

प्रत्येक HTML टैग एक ही नाम के साथ एक ग्लोबल फ़ंक्शन है: div, span, input, ... एक अपवाद <var> है, जो var_ है क्योंकि var एक आरक्षित शब्द है। प्रत्येक SVG टैग में एक Svg प्रत्यय होता है: svgSvg, pathSvg, circleSvg, ...

टैग बिल्डर कॉल करते ही एलिमेंट नहीं बनता। div('Hi') एक builder लौटाता है: एक फ़ंक्शन जो बाद में एलिमेंट बनाता है। जब आप इसे render() के साथ माउंट करते हैं या किसी अन्य बिल्डर, when() या list() के अंदर रखते हैं तो Nuclo इसे कॉल करता है। प्रत्येक प्रयोग एक नया एलिमेंट बनाता है।

card.ts
let name = 'Ada'

const Card = () =>
  div(
    { id: 'card', className: 'card' },
    h2('Profile'),
    p(() => `Hello, ${name}`),
    button('Rename', { onClick: () => { name = 'Grace'; update() } }),
  )

render(Card, document.body)
लाइवबिल्डर एट्रिब्यूट, स्टैटिक चाइल्ड और डायनेमिक टेक्स्ट लेता है। Rename स्टेट बदलकर update() कॉल करता है।खोलें ↗

Profile

Hello, Ada

आर्ग्युमेंट

एक बिल्डर किसी भी संख्या में आर्ग्युमेंट लेता है और उन्हें बाएं से दाएं लागू करता है:

  • string, number, bigint: एक टेक्स्ट नोड।
  • () => value: टेक्स्ट जो update() पर बदलता है। डायनेमिक मान देखें।
  • null, undefined: छोड़ दिया गया।
  • true, false: "true" या "false" टेक्स्ट के रूप में रेंडर होते हैं। शर्तों के लिए when() का उपयोग करें।
  • एक साधारण ऑब्जेक्ट: एट्रिब्यूट। एट्रिब्यूट देखें।
  • css(), cx() या variants() परिणाम: अपनी क्लास जोड़ता है।
  • on(...): एक इवेंट लिसनर या लाइफ़साइकल हुक।
  • एक अन्य बिल्डर, when() या list(): एक चाइल्ड नोड।
  • एक DOM Node: जैसा है वैसा ही जोड़ा गया।
  • (el, index) => …: एक कस्टम मॉडिफ़ायर। नीचे देखें।

ध्यान रखने योग्य बातें

  • div(isAdmin && span('Admin')) "false" टेक्स्ट रेंडर करता है। div(when(() => isAdmin, span('Admin'))) लिखें।
  • ऐरे चाइल्ड नोड नहीं हैं। उन्हें स्प्रेड करें: ul(...names.map((n) => li(n))). बदलने वाली सूचियों के लिए, list() का उपयोग करें।
  • टैग नाम वाला एक स्थानीय वैरिएबल बिल्डर को छुपाता है। function Row(label: string) के अंदर, label(...) कॉल करने योग्य नहीं है। label, title, data, p, a और i जैसे स्थानीय नाम बदलें।
  • ब्राउज़र हर id वाले एलिमेंट को एक ग्लोबल वेरिएबल के रूप में भी उपलब्ध कराते हैं। यदि Nuclo लोड होने से पहले पृष्ठ में <main id="main"> शामिल है, तो Nuclo उस ग्लोबल को जस का तस रखता है, इसलिए main(...) एक TypeError त्रुटि थ्रो करता है। अन्य आईडी का प्रयोग करें।

कस्टम मॉडिफ़ायर

एक फ़ंक्शन जो कम से कम एक पैरामीटर घोषित करता है, आर्ग्युमेंट क्रम में एक बार चलता है, जबकि एलिमेंट बनाया जा रहा है। इसका पहला आर्ग्युमेंट एलिमेंट है। दूसरा एक आंतरिक इंडेक्स है, इसलिए इस पर भरोसा न करें। यदि यह एक स्ट्रिंग या संख्या लौटाता है, तो वह टेक्स्ट बन जाता है। Node लौटाने पर उसे जोड़ दिया जाता है। किसी ऑब्जेक्ट को एट्रिब्यूट के रूप में लागू किया जाता है।

example.ts
const field = input((el) => {
  el.autocomplete = 'off'
})

एलिमेंट प्राप्त करना

render() मूल एलिमेंट लौटाता है। किसी ट्री के अंदर, on("mount", (el) => …) या एक कस्टम मॉडिफ़ायर का उपयोग करें। अलग एलिमेंट बनाने के लिए आप बिल्डर फ़ंक्शन को स्वयं भी कॉल कर सकते हैं:

example.ts
const el = div('Hello')() as HTMLDivElement
document.body.append(el)

इसे अगले update() से पहले संलग्न करें। update() चलते समय जो नोड document में नहीं होता, वह हमेशा के लिए अपडेट होना बंद कर देता है।

SVG

SVG बिल्डर भी यही आर्ग्युमेंट लेते हैं। Nuclo प्रत्येक SVG एट्रिब्यूट को setAttribute के साथ सेट करता है, इसलिए "stroke-width" जैसे नाम लिखित रूप में काम करते हैं।

icon.ts
const Icon = () =>
  svgSvg(
    { viewBox: '0 0 24 24', width: 24, height: 24, className: 'icon' },
    circleSvg({ cx: 12, cy: 12, r: 10, fill: 'currentColor' }),
  )

सभी बिल्डर

HTML: a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark menu meta meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script search section select slot small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var_ video wbr

SVG: aSvg animateSvg animateMotionSvg animateTransformSvg circleSvg clipPathSvg defsSvg descSvg ellipseSvg feBlendSvg feColorMatrixSvg feComponentTransferSvg feCompositeSvg feConvolveMatrixSvg feDiffuseLightingSvg feDisplacementMapSvg feDistantLightSvg feDropShadowSvg feFloodSvg feFuncASvg feFuncBSvg feFuncGSvg feFuncRSvg feGaussianBlurSvg feImageSvg feMergeSvg feMergeNodeSvg feMorphologySvg feOffsetSvg fePointLightSvg feSpecularLightingSvg feSpotLightSvg feTileSvg feTurbulenceSvg filterSvg foreignObjectSvg gSvg imageSvg lineSvg linearGradientSvg markerSvg maskSvg metadataSvg mpathSvg pathSvg patternSvg polygonSvg polylineSvg radialGradientSvg rectSvg scriptSvg setSvg stopSvg styleSvg svgSvg switchSvg symbolSvg textSvg textPathSvg titleSvg tspanSvg useSvg viewSvg

एक कंपोनेंट एक साधारण फ़ंक्शन है जो एक बिल्डर लौटाता है, जैसे button(...)। कोई कंपोनेंट API नहीं है। जब तक आप इसे रेंडर नहीं करते या इसे किसी ट्री पर नहीं रखते तब तक कुछ भी नहीं बनता है।

counter.ts
function Counter(label: string) {
  let count = 0 // one per call
  return button(() => `${label}: ${count}`, {
    onClick: () => { count++; update() },
  })
}

render(() => div(Counter('A'), Counter('B')))
लाइवकंपोनेंट एक साधारण फ़ंक्शन है। हर Counter() कॉल अपना अलग काउंटर रखती है।खोलें ↗

फ़ंक्शन के अंदर घोषित स्टेट उस इंस्टेंस का अपना है। बाहर घोषित स्टेट सभी इंस्टेंस में साझा होता है। forceUpdate() कंपोनेंट को फिर से कॉल करता है, जो उनके अंदर घोषित स्टेट को रीसेट करता है।

चाइल्ड नोड सिर्फ आर्ग्युमेंट हैं:

card.ts
function Card(heading: string, ...children: NodeModLike<'div'>[]) {
  return div({ className: 'card' }, h2(heading), ...children)
}

render(() => Card('Hi', p('one'), p('two')))

एट्रिब्यूट

#

एट्रिब्यूट सेट करने के लिए एक साधारण ऑब्जेक्ट पास करें। आप कई ऑब्जेक्ट पास कर सकते हैं। एक ही कुंजी के लिए बाद वाले मान को प्राथमिकता मिलती है। className और style अपवाद हैं: इनके मान मर्ज होते हैं। मान स्टैटिक हो सकता है, या बिना किसी पैरामीटर वाला एक फ़ंक्शन हो सकता है जो प्रत्येक update() पर फिर से चलता है।

example.ts
let busy = false

const save = button(
  { type: 'submit', disabled: () => busy, 'aria-busy': () => String(busy), 'data-id': 42 },
  'Save',
)
लाइवएट्रिब्यूट के मान फ़ंक्शन हो सकते हैं। दूसरा बटन busy बदलकर update() कॉल करता है; Save के disabled और aria-busy उसी के अनुसार बदलते हैं।खोलें ↗

कोई मान कैसे सेट किया जाता है

  • यदि HTML एलिमेंट में उस नाम (value, checked, disabled, htmlFor, tabIndex,…) के साथ एक प्रॉपर्टी है, तो Nuclo प्रॉपर्टी सेट करता है। अन्यथा यह setAttribute(key, String(value)) को कॉल करता है, उदाहरण के लिए "aria-*" और "data-*" के लिए।
  • SVG एलिमेंट पर, Nuclo हमेशा setAttribute को कॉल करता है।
  • null और undefined को छोड़ दिया गया है। एक डायनेमिक मान जो null या undefined लौटाता है, पुराना मान रखता है। मान साफ़ करने के लिए "" लौटाएँ, या बूलियन प्रॉपर्टी के लिए false लौटाएँ।
  • बूलियन मान DOM बूलियन प्रॉपर्टी के लिए काम करते हैं, जैसे एक बटन पर disabled, checked, hidden और required। कहीं और, true और false को स्ट्रिंग "true" और "false" के रूप में लिखा गया है।
  • "data-*" कुंजियों का उपयोग करें। dataset समर्थित नहीं है।
  • innerHTML एस्केप नहीं होता, और सर्वर रेंडरिंग इसे आउटपुट नहीं करता है। इसके उपयोग से बचें।

क्लास

className का उपयोग करें। स्टैटिक क्लास स्ट्रिंग्स, css() परिणाम और एक डायनेमिक className सभी विलीन हो जाते हैं। एक डायनेमिक className स्टैटिक क्लास रखता है, और जब यह "" लौटाता है तो केवल स्टैटिक क्लास ही रहती हैं।

example.ts
let active = false
const base = css({ p: 8 })

const item = li(base, { className: 'item' }, { className: () => (active ? 'active' : '') }, 'Home')
// class="<base> item", then "<base> item active" after active = true; update()

HTML एलिमेंट पर class कुंजी का उपयोग न करें। यह सीधे एट्रिब्यूट सेट करता है और css() की क्लास सहित पहले लागू सभी क्लास को बदल देता है।

इनलाइन स्टाइल

style camelCase कुंजियों के साथ एक ऑब्जेक्ट लेता है, या एक फ़ंक्शन जो एक लौटाता है।

example.ts
let progress = 30

const bar = div({ style: () => ({ width: `${progress}%`, backgroundColor: 'teal' }) })
  • Nuclo कोई इकाई नहीं जोड़ता। "12px" लिखें, 12 नहीं। इकाई रहित गुण जैसे opacity और zIndex संख्याएँ स्वीकार करते हैं।
  • "", null और undefined प्रॉपर्टी साफ़ कर देते हैं।
  • एक डायनेमिक स्टाइल केवल वही कुंजी सेट करती है जो वह लौटाती है। जिस कुंजी को वह लौटाना बंद कर देता है, उसका पुराना मान बना रहता है, इसलिए उसे साफ़ करने के लिए "" लौटाएँ।
  • स्टाइल स्ट्रिंग्स ("color: red") और कस्टम गुण ("--gap") समर्थित नहीं हैं। इसके बजाय css() का उपयोग करें। यह raw के माध्यम से कस्टम गुण लेता है।
fnसार्वजनिक API
// DOM events such as "click". El and Ev are inferred from the tag and the event name. function on(type: DOMEventName, listener: (this: El, e: Ev) => unknown, options?: boolean | AddEventListenerOptions): NodeModFn // Custom events function on<K extends string, E extends Event = Event>(type: K, listener: (this: El, e: E) => unknown, options?: boolean | AddEventListenerOptions): NodeModFn // Lifecycle, see Lifecycle function on(type: "mount", listener: MountCallback): NodeModFn function on(type: "destroy", listener: DestroyCallback): NodeModFn

इवेंट को सुनने के दो तरीके हैं: on* एट्रिब्यूट और on() मॉडिफ़ायर।

on* एट्रिब्यूट

एक एट्रिब्यूट ऑब्जेक्ट में, onClick (on जिसके बाद एक बड़ा अक्षर होता है) जैसी कुंजी के तहत एक फ़ंक्शन एक इवेंट हैंडलर है, डायनेमिक मान नहीं।

search.ts
let query = ''

const search = input({
  placeholder: 'Search',
  value: () => query,
  onInput: (e) => { query = e.currentTarget.value; update() },
  onKeyDown: (e) => { if (e.key === 'Escape') { query = ''; update() } },
})
  • TypeScript कुंजी और एलिमेंट से इवेंट टाइप का अनुमान लगाता है। e.currentTarget और this एलिमेंट हैं।
  • Nuclo मूल प्रॉपर्टी सेट करता है, इसलिए onClick el.onclick बन जाता है। प्रति इवेंट एक हैंडलर होता है: बाद वाला onClick पहले वाले की जगह लेता है।
  • onDoubleClick और onDblClick दोनों का मतलब dblclick है। onclick जैसी लोअरकेस कुंजियाँ टाइप त्रुटियाँ हैं।
  • एक नाम जो ज्ञात इवेंट नहीं है, उसके लोअरकेस रूप को सुनता है: onMyEvent "myevent" को सुनता है।
  • हैंडलर द्वारा फेंकी गई त्रुटियाँ पकड़ में नहीं आतीं।
लाइवonInput और onKeyDown सामान्य एट्रिब्यूट हैं। टेक्स्ट लिखें और साफ़ करने के लिए Escape दबाएँ।खोलें ↗

Type something, then press Escape.

on()

on() एक मॉडिफ़ायर लौटाता है जो एलिमेंट पर addEventListener को कॉल करता है।

panel.ts
const panel = div(
  on('scroll', onScroll, { passive: true }),
  on('click', logClick),
  on('click', closeMenu),
)
  • प्रत्येक on() एक लिसनर जोड़ता है। वे सभी क्रम से चलते हैं।
  • options सीधे addEventListener पर जाता है: capture, once, passive, signal।
  • सीधे बिल्डर के पास on() पास करें और एलिमेंट टाइप का अनुमान लगाया जाता है: input(on('input', (e) => e.currentTarget.value))।
  • यदि कोई लिसनर त्रुटि थ्रो करता है, तो Nuclo console.error के साथ त्रुटि लॉग करता है, और अन्य लिसनर अभी भी चलते हैं।
  • Nuclo एलिमेंट को हटाते समय लिसनर को हटा देता है। सर्वर पर, on() कुछ नहीं करता है।
  • आप कई एलिमेंट पर एक on() मान का पुन: उपयोग कर सकते हैं।

एक कस्टम इवेंट के लिए, इवेंट टाइप को जेनेरिक पैरामीटर के रूप में पास करें:

picker.ts
type PickEvent = CustomEvent<{ id: number }>

const picker = ul(on<'pick', PickEvent>('pick', (e) => choose(e.detail.id)))

कौन सा उपयोग करना है

आम मामलों में on* एट्रिब्यूट उपयोग करें। एक इवेंट के लिए कई लिसनर, लिसनर विकल्प या "my-event" जैसा कस्टम इवेंट नाम चाहिए, तो on() उपयोग करें।

fnसार्वजनिक API
// As attributes { onMount?: (el: El) => void | (() => void), onDestroy?: (el: El) => void } // Or with on() function on(type: "mount", listener: (el: El) => void | (() => void)): NodeModFn function on(type: "destroy", listener: (el: El) => void): NodeModFn

onMount एलिमेंट जुड़ने के बाद एक बार चलता है। onDestroy एक बार चलता है, जब Nuclo एलिमेंट हटा देता है। उन्हें एट्रिब्यूट के रूप में या on() के साथ लिखें। दोनों फॉर्म समान व्यवहार करते हैं, और एलिमेंट उसके टैग से टाइप किया जाता है।

timer.ts
let seconds = 0

const Timer = () =>
  span(
    {
      onMount: () => {
        const id = setInterval(() => { seconds++; update() }, 1000)
        return () => clearInterval(id) // runs when the span is removed
      },
    },
    () => `${seconds}s`,
  )

const search = input(on('mount', (el) => el.focus()))
लाइवonMount इंटरवल शुरू करता है और उसका क्लीनअप फ़ंक्शन लौटाता है। अनमाउंट होने पर क्लीनअप चलता है, इसलिए टाइमर दोबारा माउंट होने तक गिनती रुक जाती है।खोलें ↗

जब वे चलते हैं

  • onMount उस render(), hydrate(), update() या forceUpdate() कॉल के अंत में चलता है जिसने एलिमेंट डाला था। ट्री के निर्माण के दौरान यह कभी नहीं चलता।
  • यदि onMount एक फ़ंक्शन लौटाता है, तो वह एलिमेंट हटने पर उसके onDestroy कॉलबैक के बाद चलता है।
  • onDestroy तब चलता है जब Nuclo एलिमेंट को हटा देता है: एक list() पंक्ति हटा दी जाती है, एक when() शाखा स्विच हो जाती है, एक into() अपना रीजन छोड़ देता है, या forceUpdate() इसे छोड़ देता है। यह हटाने से ठीक पहले चलता है, इसलिए एलिमेंट अभी भी दस्तावेज़ में है।

DOM को सीधे हटाना

यदि आप node.remove() या innerHTML = '' के साथ किसी एलिमेंट को स्वयं हटाते हैं, तो इसका onDestroy और क्लीनअप कभी नहीं चलता है, और टाइमर चलते रहते हैं। Nuclo में कोई unmount() नहीं है। क्लीनअप चलाते हुए ऐप हटाने के लिए, उसे when() में लपेटें:

main.ts
let mounted = true
render(() => div(when(() => mounted, App())), root)

mounted = false
update() // App's onDestroy hooks and cleanups run

क्रम और त्रुटियाँ

  • आप किसी भी रूप में जितने चाहें उतने हुक पंजीकृत कर सकते हैं। वे पंजीकरण क्रम में चलते हैं।
  • onDestroy हुक हमेशा पहले चाइल्ड नोड पर चलते हैं, फिर पैरेंट पर।
  • एलिमेंट अपने पहले onMount हुक के क्रम में माउंट होते हैं, जो आर्ग्युमेंट क्रम का पालन करता है, और प्रत्येक एलिमेंट अपने सभी हुक को एक साथ चलाता है। यदि चाइल्ड नोड पैरेंट पर निर्भर हों, तो पैरेंट का मॉडिफ़ायर चाइल्ड नोड से पहले लिखें।
  • एक हुक जो त्रुटि थ्रो करता है उसे console.error के साथ लॉग किया जाता है। बाकी हुक फिर भी चलते हैं।
  • कॉलबैक को कुछ भी नहीं या क्लीनअप फ़ंक्शन लौटाना चाहिए, इसलिए इसे async न बनाएँ। लौटाया गया Promise क्लीनअप के रूप में इस्तेमाल नहीं होता। इसके अंदर async कार्य प्रारंभ करें: { onMount: (el) => { void load(el) } }।
  • हुक कभी भी सर्वर पर नहीं चलते। क्लाइंट पर, वे hydrate() के बाद चलते हैं।
नोट: हुक इफ़ेक्ट नहीं हैं। स्टेट बदलने पर वे दोबारा नहीं चलते। हर हुक प्रति एलिमेंट अधिकतम एक बार चलता है।

Nuclo कभी भी पेज को अपने आप अपडेट नहीं करता है। अपना स्टेट बदलें, फिर update() कॉल करें।

counter.ts
let count = 0

const Counter = () =>
  button(() => `Clicked ${count} times`, {
    onClick: () => { count++; update() },
  })

render(Counter)
लाइवcount++ केवल वेरिएबल बदलता है। इस बटन या पेज की किसी दूसरी गतिविधि से अगला update() चलने पर टेक्स्ट अपडेट होता है।खोलें ↗

Clicked 0 times

जितना चाहें उतना स्टेट बदलें, फिर एक बार update() कॉल करें:

example.ts
let first = 'Ada'
let last = 'Lovelace'

render(() => p(() => `${first} ${last}`))

first = 'Grace'
last = 'Hopper'
update() // one pass, both changes

इवेंट हैंडलर, टाइमर, फ़ेच कॉलबैक या onMount से update() कॉल करें। इसे डायनेमिक मान के अंदर से न बुलाएं।

डायनेमिक मान

#

बिना पैरामीटर वाला एक फ़ंक्शन एक डायनेमिक मान है। Nuclo एलिमेंट बनाते समय इसे एक बार चलाता है, और फिर प्रत्येक update() पर। टेक्स्ट चाइल्ड नोड, एट्रिब्यूट मान, style और className के लिए डायनेमिक मानों का उपयोग करें। दिखाई देने वाले, गायब होने वाले या दोहरने वाले चाइल्ड नोड के लिए, when() और list() का उपयोग करें।

example.ts
let name = 'Alice'
let busy = false

const Profile = () =>
  div(
    h1('Welcome'),              // static
    p(() => `Hello, ${name}`),  // dynamic text
    button({ disabled: () => busy }, 'Save'),
  )
लाइवनाम बदलें। स्टैटिक टेक्स्ट एलिमेंट बनते समय तय हो गया था; डायनेमिक टेक्स्ट हर update() पर दोबारा चलता है। forceUpdate() ट्री दोबारा बनाता है, इसलिए स्टैटिक टेक्स्ट भी अपडेट होता है।खोलें ↗

static: Hello, Alice

dynamic: Hello, Alice

Nuclo पैरामीटर गिनती (fn.length) की जाँच करता है। एक फ़ंक्शन जो पैरामीटर घोषित करता है वह एक कस्टम मॉडिफ़ायर है और केवल एक बार चलता है। (x = 0) => … और (...args) => … की लंबाई 0 है, इसलिए वे डायनेमिक हैं।

डायनेमिक टेक्स्ट

  • एक स्ट्रिंग, संख्या, bigint या बूलियन String(value) के रूप में रेंडर होता है। () => ok && 'Saved' "false" दिखाता है, इसलिए () => (ok ? 'Saved' : '') लिखें।
  • null और undefined खाली टेक्स्ट के रूप में रेंडर होते हैं।
  • css() या cx() परिणाम पहले रन पर फ़ंक्शन को एक डायनेमिक क्लास बनाता है, टेक्स्ट नहीं। cx() देखें।
  • एक नोड, बिल्डर, ऐरे या साधारण ऑब्जेक्ट कुछ भी रेंडर नहीं करता है, और कभी अपडेट नहीं होता है।

डायनेमिक एट्रिब्यूट

  • Nuclo मान तभी लिखता है जब वह उसके लिखे पिछले मान से अलग हो। तुलना मौजूदा DOM से नहीं होती। इसलिए उपयोगकर्ता इनपुट में कुछ टाइप करे और फ़ंक्शन पहले वाला ही मान लौटाए, तो टाइप किया टेक्स्ट बना रहता है।
  • null या undefined का परिणाम कुछ भी नहीं लिखता है, इसलिए पुराना मान बना रहता है। className अपवाद है: कोई भी falsy परिणाम डायनेमिक क्लास को हटा देता है और स्टैटिक क्लास को रखता है।
  • एक style फ़ंक्शन प्रत्येक अपडेट पर लौटाए गए ऑब्जेक्ट को लागू करता है।

त्रुटियाँ

डायनेमिक मान त्रुटि थ्रो करे, तो Nuclo उसे पकड़ लेता है और बाकी अपडेट जारी रहता है। टेक्स्ट खाली हो जाता है या पिछला मान रखता है। एट्रिब्यूट पिछला मान रखता है। अगला सफल update() दोनों को ठीक कर देता है। अपवाद: टेक्स्ट फ़ंक्शन अपने पहले रन पर थ्रो करे, तो वह हमेशा खाली रहता है।

fnसार्वजनिक API
function update(...scopeIds: string[]): void

पृष्ठ पर प्रत्येक डायनेमिक मान पर एक सिंक्रोनस पास चलाता है। आईडी देने पर यह केवल उन scope() आईडी से चिह्नित एलिमेंट के अंदर के मान अपडेट करता है। प्रत्येक पास इन चरणों को क्रम में चलाता है:

  1. list(): आइटम को दोबारा पढ़ता है और पंक्तियों को जोड़ता, स्थानांतरित या हटाता है।
  2. when(): शर्तों की दोबारा जाँच करता है। एक शाखा का पुनर्निर्माण तभी होता है जब कोई दूसरी शाखा सक्रिय हो जाती है।
  3. डायनेमिक एट्रिब्यूट, जिनमें style और className शामिल हैं।
  4. डायनेमिक टेक्स्ट।
  5. चरण 1 और 2 में जोड़े गए एलिमेंट के onMount हुक।
  • यह सब कुछ चलाता है। प्रत्येक डायनेमिक मान प्रत्येक कॉल पर चलता है, भले ही उसका डेटा नहीं बदला हो। बिना बदलाव वाले मान DOM में दोबारा नहीं लिखे जाते। डायनेमिक मानों को तेज़ और दुष्प्रभावों से मुक्त रखें।
  • केवल document में मौजूद नोड। यदि update() चलने पर दस्तावेज़ में कोई नोड नहीं है, तो Nuclo इसे ट्रैक करना बंद कर देता है। आपके द्वारा इसे संलग्न करने के बाद भी यह कभी भी दोबारा अपडेट नहीं होता है। यह list() और when() पर भी लागू होता है। दस्तावेज़ में मौजूद कंटेनर में रेंडर करें।
  • स्टैटिक मान कभी नहीं बदलते। एलिमेंट निर्मित होने पर साधारण स्ट्रिंग और एट्रिब्यूट मान तय हो जाते हैं। उन्हें फिर से बनाने के लिए, forceUpdate() का उपयोग करें।
  • त्रुटियाँ। टेक्स्ट और एट्रिब्यूट त्रुटियाँ पकड़ी जाती हैं, और पास जारी रहता है। यदि किसी when() की शर्त त्रुटि थ्रो करती है, तो त्रुटि लॉग हो जाती है और when() अपडेट होना बंद हो जाता है। यदि कोई list() items या renderItem फ़ंक्शन त्रुटि थ्रो करता है, तो वह update() से बाहर पहुँचती है और शेष चरण नहीं चलते हैं।
  • सर्वर। update() सर्वर पर कुछ नहीं करता है।
लाइवहर update() सभी डायनेमिक मान चलाता है, भले उनका डेटा न बदला हो। इस पेज के दूसरे डेमो भी काउंटर बढ़ाते हैं।खोलें ↗

This dynamic value has run 1 times.

fnसार्वजनिक API
function scope(...ids: string[]): NodeModFn

जिस एलिमेंट को यह पास किया जाता है, उसे एक या अधिक स्कोप आईडी देता है। update('id') तब केवल वही अपडेट करता है जो उस एलिमेंट के अंदर है, जिसमें एलिमेंट की अपनी एट्रिब्यूट भी शामिल हैं।

example.ts
let cartItems: string[] = []
let user = 'Ada'

const App = () =>
  div(
    header(() => `Hi, ${user}`),
    aside(scope('cart'), span(() => `Items: ${cartItems.length}`)),
  )

render(App)

cartItems.push('apple')
user = 'Grace'
update('cart') // "Items: 1", the header still says "Hi, Ada"
update()       // everything, the header now says "Hi, Grace"
लाइवदोनों बटन एक आइटम जोड़ते हैं और उपयोगकर्ता का नाम बदलते हैं। update('cart') केवल उस स्कोप का पैनल अपडेट करता है।खोलें ↗
Hi, Ada
Items: 0
  • एक एलिमेंट में कई आईडी हो सकती हैं: scope('cart', 'sidebar')।
  • update('a', 'b') a या b के अंदर सब कुछ अपडेट करता है। यदि कई एलिमेंट एक ही आईडी का उपयोग करते हैं, तो वे सभी अपडेट हो जाते हैं।
  • नेस्टेड स्कोप के साथ, update('outer') में आंतरिक स्कोप शामिल है। update('inner') बाहरी हिस्से को नहीं छूता है।
  • यदि आईडी किसी भी एलिमेंट पर नहीं है, तो कुछ भी अपडेट नहीं होता और कोई त्रुटि नहीं मिलती। इसलिए आईडी की वर्तनी जाँचें।
  • scope() सर्वर पर कुछ नहीं करता है।
fnसार्वजनिक API
function when(condition: boolean | (() => boolean), ...content: NodeModLike[]): WhenBuilder interface WhenBuilder { when(condition: boolean | (() => boolean), ...content: NodeModLike[]): WhenBuilder else(...content: NodeModLike[]): WhenBuilder }

एक शर्त के आधार पर सामग्री रेंडर करता है। Nuclo प्रत्येक update() पर शर्तों की दोबारा जाँच करता है। पहली सही शर्त चुनी जाती है। यदि कोई शर्त सही नहीं है, तो .else() सामग्री रेंडर होती है। .else() के बिना, कुछ भी रेंडर नहीं होता।

status.ts
let status: 'loading' | 'error' | 'ready' = 'loading'

const Status = () =>
  div(
    when(() => status === 'loading', p('Loading…'))
      .when(() => status === 'error', p('Something went wrong'))
      .else(p('Ready')),
  )
लाइवपहली सही शर्त चुनी जाती है; कोई शर्त सही न हो तो .else() रेंडर होता है। नई शाखा शुरू से बनती है।खोलें ↗

Loading…

शर्तें

  • स्टेट के लिए, एक फ़ंक्शन पास करें। एक साधारण बूलियन एक बार पढ़ा जाता है: when(open, …) कभी नहीं बदलता।
  • TypeScript में फ़ंक्शन को एक बूलियन लौटाना होगा। () => items.length > 0 या () => user != null लिखें।

क्या अपडेट होता है

  • जब तक एक ही शाखा सक्रिय रहती है, इसके एलिमेंट बने रहते हैं। इसके अंदर डायनेमिक मान, सूचियाँ और when() ब्लॉक अपडेट होते रहते हैं।
  • जब एक अलग शाखा सक्रिय हो जाती है, तो पुरानी सामग्री हटा दी जाती है और उसके onDestroy हुक चलते हैं। नई शाखा शुरू से बनती है और इसके onMount हुक चलते हैं। पुराने एलिमेंट की स्थिति, जैसे इनपुट टेक्स्ट या फ़ोकस, खो जाती है।

शाखा सामग्री

चाइल्ड नोड को एक शाखा में रखें: टेक्स्ट, डायनेमिक टेक्स्ट, बिल्डर, list() और नेस्टेड when()। p('Hi') जैसे बिल्डर को पास करें, DOM नोड्स को नहीं: एक DOM नोड अपनी शाखा के एक बार छिपने के बाद अपडेट होना बंद कर देता है।

एट्रिब्यूट ऑब्जेक्ट और on() मूल एलिमेंट पर लागू होते हैं, और वे शाखा के छिपने के बाद भी बने रहते हैं। एक सशर्त एट्रिब्यूट के लिए, एक डायनेमिक एट्रिब्यूट का उपयोग करें: { className: () => (active ? 'active' : '') }।

चेनिंग

.when() और .else() एक नया बिल्डर लौटाते हैं और मूल को नहीं बदलते हैं। यदि आप .else() को दो बार कॉल करते हैं, तो अंतिम कॉल जीत जाती है।

fnसार्वजनिक API
function list<T>( items: () => readonly T[] | Iterable<T>, renderItem: (item: T, index: number) => ListRenderResult, ): ListModifier

प्रति आइटम एक पंक्ति रेंडर करता है। प्रत्येक update() पर, Nuclo फिर से items() को कॉल करता है और पहचान के आधार पर नए आइटम को मौजूदा पंक्तियों से मिलाता है (===)।

fruits.ts
let fruits = ['Apple', 'Banana']

const Fruits = () => ul(list(() => fruits, (fruit) => li(fruit)))

render(Fruits)

fruits.push('Cherry')
update()

पंक्तियों का मिलान कैसे किया जाता है

  • एक आइटम जो अभी भी वहां है, अपनी पंक्ति बनाए रखता है। यदि आवश्यक हो तो Nuclo पंक्ति को स्थानांतरित करता है, और renderItem को दोबारा कॉल नहीं करता है।
  • एक नए आइटम को एक नई पंक्ति मिलती है। हटाया गया आइटम अपनी पंक्ति खो देता है, और उसके onDestroy हुक चलते हैं।
  • डुप्लिकेट आइटमों में से प्रत्येक को अपनी पंक्ति मिलती है।
  • आप ऐरे को उसके स्थान पर बदल सकते हैं (push, splice, sort) या एक नई ऐरे लौटा सकते हैं।

बनी हुई पंक्ति दोबारा रेंडर नहीं होती, इसलिए बदलने वाले फ़ील्ड के लिए डायनेमिक मान उपयोग करें:

todos.ts
const todos = [{ text: 'Write docs', done: false }]

const Todos = () =>
  ul(
    list(() => todos, (todo) =>
      li({ className: () => (todo.done ? 'done' : '') }, () => todo.text),
    ),
  )

render(Todos)

todos[0].done = true
update() // same <li>, its class is now "done"

प्रतियां नए आइटम हैं, इसलिए ऐरे को todos.map((t) => ({ ...t })) से बदलने से प्रत्येक पंक्ति का पुनर्निर्माण होता है।

लाइवपंक्तियाँ ऑब्जेक्ट की पहचान से मिलाई जाती हैं। पंक्ति पर चेक लगाएँ, फिर Reverse दबाएँ: चेकबॉक्स सहित वही <li> खिसकता है।खोलें ↗

renderItem क्या लौटाता है

  • एक एलिमेंट लौटाएँ, जैसे li(...), tr(...) या SVG बिल्डर। किसी आइटम को छोड़ने के लिए null या undefined लौटाएँ। स्ट्रिंग, टेक्स्ट नोड और फ़्रैगमेंट कुछ भी रेंडर नहीं करते हैं।
  • सीधे when() या list() वापस न करें। उन्हें पंक्ति एलिमेंट के अंदर रखें: li(todo.text, when(() => todo.done, ' ✓'))।
  • आइटम छिपाने के लिए, items() में फ़िल्टर करें, renderItem में नहीं।
  • renderItem को दुष्प्रभावों से मुक्त रखें। Nuclo एक ही आइटम के लिए इसे एक से अधिक बार कॉल कर सकता है।

इंडेक्स

index आइटम की वह स्थिति है जब उसकी पंक्ति बनाई गई थी। पंक्तियों के हिलने पर यह अपडेट नहीं होता है। वर्तमान स्थिति के लिए, एक डायनेमिक मान का उपयोग करें: li(() => `${names.indexOf(name) + 1}. ${name}`)।

इटरेबल

किसी भी इटरेबल का उपयोग कर सकते हैं: ऐरे, Set, जनरेटर और map.values()। हर बार एक ही ऐरे या Set लौटाना ठीक है। एक इटरेटर, जैसे जनरेटर या map.values(), एक पास के बाद खत्म हो जाता है, इसलिए इसे प्रत्येक कॉल पर items() के अंदर बनाएँ। Map को पुनरावृत्त करने से हर बार सीधे नए [key, value] ऐरे बनती हैं, इसलिए प्रत्येक पंक्ति का पुनर्निर्माण किया जाता है। इसके बजाय map.values() का उपयोग करें।

प्लेसमेंट

list() जहाँ लिखा है, वहीं दो कमेंट मार्कर के बीच उसकी पंक्तियाँ दिखाई देती हैं। पैरेंट के दूसरे चाइल्ड नोड जस के तस रहते हैं। list() SVG बिल्डर के अंदर भी काम करता है।

fnसार्वजनिक API
function region(options: RegionOptions): NodeModFn function into(id: string, ...content: NodeModLike[]): NodeModFn function into(regions: Record<string, NodeModLike | NodeModLike[]>): NodeModFn interface RegionOptions { id: string type?: 'latest' | 'stack' // default: 'latest' empty?: NodeModLike }

region() ट्री में एक नामित स्थान को चिह्नित करता है। into() इसे कहीं और से, किसी भी गहराई से भर देता है। लेआउट सामग्री को प्रोप के रूप में प्राप्त नहीं करता है, और सामग्री बनाने वाले कंपोनेंट को यह नहीं पता होता है कि वह कहां पहुंचती है।

app.ts
const Layout = () =>
  div(
    aside(nav(a({ href: '/' }, 'Home'))),
    main(region({ id: 'main', empty: p('Nothing open.') })),
  )

// Anywhere in the tree, before or after the region
const Article = () => into('main', article(h1('Hello')))

render(() => div(Layout(), Article()))

यह कैसे काम करता है

  • into() केवल एक टिप्पणी छोड़ता है जहां यह लिखा जाता है। इसकी सामग्री रीजन के मार्करों के बीच रेंडर होती है।
  • क्रम मायने नहीं रखता। रीजन से पहले बना into() इंतज़ार करता है और रीजन बनते ही दिखाई देता है। व्यू उसी क्रम में दिखते हैं जिस क्रम में लिखे गए हैं।
  • सामग्री तब तक जीवित रहती है जब तक व्यू रहता है। जब into() रखने वाला when() बंद हो जाता है, या इसकी list() पंक्ति हटा दी जाती है, तो सामग्री रीजन छोड़ देती है। इसे दोबारा खोलने पर एक प्रति जुड़ती है, दो नहीं।
  • जब कोई रीजन चला जाता है, उदाहरण के लिए उसका when() बंद हो जाता है, तो उसके व्यू फिर से प्रतीक्षा करते हैं। बाद में उसी आईडी से बनाया गया रीजन उन्हें दिखाता है।
  • जिस into() की आईडी किसी रीजन से नहीं मिलती, वह कुछ रेंडर नहीं करता और त्रुटि भी नहीं देता। इसलिए आईडी की वर्तनी जाँचें।
  • आईडी ट्री में अद्वितीय हैं। लाइव आईडी के अंतर्गत निर्मित दूसरा रीजन एक चेतावनी लॉग करता है।
  • रीजन सर्वर रेंडरिंग के साथ काम करते हैं। hydrate() पर हर व्यू सर्वर से मिले HTML को दोबारा बनाने के बजाय इस्तेमाल करता है।

विकल्प

  • id: वह नाम जिससे into() रीजन खोजता है।
  • type: 'latest' (डिफ़ॉल्ट) केवल नवीनतम व्यू दिखाता है। 'stack' प्रत्येक व्यू को आगमन क्रम में दिखाता है।
  • empty: उस समय रेंडर किया गया जब इस रीजन में कोई व्यू नहीं है।

स्टैक्ड रीजन

type: 'stack' के साथ नया व्यू ऊपर खुलने पर नीचे का व्यू दोबारा नहीं बनता। उसका DOM, फ़ोकस और फ़ॉर्म स्टेट बना रहता है। पेज के ऊपर डायलॉग जैसी लेयर के लिए इसका उपयोग करें।

layers.ts
let open = false

const App = () =>
  div(
    region({ id: 'main', type: 'stack' }),
    into('main', form(input({ placeholder: 'Keeps its text' }))),
    when(() => open, into('main', div('On top'))),
  )

एक साथ कई रीजन

एक into() से एक से अधिक रीजनों को भरने के लिए एक ऑब्जेक्ट पास करें। एक मान एक चाइल्ड नोड या चाइल्ड नोड का ऐरे हो सकता है। दोनों रूपों को नेस्ट कर सकते हैं: किसी अन्य व्यू की सामग्री के अंदर एक into() अपने स्वयं के रीजन को लक्षित करता है।

page.ts
const Page = () =>
  into({
    main: article(h1('Eager route')),
    sidebar: [h2('Related'), ul(li('One'), li('Two'))],
  })

ट्री में गहराई से डायलॉग खोलना

ओवरले रीजन ट्री के रूट पर रहता है। कोई भी कंपोनेंट, किसी भी गहराई पर, into() को when() में लपेटकर एक डायलॉग खोलता है। when() को बंद करने से रीजन से डायलॉग हट जाता है।

dialog.ts
let confirming = false

const App = () =>
  div(
    Page(),
    div({ className: 'overlay' }, region({ id: 'overlay' })),
  )

// Deep inside Page()
const DeleteButton = () =>
  div(
    button('Delete', { onClick: () => { confirming = true; update() } }),
    when(() => confirming,
      into('overlay',
        dialog({ open: true },
          p('Delete this item?'),
          button('Cancel', { onClick: () => { confirming = false; update() } }),
        ),
      ),
    ),
  )

टोस्ट

list() की प्रत्येक पंक्ति एक व्यू है। एक स्टैक्ड रीजन उन्हें क्रम में दिखाता है, और ऐरे से किसी आइटम को हटाने से उसका टोस्ट हट जाता है।

toasts.ts
const toasts: { text: string }[] = []

const notify = (text: string) => {
  toasts.push({ text })
  update()
}

const dismiss = (toast: { text: string }) => {
  toasts.splice(toasts.indexOf(toast), 1)
  update()
}

const App = () =>
  div(
    button('Save', { onClick: () => notify('Saved.') }),
    div({ className: 'toasts' }, region({ id: 'toasts', type: 'stack' })),
    list(() => toasts, (toast) =>
      into('toasts', div(toast.text, button('×', { onClick: () => dismiss(toast) }))),
    ),
  )

पेज से शीर्षक सेट करना

डिफ़ॉल्ट type: 'latest' में सबसे नया व्यू दिखता है। लेआउट शीर्षक की जगह तय करता है और खुला पेज उसका टेक्स्ट देता है। बिना into('title', …) वाला पेज empty का टेक्स्ट दिखने देता है।

title.ts
let page: 'home' | 'settings' = 'home'

const Layout = () =>
  div(
    h1(region({ id: 'title', empty: 'My app' })),
    when(() => page === 'home', Home()).else(Settings()),
  )

const Home = () => p('Welcome.')

const Settings = () =>
  div(
    into('title', 'Settings'),
    form(/* … */),
  )

डायनेमिक मान

into() की सामग्री सामान्य Nuclo सामग्री है। इसमें फ़ंक्शन स्टेट-निर्भर मान हैं और रीजन जहां भी हो, update() पर ताज़ा होते हैं।

status.ts
const selected = new Set<string>()

const Layout = () =>
  div(
    header(region({ id: 'status' })),
    Table(),
  )

const Table = () =>
  div(
    into('status', () => `${selected.size} selected`),
    list(() => rows, (row) =>
      label(
        input({ type: 'checkbox', onChange: (e) => {
          e.currentTarget.checked ? selected.add(row.id) : selected.delete(row.id)
          update()
        } }),
        row.name,
      ),
    ),
  )

एक रीजन जो आता और जाता है

डैशबोर्ड का into() एक ही बार लिखा गया है। उपयोगकर्ता साइन आउट हो, तो main रीजन नहीं होता और व्यू इंतज़ार करता है। साइन इन करने पर रीजन बनता है और व्यू दिखता है। साइन आउट करने पर रीजन हटता है और व्यू फिर इंतज़ार करता है।

auth.ts
let user: { name: string } | null = null

const App = () =>
  div(
    when(() => user !== null,
      main(region({ id: 'main' })),
    ).else(
      LoginForm(),
    ),
    into('main', Dashboard()),
  )

अपने रीजन के साथ एक व्यू

एक व्यू में region() हो सकता है, और अन्य व्यू इसे लक्षित कर सकते हैं। यहां संपादक पृष्ठ अपने साथ एक tabs रीजन लाता है, और प्रत्येक टैब इसे भरता है। जब संपादक बंद हो जाता है, तो उसका रीजन उसके साथ चला जाता है।

editor.ts
let tab: 'code' | 'preview' = 'code'

const Editor = () =>
  into('main',
    div(
      nav(
        button('Code', { onClick: () => { tab = 'code'; update() } }),
        button('Preview', { onClick: () => { tab = 'preview'; update() } }),
      ),
      section(region({ id: 'tabs' })),
    ),
    when(() => tab === 'code', into('tabs', textarea()))
      .else(into('tabs', iframe({ src: '/preview' }))),
  )

लाइव उदाहरण

region-app.ts
import 'nuclo'

let page: 'home' | 'article' | null = 'home'
let dialog = false

const go = (next: typeof page) => () => {
  page = next
  dialog = false
  update()
}

const Layout = () =>
  div(
    nav(
      button('Home', { onClick: go('home') }),
      button('Article', { onClick: go('article') }),
      button('Close page', { onClick: go(null) }),
    ),
    main(
      region({ id: 'main', type: 'stack', empty: p('Nothing open.') }),
    ),
    aside(region({ id: 'side', empty: p('No related links.') })),
  )

// Pages fill the layout's regions from outside it.
const Home = () =>
  into('main',
    input({ placeholder: 'Type here, then open the dialog' }),
    button('Open dialog', {
      onClick: () => { dialog = true; update() },
    }),
  )

const Article = () =>
  into({
    main: article(h2('Article'), p('One into() fills both regions.')),
    side: [h2('Related'), ul(li('One'), li('Two'))],
  })

// Stacks over Home, so Home keeps its DOM and the text you typed.
const Dialog = () =>
  into('main',
    div(
      p('On top of Home.'),
      button('Close', { onClick: () => { dialog = false; update() } }),
    ),
  )

export const RegionApp = () =>
  div(
    Layout(),
    when(() => page === 'home', Home()),
    when(() => page === 'article', Article()),
    when(() => dialog, Dialog()),
  )
लाइवस्टैक्ड मुख्य रीजन और साइड रीजन वाला लेआउट बाहर से भरा जाता है। Home में लिखें, डायलॉग खोलकर बंद करें: टेक्स्ट बना रहता है। Article एक into() से दोनों रीजन भरता है।खोलें ↗

No related links.

fnसार्वजनिक API
function forceUpdate(): void function forceUpdate(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element): ExpandedElement

update() केवल डायनेमिक मान चलाता है। h1(labels[lang].title) के टेक्स्ट जैसे स्टैटिक मान ट्री बनते समय तय होते हैं। forceUpdate() कंपोनेंट दोबारा कॉल करके लाइव DOM को नए परिणाम के अनुसार पैच करता है, स्टैटिक मान सहित। भाषा बदलने जैसे कभी-कभार पूरे ऐप में होने वाले बदलावों के लिए इसका उपयोग करें। बाकी बदलावों के लिए update() उपयोग करें।

i18n.ts
const labels = { en: { title: 'Hello' }, pt: { title: 'Olá' } }
let lang: keyof typeof labels = 'en'

const App = () => div(h1(labels[lang].title))
render(App, root)

lang = 'pt'
forceUpdate() // same <h1> element, now "Olá"
लाइवupdate() केवल डायनेमिक lang पंक्ति अपडेट करता है। forceUpdate() App को दोबारा कॉल करके उसी <h1> को पैच करता है। स्पष्ट रूप केवल इसी ट्री को दोबारा बनाता है, पूरे पेज को नहीं।खोलें ↗

lang = 'en'

Hello

Static text, fixed on build.

बिना आर्ग्युमेंट के forceUpdate(), render(App) या hydrate(App) से रेंडर हुए हर रूट को दोबारा बनाता है। render(App()) से बने रूट छोड़ दिए जाते हैं। कंपोनेंट थ्रो करे, तो त्रुटि लॉग होती है और उस रूट का पुराना DOM बना रहता है। नई बिल्ड लागू करते समय त्रुटि हो, तो वह भी लॉग होती है, लेकिन पहले से पैच हुए हिस्से वैसे ही रहते हैं।

क्या रहता है और क्या बदलता है

  • चाइल्ड नोड का मिलान क्रम से, टैग द्वारा किया जाता है। एक मिलान एलिमेंट का पुन: उपयोग किया जाता है, इसलिए फोकस और टाइप किए गए इनपुट मान बने रहते हैं, और इसके onMount हुक दोबारा नहीं चलते हैं। एक बार जब एक चाइल्ड नोड का टैग अलग हो जाता है, तो उस चाइल्ड नोड और उसके बाद के हर सिब्लिंग नोड का निर्माण नए सिरे से होता है।
  • टेक्स्ट, एट्रिब्यूट, क्लास, स्टाइल, लिसनर, when() की शाखाएँ और list() की पंक्तियाँ नए निर्माण के अनुसार अपडेट होती हैं।
  • नए एलिमेंट बनते हैं और उनके onMount हुक चलते हैं। नई बिल्ड में न आने वाले एलिमेंट हटते हैं और उनके onDestroy हुक चलते हैं।
  • list() पंक्तियों का मिलान स्थिति से भी किया जाता है, आइटम से नहीं, इसलिए पुन: उपयोग किया गया पंक्ति एलिमेंट एक अलग आइटम दिखा सकता है।
  • नई बिल्ड में सेट न किए गए एट्रिब्यूट हटाए नहीं जाते हैं।
देखें: कंपोनेंट फ़ंक्शन के अंदर घोषित स्टेट रीसेट हो जाती है, क्योंकि फ़ंक्शन फिर से चलता है। जो स्टेट बचाए रखना हो, उसे कंपोनेंट के बाहर रखें।

आर्ग्युमेंट वाला रूप। forceUpdate(App(), parent) parent में एक ट्री का पुनर्निर्माण करता है और उसका रूट लौटाता है। यह बाद की कॉल के लिए रूट को पंजीकृत नहीं करता है। hydrate() की तरह, यह उम्मीद करता है कि रूट parent में पहला नोड होगा। यदि पहले नोड में एक अलग टैग है, तो उसे बदल दिया जाता है।

सर्वर पर, forceUpdate() कुछ नहीं करता है।

render()

#
fnसार्वजनिक API
function render(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element, index?: number): ExpandedElement

आपका ट्री बनाता है और उसके मूल एलिमेंट को parent में जोड़ता है। parent डिफ़ॉल्ट रूप से document.body है। यह मूल एलिमेंट लौटाता है।

main.ts
import 'nuclo'
import { App } from './app.ts'

render(App, document.getElementById('app')!)
  • आप कंपोनेंट (render(App)) या उसके लौटाए बिल्डर (render(App())) को पास कर सकते हैं। DOM वही है। केवल render(App) forceUpdate() को बाद में रूट का पुनर्निर्माण करने देता है, इसलिए इसे प्राथमिकता दें।
  • यह जुड़ता है। यह parent को साफ़ नहीं करता है, इसलिए इसे दो बार कॉल करने से दो प्रतियां जुड़ जाती हैं।
  • render() के वापस आने पर ट्री में प्रत्येक onMount चल चुका होता है।
  • दस्तावेज़ में मौजूद एलिमेंट में रेंडर करें। update() देखें।
  • रूट एक एलिमेंट होना चाहिए, जैसे div(...), when() या list() नहीं।
  • index आंतरिक है। यह यह नहीं चुनता कि एलिमेंट कहाँ डाला गया है। इसे छोड़ दें।

render(App) के रूप में पारित एक कंपोनेंट को कोई पैरामीटर नहीं लेना चाहिए। अनिवार्य पैरामीटर टाइप त्रुटि देता है। वैकल्पिक पैरामीटर टाइप जाँच पास कर लेता है, लेकिन render() रनटाइम पर थ्रो करता है। एक कंपोनेंट लपेटें जो प्रॉप्स लेता है:

main.ts
const Page = (props: { name: string }) => div(h1('Hi ', props.name))

render(() => Page({ name: 'Ana' }), root)

hydrate()

#
fnसार्वजनिक API
function hydrate(nodeModFn: NodeModFn | (() => NodeModFn), parent?: Element): ExpandedElement

renderToString() से बने HTML को क्लाइंट पर सक्रिय करता है। यह मौजूदा एलिमेंट का पुन: उपयोग करता है, इवेंट लिसनर को जोड़ता है, और डायनेमिक मान, when() और list() को जोड़ता है, इसलिए update() बाद में काम करता है। फिर यह onMount हुक चलाता है। parent डिफ़ॉल्ट रूप से document.body है। यह मूल एलिमेंट लौटाता है।

client.ts
import 'nuclo'
import { App } from './app.ts'

hydrate(App, document.getElementById('root')!)
  • सर्वर के समान कंपोनेंट और समान डेटा का उपयोग करें।
  • एक समर्पित कंटेनर में हाइड्रेट करें। ऐप का रूट उसका पहला नोड होना चाहिए, हालांकि व्हाइटस्पेस ठीक है। यदि कोई अन्य नोड पहले आता है, तो Nuclo उस नोड को बदल देता है और सर्वर कॉपी को पीछे छोड़ देता है।
  • render() की तरह App पास करें, App() नहीं, ताकि forceUpdate() बाद में रूट दोबारा बना सके। App को कोई पैरामीटर नहीं लेना चाहिए। वैकल्पिक पैरामीटर होने पर hydrate() चुपचाप कुछ भी नहीं जोड़ता। प्रॉप्स लेने वाले कंपोनेंट को फ़ंक्शन में लपेटें: hydrate(() => Page(props), root)।
  • यदि कोई सर्वर HTML नहीं है, तो render() का उपयोग करें।

जब सर्वर और क्लाइंट भिन्न होते हैं

क्लाइंट के मान लागू होते हैं:

  • एक अलग रूट टैग: उस नोड को ताज़ा DOM से बदल दिया गया है। एक खाली कंटेनर: ताज़ा DOM जोड़ा गया है।
  • अलग टेक्स्ट पैच किया गया है। विभिन्न list() आइटम और when() शाखाएँ क्लाइंट के अनुसार बदलती हैं।
  • अतिरिक्त सर्वर चिल्ड्रेन हटा दिए जाते हैं, और गायब चिल्ड्रन बनाए जाते हैं।
  • एट्रिब्यूट जो सर्वर ने लिखीं लेकिन क्लाइंट ने सेट नहीं कीं, उन्हें वैसे ही छोड़ दिया गया है।

renderToString() HTML टिप्पणियों को मार्कर के रूप में लिखता है, जैसे प्रत्येक टेक्स्ट नोड से पहले <!-- text-0 -->। hydrate() को उनकी आवश्यकता है, इसलिए सर्वर HTML से टिप्पणियाँ न हटाएँ। मार्करों के बिना, हाइड्रेशन list() पंक्तियों को हटा देता है, और list() और when() अपडेट करना बंद कर देते हैं।

सर्वर रेंडरिंग

#
fnसार्वजनिक API
// import { ... } from 'nuclo/ssr' function renderToString(input: RenderableInput): string function renderManyToString(inputs: RenderableInput[]): string[] function renderToStringWithContainer(input: RenderableInput, containerTag?: string, containerAttrs?: Record<string, string>): string function getCssText(): string type RenderableInput = NodeModFn | (() => NodeModFn) | Element | Node | null | undefined

सर्वर पर एक कंपोनेंट को HTML स्ट्रिंग में रेंडर करें। ब्राउज़र में, उस HTML को इंटरैक्टिव बनाने के लिए उसी कंपोनेंट के साथ hydrate() कॉल करें। सर्वर रनटाइम में कोई document नहीं है, इसलिए पहले nuclo/polyfill इम्पोर्ट करें।

app.ts
let count = 0
const heading = css({ color: 'red' })

export const App = () =>
  main(
    h1(heading, () => `Count: ${count}`),
    button({ onClick: () => { count++; update() } }, '+1'),
  )
server.ts
import 'nuclo/polyfill'
import 'nuclo'
import { renderToString, getCssText } from 'nuclo/ssr'
import { App } from './app.ts'

const html = renderToString(App)
const page = `<!doctype html>
<html>
  <head><style id="nuclo-styles">${getCssText()}</style></head>
  <body>
    <div id="root">${html}</div>
    <script type="module" src="/client.js"></script>
  </body>
</html>`
client.ts
import 'nuclo'
import { App } from './app.ts'

hydrate(App, document.getElementById('root')!)

स्टाइल टैग पर id="nuclo-styles" रखें। क्लाइंट उस एलिमेंट का पुन: उपयोग करता है और केवल नए नियम जोड़ता है। getCssText() देखें।

सर्वर पर क्या चलता है

  • प्रत्येक renderToString() कॉल ट्री बनाता है और प्रत्येक डायनेमिक मान को एक बार चलाता है।
  • इवेंट हैंडलर, onMount और onDestroy कभी नहीं चलते। update(), scope() और forceUpdate() कुछ नहीं करते।
  • रेंडरिंग सिंक्रोनस है। पहले अपना डेटा लोड करें, फिर रेंडर करें।
  • रेंडर के बाद कुछ भी पंजीकृत नहीं रहता है, इसलिए एक प्रक्रिया कई अनुरोधों को पूरा कर सकती है। अनुरोध डेटा को आर्ग्युमेंट के रूप में पास करें, जैसे renderToString(Page(user))। मॉड्यूल-स्तरीय चर सभी अनुरोधों द्वारा साझा किए जाते हैं।
  • सर्वर पर render() को कॉल न करें। यह साझा document.body से जुड़ता है।

renderToString()

एक बिल्डर (App()), एक कंपोनेंट (App) या एक DOM नोड लेता है। null और undefined "" लौटाते हैं। रूट एक एलिमेंट होना चाहिए. renderToString(Page) के रूप में पारित कंपोनेंट को कोई पैरामीटर नहीं लेना चाहिए, इसलिए डेटा लेने वाले कंपोनेंट के लिए Page(user) पास करें।

example.ts
renderToString(p('Hello'))
// '<p><!-- text-0 -->Hello</p>'
  • टेक्स्ट और एट्रिब्यूट मान HTML-एस्केप्ड हैं। script() और style() के अंदर टेक्स्ट वैसे ही लिखा गया है, केवल क्लोज़िंग टैग को तोड़ा जाता है, इसलिए वहां कभी भी यूजर इनपुट न डालें।
  • यह त्रुटि बाहर थ्रो नहीं करता। कंपोनेंट, list() का फ़ंक्शन या when() की शर्त विफल हो, या पॉलीफ़िल न मिले, तो यह त्रुटि लॉग करके "" लौटाता है। ऐसी स्थिति में अनुरोध विफल करना हो, तो खाली परिणाम की जाँच करें। टेक्स्ट या एट्रिब्यूट फ़ंक्शन थ्रो करे, तो केवल उसका मान खाली रेंडर होता है।

renderManyToString()

inputs.map(renderToString) के समान।

renderToStringWithContainer()

आउटपुट को एक एलिमेंट में लपेटता है। containerTag डिफ़ॉल्ट रूप से "div" पर और containerAttrs पर डिफ़ॉल्ट रूप से {} पर सेट होता है। एट्रिब्यूट मान एस्केप होते हैं। एट्रिब्यूट के नाम दिए गए अनुसार लिखे गए हैं, इसलिए class जैसे HTML नामों का उपयोग करें। उपयोगकर्ता इनपुट को कभी भी टैग के रूप में पास न करें।

example.ts
renderToStringWithContainer(p('Hi'), 'section', { id: 'main', class: 'page' })
// '<section id="main" class="page"><p><!-- text-0 -->Hi</p></section>'

nuclo/polyfill

#
import 'nuclo/polyfill' // Named exports: // document, Event, CustomEvent, Node, Element, HTMLElement, // NucloDocument, NucloElement, NucloText, NucloNode

एक छोटा DOM ताकि Nuclo Node और Bun में ट्री बना सके। अपने पहले रेंडर से पहले इसे एक बार इम्पोर्ट करें। अधिकांश ऐप्स को केवल साधारण इम्पोर्ट की आवश्यकता होती है।

  • जब कोई window नहीं होता है, तो यह globalThis.document, Node, Element और HTMLElement सेट करता है, लेकिन केवल वे जो गायब हैं। यह window नहीं बनाता है।
  • किसी ब्राउज़र में, या jsdom के अंतर्गत, यह कुछ नहीं करता है।
  • Event और CustomEvent रनटाइम के स्वयं के कंस्ट्रक्टर हैं। Node, Element और HTMLElement पॉलीफ़िल क्लास के उपनाम हैं।

यह ट्री बनाने के लिए DOM है, ब्राउज़र नहीं। इसमें कोई innerHTML नहीं है और कोई getElementById नहीं है, querySelector() हमेशा null लौटाता है, और इवेंट लिसनर को नजरअंदाज कर दिया जाता है। HTML प्राप्त करने के लिए renderToString() का उपयोग करें।

css()

#
fnसार्वजनिक API
function css(style: Style): StyleResult function css(name: string, style: Style): StyleResult type StyleResult = { className: string }

टाइपयुक्त स्टाइल ऑब्जेक्ट को जेनरेट किए गए CSS क्लास में बदल देता है। परिणाम को किसी भी टैग बिल्डर को भेजें। ब्राउज़र में, नियम एक <style id="nuclo-styles"> एलिमेंट में जाते हैं। सर्वर पर, getCssText() उन्हें लौटाता है।

card.ts
const card = css({
  p: 16,
  rounded: 8,
  bg: '#fff7ed',
  hover: { bg: '#ffedd5' },
})

const Card = () => div(card, 'Simple card')
  • css() आधार घोषणाओं के लिए एक क्लास बनाता है, साथ ही प्रत्येक स्यूडो कुंजी, सेलेक्टर और मीडिया ब्लॉक के लिए एक क्लास बनाता है। उपरोक्त कार्ड को दो क्लास मिलते हैं।
  • क्लास नाम सामग्री के हैश हैं। समान स्टाइल एक क्लास साझा करती हैं, और सर्वर और क्लाइंट समान नाम उत्पन्न करते हैं।
  • मॉड्यूल स्तर पर एक बार स्टाइल को परिभाषित करें। css() ऑब्जेक्ट द्वारा कैश होता है, इसलिए प्रत्येक कॉल पर एक नया ऑब्जेक्ट हर बार फिर से संसाधित होता है।
  • किसी स्टाइल ऑब्जेक्ट को css() में पास करने के बाद उसे बदलने से कुछ नहीं होता है।
  • प्रत्येक स्टाइल पृष्ठ या सर्वर प्रक्रिया के जीवनकाल के लिए पंजीकृत रहती है। उन मानों के लिए जो प्रति आइटम बदलते हैं, जैसे डेटा से चौड़ाई, style एट्रिब्यूट का उपयोग करें।

नाम वाली स्टाइल

पठनीय क्लास नाम पाने के लिए पहले एक नाम पास करें:

title.ts
const pageTitle = css('page-title', { text: 24, hover: { color: 'red' } })

pageTitle.className // "page-title page-title-<hash>"
  • बेस क्लास बिल्कुल नाम है। अन्य ब्लॉकों को name-<hash> मिलता है।
  • नाम एक वैध क्लास नाम होना चाहिए। अन्यथा Nuclo चेतावनी देता है और एक उत्पन्न करता है।
  • नाम ग्लोबल हैं। यदि दो अलग-अलग स्टाइल एक ही नाम का उपयोग करती हैं, तो Nuclo चेतावनी देता है।

परिणाम का उपयोग करना

example.ts
const card = css({ p: 8 })

div(card)                           // best
div(card, { className: 'featured' }) // className merges
div(card, { class: 'featured' })     // replaces: only "featured" is left

String(card) और card.className दोनों क्लास नाम लौटाते हैं।

स्टाइल ऑब्जेक्ट

#

css() को दिए ऑब्जेक्ट में camelCase CSS प्रॉपर्टी, शॉर्टहैंड और स्यूडो स्टेट, सेलेक्टर व मीडिया क्वेरी के लिए नेस्टेड कुंजियाँ होती हैं। TypeScript इन सभी के लिए ऑटोकंप्लीट देता है। टाइपयुक्त सेट से बाहर की प्रॉपर्टी, जैसे borderTopLeftRadius, fill या paddingInline, raw में रखें (नीचे देखें)।

मान

  • संख्याएँ 0 और इकाई रहित प्रॉपर्टी को छोड़कर px बन जाती हैं: zIndex, opacity, fontWeight, lineHeight, flex, flexGrow, flexShrink, order, aspectRatio, gridColumn और gridRow। raw में, zoom, scale, column-count, orphans, widows, tab-size और animation-iteration-count भी यूनिटलेस हैं।
  • स्ट्रिंग्स का उपयोग लिखित रूप में किया जाता है। null, undefined और false को छोड़ दिया गया है।
  • lineHeight (और leading) इकाई रहित है, इसलिए leading: 24 का अर्थ फ़ॉन्ट आकार का 24 गुना है। पिक्सेल के लिए "24px" लिखें।
example.ts
css({ p: 16, m: 0, opacity: 0.5, leading: 1.5, z: 10, w: '50%' })
// padding:16px; margin:0; opacity:0.5; line-height:1.5; z-index:10; width:50%

शॉर्टहैंड

कुंजीCSS
p, pt, pr, pb, plपैडिंग, और प्रत्येक साइड
px / pyपैडिंग बाएँ और दाएँ / ऊपर और नीचे
m, mt, mr, mb, ml, mx, myमार्जिन के लिए वही दिशाएँ
w, h, minW, maxW, minH, maxHचौड़ाई, ऊंचाई और उनकी न्यूनतम और अधिकतम
sizeचौड़ाई और ऊंचाई
bgपृष्ठभूमि
text / font / weightfont-size / font-family / font-weight
leading / trackingline-height / letter-spacing
align / items / justifytext-align / align-items / justify-content
z / rounded / shadow / selectz-index / border-radius / box-shadow / user-select

ये कुंजियाँ true लेती हैं:

कुंजीCSS
rowdisplay: flex; flex-direction: row
coldisplay: flex; flex-direction: column
centeralign-items: center; justify-content: center (डिस्प्ले सेट नहीं है)
truncateoverflow: hidden; text-overflow: ellipsis; white-space: nowrap

स्यूडो स्टेट

hover, focus, focusVisible, focusWithin, active, visited, disabled, enabled, checked, required, invalid, valid, readOnly, first (:first-child), last (:last-child), only (:only-child), odd, even, empty, placeholderShown, placeholder, before, after, selection, marker, firstLine और firstLetter। इन्हें नेस्ट कर सकते हैं: hover: { focus: {…} } :hover:focus देता है।

सेलेक्टर और नियम

एक कुंजी जो & से शुरू होती है वह एक सेलेक्टर है, और प्रत्येक & क्लास बन जाता है। एक कुंजी जो @ से शुरू होती है वह एक नियम है: @media, @container या @supports।

menu.ts
const menu = css({
  p: 8,
  '& > li': { py: 4 },
  '& > li:hover': { color: 'red' },
  '&:is(.dark *)': { color: 'white' },
  '@media (min-width: 768px)': { p: 16 },
})
  • कुंजी & से शुरू होनी चाहिए। ".dark &" जैसी कुंजी को नजरअंदाज कर दिया जाता है, इसलिए "&:is(.dark *)" लिखें।
  • दो नेस्टेड @media ब्लॉक and से जुड़ते हैं। मीडिया टाइप (print, only screen) से शुरू होने वाली क्वेरी पहले रखी जाती है, क्योंकि सिंटैक्स में टाइप केवल शुरुआत में आ सकता है। यदि दोनों को एक क्वेरी में नहीं बदल सकते—दो मीडिया टाइप हों या शुरुआत में not हो—तो चेतावनी के साथ केवल अंदर वाली क्वेरी इस्तेमाल होती है। कोई भी दूसरा नेस्टेड एट-रूल बाहर वाले को बदल देता है।

raw

किसी भी प्रॉपर्टी के लिए raw का उपयोग करें, नाम बिल्कुल CSS जैसा लिखा गया है। संख्याओं में यहाँ भी px जुड़ता है, इसलिए स्ट्रिंग पास करें:

example.ts
css({ raw: { '--gap': '8px', '-webkit-line-clamp': '2' } })

किसी भी अन्य फ़्लैट कुंजी को कबाब-केस में बदल दिया जाता है और उसे वैसे ही लिखा जाता है, इसलिए colr: 'red' जैसी टाइपो एक घोषणा बन जाती है जिसे ब्राउज़र ड्रॉप कर देता है। किसी कुंजी के अंतर्गत एक नेस्टेड ऑब्जेक्ट जो एक स्यूडो स्थिति, एक स्क्रीन, या एक & या @ कुंजी नहीं है, को नजरअंदाज कर दिया जाता है। TypeScript दोनों की रिपोर्ट करता है।

fnसार्वजनिक API
function cx(...inputs: ClassInput[]): StyleResult type ClassInput = StyleResult | string | false | null | undefined | ClassInput[]

स्टाइल मर्ज करता है। दो इनपुट एक ही प्रॉपर्टी सेट करें, तो बाद वाले मान को प्राथमिकता मिलती है।

example.ts
const base = css({ p: 8, color: 'red' })
const blue = css({ color: 'blue' })

cx(base, blue)                  // padding: 8px; color: blue
cx(base, false, null, 'extra')  // base's classes plus "extra"
div(cx(base, blue), 'Blue text')
  • falsy इनपुट छोड़ दिए जाते हैं। ऐरे फ़्लैट किए जाते हैं। स्ट्रिंग में दिए क्लास नाम अलग किए जाते हैं और दोहराए गए नाम हटा दिए जाते हैं।
  • एक ही ब्लॉक (आधार, एक ही स्यूडो, एक ही मीडिया क्वेरी) के लिए जेनरेट की गई क्लास एक नई क्लास में विलय हो जाती हैं। अन्य क्लास के नाम अपरिवर्तित रहते हैं।
  • केवल cx() आर्ग्युमेंट क्रम जानता है। div(base, blue) के साथ, दोनों क्लास जोड़े जाते हैं, और जो नियम सबसे बाद में बना था उसे प्राथमिकता मिलती है। एक cx() कॉल में एक-दूसरे को ओवरराइड करने वाली स्टाइल डालें।
  • जब एक नामित स्टाइल का ब्लॉक किसी अन्य इनपुट के ब्लॉक के साथ विलय हो जाता है, तो मर्ज की गई क्लास name-<hash> होता है, न कि मूल नाम। जो ब्लॉक मर्ज नहीं होते हैं वे अपनी क्लास बनाए रखते हैं, इसलिए cx(pageTitle, 'extra') में अभी भी page-title है।

डायनेमिक क्लास

एक फ़ंक्शन पास करें जो हमेशा cx(...) लौटाता है:

toggle.ts
const baseButton = css({ px: 12, py: 8, border: '1px solid #ccc' })
const activeButton = css({ bg: '#ff3f00', color: 'white' })

let active = false

const Toggle = () =>
  button(
    () => cx(baseButton, active && activeButton),
    () => (active ? 'Active' : 'Inactive'),
    { onClick: () => { active = !active; update() } },
  )

फ़ंक्शन को अपने पहले रन पर एक स्टाइल परिणाम लौटाना होगा। () => active && activeButton एक क्लास के बजाय "false" टेक्स्ट को रेंडर करता है।

createCss()

#
fnसार्वजनिक API
function createCss<const T extends ThemeConfig>(theme?: T): CssInstance<T> // CssInstance<T> = { css, cx, variants, keyframes, globalStyle, theme }

एक थीम से बंधे अपने स्वयं के css, cx, variants, keyframes और globalStyle लौटाता है। एक थीम मानों को नाम देती है: नीचे दी गई थीम के साथ, borderColor: 'border' #e5e7eb लिखता है, और TypeScript नामों को स्वतः पूर्ण करता है। screens में प्रत्येक प्रविष्टि मीडिया क्वेरी के लिए एक स्टाइल कुंजी बन जाती है, जैसे md: {…}।

theme.ts
export const { css, cx, variants } = createCss({
  colors: { primary: '#ff3f00', border: '#e5e7eb' },
  radii: { card: '12px' },
  screens: { md: '(min-width: 768px)' },
})

const panel = css({
  p: 12,
  rounded: 'card',
  border: '1px solid',
  borderColor: 'border',
  md: { p: 24 },
})
थीम कुंजीकहाँ उपयोग होती है
colorsbg, backgroundColor, color, borderColor और प्रत्येक पक्ष, outlineColor, caretColor, textDecorationColor
fontsfont, fontFamily
shadowsshadow, boxShadow
radiirounded, borderRadius
screensनई स्टाइल कुंजियाँ, जैसे md: {…}
  • एक थीम नाम तभी हल होता है जब वह संपूर्ण मान हो। border: "1px solid primary" में नाम हल नहीं होता।
  • @ के बिना एक स्क्रीन मान @media <value> हो जाता है। @ से शुरू होने वाला मान लिखित रूप में उपयोग किया जाता है, उदाहरण के लिए "@container (min-width: 400px)"।
  • स्क्रीन केवल createCss() से बने इंस्टेंस पर मौजूद हैं। ग्लोबल css() में कोई नहीं है, लेकिन यह @media कुंजी स्वीकार करता है।
  • सभी इंस्टेंस और ग्लोबल हेल्पर एक स्टाइलशीट साझा करते हैं। स्क्रीन के मीडिया नियम घोषणा के क्रम में लिखे जाते हैं, उपयोग के क्रम में नहीं। इसलिए min-width स्क्रीन छोटे से बड़े क्रम में लिखें। हर ऐप में एक createCss() इंस्टेंस रखें ताकि क्रम आपकी थीम के अनुसार हो।
  • theme वही थीम है जो आपने पास की थी।

variants()

#
fnसार्वजनिक API
function variants(config: { base?: Style variants?: { [group: string]: { [value: string]: Style } } defaultVariants?: { [group: string]: value } compoundVariants?: ({ [group: string]: value } & { css: Style })[] }): (props?: { [group: string]: value }) => StyleResult

एक फ़ंक्शन बनाता है जो विकल्पों के एक सेट के लिए एक स्टाइल लौटाता है, जैसे कि बटन का उद्देश्य और आकार।

button.ts
const buttonClass = variants({
  base: { px: 12, py: 8, rounded: 6 },
  variants: {
    intent: { primary: { bg: '#2563eb' }, danger: { bg: '#dc2626' } },
    size: { sm: { text: 12 }, lg: { text: 18 } },
    block: { true: { w: '100%' } },
  },
  defaultVariants: { intent: 'primary', size: 'sm' },
  compoundVariants: [{ intent: 'danger', size: 'lg', css: { weight: 700 } }],
})

button(buttonClass(), 'Save')                                                // primary, sm
button(buttonClass({ intent: 'danger', size: 'lg', block: true }), 'Delete') // bold, full width
  • स्टाइल इस क्रम में मर्ज होते हैं: base, हर समूह का चुना मान और फिर हर मिलता कंपाउंड वैरिएंट। हर प्रॉपर्टी के लिए बाद वाले मान को प्राथमिकता मिलती है।
  • एक अनुपलब्ध, undefined या null विकल्प डिफ़ॉल्ट का उपयोग करता है। बिना किसी डिफ़ॉल्ट वाले समूह को छोड़ दिया जाता है।
  • केवल true और false कुंजियों वाला एक समूह एक बूलियन लेता है।
  • एक कंपाउंड तब लागू होता है जब इसमें सूचीबद्ध प्रत्येक समूह मेल खाता है, डिफ़ॉल्ट शामिल हैं।
  • प्रत्येक चयन एक बार बनाया जाता है और कैश किया जाता है।

keyframes() और globalStyle()

#
fnसार्वजनिक API
function keyframes(frames: KeyframeFrames): string function globalStyle(selector: string, style: FlatStyle): void

keyframes() एक एनीमेशन पंजीकृत करता है और उसका उत्पन्न नाम लौटाता है। फ़्रेम कुंजियाँ from, to और प्रतिशत हैं, जैसे "50%" या "0%, 100%"। फ़्रेम केवल फ़्लैट प्रॉपर्टी लेते हैं. समान फ़्रेम समान नाम लौटाते हैं.

fade.ts
const fadeIn = keyframes({
  from: { opacity: 0 },
  to: { opacity: 1 },
})

const panel = css({ animation: `${fadeIn} 200ms ease-out` })

globalStyle() किसी भी सेलेक्टर के लिए एक नियम जोड़ता है:

reset.ts
globalStyle('*, *::before, *::after', { boxSizing: 'border-box' })
globalStyle('body', { m: 0, font: 'system-ui, sans-serif' })
  • सेलेक्टर का उपयोग बिल्कुल वैसे ही किया गया है जैसा लिखा गया है।
  • केवल फ्लैट प्रॉपर्टी ही काम करती हैं। स्यूडो कुंजियाँ, स्क्रीन और @ कुंजियाँ नजरअंदाज कर दी जाती हैं, इसलिए globalStyle() मीडिया नियम नहीं लिख सकता।
  • एक ही नियम एक बार जोड़ा जाता है। एक ही सेलेक्टर के लिए एक अलग स्टाइल दूसरा नियम जोड़ती है, और बाद वाला जीतता है।

getCssText() और resetStyles()

#
fnसार्वजनिक API
function getCssText(): string // from 'nuclo/ssr' or 'nuclo' function resetStyles(): void // from 'nuclo'

getCssText() हर नियम लौटाता है जो css(), cx(), variants(), keyframes() और globalStyle() ने इस प्रक्रिया में बनाया है: पहले आधार नियम, फिर @media, @container और @supports ब्लॉक। सर्वर रेंडरिंग के लिए इसका उपयोग करें।

  • इसे renderToString() के बाद कॉल करें। रेंडर के दौरान बनाए गए नियम शामिल हैं।
  • यह एक पेज या अनुरोध को नहीं बल्कि पूरी प्रक्रिया को कवर करता है, और यह केवल बढ़ता है। यह सुरक्षित है, क्योंकि क्लास के नाम सामग्री हैश हैं।
  • इसे <style id="nuclo-styles"> में डालें। क्लाइंट उस एलिमेंट का पुन: उपयोग करता है और उसमें पहले से मौजूद नियमों को छोड़ देता है। किसी दूसरी आईडी के साथ, या बिना आईडी के, क्लाइंट प्रत्येक नियम के साथ फिर से एक दूसरी स्टाइलशीट जोड़ता है।
  • टेक्स्ट एस्केप नहीं होता, इसलिए स्टाइल मानों में कभी भी अविश्वसनीय इनपुट न डालें।

resetStyles() एक परीक्षण हेल्पर है। यह प्रत्येक नियम और #nuclo-styles एलिमेंट को हटा देता है। आपके द्वारा पहले बनाई गई स्टाइल अपने क्लास के नाम तो रखती हैं लेकिन अपना CSS खो देती हैं। इसे किसी ऐप या सर्वर पर कॉल न करें।

setup.test.ts
import { resetStyles } from 'nuclo'

beforeEach(() => resetStyles())
अलग पैकेज: nuclo-router Nuclo कोर पैकेज का हिस्सा नहीं है। इसे nuclo के साथ इंस्टॉल करें। यह एक प्रीव्यू रिलीज़ है, इसलिए इसका API अभी भी बदल सकता है।

Nuclo के लिए रूटिंग: सर्वर रेंडरिंग, हाइड्रेशन, कोड-स्प्लिट रूट और आइडल प्रीलोडिंग, list() और update() पर निर्मित। gzip के बाद लगभग 4.9 KB। Nuclo के अलावा कोई डिपेंडेंसी नहीं और कोई साइड इफ़ेक्ट नहीं, इसलिए इस्तेमाल न होने पर ट्री शेकिंग इसे हटा देती है।

terminal
$ npm install nuclo nuclo-router

या समान दो नामों के साथ pnpm add, yarn add या bun add का उपयोग करें। इसे पीयर डिपेंडेंसी के रूप में nuclo 0.3 या उससे नया संस्करण चाहिए।

राउटर तय करता है कि क्या लोड होगा। ऐप बताता है कि कहाँ दिखेगा router.outlet() के साथ: लेयर स्टैक पर एक list(), प्रति लेयर एक पंक्ति, इसलिए एक साधारण पृष्ठ एक पंक्ति है। सर्वर और क्लाइंट एक ही ट्री बनाते हैं, इसलिए hydrate() सर्वर के मौजूदा नोड का पुनः उपयोग करता है। बाकी सब कुछ history है, एक डेलिगेटेड click लिसनर और requestIdleCallback है।

एक इम्पोर्ट

main.ts
export const router = createRouter(routes)   // define once, import anywhere
await router.start()                          // resolve the current URL
render(App, document.getElementById('app')!) // App places router.outlet() and reads router.path, router.go(), …

start() लौटने से पहले मिलते रूट का मॉड्यूल लोड कर लेता है, इसलिए पहला ट्री सर्वर और क्लाइंट दोनों पर पूरा हो जाता है। उसके बाद राउटर उस रूट का स्टेट पढ़ता है जिसे उसने शुरू किया: router.path, router.pending, router.go()।

एक संपूर्ण ऐप

routes.ts
// isomorphic: imported by the browser and the server
import { createRouter } from 'nuclo-router'

export const router = createRouter({
  '/':           () => import('./pages/Home.ts'),
  '/docs':       () => import('./pages/Docs.ts'),
  '/blog/:slug': () => import('./pages/Post.ts'),
  '*':           () => import('./pages/NotFound.ts'),
})

export const App = () =>
  div(
    header(a({ href: router.href('/') }, 'Home'), a({ href: router.href('/docs') }, 'Docs')),
    main(
      when(() => router.pending, Spinner()),
      when(() => router.error !== null, ErrorView()),
      router.outlet(),   // where the pages land
    ),
    footer('© nuclo'),
  )
pages/Post.ts
import type { RouteContext } from 'nuclo-router'
import { router } from '../routes.ts'

// A page is plain content
export default function Post(ctx: RouteContext) {
  // A query-only change keeps the page, so the query is read live.
  return article(h1(ctx.params.slug), p(() => (router.search.has('preview') ? 'draft' : '')))
}
main.ts
import 'nuclo'
import { router, App } from './routes.ts'

await router.start()
hydrate(App, document.getElementById('app')!)   // or render() without SSR
server.ts
import 'nuclo/polyfill'
import 'nuclo'
import { renderToString } from 'nuclo/ssr'
import { router, App } from './routes.ts'

// one Route per request
const route = await router.start(request.url)
const html = route.run(() => renderToString(App))

यह संपूर्ण एकीकरण है। लिंक काम करते हैं क्योंकि <a href> सिर्फ एक <a href> है। document पर एक डेलिगेटेड लिसनर राउटर के स्वामित्व वाले लिंक को रोकता है और ब्राउज़र पर हर दूसरे लिंक को छोड़ देता है।

सर्वर पर, start() प्रति अनुरोध एक Route लौटाता है और राउटर को अकेला छोड़ देता है, इसलिए एक साथ चलने वाले अनुरोध एक-दूसरे का रूट नहीं देख सकते। route.run() रेंडर पूरा होने तक उस रूट को राउटर का सक्रिय रूट बनाता है, इसलिए आउटलेट भी उसी को पढ़ता है। ब्राउज़र में प्रति पेज लोड एक रूट होता है, और start() इसे राउटर का अपना बनाता है। run() की ज़रूरत नहीं होती।

एक पृष्ठ को इसका RouteContext और इसका PageProps, (ctx, { layer, data, outlet, beforeLeave }) के रूप में प्राप्त होता है, और कम आर्ग्युमेंट ले सकता है। यह list() द्वारा स्वीकार की गई कोई भी चीज़ लौटाता है। लेआउट के अन्य भाग को भी भरने के लिए, पृष्ठ के अंदर से एक into() लौटाएँ, जैसे div(article(…), into('sidebar', nav(…)))। वह सामग्री पृष्ठ के साथ चली जाती है। पेज और रीजन देखें।

पैटर्न और नेस्टेड रूट

#
पैटर्नकिससे मेल खाता हैparams
/, /docs/introबिल्कुल वही path{}
/blog/:slug/blog/hello{ slug: 'hello' }
/u/:id/edit/u/7/edit{ id: '7' }
/files/*rest/files/a/b.txt{ rest: 'a/b.txt' }
*बाकी सभी पथ, यानी आपका 404{ '*': '…' }

पहले स्टैटिक पैटर्न, फिर घोषणा के क्रम में डायनेमिक पैटर्न और अंत में सबसे लंबे प्रीफ़िक्स वाला कैच-ऑल चुना जाता है। तो '*' केवल वही मेल खाता है जो कोई और नहीं करता है और तालिका में कहीं भी बैठ सकता है। ट्रेलिंग स्लैश, डुप्लिकेट स्लैश और प्रतिशत-एन्कोडिंग को सामान्यीकृत किया जाता है, और पैरामीटर के मान डिकोड किए जाते हैं। मिलान प्रति पैटर्न एक सेगमेंट की तुलना है। कोई रेगेक्स नहीं बनाया या चलाया जाता है।

रूट के सापेक्ष पथ

./ के साथ लिखा गया एक पैटर्न इसे घोषित करने वाले पैरेंट के सापेक्ष है, और ./ के साथ लिखा गया href सक्रिय रूट के सापेक्ष है। वे मिलकर एक पुन: प्रयोज्य अनुभाग बनाते हैं जिसका अर्थ प्रत्येक पैरेंट के लिए समान होता है। किसी तालिका का मान या तो एक लोडर होता है या कोई अन्य तालिका, जिसकी कुंजियाँ उससे संबंधित होती हैं:

records/routes.ts
import type { RouteTable } from 'nuclo-router'

// a section, with no idea where it lives
export const recordSection: RouteTable = {
  '/':             () => import('./Record.ts'),   // the parent path itself
  './preview':     () => import('./Preview.ts'),
  './preview/raw': () => import('./Raw.ts'),
}
routes.ts
// declare it wherever it belongs
createRouter({
  '/': () => import('./pages/Home.ts'),
  '/invoices/:id':  recordSection,
  '/customers/:id': recordSection,
  '*': () => import('./pages/NotFound.ts'),
})

वे दो पंक्तियाँ सभी छह पैटर्न उत्पन्न करती हैं जिनमें कोई पथ दो बार नहीं लिखा होता है। पैरेंट दोनों को समान लोडर ऑब्जेक्ट मिलते हैं, इसलिए वे एक मॉड्यूल कैश और एक import() साझा करते हैं।

  • एक चाइल्ड टेबल के अंदर, './x', '/x' और 'x' सभी का मतलब एक ही है, '/' का मतलब मूल पथ ही है, और '*rest' अनुभाग को अपना स्वयं का कैच-ऑल देता है। इन्हें जितनी चाहें उतनी गहराई तक नेस्ट कर सकते हैं।
  • शीर्ष-स्तरीय कुंजियों का उपयोग लिखित रूप में किया जाता है, इसलिए '*' '*' रहता है।
  • *catch-all पैरेंट के अंतर्गत एक चाइल्ड नोड एक createRouter() त्रुटि है, क्योंकि कैच-ऑल को अपना पैटर्न समाप्त करना होता है।

पैरेंट पेज और outlet()

एक पैरेंट और उसके चाइल्ड नोड एक वास्तविक हायरार्की हैं। पैरेंट रेंडर होता है और चाइल्ड उसके अंदर रेंडर होता है, जहां भी वह outlet() कहता है:

records/Record.ts
import type { PageProps, RouteContext } from 'nuclo-router'

// a parent page takes its outlet from (ctx, { outlet })
export default function Record(ctx: RouteContext, { outlet }: PageProps) {
  return div(Header(ctx), input({ id: 'notes' }), main(outlet()))
}

चाइल्ड रूट बदलने पर पैरेंट दोबारा नहीं बनता। उसका DOM नोड वही रहता है, इसलिए फ़ोकस, स्क्रॉल स्थिति, आधा लिखा इनपुट और क्लोज़र का स्टेट बने रहते हैं।

  • चाइल्ड रूट न हो, तो पेज का आउटलेट कुछ रेंडर नहीं करता, इसलिए लेआउट उसे बिना शर्त कॉल कर सकता है। यदि पैरेंट के चाइल्ड रूट हों लेकिन वह outlet() कॉल न करे, तो चाइल्ड दिखाई नहीं देंगे।
  • अलग इंडेक्स रूट नहीं होते। '/' खुद लेआउट है, इसलिए पैरेंट पथ पर लेआउट खाली आउटलेट के साथ दिखता है, अलग इंडेक्स पेज के साथ नहीं। शुरुआती सामग्री लेआउट में रखें या उसके लिए चाइल्ड पथ बनाएँ।

फिर वही कॉल पैरेंट में से किसी एक से और एक ऐसे पेज से काम करती है जो यह नहीं जानता कि यह किसमें है:

example.ts
router.href('./preview')      // '/invoices/42/preview' or '/customers/7/preview'
await router.go('./preview')
await layer.push('./preview') // the usual shape: a child layer

./x, ../x, . और .. मौजूदा रूट के पथ के सापेक्ष हल होते हैं, URL की "डायरेक्टरी" के नहीं। /invoices/42 से ./preview का अर्थ /invoices/42/preview है, जबकि ब्राउज़र इसे /invoices/preview बनाता। URL के अंत में स्लैश से यह व्यवहार नहीं बदलता। ../ एक सेगमेंट ऊपर जाता है और रूट पर रुक जाता है। क्वेरी और हैश बचाए जाते हैं, base फिर जुड़ता है और पूरा एब्सोल्यूट पथ हिस्ट्री में जाता है। React Router भी <Link to="./x"> के लिए सामान्य URL रिज़ॉल्यूशन से यही बदलाव करता है।

  • केवल ./ और ../ रूट के सापेक्ष हैं। href में बिना प्रीफ़िक्स वाले preview को ब्राउज़र के सामान्य नियमों से हल किया जाता है।
  • हाथ से href="./preview" लिखने के बजाय router.href('./preview') के साथ सापेक्ष लिंक बनाएँ। href() रेंडर समय पर हल हो जाता है, इसलिए जो मार्कअप उपलब्ध है वह क्रॉलर के लिए, मध्य-क्लिक के लिए और JavaScript अक्षम होने पर भी पूर्ण और सही होता है।

क्या बचता है और क्या दोबारा बनता है

नोडनेविगेशन पर
पैरेंट जिसका पैटर्न और पथ अपरिवर्तित हैबना रहता है: वही नोड, और इसका load() दोबारा नहीं चलता
मिलान किया गया पृष्ठबना रहता है जब इसका पैटर्न और पथ अपरिवर्तित होता है और इसमें कोई load() नहीं होता है, तो एक क्वेरी या हैश परिवर्तन इसे रखता है। प्रत्येक विज़िट पर लोडर वाला मार्ग ताज़ा हो जाता है।
वह पैरेंट जिसका पथ बदल गयाअपने अंदर की सारी सामग्री के साथ दोबारा बनता है
पुश की गई लेयर अकेले मिलान किए गए पृष्ठ को रेंडर करता है। इसके पैरेंट पहले से ही इसके नीचे की पंक्ति में माउंट हैं।

जो पेज बना रहता है, उसका ctx भी वही रहता है जिससे वह बना था। हर जगह ctx का यही अर्थ है। लाइव मानों के लिए router.* पढ़ें। क्वेरी पर निर्भर फ़िल्टर, पेजिनेशन और टैब के लिए डायनेमिक मान में router.search पढ़ें और router.go('?page=2', { replace: true }) से लिखें। पेज, फ़ोकस और आधा लिखा इनपुट बने रहते हैं।

पेज और रीजन

#

router.outlet() पेज को एक स्थान पर रखता है। जिस पृष्ठ को लेआउट में कहीं और साइडबार, टूलबार या शीर्षक की भी आवश्यकता होती है, उसे into() से भर दिया जाता है। लेआउट region() के साथ स्थान को चिह्नित करता है, और पृष्ठ अपनी सामग्री के आगे एक into() लौटाता है। लेआउट कभी भी पृष्ठ सामग्री को प्रोप के रूप में प्राप्त नहीं करता है, और नेविगेशन कभी भी इसका पुनर्निर्माण नहीं करता है।

app.ts
export const App = () =>
  div(
    aside(
      input({ placeholder: 'Filter' }),   // keeps its text across navigations
      region({ id: 'sidebar', empty: p('Nothing to show.') }),
    ),
    main(router.outlet()),
  )
pages/Post.ts
import type { PageProps, RouteContext } from 'nuclo-router'

export default function Post(_ctx: RouteContext, { data: post }: PageProps<Post>) {
  return div(
    article(h1(post.title), p(post.body)),
    into('sidebar', nav(h2('In this post'), ul(list(() => post.headings, (h) => li(h))))),
  )
}

पृष्ठ की सामग्री आउटलेट के list() की एक पंक्ति है, और एक into() तब तक जीवित रहता है जब तक वह पंक्ति इसे धारण करती है। तो साइडबार सामग्री पृष्ठ के साथ आती है और उसके साथ चली जाती है: into('sidebar', …) के बिना किसी पृष्ठ पर नेविगेट करने से रीजन की empty सामग्री फिर से दिखाई देती है। पेज दोनों भागों के लिए अपने ctx और data का उपयोग कर सकता है, क्योंकि दोनों एक ही फ़ंक्शन द्वारा बनाए गए हैं।

नेविगेशन पर क्या होता है

  • जो पेज बना रहता है, उसके रीजन भी बने रहते हैं। पेज बचाए रखने वाले क्वेरी या हैश बदलाव रीजन की सामग्री भी नहीं बदलते। साइडबार क्वेरी पर निर्भर हो, तो डायनेमिक मान में router.search पढ़ें।
  • दोबारा बना पेज उन्हें पुनः भर देता है। जब मार्ग बदलता है, तो पुरानी पंक्ति हटा दी जाती है और नए पृष्ठ के into() व्यू आ जाते हैं। कुछ भी पीछे नहीं छूटा.
  • पुश की गई लेयर एक व्यू जोड़ती है। लेयर आउटलेट की बाद की पंक्ति है, इसलिए इसका into() उसी रीजन का एक नया व्यू है। डिफ़ॉल्ट type: 'latest' के साथ यह लेयर बंद होने तक पृष्ठ के व्यू को कवर करता है, और फिर पृष्ठ का व्यू फिर से दिखाई देता है। type: 'stack' के साथ दोनों दिखाई देते हैं।
  • पैरेंट पेज रीजनों को भी भर सकता है। इसके व्यू तब भी बने रहते हैं जब इसके चाइल्ड नोड बदलते हैं, क्योंकि पैरेंट पेज का पुनर्निर्माण नहीं किया जाता है।
  • सर्वर रेंडरिंग भी इसी तरह काम करती है। सर्वर route.run() के अंदर पेज बनाता है, इसलिए इसके into() व्यू HTML में हैं, और hydrate() उन्हें पुनः उपयोग करता है।

एक पृष्ठ से कई रीजन

एक बार में एक से अधिक रीजन भरने के लिए किसी ऑब्जेक्ट को पास करें। पृष्ठ की मुख्य सामग्री अभी भी आउटलेट के माध्यम से जाती है, इसलिए इसे into() के पास लौटा दें:

pages/Report.ts
export default function Report(ctx: RouteContext) {
  return div(
    table(…),
    into({
      toolbar: [button('Export'), button('Print')],
      sidebar: nav(a({ href: router.href('./summary') }, 'Summary')),
    }),
  )
}

किस काम के लिए कौन सा टूल

  • outlet() एक चाइल्ड रूट के लिए है: यूआरएल द्वारा तय की गई सामग्री, उसके पैरेंट पेज के अंदर रेंडर की गई है। नेस्टेड रूट देखें।
  • into() मिलान किए गए पेज के लिए है जो पूरे लेआउट में फैला हुआ है। सामग्री पृष्ठ से संबंधित है और उसके डेटा का उपयोग करती है।
  • matches() अकेले यूआरएल से लेआउट तय करने के लिए है, इसकी अपनी छोटी तालिका है और इसमें कोई पेज शामिल नहीं है। matches() देखें।

ये सभी एक ही मिलते रूट के हिस्से हैं। राउटर एक URL से दो स्वतंत्र रीजन नहीं चलाता। इसलिए अपने पथों वाला साइडबार पेज से भरा रीजन है, दूसरा आउटलेट नहीं। साइडबार को अपना राउटर, लोडिंग और फ़ॉलबैक चाहिए, तो राउटर शुरू न करें और match() से मिलान करें। कई राउटर और संयुक्त लोडिंग देखें।

रूट लोडर और कोड स्प्लिटिंग

#

एक पेज मॉड्यूल एक load फ़ंक्शन एक्सपोर्ट कर सकता है। यह पेज बनने से पहले, उस रूट के हर नेविगेशन पर चलता है, और जो कुछ भी लौटाता है उसे data के रूप में पेज पर भेज दिया जाता है:

pages/Post.ts
import type { DataLoader, PageProps, RouteContext } from 'nuclo-router'

export const load: DataLoader<Post> = (ctx) => fetchPost(ctx.params.slug)

export default function Post(_ctx: RouteContext, { data: post }: PageProps<Post>) {
  return article(h1(post.title), p(post.body))
}

रूट टेबल वही रहती है। लोडर पेज के मॉड्यूल में रहता है। उसे मिलान का RouteContext मिलता है, इसलिए params, search और path उपलब्ध हैं। यह सिंक या एसिंक हो सकता है और सर्वर पर भी चलता है। इसलिए सर्वर से भेजे HTML में डेटा पहले से होता है, उसे पहली बार दिखाने के लिए हाइड्रेशन के बाद डेटा आने का इंतज़ार नहीं करना पड़ता।

मॉड्यूल कैश्ड है। डेटा नहीं है. एक रूट का चंक एक बार लाया जाता है और पुन: उपयोग किया जाता है, और इसका लोडर हर नेविगेशन पर फिर से चलता है। यही कारण है कि /blog/a और /blog/b को भ्रमित नहीं किया जा सकता है, और एक असफल लोडर को आसानी से पुनः प्रयास किया जा सकता है।

स्थितिव्यवहार
लोडर चल रहा हैrouter.pending true रहता है और पिछला पेज स्क्रीन पर बना रहता है
लोडर थ्रो या रिजेक्ट करता हैत्रुटि router.error में मिलती है। go() रिजेक्ट नहीं होता और नेविगेशन दोबारा आज़माया जा सकता है।
start() के दौरानरिजेक्शन start() से मिलता है। त्रुटि रखने के लिए अभी कोई रूट नहीं बना है।
पुश की गई लेयर अपने स्वयं के डेटा के साथ अपना स्वयं का लोडर चलाता है
आइडल प्रीलोडरकेवल मॉड्यूल लोड करता है। इसे देने के लिए कोई कॉन्टेक्स्ट नहीं होता और डेटा लोड करने से ऐसे रूट का डेटा भी आ सकता है जिसे आप कभी खोलें ही नहीं।
उसी URL पर दोबारा नेविगेशनलोडर फिर चलता है और पंक्ति दोबारा बनती है। बिना लोडर वाला रूट अपनी पंक्ति बनाए रखता है।

पुनः सत्यापित करने के लिए, इतिहास प्रविष्टि जोड़े बिना उस स्थान पर नेविगेट करें जहां आप पहले से हैं:

example.ts
await router.go(router.url, { replace: true })   // runs load() again

चंक के साथ डेटा लोड करना

पेज मॉड्यूल द्वारा एक्सपोर्ट किया गया load केवल उस मॉड्यूल के आने के बाद ही चल सकता है, इसलिए कैश खाली होने पर नेविगेशन पहले चंक और फिर डेटा लोड करता है। दोनों को एक साथ शुरू करने के लिए, लोडर को withLoad() के साथ तालिका में घोषित करें:

routes.ts
import { createRouter, withLoad } from 'nuclo-router'
import { loadPost } from './data.ts'   // small, in the main bundle

createRouter({
  '/blog/:slug': withLoad(() => import('./pages/Post.ts'), loadPost),
})

यह मॉड्यूल के अपने load की जगह लेता है और हर विजिट पर इसी तरह चलता है। चंक लोड होने के दौरान डेटा लोडर रिजेक्ट हो, तो त्रुटि सामान्य रूप से router.error में मिलती है।

डिज़ाइन के अनुसार, HTML में सर्वर डेटा का सीरियलाइज़ेशन नहीं है। क्लाइंट का load फिर से हाइड्रेशन पर चलता है। यदि यह मायने रखता है, तो सर्वर को route.data आउटपुट में लिखने दें, उदाहरण के लिए window.__DATA__ के रूप में, और लोडर इसे पहले ले। route.data केवल मिलान किए गए पृष्ठ का डेटा है। इसमें पैरेंट के load() का परिणाम नहीं होता।

कोड स्प्लिटिंग और प्रीलोडिंग

एक रूट का मान हमेशा एक फ़ंक्शन होता है जो पृष्ठ कंपोनेंट लौटाता है, इसलिए कंपोनेंट ज़रूरत पड़ने पर ही लोड होता है:

routes.ts
'/a': () => import('./A.ts'),                      // default export (+ optional load)
'/b': () => import('./B.ts').then(m => m.BPage),   // named export
'/c': () => CPage,                                 // eager, not split
'/d': () => ({ default: DPage, load: loadD }),     // eager, module-shaped

केवल मॉड्यूल के रूप वाले विकल्प ही load एक्सपोर्ट कर सकते हैं। .then(m => m.BPage) के साथ नामित कंपोनेंट को चुनने से राउटर को अकेले ही फ़ंक्शन सौंप दिया जाता है।

हर मॉड्यूल एक बार इम्पोर्ट होकर कैश होता है। एक ही रूट पर साथ चल रहे नेविगेशन एक import() साझा करते हैं। पेज पर idleTimeout तक कोई गतिविधि न हो (डिफ़ॉल्ट एक सेकंड; कोई टैप, कुंजी, व्हील इवेंट या स्क्रॉल नहीं), तो बाकी रूट पृष्ठभूमि में लोड होते हैं: हर आइडल अवधि (requestIdleCallback) में एक। कोई भी इनपुट या प्रतीक्षारत नेविगेशन इसे पेज के फिर आइडल होने तक रोक देता है।

एक प्रीलोडेड या पहले से लोड रूट कभी भी router.pending को फ्लिप नहीं करता है और अगले पेंट से पहले लैंड करता है: कोई स्पिनर फ्रेम नहीं, कोई लेआउट शिफ्ट नहीं। router.pending केवल एक मॉड्यूल या लोडर के लिए सही है जिसके लिए इंतजार करना पड़ता है, और आउटगोइंग पेज तब भी स्क्रीन पर रहता है। चंक लोड होने में विफलता router.error में मिलती है। यह अनहैंडल्ड रिजेक्शन नहीं बनती और दोबारा कोशिश की जा सकती है।

राउटर API

#
fnसार्वजनिक API
// import { ... } from 'nuclo-router' function createRouter(table: RouteTable, options?: RouterOptions): Router function withLoad(loader: RouteLoader, load: DataLoader): RouteLoader interface RouterOptions { base?: string; preload?: boolean; idleTimeout?: number; onNavigate?: (ctx: RouteContext, kind: NavigationKind) => void }

createRouter(table, options?)

table एक पैटर्न को लोडर, या नेस्टेड टेबल पर मैप करता है। विकल्प:

विकल्पडिफ़ॉल्ट
base'/'URL उपसर्ग के तहत ऐप परोसा जाता है, जैसे '/docs'। यह केवल खंड सीमा पर मेल खाता है: /docs/intro '/docs' के अंदर है, /docsearch नहीं है। इसके बाहर के यूआरएल किसी भी तरह से मेल नहीं खाते हैं, इसलिए start() अस्वीकार कर देता है (आपका सर्वर-साइड 404) और उनके लिंक ब्राउज़र पर छोड़ दिए जाते हैं।
preloadtrueपृष्ठ निष्क्रिय होने पर शेष मार्गों के मॉड्यूल को पृष्ठभूमि में लोड करें। false इसे बंद कर देता है.
idleTimeout1000टैप, कुंजी, व्हील इवेंट या स्क्रॉल के बिना बीतने वाला समय, मिलीसेकंड में, जिसके बाद प्रीलोडिंग शुरू होती है या गतिविधि के बाद फिर शुरू होती है।
onNavigate(ctx, kind) => void, ब्राउज़र में प्रत्येक हल किए गए नेविगेशन के बाद कॉल किया जाता है, जिसमें शुरुआती नेविगेशन भी शामिल है, DOM अपडेट होने के बाद. kind 'start', 'push', 'replace', 'pop' (पीछे, आगे, close()) या 'layer' है। document.title, मेटा टैग, फोकस, स्क्रॉलिंग और एनालिटिक्स के लिए हुक। SSR के दौरान कभी कॉल नहीं होता।

यह एक राउटर लौटाता है: start(), match(), outlet(), matches() और run() को छोड़कर Route के सभी सदस्य। ये सदस्य सक्रिय रूट पढ़ते हैं: ब्राउज़र में आखिरी पूरे हुए start() का रूट, या सर्वर पर वह रूट जिसका run() चल रहा है। कोई सक्रिय रूट न होने पर सदस्य पढ़ने से त्रुटि थ्रो होती है। रूट टेबल, कंपाइल किया गया मैचर और मॉड्यूल कैश राउटर में रहते हैं, इसलिए सर्वर पर अनुरोध इन्हें साझा करते हैं।

router.start(url?)

अपने पेज मॉड्यूल में url (ब्राउज़र में डिफ़ॉल्ट: location.href) को हल करता है और एक Promise<Route> लौटाता है। जब कुछ भी मेल नहीं खाता और तालिका में कोई '*' रूट नहीं है तो अस्वीकार कर देता है। एक सर्वर पर, वह आपका 404 है। ब्राउज़र में यह उस Route को राउटर का सक्रिय रूट बनाता है, popstate और डेलिगेटेड click लिसनर को जोड़ता है और प्रीलोडिंग शेड्यूल करता है। दूसरा start() आपके लिए पिछले रूट को समाप्त कर देता है। सर्वर पर यह साझा की गई किसी भी चीज़ को नहीं छूता है, इसलिए route.run() के अंदर रेंडर करें।

router.match(url?)

बिना कुछ लोड किए टेबल से URL का मिलान करता है: कोई इम्पोर्ट, हिस्ट्री बदलाव, लिसनर या रूट नहीं बनता। मिलान होने पर RouteContext लौटाता है। URL base के बाहर हो, किसी पैटर्न से न मिले या ब्राउज़र में दूसरे ओरिजिन का हो, तो null लौटाता है। केवल एब्सोल्यूट URL लेता है, क्योंकि ./ रिज़ॉल्यूशन के लिए सक्रिय रूट चाहिए।

example.ts
// 404 before importing a single chunk
if (!router.match(request.url)) return new Response(null, { status: 404 })

// "is this link ours?" for a custom link component or a prefetch rule
const owned = router.match(href) !== null

router.outlet()

सक्रिय रूट के पेज एक list() के रूप में लौटाता है। पेज जहाँ दिखाने हों, वहाँ इसे एक बार रखें। पुश की गई लेयर इसमें बाद की पंक्ति होती है। start() पूरा होने से पहले और stop() के बाद यह खाली रहता है। सर्वर पर यह उसी रूट को रेंडर करता है जिसका run() चल रहा है।

router.matches(table)

एक दूसरी तालिका का व्यू रेंडर करता है जो सक्रिय रूट के पथ से मेल खाता है, list() के रूप में आप इसे कहीं भी रख सकते हैं। matches() देखें।

Route

run() को छोड़कर प्रत्येक सदस्य राउटर पर भी है, जहां एक ऐप इसे पढ़ता है।

सदस्य
path, pattern, params, search, hash, urlसक्रिय मिलान। search एक URLSearchParams है। search, hash और url लाइव मान हैं।
pendingकोई नेविगेशन एक मॉड्यूल या लोडर की प्रतीक्षा कर रहा है
errorअंतिम विफल लोड, अगले नेविगेशन द्वारा साफ़ किया गया
data मिलान किए गए पृष्ठ का load() क्या लौटा, सर्वर पर सीरियलाइज़ करने के लिए। इसमें पैरेंट का डेटा नहीं है.
depth स्टैक पर कितनी लेयर हैं (एक सामान्य पृष्ठ के लिए 1)
go(href, { replace })नेविगेट करता है और ज़रूरत हो तो मॉड्यूल लोड करता है। कभी रिजेक्ट नहीं होता: मॉड्यूल रिजेक्ट या थ्रो करे, तो त्रुटि error में मिलती है।
push(href)रूट को ऊपर नई लेयर में खोलता है। लेयर बंद होने पर उसके परिणाम से रिज़ॉल्व होता है। ./ स्वीकार करता है। मिलान न होने या मॉड्यूल विफल होने पर रिजेक्ट होता है।
href(path) a({ href }) के लिए base के साथ एक पथ उपसर्ग करें। सक्रिय रूट के विरुद्ध ./ और ../ का समाधान करता है।
stop() प्रत्येक लिसनर को अलग करें, खुली लेयरों को खारिज करें, आउटलेट खाली करें। दोबारा कॉल करने पर भी वही परिणाम रहता है। रुका हुआ रूट अभी भी अपने अंतिम मैच के साथ उत्तर देता है, और go() कुछ नहीं करता है।
run(fn)इस रूट को राउटर का सक्रिय रूट बनाकर fn कॉल करता है और उसका परिणाम लौटाता है। सर्वर पर: route.run(() => renderToString(App))। केवल सिंक्रोनस। fn लौटते ही रूट का सक्रिय स्कोप खत्म हो जाता है।

RouteContext

एक पृष्ठ का पहला आर्ग्युमेंट: path, pattern, params, search, hash, url। यह वह संदर्भ है जिसके साथ पेज बनाया गया था। जीवंत मानों के लिए router.* पढ़ें। वे केवल तभी भिन्न होते हैं जब क्वेरी या हैश पुनर्निर्माण के बिना बदल जाता है।

params डिफ़ॉल्ट रूप से { [name]: string } है। एक पेज इसे Params के साथ अपने पैटर्न से टाइप कर सकता है, जो '/blog/:slug' को { slug: string } में और '/files/*rest' को { rest: string } में बदल देता है:

pages/Post.ts
import type { Params, RouteContext } from 'nuclo-router'

export default function Post(ctx: RouteContext<Params<'/blog/:slug'>>) {
  return article(h1(ctx.params.slug))   // slug: string, nothing else exists
}

PageProps

एक पेज का दूसरा आर्ग्युमेंट। पेज को जो चाहिए, उसे डीस्ट्रक्चर करके लें।

प्रॉप
layerजहां पेज स्टैक में बैठता है। नीचे Layer देखें।
dataरूट का load() क्या लौटा, या undefined जब लोडर न हो। PageProps<TData> से टाइप किया जाता है।
outlet जहां एक पैरेंट पेज अपने चाइल्ड रूट को रेंडर करता है। नेस्टेड रूट देखें.
beforeLeaveएक गार्ड रजिस्टर करता है जिसे पेज हटाने से पहले कॉल किया जाता है। लीव गार्ड देखें।

Layer

स्टैक में पेज की जगह और उससे बाहर निकलने का तरीका।

सदस्य
depthबेस पेज के लिए 0, पुश की गई लेयर के लिए 1 और उससे ऊपर
push(href)Route.push() की तरह, पेज के अंदर से दूसरी लेयर खोलता है
close(result?)इस लेयर और इसके ऊपर की सभी चीज़ों को बंद करें, इसे खोलने वाले push() को हल करें। बिना आर्ग्युमेंट के कॉल करने का मतलब रद्द करना है। गहराई 0 पर यह कुछ नहीं करता।

DataLoader और withLoad()

DataLoader (ctx: RouteContext) => TData | Promise<TData> है, जो एक पेज मॉड्यूल का वैकल्पिक load एक्सपोर्ट है। withLoad(loader, load) एक मार्ग बनाता है जिसका load उसके चंक के इम्पोर्ट के बाद शुरू होने के बजाय उसके साथ-साथ शुरू होता है। रूट लोडर देखें।

टाइप

हर टाइप nuclo-router का नेम्ड एक्सपोर्ट है, इसलिए एक पेज, एक लोडर या एक गार्ड को पथ से इम्पोर्ट किए बिना टाइप किया जा सकता है।

टाइपयह क्या है
Params<'/blog/:slug'> एक पैटर्न के लिए पैरामीटर ऑब्जेक्ट। RouteParams बिना विशेष टाइप वाला रूप है, और Match मिलानकर्ता का परिणाम है।
PageComponentपेज: (ctx, { layer, data, outlet, beforeLeave }) => …
RouteModuleपेज मॉड्यूल: खुद कंपोनेंट, या import() से मिलने वाला { default, load? }
RouteLoader() => RouteModule | Promise<RouteModule>, वैकल्पिक रूप से withLoad() से मिला load भी रखता है
RouteTablecreateRouter() को दी जाने वाली टेबल: पैटर्न से लोडर या नेस्टेड टेबल का मैप
RouterOptions{ base?, preload?, idleTimeout?, onNavigate? }
NavigateOptions{ replace? }, go() का दूसरा आर्ग्युमेंट
NavigationKind'start' | 'push' | 'replace' | 'pop' | 'layer', onNavigate का दूसरा आर्ग्युमेंट
OutletPageProps में outlet() का टाइप
LeaveGuard(to) => boolean | void | Promise<…>, beforeLeave() क्या लेता है
MatchViewmatches() का व्यू: पैरामीटर लेकर सामग्री या पेज का import() लौटाता है

लेयर और लीव गार्ड

#

push() मौजूदा रूट को बदलने के बजाय उसके ऊपर नया रूट खोलता है। लेयर बंद होने पर दिए परिणाम से इसका Promise रिज़ॉल्व होता है। पेज स्टैक पर एक ही list() है, इसलिए लेयर खोलना उसमें नई पंक्ति जोड़ना है। नीचे का पेज दोबारा रेंडर नहीं होता। लेयर बंद होने पर उसका DOM, फ़ोकस और आधा भरा फ़ॉर्म बना रहता है।

इससे उपयोगकर्ता का आधा भरा फ़ॉर्म हटाए बिना नया रिकॉर्ड बनाया जा सकता है, जब ज़रूरी रिकॉर्ड अभी मौजूद न हो:

pages/Edit.ts
// the page underneath
const created = await layer.push<Option>('/options/new')
if (created) { options.push(created); selected = created.id; update() }
pages/NewOption.ts
// the pushed page: (ctx, { layer })
layer.close(created)   // resolves the push() above with the new option
layer.close()          // dismissed: resolves with undefined

लेयर को नेस्ट कर सकते हैं। दूसरे डायलॉग की ज़रूरत हो, तो डायलॉग layer.push() कॉल करके उसका इंतज़ार कर सकता है। उसे रूट का रेफ़रेंस नहीं चाहिए।

  • URL सबसे ऊपर की लेयर का होता है। push() हिस्ट्री में एंट्री जोड़ता है, इसलिए लेयर का लिंक बनाया जा सकता है। Back उसे Cancel की तरह बंद करता है और उसका push() रद्द होने के परिणाम से रिज़ॉल्व होता है। Back और close() एक ही कोड पथ इस्तेमाल करते हैं: close() परिणाम रखकर हिस्ट्री में पीछे जाता है और history.state की गहराई तय करती है कि क्या दिखेगा। ब्राउज़र हर टैब में लगभग 50 एंट्री रखते हैं। लेयर में लंबा नेविगेशन होने पर नीचे की एंट्री हट सकती है। तब close() लेयर को वहीं बंद करता है और मौजूदा एंट्री को दिख रहे पेज के URL से बदल देता है।
  • एक साधारण नेविगेशन स्टैक को साफ़ करता है। एक लिंक क्लिक या go() एक लेयर नहीं है। यह सब कुछ बदल देता है और हर खुली लेयर को खारिज कर देता है, इसलिए किसी भी कॉल करने वाले को इंतजार नहीं करना पड़ता है।
  • सीधे URL खोलने पर लेयर अलग पेज होती है। URL सबसे ऊपर की लेयर बताता है, नीचे का स्टैक नहीं। इसलिए उसका URL सीधे खोलने पर वह गहराई 1 पर रेंडर होती है, जहाँ layer.depth === 0 है और close() कुछ नहीं करता। पुश किया जाने वाला रूट अपने आप में भी उपयोगी होना चाहिए। layer.depth जाँचकर Cancel बटन की जगह बाहर जाने का लिंक दिखाएँ।
  • Promise रीलोड के बाद नहीं बच सकता, क्योंकि वह क्लोज़र के स्टेट में रहता है। परिणाम लौटाना इस प्रक्रिया को छोटा करने का तरीका है; प्रक्रिया को सीधे खोलने पर भी काम करना चाहिए।
  • प्रतीक्षा में कुछ भी नहीं बचा है। stop() प्रत्येक खुली लेयर को खारिज कर देता है, और नेविगेशन द्वारा छोड़ी गई लेयर undefined के साथ हल हो जाती है।

लीव गार्ड

एक पृष्ठ जिसमें रखने योग्य कुछ चीज़ होती है, जैसे कि आधा भरा हुआ फॉर्म, एक गार्ड को पंजीकृत करता है। पृष्ठ को त्यागने से पहले यह पूछा जाता है, और false (या इसका Promise) लौटाने से पृष्ठ बना रहता है:

pages/Edit.ts
import type { PageProps, RouteContext } from 'nuclo-router'

export default function Edit(_ctx: RouteContext, { beforeLeave }: PageProps) {
  let dirty = false
  beforeLeave(() => !dirty || confirm('Discard your changes?'))
  return form(input({ onInput: () => (dirty = true) }), …)
}
  • केवल पेज हटने पर। लेआउट के चाइल्ड रूट बदलने या पेज बचाए रखने वाले क्वेरी/हैश बदलाव पर गार्ड नहीं पूछा जाता। गार्ड का जीवनकाल पेज से जुड़ा है, इसलिए उसे अलग से अनरजिस्टर नहीं करना पड़ता।
  • पेज छोड़ने के सभी रास्तों पर। लिंक, go(), Back और Forward, लेयर का close() और राउटर के बाहर के URL पर go()—ऐप छोड़ते समय to का मान null होता है। गार्ड Back या Forward रोके, तो हिस्ट्री वापस की जाती है ताकि एड्रेस बार मौजूदा पेज से मेल खाए। टैब बंद करना या रीलोड करना beforeunload के ज़रिए संभालें।
  • to गंतव्य का RouteContext है। गार्ड सबसे अंदर वाले पेज से शुरू होते हैं और पहले इनकार पर रुक जाते हैं। गार्ड त्रुटि थ्रो करे, तो पेज बना रहता है। close(result) भी गार्ड से होकर जाता है, इसलिए परिणाम के साथ बंद करने से पहले अपना फ़्लैग साफ़ करें।
  • राउटर केवल अपनी बनाई हिस्ट्री एंट्री का नेविगेशन वापस कर सकता है। ब्राउज़र के सामान्य #fragment लिंक या दूसरी स्क्रिप्ट के pushState से बनी एंट्री पर Back या Forward करने पर गार्ड नहीं पूछा जाता।

matches() और सब-राउटर

#

router.outlet() पेज रेंडर करता है। router.matches(table) मौजूदा पथ के अनुसार अपनी छोटी टेबल से साइडबार, ब्रेडक्रंब या टूलबार जैसी दूसरी सामग्री रेंडर करता है। इसके लिए रूट लोड करने या पेज जोड़ने की ज़रूरत नहीं:

app.ts
div(
  aside(
    router.matches({
      '/':           () => nav('Home links'),
      '/blog/new':   () => nav('Drafting tips'),
      '/blog/:slug': ({ slug }) => nav(`More like ${slug}`),
      '*':           () => import('./Sidebar.ts'),   // code-split, rendered once loaded
    }),
  ),
  main(router.outlet()),
)
  • रूट टेबल के समान मिलान। पैटर्न, प्राथमिकता, कच्चे-पथ मिलान और base स्ट्रिपिंग राउटर के स्वयं के हैं, इसलिए एक व्यू और एक पृष्ठ हमेशा यूआरएल के बारे में सहमत होते हैं। यह router.path की तरह शीर्ष लेयर का पथ पढ़ता है।
  • व्यू (params, ctx) => content है, जहां params इसके पैटर्न से टाइप किया गया है और ctx व्यू के pattern और params के साथ RouteContext है। इसके बजाय यह पेज मॉड्यूल का import() लौटा सकता है, जिसे लैंड करने के बाद ctx के साथ कॉल किया जाता है।
  • कुछ भी मेल नहीं खाता, कुछ भी रेंडर नहीं करता। फ़ॉलबैक के लिए एक '*' व्यू जोड़ें। यह आउटलेट की तरह start() से पहले और stop() के बाद खाली है।
  • यह केवल रूट बदलने पर दोबारा रेंडर होता है, इसलिए जिन बदलावों पर पेज बना रहता है, उन पर व्यू का DOM भी बना रहता है।
  • लेज़ी व्यू अपने मॉड्यूल आने तक कुछ भी रेंडर नहीं करता है। एक विफल import() को console.error के साथ लॉग किया गया है, router.error सेट नहीं करता है, और अगले नेविगेशन पर पुनः प्रयास किया जाता है। एक साधारण update() पुनः प्रयास नहीं करता है।
  • सर्वर पर लेज़ी व्यू कुछ भी नहीं भेजता है, क्योंकि इसका मॉड्यूल क्लाइंट पर लैंड करता है। पहले HTML में शामिल होने वाली किसी भी चीज़ के लिए एक सिंक्रोनस व्यू का उपयोग करें।

सब-राउटर

रूट नेस्ट होते हैं, इसलिए एक सब-राउटर आमतौर पर सिर्फ एक नेस्टेड टेबल होता है। एक राउटर: सेक्शन का अपना '/' पेज इसका बाहरी UI है, और इसके चाइल्ड नोड उस पेज के outlet() में रेंडर करते हैं, इसलिए बाहरी UI सेक्शन के भीतर नेविगेशन से बच जाता है:

routes.ts
createRouter({ '/': …, '/docs': docsRoutes, '*': … })

या फ़ीचर का अलग राउटर बनाएँ और उसे शुरू न करें। पैरेंट में एक कैच-ऑल एंट्री हो और चाइल्ड अपने पैटर्न, पैरामीटर व फ़ॉलबैक के अनुसार बाकी पथ का मिलान करे:

routes.ts
// parent table
'/docs/*rest': () => import('./docs/Shell.ts'),
docs/Shell.ts
const docs = createRouter(docsRoutes, { base: '/docs' })
const hit = docs.match()                       // { pattern: '/:topic', … }
const page = await docsRoutes[hit.pattern]()   // the app's own lookup

इसे शुरू न करना ज़रूरी है। start() लिसनर जोड़ता है और दो शुरू किए गए राउटर आपस में टकराते हैं। इस रूप में चाइल्ड खुद रूट हल करता है, इसलिए शुरू किया गया राउटर जिन रूट लोडर को चलाता, उन्हें आपको कॉल करना होगा। शेष पथ बदलने पर शेल भी दोबारा बनता है: मुख्य राउटर को एक ही रूट दिखता है और शेल बाकी पथ खुद हल करता है। नेस्टेड टेबल पैरेंट पेज को माउंट रखती है। इसलिए उसे प्राथमिकता दें, जब तक फ़ीचर को अपना राउटर, बेस और फ़ॉलबैक चाहिए ही न हो।

कई राउटर और संयुक्त लोडिंग

#

ऐप में हर रीजन का अपना राउटर हो सकता है। नियम सरल है: हर document में एक ही राउटर शुरू करें। बाकी राउटर शुरू न करें और उनसे match() के ज़रिए URL का मिलान पूछें। केवल शुरू किया राउटर URL, हिस्ट्री और क्लिक लिसनर संभालता है। बाकी रूट टेबल हैं जिनसे मिलान पूछ सकते हैं। हर रीजन अपने स्पिनर के साथ स्वतंत्र रूप से लोड हो सकता है।

संयुक्त लोडिंग: लोडर की एक श्रृंखला

मिलान की श्रृंखला का हर पेज load() एक्सपोर्ट कर सकता है। कैश खाली होने पर चाइल्ड रूट खोलें, तो लेआउट और पेज के लोडर समानांतर चलते हैं। एक router.pending पूरी श्रृंखला की स्थिति बताता है। दोनों पूरे होने तक पिछला पेज स्क्रीन पर रहता है, इसलिए अधूरा ट्री कभी दिखाई नहीं देता। हर कंपोनेंट को अपना data मिलता है:

routes.ts
'/shop': {
  '/':          () => import('./pages/Shop.ts'),    // layout, has a load()
  './:section': () => import('./pages/Section.ts'), // page, has a load()
},
pages/Shop.ts
import type { DataLoader, PageProps, RouteContext } from 'nuclo-router'

export const load: DataLoader<ShopData> = async () => {
  await wait(500)                              // a slow API
  return { sections: ['toys', 'books', 'music'] }
}

// the layout gets its own data, and its outlet for the child page
export default function Shop(_ctx: RouteContext, { data, outlet }: PageProps<ShopData>) {
  return div(
    h1('Shop'),
    nav(...data.sections.map((s) => a({ href: router.href(`/shop/${s}`) }, s))),
    main(outlet()),
  )
}
pages/Section.ts
export const load: DataLoader<SectionData> = async (ctx) => {
  await wait(900)                              // slower than the layout's
  return { section: ctx.params.section, items: catalog[ctx.params.section] }
}

// the page gets its own data: the two loaders ran beside each other
export default function Section(_ctx: RouteContext, { data }: PageProps<SectionData>) {
  return article(h2(data.section), ul(...data.items.map((i) => li(i))))
}

दोनों लोडर एक के बाद एक चलने के बजाय साथ शुरू होते हैं। किसी का भी इंतज़ार हो, तो router.pending true होता है; बाकी समय false। पहले से उपलब्ध या प्रीलोड हुए रूट पर यह नहीं बदलता। सेक्शन बदलने पर लेआउट बना रहता है, इसलिए केवल सेक्शन का लोडर दोबारा चलता है। लेआउट का डेटा, DOM और आधा लिखा इनपुट बचा रहता है। उसी URL पर दोबारा नेविगेट करने पर अंतिम पेज का लोडर फिर चलता है और उसकी पंक्ति फिर बनती है, जैसे अकेले पेज के लिए होता है।

एकाधिक लोडिंग: अपने स्वयं के स्पिनर के साथ एक दूसरा राउटर

एक स्वतंत्र रीजन - एक साइडबार, एक ट्रे, एक लाइव रीजन - को अपना स्वयं का राउटर मिलता है, बनाया गया लेकिन कभी शुरू नहीं हुआ, और इसका अपना लोडिंग फ़्लैग होता है। router.pending प्रारंभ किए गए राउटर से संबंधित है और इस रीजन के बारे में कुछ नहीं कहता है, इसलिए दोनों साथ-साथ लोड हो सकते हैं और कोई भी पहले समाप्त कर सकता है:

sidebar.ts
import { createRouter } from 'nuclo-router'

// Created but never started: start() is what attaches the history and click
// listeners, and only the main router is started. match() attaches nothing.
const standIn = () => ({ default: () => div() })   // never rendered

const sidebarRouter = createRouter({
  '/shop':          standIn,
  '/shop/:section': standIn,
  '*':              standIn,
}, { base: '/docs' })              // the same base as the main router

export const sidebar = { loading: false, info: null as SidebarInfo | null }

export async function refreshSidebar(at: string) {
  const hit = sidebarRouter.match(at)          // the second router resolves
  if (!hit || (hit.pattern !== '/shop' && hit.pattern !== '/shop/:section')) {
    sidebar.info = null; update(); return
  }
  sidebar.loading = true                       // its own flag, its own spinner
  sidebar.info = null; update()
  const data = await fetchSidebar(hit.params.section)
  sidebar.loading = false; sidebar.info = data
  update()
}
routes.ts
createRouter(routes, {
  onNavigate: (ctx, kind) => {
    announce(kind)
    refreshSidebar(ctx.url)   // every navigation re-resolves the sidebar
  },
})

दूसरे राउटर से केवल मिलान पूछा जाता है; उसे नेविगेट करने को नहीं कहा जाता। match() URL पढ़कर कॉन्टेक्स्ट लौटाता है। साइडबार की कोई जानकारी मुख्य रूट टेबल में नहीं जाती। onNavigate हर पूरा हुए नेविगेशन के बाद चलता है, शुरुआती नेविगेशन सहित। इसलिए स्वतंत्र रीजन को URL के साथ सिंक रखने के लिए यही एक जगह पर्याप्त है।

  • pending शुरू किए गए राउटर का है। बिना शुरू किए राउटर में कोई रूट, pending या error नहीं होता। रीजन अपने फ़्लैग रखता है और उसका स्पिनर मुख्य स्पिनर के साथ दिख सकता है। डेमो में साइडबार 800 मिलीसेकंड लेता है और रूट के लोडर 900 मिलीसेकंड, इसलिए पेज या साइडबार में से कोई भी पहले पूरा हो सकता है।
  • match() कुछ भी नहीं जोड़ता है। किसी भी संख्या में अनस्टार्ट राउटर दस्तावेज़ साझा कर सकते हैं; प्रत्येक की अपनी तालिका, आधार और मॉड्यूल कैश है, और उनमें से कोई भी इतिहास को नहीं छूता है। उनकी कीमत एक टेबल और एक मैचर है।
  • इसके लोडर कभी नहीं चलते हैं। प्रारंभ किया गया राउटर उससे मेल खाने वाले पृष्ठों के load() को चलाता है। बिना शुरू किया गया राउटर केवल URL का मिलान करता है, इसलिए यदि इसके व्यू को डेटा की आवश्यकता है, तो इसे स्वयं प्राप्त करें - डेमो का refreshSidebar()। यदि आप लोडर चाहते हैं, तो एक नेस्टेड टेबल या एक पेज से भरा जाने वाला रीजन उस काम को स्टार्ट किए गए राउटर के साथ रखता है।
  • पुराने जवाबों से बचें। धीमे साइडबार फ़ेच का परिणाम उस सेक्शन पर लागू न हो जहाँ से आप जा चुके हैं। हर रीफ़्रेश पर जनरेशन काउंटर बढ़ाएँ और पुराने जवाब छोड़ दें। राउटर भी नए नेविगेशन के आगे निकलने पर पिछले नेविगेशन को इसी तरह निष्क्रिय कर देता है।
  • लेज़ी matches() व्यू में कोई pending भी नहीं है। यह तब तक कुछ भी रेंडर नहीं करता जब तक इसका मॉड्यूल लैंड नहीं हो जाता। तीन लोडिंग सिस्टम, तीन स्वतंत्र घड़ियाँ: मार्ग का pending, आपके अपने फ़्लैग, और लेज़ी व्यू जो अभी तक वहाँ नहीं है।

गणना किए गए मान

#

Nuclo में computed API नहीं है। एक साधारण फ़ंक्शन लिखें और उसे डायनेमिक मानों से कॉल करें:

example.ts
let items = ['apple', 'banana', 'cherry']
let filter = ''

const visible = () => items.filter((item) => item.includes(filter))

const Fruits = () =>
  div(
    p(() => `Showing ${visible().length} of ${items.length}`),
    ul(list(visible, (item) => li(item))),
  )

filter = 'an'
update()

एसिंक और लोडिंग

#

साधारण async/await का उपयोग करें, और हर बार स्टेट बदलने पर update() कॉल करें। एक फ़ेच के लिए, इसका मतलब है एक बार await से पहले और एक बार बाद में:

example.ts
let status: 'idle' | 'loading' | 'done' | 'error' = 'idle'
let result: unknown = null

async function loadData() {
  status = 'loading'
  update()
  try {
    result = await fetch('/api/data').then((r) => r.json())
    status = 'done'
  } catch {
    status = 'error'
  }
  update()
}

सुझाए गए तरीके

#
  • बदलाव एक साथ करें। ज़रूरी सभी स्टेट बदलें, फिर एक बार update() कॉल करें।
  • डायनेमिक मानों को तेज़ और साइड इफ़ेक्ट से मुक्त रखें। ये सभी प्रत्येक update() पर चलते हैं।
  • छोटे, लगातार अपडेट, जैसे टाइपिंग या टाइमर के लिए scope() का उपयोग करें।
  • document में रेंडर करें। update() चलते समय जो नोड document में नहीं हैं, वे आगे अपडेट होना बंद कर देते हैं।
  • बदलने वाले चाइल्ड नोड के लिए when() और list() उपयोग करें, नोड लौटाने वाला फ़ंक्शन नहीं।
  • className का उपयोग करें, class का नहीं, और डायनेमिक क्लास के लिए () => cx(...) या { className: () => … } का उपयोग करें।
  • मॉड्यूल स्तर पर स्टाइल को एक बार परिभाषित करें।
  • सर्वर रेंडरिंग: पहले रेंडर करें, फिर <style id="nuclo-styles"> में getCssText() डालें। अनुरोध डेटा को आर्ग्युमेंट के रूप में पास करें, और आउटपुट में HTML टिप्पणियाँ रखें।

API इंडेक्स

#

हर सार्वजनिक फ़ंक्शन और वह कहाँ से मिलता है।

nuclo (ग्लोबल और नेम्ड एक्सपोर्ट)

nuclo (केवल नेम्ड एक्सपोर्ट)

nuclo/ssr

  • renderToString(input), renderManyToString(inputs), renderToStringWithContainer(input, tag?, attrs?) और getCssText()।

nuclo/polyfill

nuclo-router (अलग पैकेज)

  • createRouter(table, options?): राउटर, start(), match(), outlet(), matches(), go(), push() के साथ, href() और stop()।
  • withLoad(loader, load): ऐसा रूट जिसका डेटा लोडर उसके चंक के साथ शुरू होता है।
  • टाइप: RouteContext, PageProps, Params, DataLoader, RouteTable और अधिक।

ग्लोबल्स

  • 175 टैग बिल्डर: div, span, ..., var_, और svgSvg, pathSvg, ....
  • टाइप: NodeModFn, NodeModLike, ExpandedElement और अधिक। StyleResult जैसे स्टाइलिंग टाइप को 'nuclo' से भी इम्पोर्ट किया जा सकता है।

विशेष कुंजी

अक्सर पूछे जाने वाले प्रश्न

#

क्या स्टेट बदलने से UI अपडेट हो जाता है?

नहीं। DOM तब तक वैसा ही रहता है जब तक आप update() कॉल नहीं करते।

क्या Nuclo सिग्नल, प्रॉक्सी या वर्चुअल DOM का उपयोग करता है?

नहीं। स्टेट साधारण JavaScript रहता है। जब एक ट्री रेंडर किया जाता है, तो Nuclo वास्तविक एलिमेंट और टेक्स्ट नोड बनाता है। update() प्रत्येक पंजीकृत डायनेमिक मान, या दिए गए दायरे के अंदर चलाता है, और उन्हीं नोड्स को पैच करता है जहां मान बदल गया है। कोई निर्भरता ग्राफ़ और कोई वर्चुअल ट्री नहीं है।

मेरा पेज "false" क्यों दिखाता है?

बूलियन मान टेक्स्ट के रूप में रेंडर करते हैं। div(ok && span('Done')) "false" दिखाता है। चाइल्ड नोड के लिए when(() => ok, span('Done')) और टेक्स्ट के लिए ok ? 'Done' : '' का उपयोग करें।

जब मैं आइटम संपादित करता हूं तो मेरी सूची पंक्ति क्यों नहीं बदलती?

उन आइटमों की पंक्तियाँ जो अभी भी सूची में हैं, रखी जाती हैं, दोबारा रेंडर नहीं की जाती हैं। बदलने वाले फ़ील्ड के लिए () => todo.text जैसे डायनेमिक मानों का उपयोग करें। list() देखें।

मेरे UI का हिस्सा अपडेट होना क्यों बंद हो जाता है?

आमतौर पर जब update() चलता था तो इसके नोड्स दस्तावेज़ में नहीं होते थे। Nuclo उन नोड्स को ट्रैक करना बंद कर देता है, भले ही आप उन्हें संलग्न कर लें, इसलिए दस्तावेज़ में मौजूद कंटेनर में रेंडर करें। दो अन्य कारण: एक when() जिसकी शर्त थ्रो करे तो अपडेट होना बंद हो जाता है, और एक टेक्स्ट फ़ंक्शन जो अपने पहले रन पर थ्रो करता है वह खाली रहता है। update() देखें।

मुझे forceUpdate() का उपयोग कब करना चाहिए?

केवल स्टैटिक मानों में दुर्लभ परिवर्तनों के लिए, जैसे भाषा बदलना। बाकी सभी चीज़ों के लिए, update() का उपयोग करें। forceUpdate() देखें।

मैं किसी पेज को प्रोप के रूप में पास किए बिना लेआउट में कैसे डालूं?

region({ id: 'main' }) से जगह चिह्नित करें और पेज से into('main', …) लौटाएँ। region() & into() देखें।

मैं किसी ऐप को कैसे अनमाउंट करूं?

इसे when() में लपेटें और शर्त false कर दें। DOM को स्वयं हटाने से onDestroy नहीं चलता है। लाइफ़साइकल देखें।

क्या Nuclo में राउटर शामिल है?

कोर पैकेज में नहीं। रूटिंग अलग nuclo-router पैकेज में मिलती है, जिसमें सर्वर रेंडरिंग, हाइड्रेशन, कोड-स्प्लिट रूट और आइडल प्रीलोडिंग हैं। इसे npm install nuclo nuclo-router से इंस्टॉल करें। nuclo-router देखें।

क्या Nuclo में इफ़ेक्ट हैं?

नहीं। onMount और onDestroy एलिमेंट जुड़ने और हटने पर प्रति एलिमेंट एक बार चलते हैं। केवल स्टेट बदलने से कुछ भी दोबारा नहीं चलता।