npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rxdi/forms

v0.7.234

Published

A lightweight, strongly-typed, reactive forms library for LitHtml applications.

Readme

Reactive Forms for LitHtml (Enhanced)

A lightweight, strongly-typed, reactive forms library for LitHtml applications.

Features

  • Strict Typing: Full TypeScript support with UnwrapValue and NestedKeyOf for deep property inference.
  • Nested Forms: Support for deep FormGroup nesting and FormArray.
  • Automatic Binding: Bind component models directly to forms with @Form({ model: 'myModel' }).
  • Reactive: based on rxjs BehaviorSubject for value streams.
  • Recursive Updates: patchValue updates deep structures recursively.

Installation

npm i @rxdi/forms

Basic Usage

1. Define Model & Component

import { html, Component, LitElement } from '@rxdi/lit-html';
import { Form, FormGroup } from '@rxdi/forms';

interface UserParams {
  firstName: string;
  address: {
    city: string;
    street: string;
  };
}

@Component({
  selector: 'user-profile',
  template(this: UserProfile) {
    return html`
      <form name="user-form" @submit=${this.onSubmit}>
        <!-- Deep Binding with Dot Notation -->
        <input
          name="firstName"
          .value=${this.form.value.firstName}
          @blur=${() => this.requestUpdate()}
        />

        <!-- Nested Group Binding -->
        <input
          name="address.city"
          .value=${this.form.value.address.city}
          @blur=${() => this.requestUpdate()}
        />

        <button type="submit">Save</button>
      </form>
    `;
  },
})
export class UserProfile extends LitElement {
  // Model to bind
  @property({ type: Object })
  user: UserParams = {
    firstName: 'John',
    address: { city: 'New York', street: '5th Ave' },
  };

  @Form({
    name: 'user-form',
    strategy: 'change',
    model: 'user', // Automatic Model Binding!
  })
  form = new FormGroup({
    firstName: '',
    address: new FormGroup({
      city: '',
      street: '',
    }),
  });

  onSubmit(e: Event) {
    e.preventDefault();
    console.log(this.form.value);
    // Output: { firstName: 'John', address: { city: 'New York', street: '5th Ave' } }
  }
}

New Features

Automatic Model Binding

Use the model property in the @Form decorator to automatically populate the form from a component property.

@Form({
  name: 'my-form',
  model: 'myData' // matches this.myData
})
form = new FormGroup({ ... });

The library reads this.myData during initialization and calls form.patchValue(this.myData).

Nested FormGroups & FormArray

You can nest FormGroups arbitrarily deep.

form = new FormGroup({
  meta: new FormGroup({
    id: 1,
    flags: new FormGroup({
      isActive: true,
      isAdmin: false,
    }),
  }),
  tags: new FormArray([new FormGroup({ label: 'red' })]),
});

Template Binding: Use dot notation for nested controls:

<input name="meta.flags.isActive" type="checkbox" />

Type Safety & Autosuggestion

The library now extensively uses advanced TypeScript features:

  • form.value: Returns the unwrapped pure object type (e.g., { meta: { flags: { isActive: boolean } } }).
  • form.get('path.to.prop'): Provides autocomplete for deep paths and infers return types!
    • form.get('key') returns exact control type (e.g. FormArray) without casting.
// TypeScript knows this is valid:
this.form.get('meta.flags.isActive');

// And this is invalid:
this.form.get('meta.flags.wrongProp'); // Error!

Recursive PatchValue

Update multiple fields deeply at once:

this.form.patchValue({
  meta: {
    flags: {
      isActive: false
    }
  }
});
// Only updates 'isActive', leaves other fields untouched.

Dynamic Array Inputs (FormArray)

For lists of primitive values, use FormArray with an itemFactory and automatic model binding. This removes the need for manual population.

Full Working Example

import { Component, html, LitElement, property } from '@rxdi/lit-html';
import { Form, FormGroup, FormArray } from '@rxdi/forms';


@Component({
  selector: 'tags-component',
  template(this: TagsComponent) {
    return html`
      <form @submit=${(e) => e.preventDefault()}>
        <h3>Tags</h3>

        <!-- List Tags -->
        ${this.form.get('tags').controls.map(
          (control, index) => html`
            <div class="tag-row">
              <input name="tags[${index}].value" .value=${control.value.value} @blur=${() => this.requestUpdate()} />
              <button type="button" @click=${() => this.removeTag(index)}>Remove</button>
            </div>
          `
        )}

        <button type="button" @click=${() => this.addTag()}>Add Tag</button>
        <button type="button" @click=${() => this.onSubmit()}>Submit</button>
      </form>
    `;
  },
})
export class TagsComponent extends LitElement {
  // Model automatically binds to 'tags' in form
  @property({ type: Array })
  tags = ['news', 'tech'];

  @Form({
    name: 'tags-form',
    model: 'tags', // Triggers form.patchValue(this.tags) on INIT
  })
  form = new FormGroup({
    tags: new FormArray<{ value: string }>(
      [],
      (value) => new FormGroup({ value: value.value || value }) // Factory handles population automatically
    ),
  });

  addTag() {
    this.form.get('tags').push(new FormGroup({ value: '' }));
  }

  removeTag(index: number) {
    this.form.get('tags').removeAt(index);
  }

