حالت بلوک (PageState)

حالت‌های بارگذاری، خطا و خالیِ یک بلوک از صفحه — اسکلتی هم‌شکل محتوا، خطا با «تلاش مجدد» و حالت خالی، همه به‌جای بلوک

معرفی

PageState حالت‌های یک بلوک از صفحه را نشان می‌دهد: وقتی حالت «آماده» است محتوای بلوک رندر می‌شود و در غیر این صورت یک عنصر به‌جای آن می‌نشیند:

  • بارگذاری — اسکلتی هم‌شکل همان محتوا (ردیف‌های جدول، کارت‌ها، کاشی‌های شاخص، ردیف‌های فرم، بخش‌ها، نمودار)؛
  • خطا — ErrorState با دکمهٔ «تلاش مجدد»؛
  • خالی — Empty با عنوان، توضیح و یک اقدام؛ یا وقتی فیلتری فعال است، «نتیجه‌ای یافت نشد» با «پاک کردن فیلترها».

هیچ‌کدام داخل محتوا رندر نمی‌شود، پس حالت خالی هرگز در سلول جدول یا داخل کارت نمی‌افتد. حالت خطا و حالت خالی دست‌کم به ارتفاع توکن --layout-state-min-height (240 پیکسل) هستند و اسکلت شکل خود محتوا را دارد، تا جابه‌جایی بین حالت‌ها صفحه را کمتر تکان دهد.

حالت را همیشه با تابع pageState() از خروجی درخواست می‌سازید؛ این تابع یک قاعده است که همهٔ صفحه‌ها را یکسان می‌کند. قالب‌های صفحه (ListPage، DetailPage، FormPage، SettingsPage، DashboardPage) همان حالت را با ویژگی state می‌گیرند و خودشان جایش را تعیین می‌کنند؛ سرِ صفحه در هیچ حالتی ناپدید نمی‌شود.

چه زمانی استفاده کنیم:

  • یک بلوک داخل صفحه که داده‌اش جدا بارگذاری می‌شود: فهرست یک زبانه، فیدی در یک بخش.
  • هر جایی که همان قاعدهٔ «به‌جای بلوک، نه داخل آن» لازم است و قالب صفحه آن را برایتان انجام نمی‌دهد.

چه زمانی استفاده نکنیم:

  • محتوای اصلی صفحه‌ای که با قالب ساخته شده است: state همان قالب را بدهید. بخشی از صفحهٔ جزئیات هم state خود DetailSection را دارد.
  • دور کل صفحه یا دور سرِ صفحه: سرِ صفحه همیشه روی صفحه می‌ماند.
  • داخل Card: جدول یا فهرستی که در کارت داشبورد است DashboardChart height="content" با state خودش است.
منبعپلتفرممنشن‌ها
صفحهٔ رسمی برنداینستاگرام1,240
کانال خبری فروشگاهتلگرام860
وبلاگ بررسی محصولوب‌سایت312

استفاده

PageState و pageState را از @partodata/ui/templates وارد کنید؛ isLoading و error همان خروجی هوک داده است (useAsync سیستم طراحی، هوک همهٔ نمونه‌ها) و data خودِ فهرست است. این‌جا هوک خودِ فهرست نظرها (Comment[]) را برمی‌گرداند؛ اگر هوک شیء صفحه ({ items, total }) برگرداند، data: data?.items را بدهید:

'use client'
import * as React from 'react'
import { useAsync } from '@partodata/ui'
import { Comment, EntityCollection, type SocialComment } from '@partodata/ui/social'
import { PageState, pageState } from '@partodata/ui/templates'

type ApiComment = { id: string; text: string; sentiment: 'positive' | 'negative' | 'neutral' }

// نظر API در مدل اجتماعی (یک Comment همهٔ نظرها را رسم می‌کند)
const toComment = (c: ApiComment): SocialComment => ({ id: c.id, text: c.text, signals: { sentiment: c.sentiment } })

// درخواست API اپ، در سطح ماژول (پس `load` پایین میان رندرها همان تابع می‌ماند)
async function getComments(postId: string): Promise<ApiComment[]> {
  const response = await fetch(`/api/posts/${postId}/comments`)
  if (!response.ok) throw new Error(`comments: ${response.status}`)
  return response.json()
}

