Signal-first, schema-driven form library for Angular 17+. Powered by Zod validation.
npm install @biyonik/zignal
Signal-first, schema-driven form library for Angular 17+
Angular 17+ için Signal tabanlı, şema güdümlü form kütüphanesi
bash
npm install @biyonik/zignal zod
`
---
TR: Hızlı Başlangıç | EN: Quick Start
$3
`typescript
import { StringField, NumberField, BooleanField, PasswordField } from '@biyonik/zignal';
// TR: E-posta alanı
// EN: Email field
const emailField = new StringField('email', 'E-posta', {
required: true,
email: true
});
// TR: Yaş alanı
// EN: Age field
const ageField = new NumberField('age', 'Yaş', {
required: true,
min: 18,
max: 100,
integer: true
});
// TR: Şifre alanı (güçlü şifre kuralları)
// EN: Password field (strong password rules)
const passwordField = new PasswordField('password', 'Şifre', {
required: true,
minLength: 8,
requireUppercase: true,
requireNumber: true,
requireSpecial: true
});
// TR: Şartları kabul checkbox
// EN: Terms acceptance checkbox
const acceptTerms = new BooleanField('acceptTerms', 'Şartları kabul ediyorum', {
required: true // true olması zorunlu
});
`
$3
`typescript
import { FormSchema } from '@biyonik/zignal';
interface UserForm {
email: string;
age: number;
password: string;
acceptTerms: boolean;
}
const userSchema = new FormSchema([
emailField,
ageField,
passwordField,
acceptTerms
]);
`
$3
`typescript
import { Component } from '@angular/core';
@Component({
selector: 'app-user-form',
template:
})
export class UserFormComponent {
form = userSchema.createForm({
email: '',
age: null,
password: '',
acceptTerms: false
});
async onSubmit() {
if (await this.form.validateAll()) {
const data = this.form.getValues();
console.log('Form data:', data);
}
}
}
`
---
TR: Dil Desteği (i18n) | EN: Language Support (i18n)
`typescript
import { setLocale, addMessages, t, detectBrowserLocale, useAutoLocale } from '@biyonik/zignal';
// TR: Browser diline göre otomatik ayarla
// EN: Auto-detect browser language
const locale = useAutoLocale(); // Returns 'tr' or 'en'
// TR: Manuel dil değiştir
// EN: Change language manually
setLocale('en');
// TR: Custom mesajlar ekle (firma özel)
// EN: Add custom messages (company specific)
addMessages('tr', {
required: 'Bu bilgi zorunludur',
'string.email': 'Lütfen kurumsal e-posta adresinizi giriniz',
});
// TR: Yeni dil ekle (örn: Almanca)
// EN: Add new language (e.g., German)
addMessages('de', {
required: 'Dieses Feld ist erforderlich',
'string.min': 'Mindestens {min} Zeichen erforderlich',
});
setLocale('de');
// TR: Validation mesajını al
// EN: Get validation message
const msg = t('password.min', { min: 8 });
// TR: "Şifre en az 8 karakter olmalıdır"
// EN: "Password must be at least 8 characters"
`
---
TR: Form Persistence | EN: Form Persistence
`typescript
import { createFormPersistence } from '@biyonik/zignal';
// TR: Form verilerini localStorage'a kaydet
// EN: Save form data to localStorage
const persistence = createFormPersistence('user-form', {
storage: 'local', // 'local' | 'session'
debounceMs: 500, // Auto-save debounce
exclude: ['password'], // Hassas alanları hariç tut
ttl: 24 60 60 * 1000, // 24 saat sonra expire
});
// TR: Form verilerini yükle
// EN: Load form data
const savedData = persistence.load();
if (savedData) {
form.patchValues(savedData);
}
// TR: Auto-save aktifleştir
// EN: Enable auto-save
persistence.enableAutoSave(form.values);
// TR: Manuel kaydet
// EN: Manual save
persistence.save(form.getValues());
// TR: Temizle
// EN: Clear
persistence.clear();
`
---
TR: Asenkron Validasyon | EN: Async Validation
`typescript
import { createEmailValidator, createUsernameValidator } from '@biyonik/zignal';
// TR: E-posta benzersizlik kontrolü
// EN: Email uniqueness check
const emailValidator = createEmailValidator(
async (email) => {
const response = await fetch(/api/check-email?email=${email});
const { exists } = await response.json();
return !exists; // true = geçerli, false = zaten var
},
{
debounceMs: 300,
cacheSize: 50,
errorMessage: 'Bu e-posta adresi zaten kullanımda'
}
);
// TR: Component'te kullan
// EN: Use in component
@Component({...})
export class RegisterComponent {
emailValidator = emailValidator;
async checkEmail(email: string) {
const error = await this.emailValidator.validate(email);
if (error) {
console.log('Validation error:', error);
}
}
// TR: Reactive state
isPending = this.emailValidator.pending; // Signal
isValid = this.emailValidator.valid; // Signal
errorMsg = this.emailValidator.error; // Signal
}
`
---
TR: Türkiye Validatorları | EN: Turkey Validators
`typescript
import {
tcknSchema,
vknSchema,
turkishIbanSchema,
turkishPhoneSchema,
turkishPlateSchema,
isValidTCKN,
isValidVKN
} from '@biyonik/zignal';
// TR: TCKN validasyonu
// EN: Turkish ID validation
const tckn = tcknSchema.safeParse('12345678901');
if (!tckn.success) {
console.log('Geçersiz TCKN');
}
// TR: Direkt fonksiyon kullanımı
// EN: Direct function usage
if (isValidTCKN('12345678901')) {
console.log('TCKN geçerli');
}
// TR: Form field olarak
// EN: As form field
const tcknField = new StringField('tckn', 'T.C. Kimlik No', {
required: true,
pattern: /^\d{11}$/,
customValidator: (value) => isValidTCKN(value) ? null : 'Geçersiz TCKN'
});
`
---
TR: Field Tipleri | EN: Field Types
| Field | Type | TR: Açıklama | EN: Description |
|-------|------|--------------|-----------------|
| StringField | string | Tek satır metin | Single line text |
| NumberField | number | Sayısal değer | Numeric value |
| BooleanField | boolean | Evet/Hayır | Yes/No |
| DateField | Date | Tarih seçici | Date picker |
| PasswordField | string | Şifre (güç göstergeli) | Password (with strength) |
| EmailField | string | E-posta | Email |
| UrlField | string | URL | URL |
| TextareaField | string | Çok satırlı metin | Multi-line text |
| SelectField | T | Dropdown seçimi | Dropdown selection |
| MultiselectField | T[] | Çoklu seçim | Multiple selection |
| ArrayField | object[] | Tekrarlayan kayıtlar | Repeatable records |
| JsonField | object | JSON/Object | JSON/Object |
| FileField | FileInfo | Dosya yükleme | File upload |
---
TR: FormState API | EN: FormState API
`typescript
const form = schema.createForm(initialValues);
// ===============================================
// TR: Signals (Reaktif)
// EN: Signals (Reactive)
// ===============================================
form.values(); // Signal - Tüm değerler / All values
form.valid(); // Signal - Geçerlilik / Validity
form.dirty(); // Signal - Değişiklik var mı? / Has changes?
form.errors(); // Signal>
// ===============================================
// TR: Field Erişimi
// EN: Field Access
// ===============================================
form.fields.email.value(); // Signal
form.fields.email.error(); // Signal
form.fields.email.touched(); // Signal
form.fields.email.valid(); // Signal
form.fields.email.dirty(); // Signal
// ===============================================
// TR: Aksiyonlar
// EN: Actions
// ===============================================
form.setValue('email', 'test@example.com');
form.patchValues({ email: 'a@b.com', age: 25 });
form.touchAll(); // Tüm hataları göster / Show all errors
form.reset(); // Başlangıç değerlerine dön / Reset to initial
form.validateAll(); // Async validation
// ===============================================
// TR: Getter'lar
// EN: Getters
// ===============================================
form.getValues(); // Type-safe data (Zod parsed)
form.getDirtyValues(); // Sadece değişenler / Only changed fields
`
---
TR: JSON'dan Dinamik Form | EN: Dynamic Form from JSON
`typescript
import { SchemaFactory } from '@biyonik/zignal';
@Component({...})
export class DynamicFormComponent {
private factory = inject(SchemaFactory);
form = this.factory.parse([
{
type: 'string',
name: 'email',
label: 'E-posta',
config: { required: true, email: true }
},
{
type: 'number',
name: 'age',
label: 'Yaş',
config: { min: 18, max: 100 }
},
{
type: 'select',
name: 'country',
label: 'Ülke',
config: {
required: true,
options: [
{ value: 'TR', label: 'Türkiye' },
{ value: 'US', label: 'USA' }
]
}
}
]);
}
`
---
TR: Karşılaştırma | EN: Comparison
| TR: Özellik / EN: Feature | ngx-formly | Reactive Forms | Zignal |
|---------------------------|------------|----------------|------------|
| Reactivity | RxJS | RxJS | Angular Signals |
| Zoneless Support | ❌ | ⚠️ | ✅ |
| Type Safety | Limited | Limited | Full (Zod) |
| i18n Support | Plugin | Manual | Built-in |
| Form Persistence | ❌ | Manual | Built-in |
| Async Validation | ✅ | ✅ | ✅ + Cache |
| Bundle Size | ~50KB | ~0KB (Angular) | <20KB |
| Learning Curve | High | Medium | Low |
---
TR: Gereksinimler | EN: Requirements
- Angular 17.0.0+
- Zod 3.22.0+
- TypeScript 5.0+
---
TR: Form Validation Behavior | EN: Form Validation Behavior
$3
Zignal forms provide comprehensive validation with configurable behavior:
#### Default Behavior
`typescript
const schema = new FormSchema([
new EmailField('email', 'Email', { required: true }),
new PasswordField('password', 'Password', { required: true })
]);
const form = schema.createForm({ email: '', password: '' });
`
Key Concepts:
- valid(): Returns true if all fields are valid, independent of touched state
- error(): Shows error only if field is touched
- validateAll(): Validates all fields and automatically touches them (default behavior)
- touchAll(): Marks all fields as touched to show all errors
#### Form Renderer Configuration
`typescript
[schema]="schema"
[formState]="form"
[config]="{
submitDisabledWhenInvalid: true, // Disable submit if invalid (default: false)
validateOnInit: true, // Validate when form loads (default: true)
validateOnChange: true, // Validate on value change (default: true)
touchAllOnSubmit: true, // Touch all on submit (default: true)
showErrorsWhenUntouched: false // Show errors before touch (default: false)
}"
(submitted)="onSubmit($event)"
/>
`
#### Submit Validation Workflow
`typescript
async handleSubmit() {
// 1. Mark submit attempted (for error display)
form.setSubmitAttempted(true);
// 2. Touch all fields (default behavior)
form.touchAll();
// 3. Wait for signal updates
await new Promise(resolve => setTimeout(resolve, 0));
// 4. Validate all fields
const isValid = await form.validateAll();
// 5. Submit only if valid
if (isValid) {
const data = form.getValues();
// Send to API...
}
}
`
#### Validation Options
validateAll() Parameters:
`typescript
// Touch all fields before validation (default)
await form.validateAll(); // Same as validateAll(true)
// Skip touching fields
await form.validateAll(false);
`
Partial Validation:
`typescript
// Validate only specific fields
const isValid = form.validateFields(['email', 'password']);
// Only email and password are touched and validated
`
#### Error Display Patterns
Pattern 1: Show errors only after touch
`html
@if (form.fields.email.error()) {
{{ form.fields.email.error() }}
}
`
Pattern 2: Show errors after submit attempt
`html
@if (form.fields.email.error() || form.submitAttempted()) {
{{ form.fields.email.error() }}
}
`
Pattern 3: Always show errors (not recommended)
`html
@if (!form.fields.email.valid()) {
Required field validation failed
}
`
#### Button State Management
The form renderer uses a reactive isFormValid() computed signal:
`typescript
// In component
readonly isFormValid = computed(() => {
const state = this.formState();
return state ? state.valid() : false;
});
// In template
type="submit"
[disabled]="config()?.submitDisabledWhenInvalid && !isFormValid()">
Submit
`
This ensures the button state is always synchronized with form validity.
#### Best Practices
1. Always use validateAll() before submit
`typescript
if (await form.validateAll()) {
// Submit...
}
`
2. Touch fields on blur for better UX
`html
(blur)="form.fields.email.touched.set(true)"
/>
`
3. Use submitAttempted for comprehensive error display
`typescript
const shouldShowError = computed(() =>
form.fields.email.error() &&
(form.fields.email.touched() || form.submitAttempted())
);
`
4. Leverage auto-validation on change
`typescript
// Enabled by default in form renderer
validateOnChange: true // Validates 100ms after value change
`
#### Migration Notes
If you're upgrading from an earlier version:
- validateAll() now touches all fields by default (use validateAll(false) to disable)
- valid() is independent of touched state (always reflects true validity)
- submitAttempted signal is now available on FormState
- Form renderer validates on init by default (use validateOnInit: false to disable)
---
TR: Lisans | EN: License
MIT License - Ahmet ALTUN
---
TR: Katkıda Bulunma | EN: Contributing
1. Fork edin / Fork
2. Feature branch oluşturun / Create feature branch (git checkout -b feature/amazing-feature)
3. Commit edin / Commit (git commit -m 'feat: add amazing feature')
4. Push edin / Push (git push origin feature/amazing-feature`)