  onSubmit() {
    const dirtyTags = this.form.value.tags;
    console.log(dirtyTags.map((t) => t.value));
  }
}

Typed Subscriptions & Virtual Inputs

You can subscribe to valueChanges on individual inputs, even if they aren't in the DOM yet!

// Works even if 'email' input is inside an *ngIf or not yet rendered
this.form.get('email').valueChanges.subscribe(value => {
  console.log('Email changed:', value); // 'value' is strongly typed!
});

This is powered by "Virtual Inputs" which mock the input interface if the model key exists but the DOM element is missing.

Working with Value Changes

The valueChanges observable is powerful for creating interactive forms.

1. Debounced Search

import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

this.form.get('search').valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged()
).subscribe(term => {
  this.searchService.search(term);
});

2. Dependant Fields (Cascading Dropdowns)

Reset or modify dependent fields when a parent field changes.

this.form.get('country').valueChanges.subscribe(country => {
  // Reset state when country changes
  this.form.get('state').value = '';
  
  // Update state options dynamically based on country
  this.loadStatesFor(country);
});

3. Dynamic Disabling

Disable controls based on the value of others.

this.form.get('subscribeNewsletter').valueChanges.subscribe(shouldSubscribe => {
   const emailControl = this.form.get('newsletterEmail');
   if (shouldSubscribe) {
     emailControl.disabled = false;
   } else {
     emailControl.disabled = true;
     emailControl.value = ''; // Optional: clear value
   }
});

Generic Typed AbstractInput

AbstractInput is now generic, propagating types through the form.

const emailInput: AbstractInput<string> = this.form.get('email');
// emailInput.value is string

API Reference

Validators

Validators are async functions returning InputErrorMessage or void.

export function CustomValidator(element: AbstractInput) {
  if (element.value === 'invalid') {
    return { key: 'customError', message: 'Value is invalid' };
  }
}

// Usage
new FormGroup({
  field: ['', [CustomValidator]],
});

Error Display Information

Use the touched and validity.valid properties for clean UI.

function ErrorTemplate(input: AbstractInput) {
  if (input?.touched && !input.validity.valid) {
    return html`<div class="error">${input.validationMessage}</div>`;
  }
  return html``;
}

Advanced Usage

1. Grouping Multiple Inputs (Checkbox Groups)

By default (multi: false), inputs with the same name attribute behave like radio buttons (single selection). To allow multiple selections (array of values), set multi: true in the form options.

Scenario: A list of permissions where multiple can be selected.

@Form({
  name: 'permissions-form',
  multi: true // Enable multi-value binding for SAME-NAME inputs
})
form = new FormGroup({
  roles: [] // Will be an array of values ['admin', 'viewer']
});
<label> <input name="roles" type="checkbox" value="admin" /> Admin </label>
<label> <input name="roles" type="checkbox" value="editor" /> Editor </label>
<label> <input name="roles" type="checkbox" value="viewer" /> Viewer </label>

2. Single Selection Checkbox (Default Behavior)

If you want multiple checkboxes to act like a radio button (only one valid at a time) but with uncheck capability, simply use the default multi: false.

@Form({
  name: 'settings-form',
  // multi: false // Default behavior
})
form = new FormGroup({
  mode: '' // Will be a single string 'dark' or 'light'
});
<label> <input name="mode" type="checkbox" value="dark" /> Dark </label>
<label> <input name="mode" type="checkbox" value="light" /> Light </label>

3. Per-Field Multi-Select Override

You can mix single-select and multi-select groups in the same form by keeping the global multi: false (default) and adding the multiple attribute to specific inputs.

@Form({
  name: 'mixed-form',
  multi: false // Default (Single Select)
})
form = new FormGroup({
  mode: '',     // Single value
  tags: []      // Array of values
});
<!-- Single Select (Radio behavior) -->
<input name="mode" type="checkbox" value="A" />
<input name="mode" type="checkbox" value="B" />

<!-- Multi Select (Array behavior) via 'multiple' attribute -->
<input name="tags" type="checkbox" value="news" multiple />
<input name="tags" type="checkbox" value="tech" multiple />

4. Framework-Agnostic Usage (Vanilla JS)

You can use this library without Decorators or LitHtml, with any UI library or vanilla HTML.

import { FormGroup } from '@rxdi/forms';

const form = new FormGroup({
  email: '',
  password: '',
});

// manually attach to DOM
const formElement = document.querySelector('form');
form
  .setParentElement(document.body)
  .setOptions({ name: 'my-form' })
  .setFormElement(formElement)
  .prepareValues()
  .setInputs(form.mapEventToInputs(form.querySelectorAllInputs()));

// Listen to changes
form.valueChanges.subscribe((val) => console.log(val));

4. Custom Error Handling Strategies

By default, verification happens on change or blur. You can control this via strategy.

@Form({
  name: 'login',
  strategy: 'input' // Validate on every keystroke
})

You can also manually check error states (e.g. for async validation):

async validateEmail(element: HTMLInputElement) {
  const exists = await checkServer(element.value);
  if (exists) {
    return { key: 'emailExists', message: 'Email already taken' };
  }
}

// In Template
${this.form.hasError('email', 'emailExists')
  ? html`<div class="error">Email taken!</div>`
  : ''}