kreftjs 0.0.19-beta → 0.0.20-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,258 @@
1
+ # الفصل الثالث: المكوّنات ومشاركة الحالة
2
+
3
+ ## استخدام مكوّن
4
+
5
+ سجّله مرة واحدة بمساره، أو استورده حيث تحتاجه:
6
+
7
+ ```js
8
+ Kreftjs.app('App.kreft')
9
+ .component('UserCard', 'components/UserCard.kreft')
10
+ .mount('#app')
11
+ ```
12
+
13
+ ```html
14
+ import UserCard from './UserCard.kreft'
15
+
16
+ <view>
17
+ <user-card name="سارة أحمد" age={30}></user-card>
18
+ </view>
19
+ ```
20
+
21
+ في الصفحة (`view`) اكتب أسماء المكوّنات بنمط **kebab-case** (`<user-card>`).
22
+
23
+ ## Props: من الأب إلى الابن
24
+
25
+ ```html
26
+ <!-- UserCard.kreft -->
27
+ prop name
28
+ prop age = 0
29
+ prop admin = false
30
+
31
+ <view>
32
+ <div class="card">{name} ({age}) <b k-if="admin">مدير</b></div>
33
+ </view>
34
+ ```
35
+
36
+ ```html
37
+ <user-card name="سارة أحمد" age={30} admin></user-card>
38
+ ```
39
+
40
+ نوع القيمة الافتراضية يحدد كيف تُقرأ الخصائص النصية: `age="30"` تصير الرقم `30`، و`admin` بلا قيمة تصير `true`. الابن لا يستطيع تغيير prop؛ أرسل حدثًا بدلًا من ذلك.
41
+
42
+ ## الأحداث: من الابن إلى الأب
43
+
44
+ ```html
45
+ <!-- TodoItem.kreft -->
46
+ prop todo
47
+ event remove
48
+
49
+ <view>
50
+ <li>{todo.text} <button @click="send('remove', todo.id)">✕</button></li>
51
+ </view>
52
+ ```
53
+
54
+ ```html
55
+ <!-- الأب -->
56
+ action removeTodo(id) { todos = todos.filter((t) => t.id !== id) }
57
+
58
+ <view>
59
+ <todo-item k-each="t in todos" k-key="t.id" todo={t} @remove={removeTodo}></todo-item>
60
+ </view>
61
+ ```
62
+
63
+ ## k-model على مكوّن
64
+
65
+ ```html
66
+ <!-- Toggle.kreft -->
67
+ prop modelValue = false
68
+ event update:modelValue
69
+
70
+ <view>
71
+ <button @click="send('update:modelValue', !modelValue)">{modelValue ? 'مفعّل' : 'معطّل'}</button>
72
+ </view>
73
+ ```
74
+
75
+ ```html
76
+ <toggle k-model="darkMode"></toggle>
77
+ ```
78
+
79
+ النماذج المسمّاة: `k-model:title="pageTitle"` يستعمل `prop title` والحدث `update:title`.
80
+
81
+ ## الفتحات (Slots)
82
+
83
+ ```html
84
+ <!-- Card.kreft -->
85
+ prop items = []
86
+
87
+ <view>
88
+ <div class="card">
89
+ <header><slot name="header">عنوان افتراضي</slot></header>
90
+ <slot></slot>
91
+ <li k-each="item in items"><slot name="item" item={item}>{item}</slot></li>
92
+ </div>
93
+ </view>
94
+ ```
95
+
96
+ ```html
97
+ <card items={users}>
98
+ <template k-slot:header>المستخدمون</template>
99
+ <p>محتوى الجسم</p>
100
+ <template k-slot:item="{ item }"><b>{item.name}</b></template>
101
+ </card>
102
+ ```
103
+
104
+ ## الأنماط (Styles)
105
+
106
+ - `<style>` يسري على الصفحة كلها.
107
+ - `<style scoped>` يسري على هذا المكوّن فقط.
108
+ - `.parent :deep(.child)` يصل إلى المكوّنات الأبناء من CSS محصور.
109
+ - الخصائص مثل `class` التي تضعها على وسم المكوّن تنتقل إلى عنصره الجذر.
110
+
111
+ ## الاحتفاظ والانتظار (Keep و Wait)
112
+
113
+ **Keep** يضع المكوّن جانبًا بدل أن يزيله. النص المكتوب وموضع التمرير والحالة كلها تبقى حين يعود:
114
+
115
+ ```html
116
+ <kreft-component :is="tab" keep></kreft-component>
117
+ <kreft-view keep></kreft-view>
118
+ <kreft-view keep="5"></kreft-view> <!-- احتفظ بخمسة على الأكثر؛ الأقدم استعمالًا يذهب -->
119
+ ```
120
+
121
+ ```html
122
+ on show { timer.start() } <!-- عاد (وكذلك أول مرة) -->
123
+ on hide { timer.stop() } <!-- وُضع جانبًا -->
124
+ ```
125
+
126
+ **Wait** يعرض بديلًا إلى أن يجهز كل ما بداخله (المكوّنات الكسولة، وكل `load`، وكل `on start` ينتظر):
127
+
128
+ ```html
129
+ <kreft-wait>
130
+ <user-profile id={5}></user-profile>
131
+ <p k-waiting>جارٍ التحميل…</p>
132
+ <p k-failed="error">تعذر التحميل: {error.message}</p>
133
+ </kreft-wait>
134
+ ```
135
+
136
+ ## مشاركة الحالة
137
+
138
+ | الحاجة | الأداة |
139
+ |---|---|
140
+ | من الأب إلى الابن | `prop` |
141
+ | من الابن إلى الأب | `event` + `send()` أو `k-model` |
142
+ | مكوّنات كثيرة في أي مكان | المخازن: `Kreftjs.store()` + `use` |
143
+ | أب إلى كل ما بداخله | `share` / `receive` |
144
+ | رسائل "حدث شيء ما" | `bus.send()` / `bus.on()` |
145
+
146
+ ### المخازن (Stores)
147
+
148
+ أنشئ المخزن مرة واحدة:
149
+
150
+ ```js
151
+ Kreftjs.store('cart', {
152
+ items: [],
153
+ get count() { return this.items.length },
154
+ get total() { return this.items.reduce((s, p) => s + p.price, 0) },
155
+ add(product) { this.items.push(product) },
156
+ })
157
+ ```
158
+
159
+ استعمله في أي ملف `.kreft`. كل المكوّنات ترى البيانات نفسها:
160
+
161
+ ```html
162
+ use cart
163
+
164
+ <view>
165
+ <span>السلة: {cart.count} · {cart.total}</span>
166
+ <button @click="cart.add(product)">أضف</button>
167
+ </view>
168
+ ```
169
+
170
+ لكل مخزن أيضًا `$patch` (يغيّر عدة قيم دفعة واحدة) و`$reset()` (يعود للبداية) و`$watch(fn)` (يعمل عند كل تغيير ويتوقف وحده حين يزول المكوّن).
171
+
172
+ ### المنطق المشترك
173
+
174
+ المنطق الذي تحتاجه مكوّنات كثيرة يوضع في ملف `.js` كدالة. يمكنه استعمال `Kreftjs.state` و`Kreftjs.watch` و`Kreftjs.onDestroy` وغيرها:
175
+
176
+ ```js
177
+ // logic/mouse.js
178
+ export function trackMouse() {
179
+ const pos = Kreftjs.state({ x: 0, y: 0 })
180
+ const move = (e) => { pos.x = e.clientX; pos.y = e.clientY }
181
+ window.addEventListener('pointermove', move)
182
+ Kreftjs.onDestroy(() => window.removeEventListener('pointermove', move))
183
+ return pos
184
+ }
185
+ ```
186
+
187
+ ```html
188
+ import { trackMouse } from '../logic/mouse.js'
189
+ use mouse = trackMouse()
190
+
191
+ <view><p>{mouse.x}, {mouse.y}</p></view>
192
+ ```
193
+
194
+ ### share و receive
195
+
196
+ ```html
197
+ <!-- Layout.kreft -->
198
+ state settings = { theme: 'dark', lang: 'ar' }
199
+ share settings = settings
200
+
201
+ <!-- أي مكوّن بداخله مهما كان عمقه -->
202
+ receive settings
203
+
204
+ <view><p>السمة: {settings.theme}</p></view>
205
+ ```
206
+
207
+ شارك كائن `state` فيتحدث كل المستقبِلين حين يتغير. و`Kreftjs.app(…).share('key', value)` يشارك مع التطبيق كله.
208
+
209
+ ### ناقل الأحداث (Event bus)
210
+
211
+ ```html
212
+ <!-- الإرسال -->
213
+ action save() { bus.send('toast', 'تم الحفظ!') }
214
+
215
+ <!-- الاستماع -->
216
+ state message = ''
217
+ on ready { bus.on('toast', (text) => (message = text)) }
218
+ ```
219
+
220
+ المستمعون المضافون في `on start` أو `on ready` يُزالون تلقائيًا حين يزول المكوّن. والآخرون يبقون حتى تستدعي `bus.off('toast', fn)`.
221
+
222
+ ## الحفظ في المتصفح: saved
223
+
224
+ اكتب `saved` بدل `state` فتُحفظ القيمة في localStorage، وتنجو من إعادة التحميل وإغلاق المتصفح.
225
+
226
+ ```html
227
+ saved todos = []
228
+ saved theme = 'light'
229
+ saved token = '' as 'auth.token' <!-- اختر مفتاح التخزين بنفسك -->
230
+ ```
231
+
232
+ - كل تغيير يُحفظ وحده، حتى العميق منه: `todos.push(…)` و`todo.done = true`.
233
+ - المفتاح الافتراضي `<Component>.<name>`، مثل `Home.todos`. استعمل `as '…'` لتشارك قيمة واحدة بين مكوّنات.
234
+ - تغيير في تبويب آخر من الموقع نفسه يظهر في هذا التبويب أيضًا.
235
+
236
+ المخزن يحتفظ بقيمه البسيطة إذا أضفت `saved`:
237
+
238
+ ```js
239
+ Kreftjs.store('cart', { items: [] }, { saved: true }) // أو { saved: ['items'] } لبعض الحقول
240
+ ```
241
+
242
+ ومن JavaScript:
243
+
244
+ ```js
245
+ const theme = Kreftjs.saved('theme', 'light') // signal: theme.value = 'dark' تُحفظ
246
+ Kreftjs.storage.set('user', { name: 'سارة' })
247
+ Kreftjs.storage.get('user')
248
+ Kreftjs.storage.remove('user')
249
+ Kreftjs.storage.clear() // يزيل ما حفظه Kreft فقط
250
+ ```
251
+
252
+ > **تحذير:** localStorage يخص متصفحًا واحدًا وموقعًا واحدًا، وسعته نحو 5 ميغابايت، ويقرؤه أي شخص يستعمل ذلك المتصفح وأي سكربت يعمل في صفحتك. لا تخزّن فيه كلمات مرور أو أسرارًا طويلة الأمد.
253
+
254
+ ## تمارين
255
+
256
+ 1. اصنع مكوّن `Rating` له `prop value` وحدث `update:value`، واستعمله بـ `k-model`.
257
+ 2. اصنع مخزن `toast` وعرض رسائل من أي مكوّن، بدل الـ bus.
258
+ 3. اصنع مكوّن `Modal` بفتحات `header` و`footer`، يظهر بـ `kreft-portal`.
@@ -0,0 +1,333 @@
1
+ # الفصل الرابع: الموجِّه والبيانات والنماذج
2
+
3
+ ## تعريف المسارات
4
+
5
+ ```js
6
+ const router = Kreftjs.router({
7
+ routes: [
8
+ { path: '/', component: 'pages/Home.kreft', meta: { title: 'الرئيسية' } },
9
+ { path: '/users/:id', name: 'user', component: 'pages/User.kreft', props: true },
10
+ { path: '/posts/:slug?', component: 'pages/Posts.kreft' },
11
+ { path: '/old', redirect: '/' },
12
+ { path: '/admin', component: 'pages/Admin.kreft', guard: () => isAdmin() || '/login' },
13
+ { path: '*', component: 'pages/NotFound.kreft' },
14
+ ],
15
+ })
16
+
17
+ Kreftjs.app('App.kreft').use(router).mount('#app')
18
+ ```
19
+
20
+ ```html
21
+ <!-- App.kreft -->
22
+ <view>
23
+ <nav>
24
+ <kreft-link to="/">الرئيسية</kreft-link>
25
+ <kreft-link to="/users/{user.id}">الملف الشخصي</kreft-link>
26
+ </nav>
27
+ <kreft-view></kreft-view>
28
+ </view>
29
+ ```
30
+
31
+ مع `props: true` يصل المعامل `:id` كـ prop:
32
+
33
+ ```html
34
+ <!-- pages/User.kreft -->
35
+ prop id
36
+ derive user = users.find(id)
37
+ action back() { $router.back() }
38
+ ```
39
+
40
+ | الميزة | الطريقة |
41
+ |---|---|
42
+ | المسار الحالي | `$route.path` و`$route.params.id` و`$route.query.tab` و`$route.meta` |
43
+ | التنقل | `$router.go('/users/5?tab=info')` و`$router.replace(to)` و`$router.back()` |
44
+ | الرابط النشط | الفئتان `kreft-link-active` و`kreft-link-exact` |
45
+ | عنوان الصفحة | `meta: { title: '…' }` |
46
+ | فحص قبل كل صفحة | `router.before((to, from) => …)`. أرجع `false` للإيقاف أو مسارًا للذهاب إليه. |
47
+ | فحص مسار واحد | `guard: (to, from) => …` |
48
+ | بعد كل صفحة | `router.after((to, from) => …)` |
49
+
50
+ كل ملف صفحة يُحمَّل أول مرة يفتحها أحد.
51
+
52
+ ## ملفات المسارات
53
+
54
+ المشاريع الجديدة تحفظ صفحاتها في `frontend/routes/web.kreft`، و`index.html` يشير إليه فقط:
55
+
56
+ ```js
57
+ Kreftjs.router({
58
+ routes: ['routes/web.kreft', 'routes/shop.kreft'],
59
+ layout: 'app',
60
+ errors: 'errors',
61
+ middleware: {
62
+ auth: (to) => (auth.user ? true : '/login?redirect=' + encodeURIComponent(to.fullPath)),
63
+ guest: () => (auth.user ? '/dashboard' : true),
64
+ },
65
+ })
66
+ ```
67
+
68
+ ```js
69
+ // routes/web.kreft
70
+ Route.view('/', 'pages/Home.kreft').name('home').title('الرئيسية')
71
+ Route.view('/posts/:id', 'pages/Post.kreft')
72
+
73
+ Route.middleware('auth').group(() => {
74
+ Route.view('/dashboard', 'pages/Dashboard.kreft').middleware('verified')
75
+ })
76
+
77
+ Route.layout('guest').middleware('guest').group(() => {
78
+ Route.view('/login', 'pages/auth/Login.kreft').title('تسجيل الدخول')
79
+ })
80
+
81
+ Route.prefix('/account').name('account.').middleware('auth').group(() => {
82
+ Route.view('/settings', 'pages/account/Settings.kreft').children(() => {
83
+ Route.view('', 'pages/account/General.kreft') // /account/settings
84
+ Route.view('mail', 'pages/account/Mail.kreft') // /account/settings/mail
85
+ })
86
+ })
87
+
88
+ Route.redirect('/home', '/')
89
+ Route.fallback('pages/NotFound.kreft')
90
+ ```
91
+
92
+ | على المسار | ماذا يفعل |
93
+ |---|---|
94
+ | `.name('home')` | `$router.go({ name: 'home' })` |
95
+ | `.title('Home')` | عنوان الصفحة. |
96
+ | `.layout('guest')` و`.layout('none')` | تخطيط آخر أو بلا تخطيط. |
97
+ | `.middleware('auth', 'role:editor')` | ينفّذ هذه قبل الصفحة. |
98
+ | `.meta({ … })` | أي شيء آخر، في `$route.meta`. |
99
+ | `.children(() => …)` | صفحات متداخلة تظهر في `<kreft-view>` الخاص بالصفحة. |
100
+
101
+ > **ملاحظة:** وسيط `auth` في المتصفح يقرر أي الشاشات تُعرض فقط. هو لا يحمي البيانات: كل مسار API يجب أن يفحص المستخدم على الخادم.
102
+
103
+ ## التخطيطات (Layouts)
104
+
105
+ التخطيط هو الإطار حول صفحاتك: الشريط والقائمة والتذييل. ضعه في `layouts/` واستعمل `<slot>` حيث تذهب الصفحة.
106
+
107
+ ```html
108
+ <!-- layouts/app.kreft -->
109
+ state menuOpen = false
110
+
111
+ <view>
112
+ <header>…</header>
113
+ <main><slot></slot></main>
114
+ </view>
115
+ ```
116
+
117
+ | أين | ماذا تكتب |
118
+ |---|---|
119
+ | كل الصفحات | `Kreftjs.router({ layout: 'app', routes })` |
120
+ | مسار واحد وأبناؤه | `{ path: '/admin', component: …, layout: 'admin' }` |
121
+ | داخل ملف صفحة | `layout admin` في سطر وحده |
122
+ | بلا تخطيط | `layout: false` على المسار أو `layout none` في الصفحة |
123
+
124
+ عند انطباق أكثر من واحد: المسار يغلب، ثم ملف الصفحة، ثم الموجِّه. التنقل بين صفحتين تستعملان التخطيط نفسه يُبقي التخطيط على الشاشة مع حالته.
125
+
126
+ وللصفحة أن تملأ أماكن أخرى في تخطيطها:
127
+
128
+ ```html
129
+ <!-- في التخطيط -->
130
+ <kreft-yield name="actions">يظهر إن لم يكن للصفحة هذا القسم</kreft-yield>
131
+
132
+ <!-- في الصفحة -->
133
+ <kreft-section name="actions">
134
+ <button class="primary" @click="save()">حفظ</button>
135
+ </kreft-section>
136
+ ```
137
+
138
+ ## صفحات الأخطاء
139
+
140
+ لكل خطأ HTTP صفحته في `errors/`. فعّلها بـ `errors: 'errors'` في الموجِّه:
141
+
142
+ ```text
143
+ errors/401.kreft 402.kreft 403.kreft 404.kreft 419.kreft 429.kreft 500.kreft 503.kreft
144
+ errors/error.kreft أي حالة أخرى
145
+ errors/minimal.kreft الإطار المشترك
146
+ ```
147
+
148
+ عنوان غير معروف يعرض `errors/404.kreft`. ومن أي ملف `.kreft` يعرض `abort(404)` الصفحة المناسبة:
149
+
150
+ ```html
151
+ prop id
152
+ load post = api.get('/api/posts/' + id)
153
+ watch post.error { if (post.error && post.error.status === 404) abort(404) }
154
+ ```
155
+
156
+ ## العناوين النظيفة
157
+
158
+ يستعمل الموجِّه افتراضيًا عناوين بلا `#` (`/users/5`). ويلزم لذلك أمران:
159
+
160
+ 1. وسم `<base href="/">` في `index.html` (استعمل `<base href="/demo/">` إن كان التطبيق في مجلد).
161
+ 2. أن يرسل الخادم `index.html` لكل مسار صفحة، حتى لا يكون إعادة تحميل `/users/5` خطأ 404. الأمر `npm run dev` يفعل هذا عنك.
162
+
163
+ > **تنبيه مهم عند النشر:** إن بنيت الموقع بقاعدة خاطئة (مثلًا `/demo/`) وهو في الجذر، ستظهر صفحة بيضاء لأن المتصفح يطلب الملفات من `/demo/app.js`. ابنِ بـ `kreft build --base /`. تفاصيل أكثر في فصل النشر.
164
+
165
+ إن لم تستطع تغيير الخادم فاستعمل `mode: 'hash'`؛ فتصير العناوين `/#/users/5`.
166
+
167
+ ## جلب البيانات
168
+
169
+ العميل العام `api` متاح في كل ملف `.kreft`، وكذلك `Kreftjs.api` في JavaScript العادي:
170
+
171
+ ```js
172
+ const users = await api.get('/api/users', { query: { page: 2 } })
173
+ const user = await api.post('/api/users', { name: 'سارة أحمد' })
174
+ await api.put('/api/users/1', user)
175
+ await api.patch('/api/users/1', { status: 'active' })
176
+ await api.delete('/api/users/1')
177
+ ```
178
+
179
+ ### load: تحميل البيانات
180
+
181
+ `load` يعلن مصدرًا تفاعليًا. يتولى حالة التحميل والخطأ ويجلب من جديد إذا تغيّرت القيم التي يقرؤها:
182
+
183
+ ```html
184
+ <!-- pages/Posts.kreft -->
185
+ state query = ''
186
+ state page = 1
187
+
188
+ load posts = api.get('/api/posts', { query: { q: query || undefined, page } })
189
+
190
+ <view>
191
+ <input type="search" k-model.lazy="query" placeholder="ابحث في المقالات…">
192
+
193
+ <p k-if="posts.loading">جارٍ تحميل المقالات…</p>
194
+ <div k-elif="posts.error">
195
+ فشل التحميل: {posts.error.message}
196
+ <button @click={posts.reload}>حاول مجددًا</button>
197
+ </div>
198
+ <div k-else>
199
+ {for post in posts.data || [] key post.id}
200
+ <article><h3>{post.title}</h3><p>{post.body}</p></article>
201
+ {empty}
202
+ <p>لا نتائج لـ "{query}".</p>
203
+ {/for}
204
+ </div>
205
+
206
+ <button @click={() => page++}>الصفحة التالية ({page})</button>
207
+ </view>
208
+ ```
209
+
210
+ | الخاصية | المعنى |
211
+ |---|---|
212
+ | `posts.data` | النتيجة (`null` حتى تُحمَّل). |
213
+ | `posts.loading` | `true` أثناء الطلب. |
214
+ | `posts.loaded` | `true` بعد وصول أول جواب. |
215
+ | `posts.error` | الخطأ أو `null`. |
216
+ | `posts.reload()` | اجلب الآن من جديد. |
217
+
218
+ ### الجداول
219
+
220
+ للجداول التي يقدّمها `kreft dev --db` أو واجهة الجداول التلقائية، يعطيك `table(name)` استدعاءات جاهزة:
221
+
222
+ ```html
223
+ const posts = table('posts')
224
+
225
+ load list = posts.all({ q: search, _sort: '-created_at', _limit: 10 })
226
+
227
+ action createPost() {
228
+ const created = await posts.create({ title: 'مقال جديد' })
229
+ list.data = [created, ...list.data]
230
+ }
231
+ ```
232
+
233
+ الدوال: `all(query)` و`find(id)` و`count(query)` و`create(data)` و`update(id, data)` (PUT) و`patch(id, data)` و`remove(id)`.
234
+
235
+ ### الأخطاء
236
+
237
+ الطلب الفاشل يرمي `ApiError` فيه `status` و`message` و`data` و`errors` (رسائل التحقق). مشاكل الشبكة وانتهاء المهلة لها `status` يساوي 0.
238
+
239
+ ```html
240
+ state errors = {}
241
+
242
+ action save() {
243
+ try {
244
+ await api.post('/api/users', form)
245
+ } catch (e) {
246
+ errors = e.errors || {} // { email: ['The email has already been taken.'] }
247
+ }
248
+ }
249
+ ```
250
+
251
+ ### الإعدادات
252
+
253
+ ```js
254
+ Kreftjs.api.setup({
255
+ base: '/api', // فيصير api.get('users') هو /api/users
256
+ token: () => Kreftjs.storage.get('token'), // يُرسل كـ Authorization: Bearer …
257
+ headers: { 'Accept-Language': 'ar' },
258
+ timeout: 10000,
259
+ })
260
+
261
+ Kreftjs.api.onError((e) => { if (e.status === 401) location.href = '/login' })
262
+
263
+ const github = Kreftjs.api.create({ base: 'https://api.github.com' }) // عميل ثانٍ
264
+ ```
265
+
266
+ خيارات للطلب الواحد: `query` و`headers` و`timeout` و`signal` (للإلغاء) و`full: true` (يعيد `{ data, status, headers }`).
267
+
268
+ الطلبات إلى خادمك تُرسل أيضًا `X-XSRF-TOKEN` من كوكي `XSRF-TOKEN` (و`X-CSRF-TOKEN` من `<meta name="csrf-token">`)، و`X-Requested-With`.
269
+
270
+ ### واجهة تجريبية للتدرّب
271
+
272
+ ```bash
273
+ kreft dev --api db.json # واجهة REST على /api من ملف JSON
274
+ kreft dev --api db.json --delay 500 # أجب ببطء لترى حالات التحميل
275
+ ```
276
+
277
+ > **تحذير:** `--api` و`--db` للتطوير فقط. لا تسجيل دخول فيهما. في الإنتاج اكتب مسارات حقيقية.
278
+
279
+ ## النماذج والتحقق
280
+
281
+ أعطِ النموذج قواعده بأسلوب تصريحي. النموذج الذي فيه أخطاء لا يُرسل أبدًا:
282
+
283
+ ```html
284
+ state post = form({ title: '', email: '' }, { title: 'required|min:3|max:120', email: 'required|email' })
285
+
286
+ action save() {
287
+ const created = await post.$submit((values) => api.post('/api/posts', values))
288
+ if (created) post.$reset()
289
+ }
290
+
291
+ <view>
292
+ <form @submit.prevent={save}>
293
+ <input k-model="post.title">
294
+ <p k-if="post.$error('title')" class="error">{post.$error('title')}</p>
295
+ <button disabled={post.$busy}>حفظ</button>
296
+ </form>
297
+ </view>
298
+ ```
299
+
300
+ | على النموذج | ما هو |
301
+ |---|---|
302
+ | `post.title` و`post.email` | القيم، لـ `k-model`. |
303
+ | `post.$error('title')` | أول رسالة للحقل أو `''`. |
304
+ | `post.$errors` | كل الرسائل: `{ title: ['…'] }`. |
305
+ | `post.$valid` و`post.$busy` | لا أخطاء / يُرسل الآن. |
306
+ | `post.$submit(send)` | يفحص ثم يستدعي `send(values)`. وخطأ 422 من الخادم يملأ الأخطاء نفسها. يعيد `false` إن لم يُرسل. |
307
+ | `post.$check()` و`post.$reset(values?)` و`post.$setErrors(e)` | افحص الآن / ابدأ من جديد / اعرض أخطاء من مكان آخر. |
308
+
309
+ تظهر الأخطاء بعد أول إرسال، ثم تتبع كتابتك.
310
+
311
+ بلا نموذج: `validate(values, rules)` يعطي `{}` أو `{ field: ['…'] }`. ورسائلك الخاصة في الوسيط الثالث:
312
+
313
+ ```js
314
+ form(values, rules, { messages: { 'title.required': 'اكتب عنوانًا.' } })
315
+ ```
316
+
317
+ أسماء القواعد نفسها تعمل على الخادم في `req.validate()`، فمجموعة قواعد واحدة تصف الطرفين.
318
+
319
+ ### كلمات المرور
320
+
321
+ القاعدة `not_common_password` ترفض الكلمات الشائعة (`password` و`12345678` …) وحرفًا مكرّرًا (`aaaaaaaa`) وتسلسلًا بسيطًا (`abcdefgh`) وما يطابق البريد. تستعملها المشاريع الجديدة مع `min:8`:
322
+
323
+ ```js
324
+ password: 'required|min:8|confirmed|not_common_password'
325
+ ```
326
+
327
+ هي قائمة قصيرة تعمل دون اتصال، فتمسك أسوأ الاختيارات فقط. ارفع `min` لما هو أطول (عبارة من أربع كلمات خير من خليط قصير من الرموز).
328
+
329
+ ## تمارين
330
+
331
+ 1. اكتب صفحة `Posts` فيها بحث وترقيم صفحات بـ `load`، مع زر "حاول مجددًا" عند الخطأ.
332
+ 2. اكتب نموذج تسجيل بقواعد `required|email` و`required|min:8|confirmed`، وارسمه برسائل خطأ بالعربية.
333
+ 3. أنشئ مسارات متداخلة: `/account/settings` مع تبويبين `general` و`mail`.