التوثيق
JSX والمكوّنات
تصف تطبيقات NSH واجهتها بالعناصر (elements) والمكوّنات (components)، سواء في Rust أو في TSX. تبني Rust العناصر بدوال وبطرق بناء (builder) مُنمَّطة (view().gap_4().child(..))، بينما يكتبها TSX بصيغة JSX التي يحوّلها مترجم NSH إلى استدعاءات مباشرة للمُصيِّر الأصلي نفسه، دون React ودون DOM افتراضي. وفي الحالتين، المكوّن دالة تعمل مرة واحدة لبناء جزئها من الشجرة. أما ما يُبقي الواجهة محدّثة بعد ذلك فهو التفاعلية: تُتتبَّع الارتباطات (bindings) التي تقرأ الإشارات (signals) بشكل أصلي، وهي وحدها التي تُعاد.
import { type InputRef, type JSX, mount, Scroll, signal, split, Text, View } from "nsh";
import { Input, type InputProps } from "nsh/base";
import { button, buttonText, desktop, field, tint, ui, wide } from "./palette";
/** A component: a function of its props, run once. */
function Badge(props: { label: string; color?: string }) {
return (
<View style={{ alignSelf: "flex-start", borderRadius: 999, paddingHorizontal: 10, paddingVertical: 4, backgroundColor: props.color ?? tint(ui().accent, 0.14) }}>
<Text style={{ fontSize: 13, fontWeight: "600", color: ui().text }}>{props.label}</Text>
</View>
);
}
/** Children are whatever the caller put between the tags. */
function Card(props: { title: string; children?: JSX.Element }) {
return (
<View
style={{ ...ui().card, borderRadius: 16, padding: 14, gap: 10 }}
>
<Text style={{ fontSize: 13, fontWeight: "600", color: ui().muted }}>{props.title}</Text>
{props.children}
</View>
);
}
/** Own props are split off; the rest is spread onto the input and stays reactive. */
function Field(props: InputProps & { label: string }) {
const [own, input] = split(props, ["label"]);
return (
<View style={{ gap: 4 }}>
<Text style={{ fontSize: 13, color: ui().muted }}>{own.label}</Text>
<Input {...field()} {...input} />
</View>
);
}
function App() {
const name = signal("");
const taps = signal(0);
let nameInput: InputRef | undefined;
return (
<Scroll style={{ flex: 1, backgroundColor: ui().page }}>
<View
style={
wide()
? { padding: 20, paddingTop: 24, gap: 16, display: "grid", gridTemplateColumns: "repeat(2, 1fr)", alignItems: "start" }
: { padding: 20, paddingTop: 24, gap: 16 }
}
>
<Card title="Props are live">
<Badge label={`${desktop ? "Clicked" : "Tapped"} ${taps()} times`} color={taps() % 2 ? tint(ui().warn, 0.16) : tint(ui().accent, 0.14)} />
<View onPress={() => taps.update((n) => n + 1)} {...button("primary")}>
<Text style={buttonText("primary")}>{desktop ? "Click" : "Tap"}</Text>
</View>
</Card>
<Card title="split, spread and ref">
<Field label="Your name" placeholder="Type here" value={name()} onChange={({ value }) => name.set(value)} ref={(node) => (nameInput = node)} />
<Text style={{ color: ui().text }}>{name() ? `Hello, ${name()}!` : "Nobody yet."}</Text>
<View style={{ flexDirection: "row", gap: 8 }}>
<View onPress={() => nameInput?.focus()} {...button("accent")}>
<Text style={buttonText("accent")}>Focus</Text>
</View>
<View onPress={() => (name.set(""), nameInput?.blur())} {...button()}>
<Text style={buttonText()}>Clear</Text>
</View>
</View>
</Card>
</View>
</Scroll>
);
}
mount(App);#العناصر
كل عنصر مضيف (host element) مدعوم بعرض أصلي (native view). تُنشئها Rust بدوال من prelude، ويستوردها TSX من nsh بوصفها مكوّنات، ولا يوجد سوى If وEach كعناصر عامة (globals) لا تحتاج إلى استيراد:
| Rust | TSX | العرض الأصلي | الصفحة |
|---|---|---|---|
view() | <View> | UIView / ViewGroup: صندوق للتخطيط والتنسيق واللمسات | View |
text(..) | <Text> | عرض نص أصلي؛ عناصر النص المتداخلة تكون spans | Text |
image(src) | <Image> | عرض صورة (أصول محلية وعناوين URL) | Image |
scroll() | <Scroll> | UIScrollView / ScrollView | Scroll |
input() | <Input> | UITextField / UITextView / EditText | Input |
virtual_list(count, row) | <List> | قائمة افتراضية (virtualized) تعيد تدوير عناصرها | List |
canvas(..)، web_view(url)، video(url) | <Canvas>، <WebView>، <Video> | رسم، وصفحة ويب، ومشغّل فيديو | Elements |
nsh_base::dialog() | <Dialog> من nsh/base | طبقة معروضة | Elements |
when(..)، each(..) | <If>، <Each> | تحكم في التدفق، دون عرض خاص به | Control flow |
use nsh_core::prelude::*; // view, text, image, scroll, input, virtual_list, ..., when, eachimport { Image, mount, Text, View } from "nsh";في Rust، الأنماط (styles) طرق مُنمَّطة بأسماء تطابق أدوات Tailwind (.px_4()، .rounded_lg()، .bg(..))، والمستمعات (listeners) طرق أيضًا (.on_press(..))، وللإعدادات الخاصة بمنصة معيّنة طرقها الخاصة. أما في TSX فتحتفظ الخصائص (props) بأسمائها بصيغة camelCase (onPress، allowFontScaling، backgroundColor)؛ ويقبل كل عنصر style وclass / classList (Tailwind) وref ومعالجات اللمس وخصائص إمكانية الوصول (accessibility) وtestID، وتوضع الخصائص الخاصة بمنصة معيّنة في ios={{…}} وandroid={{…}}، حيث يُرسَل كائن المنصة الحالية فقط:
view()
.rounded(px(12.0))
.bg(move || theme().background)
.on_press(move || open.set(true))
.child(text("Open").font_weight(FontWeight::SEMIBOLD))<View android={{ elevation: 4, ripple: { color: "#0001" } }} style={{ borderRadius: 12 }} onPress={() => open.set(true)}>
<Text style={{ fontWeight: "600" }}>Open</Text>
</View>وسوم TSX مكتوبة بصيغة PascalCase ليتحقق TypeScript من كل وسم مقابل الربط (binding) الذي يسمّيه: فالوسم بأحرف صغيرة يُفحص مقابل نوع عام واحد لكل اسم، ولذلك لن يتمكن مكوّن عام (generic) مثل List أو Each من استنتاج نوع صفوفه من each / of، ولن تكون مكوّنات التطبيق نفسه مُنمَّطة إطلاقًا. والاستيرادات لا تكلّف شيئًا وقت التشغيل: يتعرّف المترجم على العناصر الأساسية (primitives) من خلال استيراداتها (بما في ذلك الأسماء المستعارة)، وينشئ العنصر الأصلي في مكانه، فيصبح <View> استدعاءً مباشرًا للمُصيِّر، ويُحذف الاستيراد من الحزمة (bundle). أما الوسم بأحرف صغيرة فهو خطأ في البناء، وتبيّن رسالته ما يجب كتابته:
x lowercase tag <view>: tags are PascalCase components: write <View> and
| `import { View } from "nsh"`
#المكوّنات
شكل التطبيق شأنٌ خاص به. توفّر NSH طبقتين: العناصر الأساسية (العناصر أعلاه، والأنماط، وفئات Tailwind)، والمكوّنات الأساسية غير المنسّقة في nsh/base (nsh_base في Rust)، وهي التي تتولى السلوك مثل التنقل، والحوارات، ولوحة المفاتيح، وشريط الحالة، والمناطق الآمنة. أما الطبقة الثالثة، مكتبة المكوّنات، فهي لك: الأزرار والبطاقات وكل جزء آخر من واجهتك مكوّنات تكتبها فوق هاتين الطبقتين، في crate تطبيقك (Building with Rust) أو في TSX، أو مكتبة من المجتمع (الطبقات الثلاث).
مكوّن Rust هو #[component] fn يأخذ بنية خصائصه (props struct) (وعندما تكون له أحداث، يأخذ Scope لإطلاقها) ويعيد عناصره. ويُنشئ #[component] أيضًا الدالة المُنشِئة (constructor) للمكوّن، وهي باسم الدالة بصيغة snake case: يوضع Badge باستخدام badge()، كما يضع view() عرضًا، مع دالة تعيين (setter) لكل خاصية (.label(..)، .color(..): قيمة أو signal أو closure). ومكوّن TSX دالة تأخذ كائن خصائص وتعيد JSX:
#[derive(Debug, Clone, PartialEq, Props)]
pub struct BadgeProps {
/// The text.
pub label: Prop<SharedString>,
/// The background (light blue when left out).
#[prop(default = Hsla::from(rgb(0xddf4ff)))]
pub color: Prop<Hsla>,
}
/// A small rounded label.
#[component]
pub fn Badge(props: BadgeProps) -> impl IntoElement {
view()
.rounded_full()
.px(px(10.0))
.py_1()
.bg(move || props.color.get())
.child(text(props.label).text_size(px(13.0)).font_weight(FontWeight::SEMIBOLD))
}
// Placed in another component's view:
badge()
.label(move || format!("{} new", unread.get()))
.color(rgb(0xfff8c5))function Badge(props: { label: string; color?: string }) {
return (
<View style={{ borderRadius: 999, paddingHorizontal: 10, paddingVertical: 4, backgroundColor: props.color ?? "#ddf4ff" }}>
<Text style={{ fontSize: 13, fontWeight: "600" }}>{props.label}</Text>
</View>
);
}
<Badge label={`${unread()} new`} color="#fff8c5" />تعمل الدالة مرة واحدة عند إنشاء المكوّن، ولا تتتبّع ما تقرأه: فلا توجد إعادة تصيير (re-rendering). فالشيفرة في جسمها (إنشاء signals، وبدء effects، وحساب الثوابت) تعمل مرة واحدة. أما العناصر التي تعيدها فتبقى مرتبطة بالخصائص والـ signals التي تقرأها، فيحدّث Badge أعلاه نصه كلما تغيّر unread، دون أن يعمل Badge من جديد.
اكتب #[component] عندما تكون للواجهة خصائص أو أحداث خاصة بها، أو عندما يستورده مدخل TSX: يُسجَّل مكوّن Rust تحت نوعه (registry.register::<Badge>())، بخصائص مُنمَّطة من البنية (Building with Rust). أما قطعة الواجهة التي تستعملها Rust وحدها، فتكفي دالة عادية تعيد impl IntoElement، وتكون معاملاتها هي الخصائص. وفي TSX، يجوز أن يعيد المكوّن عنصرًا، أو سلسلة نصية أو رقمًا، أو مصفوفة، أو null، أو دالة تعيد أيًّا من هذه (يتتبعها المُصيِّر)؛ ويصف النوع Component<P> من nsh واحدًا منها:
fn row(title: impl Into<SharedString>, content: impl IntoElement) -> impl IntoElement {
view()
.flex_row()
.gap_2()
.child(text(title.into()))
.child(content)
}import type { Component, JSX } from "nsh";
import { Text, View } from "nsh";
const Row: Component<{ title: string; children?: JSX.Element }> = (props) => (
<View style={{ flexDirection: "row", gap: 8 }}>
<Text>{props.title}</Text>
{props.children}
</View>
);#ما الذي يُعاد تشغيله
القيمة المعطاة لعنصر تُعيَّن مرة واحدة؛ أما الارتباط (binding) فيتتبّع ما يقرؤه. في Rust يكون الارتباط closure (أو signal أو memo أو خاصية) يوضع حيث تذهب القيمة. وفي TSX يقرّر المترجم ذلك لكل تعبير في JSX: يتغيّر التعبير عندما يستدعي دالة أو يقرأ خاصية (خارج الدوال المتداخلة)، وكل ما عدا ذلك يُعيَّن مرة واحدة:
text(move || format!("{}", count.get())) // follows count
text(props.title) // follows the prop
text(label.clone()) // a plain value: set once
view().opacity(move || if busy.get() { 0.5_f32 } else { 1.0 }) // follows busy<Text>{count()}</Text> // follows count
<Text>{props.title}</Text> // follows the prop
<Text>{user.name}</Text> // re-read when something it depends on changes
<Text>{label}</Text> // a plain variable: set once
<View style={{ opacity: busy() ? 0.5 : 1 }} /> // the style object is rebuilt when busy changesالخاصية أو العنصر الفرعي المتغيّر هو effect أصلي صغير يعيّن تلك الخاصية وحدها أو يستبدل ذلك العنصر الفرعي وحده. وتُقارَن الأنماط (diff) في Rust، فالنمط الذي يتغيّر لا يرسل إلا الخصائص التي تغيّرت.
معالجات الأحداث تُعيَّن مرة واحدة: closure في Rust، ودالة أو اسم في TSX (onPress={() => …}، onPress={save}). وفي TSX تُتتبَّع التعبيرات الأخرى (onPress={props.onPress} يتتبّع الخاصية).
#الخصائص (Props)
خصائص مكوّن Rust هي حقول بنية #[derive(Props)]، وكل منها Prop<T>: قيمة، أو ارتباط مرّره المكوّن الأب. اقرأ الخاصية داخل ارتباط (text(props.title)، move || props.title.get()) لتتبّعها؛ أما props.title.get() في الجسم فيقرؤها مرة واحدة. الحقول التي عليها #[prop(default)] أو #[prop(default = …)]، وكذلك Option، اختيارية. تبدأ الدالة المُنشِئة كل خاصية بقيمتها الافتراضية، ويستدعي الأب دوال التعيين للخصائص التي يحتاجها: badge().label("New").
الخاصية في TSX التي تُعطى تعبيرًا متغيّرًا تُمرَّر على شكل getter: قراءة props.title تستدعيه، وهذا ما يتيح لـ JSX الذي يقرؤه أن يتتبّعه. أما الخصائص الثابتة فقيم عادية. وفي الحالتين، تُقرأ الخصائص في المكان الذي تُستخدم فيه:
// Wrong: reads the prop once, when the component runs.
#[component]
fn Title(props: TitleProps) -> impl IntoElement {
let content = props.text.get();
text(content)
}
// Right: binds the text to the prop.
#[component]
fn Title(props: TitleProps) -> impl IntoElement {
text(props.text)
}// Wrong: reads the prop once, when the component runs.
function Title(props: { text: string }) {
const { text } = props;
return <Text>{text}</Text>;
}
// Right: reads it in JSX, which follows it.
function Title(props: { text: string }) {
return <Text>{props.text}</Text>;
}ومكوّن TSX الذي يفكّك خصائصه في المعامل، function Title({ text }: …)، يقرؤها بالطريقة نفسها مرة واحدة، ويحذّر البناء من ذلك مع ذكر الخصائص التي ينبغي قراءتها بدلًا منها:
warn App.tsx:3:16: Title takes its props apart (`{ text }`): each is read once, when Title runs, and does not follow changes; take `props` and read `props.text` where it is used (`split(props, [...])` to pass the rest on)
الخاصية التي يريدها المكوّن مرة واحدة فقط، مثل معرّف (id) يبدأ به، تُقرأ في الجسم (props.id.get_untracked()، const { id } = props;)، فيكون ذلك صريحًا.
وفي TSX ينطبق الأمر نفسه على القيم الافتراضية: اكتب props.color ?? "#ddf4ff" في JSX بدلًا من التفكيك مع قيمة افتراضية. ولحساب شيء انطلاقًا من الخصائص، استخدم closure أو computed:
#[component]
fn Price(props: PriceProps) -> impl IntoElement {
let formatted = move || format!("${:.2}", props.cents.get() as f64 / 100.0);
text(formatted)
}function Price(props: { cents: number }) {
const formatted = () => `$${(props.cents / 100).toFixed(2)}`;
return <Text>{formatted()}</Text>;
}#العناصر الفرعية (Children)
ما يضعه الأب داخل مكوّن يصل إليه بوصفه عناصره الفرعية: حقل children: Prop<Children> في Rust، يملؤه الأب بـ .child(..)؛ وprops.children في TSX، وهو getter يبني تلك العناصر عند قراءته:
#[derive(Debug, Clone, PartialEq, Props)]
pub struct CardProps {
/// The title.
pub title: Prop<SharedString>,
/// The content.
pub children: Prop<Children>,
}
#[component]
pub fn Card(props: CardProps) -> impl IntoElement {
view()
.p(px(14.0))
.rounded(px(12.0))
.bg(move || theme().background)
.gap_2()
.child(text(props.title).font_weight(FontWeight::SEMIBOLD))
.child(props.children)
}
card()
.title("Account")
.child(text(move || format!("Signed in as {}", user.get().name)))function Card(props: { title: string; children?: JSX.Element }) {
return (
<View style={{ padding: 14, borderRadius: 12, backgroundColor: "#ffffff", gap: 8 }}>
<Text style={{ fontWeight: "600" }}>{props.title}</Text>
{props.children}
</View>
);
}
<Card title="Account">
<Text>Signed in as {user().name}</Text>
</Card>في TSX، كل قراءة لـ props.children تبني العناصر الفرعية من جديد (عروض أصلية جديدة): لذا اقرأها مرة واحدة، في المكان الذي توضع فيه العناصر الفرعية. ومن بين عدة عناصر فرعية، يصل العنصر المتغيّر (<If> أو <Each> أو {cond() ? a : b}) على شكل دالة، ويحدّث {props.children} ذلك العنصر وحده: فتحتفظ العناصر المجاورة بعروضها، ويحتفظ حقل الإدخال بنصه وتركيزه. وتجمع الأجزاء (Fragments) (<>…</>) العناصر دون عرض.
ويمكن أيضًا بناء العناصر الفرعية بدالة يستدعيها المكوّن مع معاملات، أي دالة استدعاء للتصيير (render callback):
fn repeat<E: IntoElement>(times: usize, row: impl Fn(usize) -> E) -> impl IntoElement {
view().child((0..times).map(row))
}
repeat(3, |i| text(format!("Row {i}")))function Repeat(props: { times: number; children: (i: number) => JSX.Element }) {
return <View>{Array.from({ length: props.times }, (_, i) => props.children(i))}</View>;
}
<Repeat times={3}>{(i) => <Text>Row {i}</Text>}</Repeat>#التحكم في التدفق
when وeach في Rust، و<If> و<Each> في TSX، هي أدوات تحكم في التدفق مضمّنة. ولا تُصيّر أي عرض خاص بها.
when(move || user.get().is_some(), move || {
text(move || format!("Hello, {}", user.get().map(|u| u.name).unwrap_or_default()))
})
.otherwise(|| text("Signed out"))
each(todos, |todo| todo.id, |todo, index| {
text(move || format!("{}. {}", index.get() + 1, todo.get().title))
})<If when={user()} else={<Text>Signed out</Text>}>
{(u) => <Text>Hello, {u().name}</Text>}
</If>
<Each of={todos()} key={(t) => t.id}>
{(todo, index) => (
<Text>
{index() + 1}. {todo().title}
</Text>
)}
</Each>when/<If>يعرض فرعه ما دام الشرط متحققًا، وإلا عرض فرعotherwise/else. ولا يُعيد تصيير فرع إلا عندما يتغيّر كون الشرط متحققًا أم لا، وليس في كل مرة يُعاد فيها تقييم الشرط. وفي TSX، تحصل الدالة الفرعية على accessor لقيمةwhen، منمَّط بدونnullوundefined:u()هو المستخدم ما دام الفرع معروضًا، ويتتبّعه عندما يتغيّر.each/<Each>يصيّر صفًا لكل عنصر، تبنيه دالة الصف الخاصة به. تُحفظ الصفوف بحسب المفتاح (Rust: دالة المفتاح؛ TSX:key، وهو افتراضيًا العنصر نفسه، فالكائنات بالهوية والقيم الأولية بالقيمة): وعندما تتغيّر القائمة، تُنقل الصفوف المحفوظة بدل إعادة بنائها، ولا تُنشأ صفوف جديدة إلا للمفاتيح الجديدة. تستقبل دالة الصف signals للعنصر وفهرسه (todo.get()وindex.get()في Rust، وtodo()وindex()في TSX)، لأن عنصر الصف المحفوظ وموضعه قد يتغيّران. وفي TSX، يُبقيkey="index"(أوkey={(item, i) => i}) الصفوف بحسب الموضع بدلًا من ذلك: فالقائمة من السلاسل النصية التي تتغيّر في مكانها، مثل نص يصل تدفقًا، تحتفظ بصفوفها ولا يتغيّر فيها إلاitem().
الاختيار الذي يتم داخل closure (dynamic(move || if busy.get() { … } else { … }) في Rust، أو عامل ثلاثي أو && في TSX) يبني فرعه من جديد في كل مرة يتغيّر فيها أي شيء يقرؤه. فضّل when / <If> عندما يتجاوز الفرع بضعة عناصر.
للقوائم الطويلة أو اللانهائية، استخدم List (virtual_list في Rust): فهو ينشئ عروضًا للصفوف الظاهرة على الشاشة فقط ويعيد تدويرها. انظر التحكم في التدفق للتفاصيل.
#المراجع (Refs)
يسمّي الـ ref عنصرًا مضيفًا من أجل الاستدعاءات الأمرية (imperative): focus()، blur()، scroll_to() / scrollTo()، scroll_to_index() / scrollToIndex()، measure() (مكان العرض)، … في Rust، ينشئ element_ref() واحدًا ويضعه .element_ref(&handle) على عنصر؛ وفي TSX، يعطيك ref الـ ref الخاص بالعنصر، ونوعه هو نوع العنصر نفسه (InputRef هنا، وViewRef لـ View؛ انظر دوال ref):
fn search() -> impl IntoElement {
let field = element_ref();
view()
.child(input().element_ref(&field).placeholder("Search"))
.child(text("Focus").on_press(move || field.focus()))
}import type { InputRef } from "nsh";
import { Text, View } from "nsh";
import { Input } from "nsh/base";
function Search() {
let input: InputRef | undefined;
return (
<View>
<Input ref={input} placeholder="Search" />
<View onPress={() => input?.focus()}>
<Text>Focus</Text>
</View>
</View>
);
}الـ ref في Rust مقبض (handle) من نوع Copy؛ تذهب استدعاءاته إلى العنصر الذي يحمله، ولا تفعل شيئًا بعد زوال مالكه. وفي TSX، يستقبل ما يسمّيه ref={…} العقدة:
- المتغيّر المُعلَن بـ
letتُسنَد إليه؛ - الحقل (
ref={refs.input}،ref={refs[name]}) تُسنَد إليه؛ - الـ signal تُعيَّن إليه، فيتتبّعها ما يقرأ الـ signal؛
- الدالة تُستدعى بها (
<Scroll ref={(node) => (scroller = node)} />).
يحمل const أو الاستيراد signal أو دالة، كما في const input = signal<InputRef>() مع <Input ref={input} /> وinput()?.focus()؛ وأي شيء آخر يُبلَّغ عنه عند إنشاء العنصر، مع بيان ما يجب كتابته.
يُعيَّن الـ ref عند إنشاء العنصر، قبل أن يعيد المكوّن قيمته، فيكون متاحًا في effects ومعالجات الأحداث. وتُرسَل الاستدعاءات إلى العرض بالترتيب مع التغييرات التي حدثت قبلها. وتقبل المكوّنات الأساسية ومكوّنات Rust في crate تطبيقك ref أيضًا (في Rust component_ref()، يوضع على العنصر بـ .component_ref(&r): dialog().component_ref(&r)).
#تمرير الخصائص بالنشر (Spreading)
في Rust، يستدعي الأب دوال التعيين للخصائص التي يحتاجها وتحتفظ البقية بقيمها الافتراضية، ويمرّر المغلِّف (wrapper) Prop الخاصة به كما هي (card().title(props.title)): فيظل Prop يتتبّع ما أُعطيه.
وفي TSX، انشر كائن خصائص على عنصر أو مكوّن باستخدام {...props}. تبقى الـ getters كما هي، فتظل الخصائص المنشورة تتتبّع ما تقرؤه. ولأخذ بعض الخصائص لنفسك وتمرير الباقي، استخدم split:
import { split, Text, View } from "nsh";
import { Input, type InputProps } from "nsh/base";
function Field(props: InputProps & { label: string }) {
const [own, rest] = split(props, ["label"]);
return (
<View style={{ gap: 4 }}>
<Text>{own.label}</Text>
<Input style={{ borderWidth: 1, borderRadius: 8, padding: 8 }} {...rest} />
</View>
);
}
<Field label="Email" value={email()} onChange={({ value }) => email.set(value)} keyboardType="email-address" />
يعيد split(props, keys) القيمة [picked, rest]، وكلاهما لا يزال يقرأ عبر الخصائص الأصلية (أما تفكيك { label, ...rest } = props فكان سيقرأ كل خاصية مرة واحدة). وخصائص العنصر التي تأتي لاحقًا في JSX تتغلّب على السابقة لها.
لا يقبل <If> و<Each> خصائص منشورة، ولا يُدعم نشر العناصر الفرعية ({...items}): استخدم <Each> أو تعبير مصفوفة.
#الوسوم والأسماء
عنصر Rust هو أي شيء تستدعيه الشيفرة: دالة عنصر أساسي، أو الدالة المُنشِئة لمكوّن (badge()، nsh_base::button())، أو دالة من كتابتك. ووسم TSX يسمّي ربطًا (binding) موجودًا في النطاق، كالمعتاد: <Badge />، <Theme.Provider>، أو <Button> من كتابتك. والوسم الذي لا يسمّي شيئًا في النطاق خطأ في البناء، باستثناء <If> و<Each> فهما عنصران عامان.
/// The app's button: a press target styled by the app.
fn primary_button(title: &'static str, on_press: impl Fn() + 'static) -> impl IntoElement {
view()
.role(AccessibilityRole::Button)
.rounded_lg()
.bg(rgb(0x2563eb))
.px_5()
.py_3()
.active(|s| s.opacity(0.6))
.on_press(on_press)
.child(text(title).text_base().font_weight(FontWeight::SEMIBOLD).text_color(white()))
}
primary_button("Save", save)import { Text, View } from "nsh";
function Button(props: { title: string; onPress: () => void }) {
return (
<View onPress={props.onPress} activeStyle={{ opacity: 0.6 }} role="button" class="rounded-lg bg-blue-600 px-5 py-3">
<Text class="text-base font-semibold text-white">{props.title}</Text>
</View>
);
}
<Button title="Save" onPress={save} />إن Button الخاص بـ nsh_base (button() في Rust، و<Button> من nsh/base) هو هدف الضغط غير المنسّق، مع حالتي التعطيل والتحديد وما يخصهما من إمكانية الوصول؛ ويمنحه التطبيق مظهره: button().disabled(move || saving.get()).on_press(move || save()).px_4().active(|s| s.opacity(0.6)).child(text("Save")).
وفي TSX، تتغلّب ارتباطاتك على العناصر العامة: فمكوّن باسم Each في النطاق يجعل <Each> ذلك المكوّن. ويُتعرَّف على العنصر الأساسي من خلال استيراده، فيجعل import { View as Box } from "nsh" العنصر <Box> عرضًا، أما مكوّن من كتابتك باسم Text فهو مجرد مكوّنك. والعناصر الأساسية مكوّنات حقيقية أيضًا: const Tag = inline ? Text : View; ثم <Tag>…</Tag> يعمل.
تُنمَّط المكوّنات بتصريحاتها الخاصة: يفحص المترجم بنى خصائص Rust، ويفحص TypeScript خصائص مكوّن TSX أينما استُخدم، دون أي شيء يلزم توليده. ويحصل مكوّن Rust الذي يستورده مدخل TSX على تصريحات TypeScript تُولَّد من بنيته.
#التركيب (Mounting) والجذر
جذر تطبيق Rust هو المكوّن الذي يسمّيه "root" الخاص بـ nsh.json؛ ويركّبه nsh run وnsh package. ويستدعي مدخل TSX الدالة mount(App) لتصيير المكوّن الجذر (mount(App, props) يمرّر له الخصائص). ويملأ العرض الجذر السطح (النافذة، أو العرض الذي يضمّن التطبيق فيه NSH). أعطِ الجذر flex: 1 ليملأ الشاشة، ولونَ خلفية: فخلفية السطح نفسه تعتمد على المنصة.
// nsh.json: { "rust": ".", "root": "App" }
#[component]
pub fn App() -> impl IntoElement {
view()
.flex_1()
.bg(move || theme().background)
.child(home())
}mount(() => (
<View style={{ flex: 1, backgroundColor: "#ffffff" }}>
<App />
</View>
));#ذات صلة
- Reactivity: الإشارات (signals) والتأثيرات (effects) والسياق (context).
- Elements: جميع عناصر المضيف (host elements) وخصائصها (props).
- Styling وTailwind: مظهر مكوّناتك.
- Building with Rust: تطبيقات Rust، والمكوّنات في الحزمة (crate) الخاصة بتطبيقك، وTSX فوقها.