{"id":28683712,"url":"https://github.com/chocosd/angular-template-signal-forms","last_synced_at":"2025-06-14T03:03:53.857Z","repository":{"id":291414350,"uuid":"976884450","full_name":"chocosd/angular-template-signal-forms","owner":"chocosd","description":"A template first signal forms approach built from the ground up","archived":false,"fork":false,"pushed_at":"2025-06-03T15:57:52.000Z","size":815,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-03T16:35:28.438Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chocosd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-02T23:56:46.000Z","updated_at":"2025-06-03T15:57:54.000Z","dependencies_parsed_at":"2025-05-21T16:28:17.545Z","dependency_job_id":"584fee1b-1a94-49ce-ba14-c8bba77c79c5","html_url":"https://github.com/chocosd/angular-template-signal-forms","commit_stats":null,"previous_names":["chocosd/angular-template-signal-forms"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/chocosd/angular-template-signal-forms","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chocosd%2Fangular-template-signal-forms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chocosd%2Fangular-template-signal-forms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chocosd%2Fangular-template-signal-forms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chocosd%2Fangular-template-signal-forms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chocosd","download_url":"https://codeload.github.com/chocosd/angular-template-signal-forms/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chocosd%2Fangular-template-signal-forms/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259752030,"owners_count":22905968,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-14T03:01:12.272Z","updated_at":"2025-06-14T03:03:53.844Z","avatar_url":"https://github.com/chocosd.png","language":"TypeScript","readme":"# Signal Template Forms\n\n[![npm version](https://img.shields.io/npm/v/signal-template-forms.svg)](https://www.npmjs.com/package/signal-template-forms)\n\nA modern Angular form library built from the ground up with Signals — flexible, type-safe, and fully themeable.\n\nBorn from an itch to reimagine template-driven forms, `signal-template-forms` gives you a clean, declarative API powered by Angular Signals and full control over layout, styling, and behavior.\n\n\u003e ⚠️ Built with Angular 19.2.\n\n## Features\n\n- 🎯 **Type-safe**: Full TypeScript support with intelligent autocompletion\n- ⚡ **Signal-based**: Reactive forms using Angular signals for optimal performance\n- 🔧 **Rich field types**: Text, number, select, autocomplete, date, file upload, and more\n- ✅ **Validation**: Field-level, cross-field, and async validation support\n- 🎨 **Customizable**: CSS variables for easy theming and styling\n- 📱 **Responsive**: Built-in responsive design patterns\n- 🔄 **Unit conversion**: Advanced number fields with automatic unit conversions\n- 📊 **Word counting**: Text fields with character/word count display\n- 🧙 **Stepped forms**: Multi-step form wizard support\n\n## Installation\n\n```bash\nnpm install signal-template-forms\n```\n\n## Quick Start\n\n```typescript\nimport { SignalFormBuilder } from \"signal-template-forms\";\n\n@Component({\n  // ...\n})\nexport class MyComponent {\n  form = SignalFormBuilder.createForm({\n    model: { name: \"\", email: \"\", age: 0 },\n    fields: [\n      { name: \"name\", label: \"Full Name\", type: FormFieldType.TEXT },\n      { name: \"email\", label: \"Email\", type: FormFieldType.TEXT },\n      { name: \"age\", label: \"Age\", type: FormFieldType.NUMBER },\n    ],\n    onSave: (value) =\u003e console.log(\"Form saved:\", value),\n  });\n}\n```\n\n```html\n\u003csignal-form [form]=\"form\" /\u003e\n```\n\n## Form Container API\n\nThe form container provides these reactive properties and methods:\n\n### Properties\n\n- `status: WritableSignal\u003cFormStatus\u003e` - Current form status (Idle, Submitting, Success, Error)\n- `value: Signal\u003cTModel\u003e` - Current form values (excluding disabled fields)\n- `rawValue: Signal\u003cTModel\u003e` - All form values including disabled fields\n- `anyTouched: Signal\u003cboolean\u003e` - True if any field has been touched\n- `anyDirty: Signal\u003cboolean\u003e` - True if any field has been modified\n- `saveButtonDisabled: Signal\u003cboolean\u003e` - Whether save button should be disabled\n- `fields: SignalFormField\u003cTModel\u003e[]` - Array of all form fields\n\n### Methods\n\n- `getField\u003cK extends keyof TModel\u003e(key: K)` - Get a specific field instance\n- `getValue(): TModel` - Get current form values\n- `getRawValue(): TModel` - Get all form values including disabled\n- `getErrors(): ErrorMessage\u003cTModel\u003e[]` - Get all validation errors\n- `validateForm(): boolean` - Validate entire form\n- `setValue(model: TModel): void` - Set complete form values\n- `patchValue(partial: DeepPartial\u003cTModel\u003e): void` - Update specific fields\n- `reset(): void` - Reset form to initial state\n- `save(): void` - Save form (runs validation first)\n\n## Field Access\n\nFields are signals that can be accessed and modified directly:\n\n```typescript\n// Get a field\nconst nameField = form.getField(\"name\");\n\n// Access field signals\nconst currentValue = nameField.value();\nconst hasError = nameField.error();\nconst isTouched = nameField.touched();\nconst isDirty = nameField.dirty();\nconst hasFocus = nameField.focus();\n\n// Modify field signals\nnameField.value.set(\"New Value\");\nnameField.disabled.set(true);\nnameField.disabled.update((current) =\u003e !current);\nnameField.focus.set(true);\nnameField.error.set(\"Custom error message\");\n\n// Reactive field interactions\nconst isSubmitDisabled = computed(() =\u003e form.getField(\"email\").error() || !form.getField(\"terms\").value());\n\n// Set field value based on another field\neffect(() =\u003e {\n  const country = form.getField(\"country\").value();\n  if (country === \"US\") {\n    form.getField(\"currency\").value.set(\"USD\");\n  }\n});\n```\n\n## Field Types Reference\n\n### Text Fields\n\n```typescript\n{ name: 'username', label: 'Username', type: FormFieldType.TEXT }\n{ name: 'description', label: 'Description', type: FormFieldType.TEXTAREA }\n{ name: 'password', label: 'Password', type: FormFieldType.PASSWORD }\n```\n\n### Number Fields with Unit Conversion\n\n```typescript\n// Standard number\n{ name: 'quantity', label: 'Quantity', type: FormFieldType.NUMBER }\n\n// Currency formatting\n{\n  name: 'price',\n  label: 'Price',\n  type: FormFieldType.NUMBER,\n  config: {\n    inputType: NumberInputType.CURRENCY,\n    currencyCode: 'USD'\n  }\n}\n\n// Percentage\n{\n  name: 'discount',\n  label: 'Discount',\n  type: FormFieldType.NUMBER,\n  config: { inputType: NumberInputType.PERCENTAGE }\n}\n\n// Unit conversion (weight)\n{\n  name: 'weight',\n  label: 'Weight',\n  type: FormFieldType.NUMBER,\n  config: {\n    inputType: NumberInputType.UNIT_CONVERSION,\n    unitConversions: ConversionUtils.createWeightConfig('kg', 1)\n  }\n}\n```\n\n### Selection Fields\n\n```typescript\n// Select dropdown\n{\n  name: 'country',\n  label: 'Country',\n  type: FormFieldType.SELECT,\n  options: [\n    { label: 'United States', value: 'US' },\n    { label: 'Canada', value: 'CA' }\n  ]\n}\n\n// Radio buttons\n{\n  name: 'size',\n  label: 'Size',\n  type: FormFieldType.RADIO,\n  options: [\n    { label: 'Small', value: 'S' },\n    { label: 'Medium', value: 'M' },\n    { label: 'Large', value: 'L' }\n  ]\n}\n\n// Multi-select\n{\n  name: 'skills',\n  label: 'Skills',\n  type: FormFieldType.MULTISELECT,\n  options: [\n    { label: 'JavaScript', value: 'js' },\n    { label: 'TypeScript', value: 'ts' },\n    { label: 'Angular', value: 'angular' }\n  ]\n}\n```\n\n### Autocomplete Fields\n\n```typescript\n// Static options autocomplete\n{\n  name: 'city',\n  label: 'City',\n  type: FormFieldType.AUTOCOMPLETE,\n  loadOptions: (search: string) =\u003e {\n    const cities = [\n      { label: 'New York', value: 'ny' },\n      { label: 'Los Angeles', value: 'la' },\n      { label: 'Chicago', value: 'chi' }\n    ];\n    return of(cities.filter((city) =\u003e\n      city.label.toLowerCase().includes(search.toLowerCase())\n    ));\n  },\n  config: {\n    debounceMs: 300,\n    minChars: 2\n  }\n}\n\n// Observable-based autocomplete with HTTP service\n{\n  name: 'country',\n  label: 'Country',\n  type: FormFieldType.AUTOCOMPLETE,\n  loadOptions: (search: string) =\u003e\n    this.httpService.searchCountries(search).pipe(\n      map((countries) =\u003e countries.map(country =\u003e ({\n        label: country.name,\n        value: country.code\n      })))\n    ),\n  config: {\n    debounceMs: 200,\n    minChars: 1\n  }\n}\n```\n\n### Boolean Fields\n\n```typescript\n{ name: 'agreeToTerms', label: 'I agree to terms', type: FormFieldType.CHECKBOX }\n{ name: 'enableNotifications', label: 'Notifications', type: FormFieldType.SWITCH }\n```\n\n### Advanced Fields\n\n```typescript\n{ name: 'birthDate', label: 'Birth Date', type: FormFieldType.DATETIME, config: {\n  format: 'YYYY-MM-DD'\n} }\n{ name: 'favoriteColor', label: 'Color', type: FormFieldType.COLOR, config: { view: 'pickerWithInput' } }\n{ name: 'volume', label: 'Volume', type: FormFieldType.SLIDER, config: { min: 0, max: 100 } }\n{ name: 'rating', label: 'Rating', type: FormFieldType.RATING, config: { max: 5 } }\n{ name: 'avatar', label: 'Profile Picture', type: FormFieldType.FILE, { config: {\n  accept: ['jpg', 'png'],\n  maxSizeMb: 10,\n  multiple: false,\n  uploadText: 'upload your jpegs or pngs here'\n}} }\n```\n\n### Word Count Feature\n\n```typescript\n{\n  name: 'description',\n  label: 'Description',\n  type: FormFieldType.TEXTAREA,\n  config: {\n    wordCount: {\n      enabled: true,\n      maxWords: 150,\n      showCharacters: true\n    }\n  }\n}\n```\n\n## Validation\n\n### Field-Level Validation\n\n```typescript\nimport { SignalValidators } from 'signal-template-forms';\n\n{\n  name: 'email',\n  label: 'Email',\n  type: FormFieldType.TEXT,\n  validators: [\n    SignalValidators.required(),\n    SignalValidators.email(),\n    SignalValidators.minLength(5)\n  ]\n}\n```\n\n### Cross-Field Validation\n\n```typescript\n{\n  name: 'confirmPassword',\n  label: 'Confirm Password',\n  type: FormFieldType.PASSWORD,\n  validators: [\n    SignalValidators.required(),\n    (value, form) =\u003e {\n      const password = form.getField('password').value();\n      return value === password ? null : 'Passwords must match';\n    }\n  ]\n}\n```\n\n### Async Validation\n\n```typescript\n{\n  name: 'username',\n  label: 'Username',\n  type: FormFieldType.TEXT,\n  asyncValidators: [\n    (value: string) =\u003e\n      this.userService.checkUsername(value).pipe(\n        map((isAvailable) =\u003e isAvailable ? null : 'Username taken')\n      )\n  ]\n}\n```\n\n## Stepped Forms (Wizards)\n\n```typescript\nconst steppedForm = SignalFormBuilder.createSteppedForm({\n  model: { personal: {}, contact: {}, preferences: {} },\n  steps: [\n    {\n      title: \"Personal Information\",\n      fields: [\n        { name: \"firstName\", label: \"First Name\", type: FormFieldType.TEXT },\n        { name: \"lastName\", label: \"Last Name\", type: FormFieldType.TEXT },\n      ],\n    },\n    {\n      title: \"Contact Details\",\n      fields: [\n        { name: \"email\", label: \"Email\", type: FormFieldType.TEXT },\n        { name: \"phone\", label: \"Phone\", type: FormFieldType.TEXT },\n      ],\n    },\n  ],\n  onSave: (value) =\u003e this.submitForm(value),\n});\n```\n\n```html\n\u003csignal-form-stepper [form]=\"steppedForm\" (afterSaveCompletes)=\"handleAfterSaveHasFinished()\" /\u003e\n```\n\n## Form Actions\n\n### Setting Field Values\n\n```typescript\n// Set individual field values\nform.getField(\"name\").value.set(\"John Doe\");\nform.getField(\"email\").value.set(\"john@example.com\");\n\n// Set field state\nform.getField(\"email\").disabled.set(true);\nform.getField(\"name\").error.set(\"Custom error\");\nform.getField(\"description\").focus.set(true);\n\n// Update field values reactively\nform.getField(\"quantity\").value.update((current) =\u003e current + 1);\nform.getField(\"enabled\").disabled.update((current) =\u003e !current);\n```\n\n### Form-Level Actions\n\n```typescript\n// Set complete form values (requires full model)\nform.setValue({\n  name: \"John Doe\",\n  email: \"john@example.com\",\n  age: 30,\n});\n\n// Patch partial form values\nform.patchValue({\n  email: \"newemail@example.com\",\n  age: 31,\n});\n\n// Reset form to initial state\nform.reset();\n\n// Validate and save\nif (form.validateForm()) {\n  form.save();\n}\n```\n\n## Conditional Fields\n\n```typescript\n{\n  name: 'reason',\n  label: 'Reason for leaving',\n  type: FormFieldType.TEXTAREA,\n  hidden: (form) =\u003e form.getField('isStaying').value() === true\n}\n\n{\n  name: 'managerEmail',\n  label: 'Manager Email',\n  type: FormFieldType.TEXT,\n  disabled: (form) =\u003e form.getField('hasManager').value() === false\n}\n```\n\n## Styling\n\n### CSS Color System\n\nSignal Template Forms includes a comprehensive color system with automatic dark mode support:\n\n```css\n:root {\n  /* Base colors - customize these to rebrand your entire form library */\n  --signal-forms-primary: #3b82f6; /* Blue */\n  --signal-forms-accent: #8b5cf6; /* Purple */\n  --signal-forms-success: #10b981; /* Green */\n  --signal-forms-warning: #f59e0b; /* Amber */\n  --signal-forms-info: #0ea5e9; /* Sky */\n  --signal-forms-danger: #ef4444; /* Red */\n}\n```\n\nEach color automatically generates a complete scale (50-950) using CSS `color-mix()`:\n\n- **50-400**: Light tints (mixed with white)\n- **500**: Base color (your custom value)\n- **600-950**: Dark shades (mixed with black)\n\n```css\n/* These are automatically generated from your base colors */\n--signal-forms-primary-50: color-mix(in srgb, var(--signal-forms-primary) 5%, white);\n--signal-forms-primary-100: color-mix(in srgb, var(--signal-forms-primary) 10%, white);\n/* ... up to 950 */\n```\n\n### Dark Mode Support\n\nto enable forms to use dark-mode simply use this provider within your apps config.\n\n```typescript\nprovideSignalFormsTheme({ darkMode: true }),\n```\n\nthis allows you to decide if you want the forms to use dark mode or not. if you want to default the theme we can also use `defaultTheme` as an argument withing the provideSignalFormsTheme provider, this allows us to choose between `'light' | 'dark' | 'auto'` and originally defaults to auto.\n\n### Theming Variables\n\nCustomize form appearance with CSS variables:\n\n```css\n:root {\n  /* Layout */\n  --signal-form-padding: 1rem;\n  --signal-form-border-radius: 4px;\n  --signal-form-max-width: 600px;\n\n  /* Colors (use semantic color tokens) */\n  --signal-form-bg: var(--signal-forms-neutral-50);\n  --signal-form-text: var(--signal-forms-neutral-700);\n  --signal-form-border-color: var(--signal-forms-neutral-300);\n  --signal-form-outline-focus: var(--signal-forms-primary-500);\n  --signal-form-error-color: var(--signal-forms-danger-600);\n\n  /* Typography */\n  --signal-form-font-size-base: 1rem;\n  --signal-form-font-size-sm: 0.75rem;\n\n  /* Buttons */\n  --signal-form-button-primary-bg: var(--signal-forms-primary-500);\n  --signal-form-button-primary-bg-hover: var(--signal-forms-primary-600);\n\n  /* Form spacing */\n  --signal-form-fields-gap: 1rem;\n  --signal-form-group-gap: 1rem;\n}\n```\n\n### Custom Brand Theme Example\n\n```css\n:root {\n  /* Custom brand colors */\n  --signal-forms-primary: #ff6b35; /* Orange brand */\n  --signal-forms-accent: #6c5ce7; /* Purple accent */\n  --signal-forms-success: #2ecc71; /* Custom green */\n\n  /* Adjust layout for your design */\n  --signal-form-border-radius: 8px;\n  --signal-form-padding: 1.5rem;\n  --signal-form-max-width: 800px;\n}\n```\n\n## Layout Configurations\n\n### Standard Layouts\n\nConfigure form layout using the `view` property:\n\n```typescript\n// Stacked layout (default)\n{\n  config: {\n    view: \"stacked\";\n  }\n}\n\n// Row layout (horizontal)\n{\n  config: {\n    view: \"row\";\n  }\n}\n\n// Collapsable sections\n{\n  config: {\n    view: \"collapsable\";\n  }\n}\n```\n\n### CSS Grid Layout with `gridArea` 🎯\n\nThe most powerful layout feature allows you to create custom CSS Grid layouts using `gridArea`:\n\n```typescript\nconst form = SignalFormBuilder.createForm({\n  model: { name: \"\", email: \"\", phone: \"\", address: \"\", city: \"\", zip: \"\" },\n  fields: [\n    { name: \"name\", label: \"Full Name\", type: FormFieldType.TEXT },\n    { name: \"email\", label: \"Email\", type: FormFieldType.TEXT },\n    { name: \"phone\", label: \"Phone\", type: FormFieldType.TEXT },\n    { name: \"address\", label: \"Address\", type: FormFieldType.TEXT },\n    { name: \"city\", label: \"City\", type: FormFieldType.TEXT },\n    { name: \"zip\", label: \"ZIP Code\", type: FormFieldType.TEXT },\n  ],\n  config: {\n    layout: \"grid-area\",\n    gridArea: [\n      [\"name\", \"name\", \"email\"], // Row 1: name spans 2 cols, email 1 col\n      [\"phone\", \"phone\", \"phone\"], // Row 2: phone spans all 3 cols\n      [\"address\", \"address\", \"address\"], // Row 3: address spans all 3 cols\n      [\"city\", \"city\", \"zip\"], // Row 4: city spans 2 cols, zip 1 col\n    ],\n  },\n});\n```\n\nThis creates a CSS Grid with:\n\n- **3 columns** (based on array length)\n- **4 rows** (based on gridArea array length)\n- Each field automatically gets `grid-area: fieldName`\n\n### Advanced Grid Examples\n\n**Complex Dashboard Layout:**\n\n```typescript\nconfig: {\n  layout: 'grid-area',\n  gridArea: [\n    ['title', 'title', 'status', 'priority'],\n    ['description', 'description', 'description', 'tags'],\n    ['startDate', 'endDate', 'assignee', 'tags'],\n    ['budget', 'category', 'assignee', 'tags'],\n    ['notes', 'notes', 'notes', 'notes'],\n  ]\n}\n```\n\n**Responsive Contact Form:**\n\n```typescript\nconfig: {\n  layout: 'grid-area',\n  gridArea: [\n    ['firstName', 'lastName'],         // 2-column row\n    ['email', 'phone'],               // 2-column row\n    ['company', 'jobTitle'],          // 2-column row\n    ['message', 'message'],           // Full-width message\n    ['newsletter', 'submit'],         // Checkbox + submit\n  ]\n}\n```\n\n**Use Empty Slots for Spacing:**\n\n```typescript\nconfig: {\n  layout: 'grid-area',\n  gridArea: [\n    ['name', '.', 'email'],           // '.' creates empty grid cell\n    ['address', 'address', 'address'],\n    ['.', 'submit', '.'],             // Center the submit button\n  ]\n}\n```\n\n### Grid Layout Benefits\n\n- ✅ **Pixel-perfect layouts** - No flexbox guesswork\n- ✅ **Responsive by design** - CSS Grid handles mobile gracefully\n- ✅ **Visual layout definition** - See your layout in the array structure\n- ✅ **Automatic field positioning** - No manual CSS grid-area declarations needed\n- ✅ **Empty cell support** - Use `'.'` for intentional spacing\n- ✅ **Type-safe field names** - TypeScript ensures field names exist\n\n### Custom Grid Styling\n\nThe grid container automatically gets:\n\n```css\n.grid {\n  display: grid;\n  grid-template-areas: \"name name email\" \"phone phone phone\" /* ... */;\n  gap: var(--signal-form-fields-gap);\n  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));\n}\n```\n\n## Form Outputs\n\n### Template Output Pattern\n\n```html\n\u003csignal-form [form]=\"form\" (onSave)=\"handleSave($event)\" /\u003e\n```\n\n### Callback Pattern\n\n```typescript\nimport { SignalFormBuilder } from \"signal-template-forms\";\n\nform = SignalFormBuilder.createForm({\n  model: myModel,\n  fields: myFields,\n  onSave: (value) =\u003e {\n    console.log(\"Form saved with:\", value);\n    this.apiService.saveData(value);\n  },\n});\n```\n\n## License\n\nMIT @ Steven Dix\n","funding_links":[],"categories":["Third Party Components"],"sub_categories":["Forms"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchocosd%2Fangular-template-signal-forms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchocosd%2Fangular-template-signal-forms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchocosd%2Fangular-template-signal-forms/lists"}