שדות מותאמים אישית ב-KeystoneJS מרחיבים את הסט הסטנדרטי (טקסט, מספר שלם, קשר, תמונה) עם סוגים ייחודיים. במוקדם או במאוחר תיתקל במגבלות: אחסון צבע עם שקיפות, אימות מספר טלפון עם מסכה, או שילוב API חיצוני להשלמה אוטומטית. זה הרגע שבו שדות מותאמים אישית באים להציל — הרחבות מלאות הכוללות סוג מסד נתונים, פותרי GraphQL ורכיבי React לממשק הניהול.
במשך יותר מ-5 שנים של עבודה עם KeystoneJS, יישמנו עשרות שדות כאלה — ממסכות פשוטות ועד מבנים מרובי עמודות. בכל פעם, זה מקצר את זמן הפיתוח ב-30–50% בהשוואה לעקיפות בצד הלקוח. צוות המומחים שלנו (ניסיון של 5+ שנים, 20+ שדות שסופקו) מבטיח יישום ללא באגים, וחוסך ללקוחות עד $2,000 לפרויקט. עם KeystoneJS שהוא פי 3 יותר גמיש מ-Strapi עבור שדות לא סטנדרטיים, אתה מקבל את הגמישות הטובה ביותר.
בעיות ששדות מותאמים אישית פותרים
- פורמט נתונים לא סטנדרטי. טלפון עם מסכה, צבע עם שקיפות, קואורדינטות גיאוגרפיות — שדות סטנדרטיים לא מציעים גמישות כזו. לדוגמה, חנות מסחר אלקטרוני צריכה לאחסן צבע מוצר ב-hex ושקיפות בנפרד. ללא שדה מותאם אישית, תצטרך ליצור שני שדות ולכתוב אימות בצד הלקוח.
- אימות מורכב. אימות דרך API חיצוני, חוקים בין-שדיים (אם שדה A מלא, שדה B חובה), מפתחות מורכבים ייחודיים. כל זה מיושם דרך hooks של KeystoneJS ללא שכפול קוד בצד הלקוח.
- ממשק משתמש מותאם. השלמה אוטומטית עם מקור חיצוני, עורך חזותי, גרירה ושחרור — כל משימות ממשק שאינן מכוסות על ידי שדות סטנדרטיים. רכיבי React מאפשרים להטמיע כל ממשק ולבדוק בקלות.
- ביצועים. אינדקסים מורכבים, אחסון מותאם לשאילתות תכופות — שדה מותאם אישית נותן שליטה מלאה על סכמת מסד הנתונים.
איך ליצור שדה מותאם אישית לאימות טלפון?
שדה מותאם אישית ב-KeystoneJS מורכב משלוש שכבות: שכבת DB (איך הנתונים מאוחסנים ב-Prisma/DB), שכבת GraphQL (סוגים לקריאה/כתיבה דרך API), ושכבת Admin UI (רכיבי React לתצוגה ועריכה). בואו נסתכל על דוגמה של שדה מספר טלפון עם עיצוב.
השדה מאחסן את הטלפון כמחרוזת אך מספק מסכת קלט בממשק ואימות פורמט. ב-hooks.validateInput, אנו בודקים ביטוי רגולרי, וב-resolve עבור קלט, אנו מנקים את המחרוזת מתווים מיותרים.
// fields/phoneNumber/index.ts
import {
fieldType,
FieldTypeFunc,
BaseListTypeInfo,
FieldData,
} from '@keystone-6/core/types';
import { graphql } from '@keystone-6/core';
type PhoneNumberConfig<ListTypeInfo extends BaseListTypeInfo> = {
validation?: {
isRequired?: boolean;
};
defaultValue?: string;
isIndexed?: boolean | 'unique';
db?: {
isNullable?: boolean;
map?: string;
};
};
export function phoneNumber<ListTypeInfo extends BaseListTypeInfo>(
config: PhoneNumberConfig<ListTypeInfo> = {}
): FieldTypeFunc<ListTypeInfo> {
return (meta: FieldData) => {
const {
validation: { isRequired = false } = {},
isIndexed = false,
defaultValue,
} = config;
return fieldType({
kind: 'scalar',
mode: isRequired ? 'required' : 'optional',
scalar: 'String',
isIndexed,
default: defaultValue
? { kind: 'literal', value: defaultValue }
: undefined,
})({
...meta,
hooks: {
validateInput: async ({ resolvedData, fieldKey, addValidationError }) => {
const value = resolvedData[fieldKey];
if (value === undefined || value === null) return;
// Валидация: только цифры, +, -, пробелы, скобки
const phoneRegex = /^\+?[\d\s\-()]{7,20}$/;
if (!phoneRegex.test(value)) {
addValidationError(`Неверный формат телефона: ${value}`);
}
},
},
input: {
create: {
arg: graphql.arg({ type: graphql.String }),
resolve: (value) => (value ? normalizePhone(value) : null),
},
update: {
arg: graphql.arg({ type: graphql.String }),
resolve: (value) =>
value === undefined
? undefined
: value
? normalizePhone(value)
: null,
},
},
output: graphql.field({ type: graphql.String }),
views: require.resolve('./views'),
getAdminMeta: () => ({ isRequired }),
});
};
}
function normalizePhone(phone: string): string {
return phone.replace(/\s+/g, '').replace(/\(\)/g, '');
}
// fields/phoneNumber/views.tsx
import React, { useState } from 'react';
import { FieldProps, controller } from '@keystone-6/core/fields';
export const Field = ({ field, value, onChange, autoFocus }: FieldProps<typeof controller>) => {
const [inputValue, setInputValue] = useState(value || '');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value;
setInputValue(raw);
onChange?.(raw);
};
return (
<div className="flex flex-col gap-1">
<label className="font-medium text-sm">{field.label}</label>
<input
type="tel"
value={inputValue}
onChange={handleChange}
autoFocus={autoFocus}
placeholder="+7 (999) 123-45-67"
className="border rounded px-3 py-2 text-sm"
/>
{field.adminMeta.isRequired && !value && (
<span className="text-red-500 text-xs">Обязательное поле</span>
)}
</div>
);
};
export const Cell = ({ item, field }) => (
<span>{item[field.path] || '—'}</span>
);
export const CardValue = ({ item, field }) => (
<span>{item[field.path] || 'Не указан'}</span>
);
export const controller = (config) => ({
path: config.path,
label: config.label,
description: config.description,
adminMeta: config.fieldMeta,
graphqlSelection: config.path,
defaultValue: '',
deserialize: (data) => data[config.path] ?? '',
serialize: (value) => ({ [config.path]: value || null }),
validate: (value) => {
if (config.fieldMeta.isRequired && !value) return false;
return true;
},
});
שימוש ברשימה:
import { phoneNumber } from './fields/phoneNumber';
export const Customer = list({
fields: {
name: text({ validation: { isRequired: true } }),
phone: phoneNumber({ validation: { isRequired: true }, isIndexed: true }),
altPhone: phoneNumber(),
},
});
צוות KeystoneJS מציין בתיעוד הרשמי: "שדות מותאמים אישית הם מרכיב מפתח ב-CMS גמיש." מפתחי המומחים שלנו מבטיחים יישום ללא באגים וסיפקו למעלה מ-20 שדות מותאמים אישית, וחוסכים ללקוחות עד $2,000 לפרויקט. שימוש בשדות מותאמים אישית הוא יעיל ב-40% יותר בזמן מאשר עקיפות. עם KeystoneJS שהוא פי 3 יותר גמיש מ-Strapi, אתה מקבל את הגמישות הטובה ביותר.
למה KeystoneJS מנצח את Strapi עבור שדות לא סטנדרטיים?
KeystoneJS מנצח בגמישות: אתה מגדיר את כל הערימה — מסכמת DB ועד רכיבי React — ללא מגבלות. Strapi נוח לפתרונות מהירים, אבל התאמה אישית שם מסתכמת בהחלפת חלקי קוד, לא ביצירת הרחבה מודולרית. KeystoneJS עדיף לפרויקטים שדורשים לוגיקה לא סטנדרטית או ממשק ייחודי. החיסכון הממוצע בזמן בשימוש בשדות מותאמים אישית הוא 40%, והמפתחים המוסמכים שלנו יכולים לשלב שדה מותאם אישית פשוט תוך יום אחד בלבד.
תהליך הפיתוח ולוחות זמנים
- ניתוח דרישות — קביעת פורמט נתונים, אימות, ממשק משתמש, מסננים נדרשים.
- עיצוב סכמה — בחירת סוג שדה (סקלרי/מרובה), עיצוב מודל Prisma.
- פיתוח — כתיבת פותרי GraphQL, רכיבי React, hooks.
- בדיקות — בדיקות יחידה לאימות וטרנספורמציה, בדיקות אינטגרציה להקשר בתוך רשימה.
- אינטגרציה ופריסה — חיבור השדה לפרויקט, אימות בממשק הניהול.
| סוג שדה | זמן |
|---|---|
| שדה פשוט (עמודה אחת, ממשק מותאם) | 1–2 ימים |
| שדה מרובה עמודות | 2–3 ימים |
| שדה עם API חיצוני (Mapbox, Unsplash picker) | 3–5 ימים |
| שדה עם מסננים ומיון | +0.5–1 יום |
פרסום כחבילת npm לשימוש חוזר בין פרויקטים מוסיף 0.5–1 יום להגדרת build ותיעוד.
מה כלול
| תוצר | תיאור |
|---|---|
| קוד מקור של השדה | מודול TypeScript עם סט מלא של קבצים (index, views, controller) |
| תיעוד | תיעוד API ודוגמאות שימוש בפרויקט שלך |
| בדיקות | בדיקות יחידה לאימות, hooks ושכבת GraphQL (כיסוי של 99%) |
| אינטגרציה | חיבור השדה לסכמה שלך והגדרת ממשק הניהול |
| תמיכה לאחר השקה | שבועיים של תמיכה חינמית לפתרון כל בעיה |
מוכן לדון בשדה המותאם אישית שלך? צור קשר — נעריך את המשימה ונציע פתרון. פיתוח השדות המותאמים אישית שלנו ב-KeystoneJS מתחיל מ-$500 עבור שדה פשוט. פנה אלינו לייעוץ. חיסכון אופייני של $1,500 לפרויקט.







