typescript xml 98 lines · 3 tabs

Cross-Field Password-Confirmation Validator for Angular Reactive Forms

Shared by codesnips Aug 2026
3 tabs
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

function hasActualValue(control: AbstractControl | null): boolean {
  return !!control && control.value !== null && control.value !== '';
}

export function passwordMatchValidator(
  passwordKey: string,
  confirmKey: string
): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const password = group.get(passwordKey);
    const confirm = group.get(confirmKey);

    if (!password || !confirm) {
      return null;
    }

    // Don't fire before the user has typed a confirmation.
    if (!hasActualValue(confirm)) {
      return null;
    }

    const mismatch = password.value !== confirm.value;
    const existing = confirm.errors ?? {};

    if (mismatch) {
      confirm.setErrors({ ...existing, passwordMismatch: true });
      return { passwordMismatch: true };
    }

    // Clear only our key so required/minLength survive.
    const { passwordMismatch, ...rest } = existing;
    confirm.setErrors(Object.keys(rest).length ? rest : null);
    return null;
  };
}
3 files · typescript, xml Explain with highlit

This snippet shows how to validate that two fields — a password and its confirmation — match in an Angular reactive form. The subtle part is that the matching rule spans two controls, so it cannot live on either control individually; it belongs to the FormGroup that contains both.

In password-match.validator.ts, passwordMatchValidator is a factory that returns a ValidatorFn bound to the two control names. Applying it at the group level is the idiomatic way to express a cross-field constraint in Angular: the validator receives the AbstractControl for the whole group and reads its children with group.get(...). When the values differ it writes a passwordMismatch error onto the confirm control via setErrors, rather than only onto the group, so the error sits next to the field the user must fix. Crucially it merges with any existing errors and clears only the passwordMismatch key when values match, so it never stomps other validators such as required or minLength. It also short-circuits while the confirm field is empty so the mismatch message doesn't fire before the user has typed anything.

The hasActualValue guard avoids a common pitfall where an unrelated required error would otherwise be masked by the mismatch state. Returning the error object from the group as well keeps form.valid honest for consumers that inspect the group directly.

In signup-form.component.ts, the form is assembled with FormBuilder. The validator is passed as the second argument to group(...) — the group-level options slot — which is where cross-field validators must be registered. The component exposes small getters like confirmControl and mismatch so the template stays declarative, and it only surfaces the message once the control is touched or dirty to keep the UX calm.

The signup-form.component.html template wires each input to its control and shows Passwords do not match conditionally. Because the error lives on the confirm control, the template reads confirmControl?.errors?.['passwordMismatch'] directly. This approach scales to other paired fields (email confirmation, date ranges) by parameterizing the two control names, and keeps validation logic pure, testable, and free of manual subscriptions.


Related snips

python
class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    cost = models.DecimalField(max_digits=10, decimal_places=2)
    margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)

Django model signals vs overriding save

django python models
by Priya Sharma 2 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
javascript
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"

const application = Application.start()
application.debug = false

Disable submit button while Turbo form is submitting

rails hotwire stimulus
by codesnips 3 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    url: String,
    delay: { type: Number, default: 800 },

Stimulus: autosave draft with Turbo-friendly requests

rails stimulus hotwire
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Cross-Field Password-Confirmation Validator for Angular Reactive Forms — share card
Link copied