export function CommentsBlock({ postId }: { postId: string }) {
  const { data, isLoading, error, run } = useAsync<ApiComment[]>()
  // مقدار درخواست در خود فراخوانی، همان مقدار در وابستگی‌ها — هرگز شیء یا تابعی که هنگام رندر ساخته شود
  const load = React.useCallback(() => run(() => getComments(postId)), [run, postId])
  React.useEffect(() => {
    load()
  }, [load])
  return (
    <PageState
      state={pageState({ data, isLoading, error, onRetry: load, emptyCopy: { title: 'هنوز نظری ثبت نشده است' } })}
      skeleton="list"
    >
      <EntityCollection
        entity="comment"
        items={(data ?? []).map(toComment)}
        getId={(comment) => comment.id}
        renderItem={(comment, item) => <Comment comment={comment} {...item} />}
      />
    </PageState>
  )
}

حالت‌ها و انواع

قاعدهٔ pageState

pageState({ data, isLoading, error, onRetry, filtered?, onClearFilters?, isEmpty?, emptyCopy? }) به ترتیب:

وضعیت درخواستحالت
هنوز چیزی بارگذاری نشده (data تعریف‌نشده یا null)، یا فهرست خالی در حال بارگذاریبارگذاری
آخرین درخواست شکست خورده (حتی اگر دادهٔ قبلی روی صفحه باشد)خطا با «تلاش مجدد» که onRetry را صدا می‌زند
فهرست بارگذاری‌شده خالی است و فیلتری فعال است (filtered خود ListPage، یا filtered این‌جا)«نتیجه‌ای یافت نشد» با «پاک کردن فیلترها» (متن خود سیستم طراحی)
فهرست بارگذاری‌شده خالی استمتن اولین استفاده از emptyCopy
ردیف‌هایی روی صفحه است و درخواست تازه‌ای در جریان است (صفحهٔ بعد، جست‌وجو، فیلتر)آماده و در حال تازه‌شدن: همان ردیف‌ها، کم‌رنگ
با live: تازه‌شدن همان درخواست در جریان است، یا شکست خورده و داده‌ای روی صفحه هستآماده (یا خالی) با live: همان محتوا، بی‌تغییر؛ اعلان شکست در قالب
بقیهآماده: خود محتوا
  • data برای فهرست خودِ فهرست است (result?.items)، نه شیء صفحه‌ای که فهرست در آن است؛ برای صفحهٔ جزئیات یا فرم ویرایش، خود موجودیت، که هیچ‌وقت خالی نیست مگر isEmpty بگوید. دادن شیء صفحه ({ items, total }) به data خطای نوع است: آن شیء هیچ‌وقت خالی نیست و جدول ردیف «داده‌ای نیست» خودش را می‌کشید.
  • هوک را از «هیچ» شروع کنید (useAsync() بی دادهٔ اولیه، useState<T[] | undefined>()): آرایهٔ خالی اولیه نتیجهٔ خالی خوانده می‌شود و پیش از اولین درخواست حالت خالی را نشان می‌دهد.
  • ردیف‌هایی که روی صفحه‌اند تا رسیدن صفحهٔ بعد یا نتیجهٔ جست‌وجو و فیلتر تازه می‌مانند، کم‌رنگ، و ناحیهٔ وضعیت بلوک «در حال بارگذاری» را اعلام می‌کند ({ status: 'ready', refreshing: true })؛ اسکلت فقط وقتی است که چیزی برای نمایش نیست. نشانهٔ بارگذاری دیگری (اسپینر، کم‌رنگ کردن دستی، isLoading جدول) لازم نیست.
  • دادهٔ زنده (صفحه‌ای که خودش تازه می‌شود): live خروجی useLiveRefresh است. تازه‌شدن همان درخواست محتوا را همان‌طور که هست نگه می‌دارد (نه اسکلت، نه کم‌رنگ شدن، نه اعلام «در حال بارگذاری») و شکستش هم داده را پاک نمی‌کند: قالب اعلان هشدار با «تلاش مجدد» نشان می‌دهد. error آن‌جا فقط برای وقتی است که هیچ داده‌ای نیست (هوک اولین بارگذاری را بی‌صدا دوباره می‌کوشد و خطا تا پاسخ می‌ماند) یا خود درخواست عوض شده و شکست خورده است. هر live فقط به یک state می‌رود: صفحه، یا همان یک بخشی که جدا بارگذاری می‌شود؛ فرم، EntityDrawer و فهرست با loadMore هرگز زنده نیستند. جزئیات: دادهٔ زنده.
  • error و onRetry الزامی‌اند: خطای بارگذاری همیشه نشان داده می‌شود و همیشه از همان‌جا دوباره تلاش می‌شود. خطایی که با تلاش دوباره حل نمی‌شود (دسترسی نداشتن) صفحهٔ UtilityPage نوع 403 است. پیام فنی خطا به کاربر نشان داده نمی‌شود و متن حالت خطا همیشه متن خود سیستم طراحی است («بارگذاری انجام نشد»).
  • emptyCopy فقط متن «هنوز داده‌ای نیست» است (اولین استفاده، بی فیلتر): title، description و یک action به شکل داده — { label, onClick } یا { label, href } — که سیستم طراحی آن را دکمهٔ ثانوی (default) رندر می‌کند؛ اقدام اصلی صفحه جای خودش را دارد. در حالت «نتیجه‌ای یافت نشد» به کار نمی‌رود.
  • در ListPage، filtered و onClearFilters را به خود صفحه بدهید، نه به pageState: صفحه با همان مقدار دکمهٔ پاک کردن نوارابزار را هم نشان می‌دهد (اگر حالت با filtered ساخته شده باشد، در محیط توسعه هشدار می‌دهد). filtered و onClearFilters در pageState — همیشه با هم — برای PageState یا DetailSectionای است که فهرستش فیلتر خودش را دارد؛ دکمهٔ «پاک کردن فیلترها»ی حالت «نتیجه‌ای یافت نشد» همان onClearFilters را صدا می‌زند.

