Builder
ranui/builder بدون DOM مجازی و با واکنشپذیری ریزدانه، DOM را بهشکل اعلانی میسازد. خودِ کامپوننتها با همین نوشته شدهاند و بهعنوان نقطه ورودی مستقل منتشر میشود تا یک برنامه بتواند آن را برای چیدمان و اتصالهای خودش هم به کار ببرد.
کجا به کار میآید: وقتی نمای واکنشی بدون فریمورک میخواهید (یک صفحه، یک مسیر، یک ابزارک)، یا وقتی عنصر سفارشی مینویسید و همان سبک ساختی را میخواهید که ranui در درون خود به کار میبرد.
اصل کار: یک بار بساز، در جا بهروز کن. تابع نما یک بار اجرا میشود. تغییر وضعیت فقط همان گرهی را بهروز میکند که به آن سیگنال بسته است؛ هیچ درختی از نو رسم نمیشود. برای هر شکل، ابزار متناسبش را بردارید: مقدار → یک بستن getter؛ شرط →
Show/Switch؛ فهرست →For/Index.
import {
View,
Div,
Span,
ButtonBuilder, // کارخانههای عنصر
signal,
computed,
createEffect,
batch,
untrack, // واکنشپذیری
createRoot,
onCleanup,
getOwner,
runWithOwner, // مالکیت
EventManager, // رویدادهای بسته به چرخه عمر
} from 'ranui/builder';سازنده هیچ عنصر سفارشیای ثبت نمیکند. برای استفاده از <r-button> و همخانوادههایش، نقطه ورودی کامپوننت را هم وارد کنید: import 'ranui/button'.
عناصر
کارخانهها یک ElementBuilder زنجیرهپذیر برمیگردانند و build() گره DOM را میدهد.
const header = Div()
.class('panel-header')
.attr('part', 'header')
.role('heading')
.children(Span().class('title').text('Deploys'), Slot().attr('name', 'extra'))
.build();Div()، Span()، ButtonBuilder()، InputBuilder()، Label()، Ul()، Li()، Section()، Article()، Nav()، Header()، Footer()، Main()، Style()، Slot()، بهعلاوه View('any-tag') برای هر چیز دیگری، از جمله عناصر سفارشی.
API زنجیرهپذیر
| گروه | متدها |
|---|---|
| شناسه و کلاس | id(v)، class(v)، addClass(...v)، removeClass(...v) |
| اتریبیوتها | attr(name, v)، attrs({…})، boolAttr(name, on, enabledValue?)، part(v)، data(key, v) |
| استایل | style(prop, v) / style({…})، cssVar(name, v) |
| دسترسپذیری | aria(key, v)، role(v)، tabIndex(n)، label(v)، labelledBy(id)، describedBy(id)، ariaHidden(b?) |
| محتوا | text(v)، children(…nodes)، replaceChildren(…nodes) |
| ref و shadow | ref(holder)، shadow(opts?) → ShadowBuilder |
| رویدادها | on(type, handler, options?)، listen(manager, type, handler)، delegate(manager, selector, type, handler) |
| پایانی | build()، serialize() (رشته HTML برای SSR) |
children() عنصر، رشته، سازندههای دیگر، آرایه، null / undefined (که رد میشوند) و getter (نواحی زنده، پایینتر) را میپذیرد.
Refها
createRef<T>() بههمراه .ref(holder) عنصر ساختهشده را میگیرد. اگر ref را با کلاس عنصرِ یک کامپوننت تایپ کنید، متدهای دستوری آن را بدون هیچ تبدیلی در اختیار دارید:
import { Popover } from 'ranui';
import { View, createRef } from 'ranui/builder';
const ref = createRef<Popover>();
View<Popover>('r-popover').attr('trigger', 'click').ref(ref).children(/* … */).build();
ref.current?.closePopover();واکنشپذیری
const [count, setCount] = signal(0);
count(); // خواندن — درون اثرها و memoها ردیابی میشود
setCount(1); // نوشتن؛ setCount((n) => n + 1) هم کار میکند
// نوشتنی با مقدار بدون تغییر کاری نمیکند (Object.is؛ با signal(v, { equals }) قابل تعویض)
const double = computed(() => count() * 2); // تنبل و بهخاطرسپرده
const dispose = createEffect(() => {
console.log(count()); // همین حالا اجرا میشود، و با هر تغییر وابستگی
return () => {
/* پاکسازی اختیاری، پیش از اجرای بعدی و هنگام دور انداختن */
};
});
batch(() => {
setCount(1);
setName('x');
}); // یک بار تخلیه، اثرها بدون تکرار
untrack(() => count()); // خواندن بدون اشتراکcomputedتنبل است: memoای که خوانده نشود هرگز دوباره حساب نمیشود، و تنها وقتی مقدارش عوض شود دوباره خبر میدهد؛ پس اثرهایی که پشت یک memoی پایدارند دوباره اجرا نمیشوند.- اثرها خودشان ردیابی میکنند: تنها سیگنالهایی که در آخرین اجرا خوانده شدهاند مشترک میمانند، پس یک شرط هرگز اشتراکی کهنه بهجا نمیگذارد.
- اثر حلقوی بهجای چرخیدن، خطا پرتاب میکند: اثری که در سیگنالی مینویسد که خودش میخواند یک اشکال است، و زمان اجرا بهجای رها کردن آن در حلقه بیپایان، خطا میدهد.
بستنهای واکنشی
text، attr، class، boolAttr، style، part، data، aria، role و label همگی یک getter میپذیرند، پس بستن بدون هیچ اثر صریحی خودش را بهروز میکند:
const [active, setActive] = signal(true);
Div()
.class(() => (active() ? 'row active' : 'row'))
.boolAttr('disabled', () => !active())
.build();تنها شکلهای تکمقداری واکنشیاند: style(prop, getter) هست، اما شکلهای نگاشتیِ style({…}) و attrs({…}) یک بار اعمال میشوند.
شرطها و فهرستها
| شکل | این را به کار ببرید | رفتار |
|---|---|---|
| یک شاخه | Show({ when, children, fallback }) | تنها وقتی درستی when برگردد، از نو میسازد. |
| چند شاخه | Switch + Match | تنها وقتی شاخه برگزیده عوض شود، از نو میسازد. |
| فهرستی با شناسههای پایدار | For({ each, key, render }) | آیتمها را با key تطبیق میدهد و گرههایشان را دوباره به کار میگیرد. |
| فهرستی که موقعیت در آن یعنی هویت | Index({ each, render }) | گرهِ هر موقعیت را دوباره به کار میگیرد؛ خود آیتم یک سیگنال است. |
| محتوایی که کل شکلش عوض میشود | یک getter خام بهعنوان فرزند | درشت: با هر خواندن، کل ناحیه را ویران و از نو میسازد. |
Ul().children(
For({
each: () => rows(), // آرایه منبع واکنشی
key: (row) => row.id, // پایدار و یکتا
render: (row, index) => Li().text(() => `${index()}. ${row.title}`),
}),
);چهار قاعده تعیین میکند که آیا For واقعاً چیزی را دوباره به کار میگیرد:
keyباید یکتا باشد. کلید تکراری نادیده گرفته میشود (تنها آیتم نخست رسم میشود) و در حالت توسعه هشدار میدهد. اندیس آرایه را کلید نکنید: این کار بازاستفاده را هنگام مرتبسازی دوباره از بین میبرد.- با آرایهای تازه بهروز کنید.
eachیک سیگنال میخواند، پس دستکاری همان آرایه در جا و دوباره گذاشتنش بهخاطر برابری رد میشود و فهرست هرگز بهروز نمیشود. renderبرای هر آیتم یک بار اجرا میشود، نه با هر تغییر فهرست. بهروزرسانی هر سطر را با سیگنالها برانید؛indexیک getter است، پس پس از مرتبسازی دوباره هم درست میماند.- حذف یک آیتم، دامنه آن سطر را دور میریزد: اثرها و پاکسازیهایش هم با آن میروند.
Show / For را بر getterِ خامِ فرزند ترجیح دهید: getter با هر تغییری که میخواند کل ناحیهاش را از نو میسازد، حتی تغییری که نتیجه را عوض نمیکند؛ پس فوکوس، جای اسکرول، مقدار ورودیها و گذارهای درون آن از دست میروند.
مالکیت
هر اثر، memo و بستن واکنشی از آنِ دامنهای است که آن را ساخته. دور انداختن دامنه، هرچه زیر آن است را دور میاندازد.
import { createRoot, onCleanup } from 'ranui/builder';
const dispose = createRoot((dispose) => {
const el = Div().text(message).build(); // این بستن از آنِ ریشه است
onCleanup(() => console.log('torn down'));
mount(el);
return dispose;
});
dispose(); // اثر بستن را برمیدارد و پاکسازیها را اجرا میکندرابط واکنشی را درون یک createRoot بسازید. بستنی که بیمالک ساخته شود کار میکند، اما هرگز خودبهخود دور انداخته نمیشود.
برچیدن در سطح صفحه
به هر صفحه یا مسیر ریشه خودش را بدهید و هنگام ناوبری دورش بیندازید: هر اثر، بستن، تایمر و شنوندهای که آن صفحه ساخته با یک فراخوانی میرود:
let disposePage = null;
function showPage(render, host) {
disposePage?.();
disposePage = createRoot((dispose) => {
render(host);
return dispose;
});
}<r-route> این را در خود دارد: با src، ماژول صفحه هنگام تطبیق وارد میشود، خروجی پیشفرضش درون یک createRoot اجرا میشود و آن ریشه هنگام ترک دور انداخته میشود. getOwner() / runWithOwner() به یک مسیریاب اجازه میدهند دامنهای را از میان یک await عبور دهد.
درون یک Web Component از بستنهای getter استفاده نکنید
constructor و connectedCallback یک کامپوننت دامنه واکنشی نیستند، پس بستن getter یا createEffectی که آنجا ساخته شود بیسرپرست میماند و هرگز دور انداخته نمیشود؛ روی گره جداشده همچنان شلیک میکند، و اگر سیگنال از عنصر بیشتر عمر کند، عنصر را در حافظه میخکوب میکند. با مقدارهای ساده بسازید و بهروزرسانیها را با createEffectهای صریح برانید، توابع دور انداختنشان را جمع کنید و در disconnectedCallback صدا بزنید و هنگام اتصال دوباره از نو ببندید. راهنمای کدنویسی را ببینید.
شنوندهها درون یک عنصر سفارشی
EventManager بر یک AbortController سوار است، پس یک فراخوانی همه شنوندهها را برمیدارد:
const events = new EventManager();
connectedCallback() {
events
.on(this.input, 'input', this.onInput)
.delegate(this, '[data-action]', 'click', (event, el) => this.run(el.dataset.action));
}
disconnectedCallback() {
events.abort(); // همه را برمیدارد و برای اتصال بعدی بازنشانی میشود
}رندر سمت سرور
سازندهها زیر SSR کار میکنند: build() یک گره ساختگی و serialize() یک HTML برمیگرداند. بستنهای واکنشی و For و Show روی سرور یک بار رسم میشوند، مثل یک عکس ثابت؛ تا وقتی کد در مرورگر اجرا نشود هیچ تطبیقی رخ نمیدهد.
مرجع کامل
این صفحه زیرمجموعهای کاربردی است. مرجع کامل (هر کارخانه، هر عملگر، قواعد فضاینام SVG و جزئیات Switch / Match) در BUILDER.md در مخزن است، که درون بسته npm هم عرضه میشود.