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.
- package/README.md +1 -0
- package/bin/build.js +13 -2
- package/bin/kreftjs.js +1 -0
- package/book/ar/01-getting-started.md +169 -0
- package/book/ar/02-language.md +330 -0
- package/book/ar/03-components-and-state.md +258 -0
- package/book/ar/04-router-data-forms.md +333 -0
- package/book/ar/05-arabic-rtl.md +175 -0
- package/book/ar/06-backend.md +247 -0
- package/book/ar/07-auth-and-security.md +157 -0
- package/book/ar/08-background-work.md +250 -0
- package/book/ar/09-testing-and-deploy.md +234 -0
- package/book/ar/10-electron-desktop.md +258 -0
- package/book/ar/11-advanced-blog.md +370 -0
- package/book/ar/12-advanced-shop.md +426 -0
- package/book/ar/13-advanced-ui.md +440 -0
- package/book/ar/14-advanced-ops.md +167 -0
- package/book/ar/15-appendix.md +81 -0
- package/book/ar/README.md +62 -0
- package/book/kreft-book-ar.docx +0 -0
- package/docs/README.md +2 -2
- package/kreftjs.d.ts +1 -1
- package/kreftjs.js +436 -74
- package/package.json +4 -3
- package/templates/app/frontend/errors/ErrorMonitor.kreft +2 -2
- package/templates/app/frontend/kreftjs.d.ts +1 -1
- package/templates/app/frontend/kreftjs.js +436 -74
- package/templates/app/frontend/pages/Dashboard.kreft +1 -1
- package/templates/errors/ErrorMonitor.kreft +1 -1
|
@@ -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`.
|