مقداری که pageState برمی‌گرداند

statusچه چیزی به‌جای بلوک می‌آیدفیلدها
readyخود محتوا (بدون هیچ پوششی)؛ با refreshing کم‌رنگ، تا رسیدن دادهٔ تازهrefreshing?، live?
loadingاسکلت به شکل skeletonlive? (نشانگر سر جایش می‌ماند)
errorErrorState (اندازهٔ md) با دکمهٔ «تلاش مجدد»onRetry (متن همیشه از سیستم طراحی)؛ live?
emptyEmpty با آیکون، عنوان، توضیح و یک اقدامreason?: 'no-data'، title?، description?، action? — یا reason: 'no-results' با onClearFilters؛ live?

این شیء را دستی ننویسید؛ تنها جاهای نوشتن آن state={{ status: 'loading' }} در فایل loading.tsx مسیر و state={{ status: 'ready' }} در ListPageای است که ردیف‌هایش در خود کد نوشته شده است (هرگز بارگذاری نمی‌شود).

شکل اسکلت

skeleton نوع محتوایی است که اسکلت جایش را می‌گیرد:

skeletonشکل
tableسرِ جدول و هشت ردیف
listردیف‌های آواتار و متن (فید، نظرها)
cardsشبکهٔ کارت با عرض کمینهٔ --layout-tile-min-width
kpisردیف کاشی‌های شاخص
formردیف‌های فرم در یک قاب
sectionsدو بخش، هر کدام عنوان و یک بلوک
chartناحیهٔ نمودار

راهنمای استفاده

بکنید

  • حالت را با pageState({ data: result?.items, isLoading, error, onRetry }) از خروجی هوک داده بسازید و به PageState یا state قالب بدهید؛ data خودِ فهرست است (یا موجودیت).
  • در ListPage filtered و onClearFilters را به صفحه بدهید؛ برای فهرست یک PageState یا DetailSection که فیلتر خودش را دارد، هر دو را با هم به pageState.
  • skeleton را هم‌نوع محتوا انتخاب کنید تا جای خالی هنگام بارگذاری شکل همان محتوا باشد.

نکنید

  • شیء حالت یا شرط سه‌تایی isLoading ? … : error ? … را دستی ننویسید؛ ترتیب و قاعده را pageState تعیین می‌کند.
  • شیء صفحهٔ هوک ({ items, total }) را به data ندهید؛ خودِ فهرست را بدهید.
  • برای بارگذاری دوباره، وقتی ردیف‌ها روی صفحه‌اند، اسپینر، کم‌رنگ کردن دستی یا isLoading جدول اضافه نکنید؛ حالت خودش ردیف‌ها را کم‌رنگ می‌کند.
  • Empty را در emptyState جدول یا داخل Card نگذارید؛ حالت خالی جای بلوک را می‌گیرد (قرار گرفتن PageState داخل کارت در محیط توسعه هشدار می‌دهد).
  • اسپینر یا PageLoader را به‌جای اسکلت بلوک به کار نبرید.
  • PageState را دور PageHeader یا کل صفحه نپیچید؛ سرِ صفحه در هیچ حالتی ناپدید نمی‌شود.
  • برای خطای بارگذاری toast نشان ندهید؛ خطا جای بلوک را می‌گیرد و دکمهٔ تلاش مجدد دارد.

Props

PageState

Prop

Type

PageStateInput

ورودی pageState():

Prop

Type

دسترسی‌پذیری

  • هر بلوک یک ناحیهٔ زندهٔ role="status" پنهان دارد که در همهٔ حالت‌ها (آماده هم) روی صفحه می‌ماند، پس هر تغییر اعلام می‌شود: «در حال بارگذاری» و سپس عنوان حالت خالی («نتیجه‌ای یافت نشد»). شکل‌های اسکلت از فناوری کمکی پنهان‌اند، پس صفحه‌خوان یک بار اعلام می‌کند، نه یک بار برای هر ردیف. حالت خطا role="alert" خودش را دارد.
  • هنگام تازه‌شدن (ردیف‌ها روی صفحه، درخواست تازه در جریان) همان ناحیه «در حال بارگذاری» را اعلام می‌کند و محتوا پس از 150 میلی‌ثانیه کم‌رنگ می‌شود (پاسخ سریع چشمک نمی‌زند) و با رسیدن داده بی‌درنگ برمی‌گردد. aria-busy روی محتوا گذاشته نمی‌شود: محتوا عنصر خود صفحه است و والد مشغول، اعلام ناحیهٔ وضعیت را نگه می‌داشت.
  • دادهٔ زنده: تازه‌شدن همان درخواست اعلام نمی‌شود و محتوا کم‌رنگ نمی‌شود؛ شکستش را نشانگر «آخرین به‌روزرسانی» قالب (یا، در PageState تنها، ناحیهٔ وضعیت همین بلوک) یک بار و مؤدبانه اعلام می‌کند.
  • فوکوس در بلوک می‌ماند: وقتی «تلاش مجدد» یا «پاک کردن فیلترها» با تغییر حالت از صفحه برداشته می‌شود، فوکوس به خود بلوک می‌رود (همان عنصر در بارگذاری، خطا و خالی) و وقتی محتوا برگشت، به ابتدای بلوک؛ با Tab به اولین کنترل محتوا می‌رسید. فوکوسی که جای دیگری است جابه‌جا نمی‌شود.
  • سرعنوان حالت خطا و حالت خالی ترتیب سرعنوان‌های صفحه را ادامه می‌دهد: مستقیم زیر عنوان صفحه h2 و زیر عنوان یک بخش h3. در کارت نمودار، عنوان کارت نام نمودار است و عنوان حالت پاراگراف است.

کامپوننت‌های مرتبط

  • ListPage و بقیهٔ قالب‌های صفحه — همین شیء را با ویژگی state می‌گیرند و جایش را خودشان تعیین می‌کنند.
  • ErrorState و Empty — اجزایی که حالت‌ها با آن‌ها ساخته می‌شوند.
  • Skeleton — پیش‌تنظیم‌های اسکلتی که skeleton از آن‌ها استفاده می‌کند.
  • دادهٔ زنده — useLiveRefresh: تازه‌شدن بی اسکلت، زمان آخرین به‌روزرسانی، و شکستی که داده را نگه می‌دارد.