Sign-in Form

The <smart-sign-in-form> component renders a single form that supports both sign-in and sign-up flows via the mode input. It exposes submit and socialClick outputs, optional social provider buttons, optional forgot/sign-in/sign-up links, an optional extra slot, and standard form fields. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract SignInFormBaseComponent defines the shared API — mode ('sign-in' | 'sign-up'), disabled, optional ISignInFormOptions, cssClass (alias class), and the submit / socialClick outputs. SignInFormStandardComponent is a barebones placeholder concrete implementation using a native <form> with <input type="email"> and <input type="password">. SignInFormComponent is the public wrapper that renders SignInFormStandardComponent by default and accepts a custom replacement via SIGN_IN_FORM_STANDARD_COMPONENT_TOKEN.


Usage

<smart-sign-in-form
  [options]="options"
  [mode]="mode"
  [disabled]="loading()"
  (submit)="onSubmit($event)"
  (socialClick)="onSocialClick($event)"
/>

Components

SignInFormComponent (<smart-sign-in-form>)

Main wrapper component. Renders SignInFormStandardComponent by default. When SIGN_IN_FORM_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet. The wrapper re-emits submit and socialClick from the standard child, or from the custom implementation registered through the token.

SignInFormStandardComponent (<smart-sign-in-form-standard>)

Barebones placeholder concrete implementation using a native <form>. Renders an outer wrapper, the form with optional <label>s (toggle via options.showLabels), <input type="email">, <input type="password"> (with autocomplete="new-password" in sign-up mode), an optional forgot-password link (sign-in mode only), an optional submit-label override, optional social provider buttons, optional alt-link (sign-up href in sign-in mode, sign-in href in sign-up mode), and an optional extraTpl slot. The submit handler emits { email, password, mode } via the inherited submit output unless disabled is true. It stops the native submit event from bubbling, because the output shares its name with the DOM event: without that, a (submit) listener on the host would also receive the raw SubmitEvent. A custom implementation must do the same. Each social provider button emits { providerId, mode } via socialClick.

SignInFormBaseComponent (abstract)

Abstract base directive. Exposes:

  • mode: InputSignal<SmartSignInFormMode> (default 'sign-in')
  • disabled: InputSignal<boolean> (default false)
  • options: InputSignal<ISignInFormOptions | undefined>
  • cssClass: InputSignal<string> (alias class)
  • submit: OutputEmitterRef<ISignInFormSubmit>
  • socialClick: OutputEmitterRef<ISignInFormSocialClick>

API

Inputs

InputTypeDefaultDescription
modeInputSignal<SmartSignInFormMode>'sign-in''sign-in' or 'sign-up'
disabledInputSignal<boolean>falseDisables inputs and prevents emit
optionsInputSignal<ISignInFormOptions | undefined>-Optional configuration (social providers, layout, slots etc.)
classInputSignal<string>''External CSS classes (alias for cssClass)

Outputs

OutputTypeDescription
submitOutputEmitterRef<ISignInFormSubmit>Emitted on form submit (when not disabled): { email, password, mode }
socialClickOutputEmitterRef<ISignInFormSocialClick>Emitted on social provider button click: { providerId, mode }

ISignInFormOptions

The default SignInFormStandardComponent consumes socialProviders, showLabels, forgotPasswordHref, signUpHref, signInHref, submitLabel, emailPlaceholder, passwordPlaceholder, extraTpl, and ariaLabel. layout and heroImageUrl are hints for custom implementations registered via the token.

Extending the base class

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  input,
  signal,
  ViewEncapsulation,
} from '@angular/core';

import {
  ISignInFormOptions,
  SIGN_IN_FORM_STANDARD_COMPONENT_TOKEN,
  SignInFormBaseComponent,
  SignInFormComponent,
} from '@smartsoft001/angular';

@Component({
  selector: 'docs-custom-sign-in-form',
  template: `
    <form
      [class]="containerClasses()"
      [attr.aria-label]="options()?.ariaLabel ?? submitLabel()"
      (submit)="onSubmit($event)"
    >
      <label class="docs-sign-in-form__field">
        @if (options()?.showLabels) {
          <span>Email address</span>
        }
        <input
          type="email"
          class="docs-sign-in-form__email"
          autocomplete="email"
          [placeholder]="options()?.emailPlaceholder ?? 'you@example.com'"
          [value]="email()"
          (input)="email.set($any($event.target).value)"
        />
      </label>

      <label class="docs-sign-in-form__field">
        @if (options()?.showLabels) {
          <span>Password</span>
        }
        <input
          type="password"
          class="docs-sign-in-form__password"
          [attr.autocomplete]="
            mode() === 'sign-up' ? 'new-password' : 'current-password'
          "
          [placeholder]="options()?.passwordPlaceholder ?? ''"
          [value]="password()"
          (input)="password.set($any($event.target).value)"
        />
      </label>

      @if (options()?.forgotPasswordHref) {
        <a
          class="docs-sign-in-form__forgot"
          [href]="options()?.forgotPasswordHref"
        >
          Forgot password?
        </a>
      }

      <button
        type="submit"
        class="docs-sign-in-form__submit"
        [disabled]="disabled()"
      >
        {{ submitLabel() }}
      </button>

      @for (provider of options()?.socialProviders ?? []; track provider.id) {
        <button
          type="button"
          class="docs-sign-in-form__social"
          [disabled]="disabled()"
          (click)="socialClick.emit({ providerId: provider.id, mode: mode() })"
        >
          {{ provider.label }}
        </button>
      }
    </form>
  `,
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomSignInFormComponent extends SignInFormBaseComponent {
  // NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
  override cssClass = input<string>('');

  readonly email = signal('');
  readonly password = signal('');

  readonly submitLabel = computed(
    () =>
      this.options()?.submitLabel ??
      (this.mode() === 'sign-up' ? 'Create account' : 'Sign in'),
  );

  readonly containerClasses = computed(() => {
    const classes = [
      'docs-sign-in-form',
      `docs-sign-in-form--${this.options()?.layout ?? 'simple'}`,
    ];
    const extra = this.cssClass();
    if (extra) classes.push(extra);
    return classes.join(' ');
  });

  onSubmit(event: Event): void {
    event.preventDefault();
    // Keep the native submit event inside the component: only the typed
    // `submit` output should reach a `(submit)` listener on the host.
    event.stopPropagation();
    if (this.disabled()) return;

    this.submit.emit({
      email: this.email(),
      password: this.password(),
      mode: this.mode(),
    });
  }
}

@Component({
  selector: 'docs-sign-in-form-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [SignInFormComponent],
  // The token swaps the standard sign-in form for the custom one everywhere
  // below this component, so consumers keep writing `<smart-sign-in-form>`.
  providers: [
    {
      provide: SIGN_IN_FORM_STANDARD_COMPONENT_TOKEN,
      useValue: CustomSignInFormComponent,
    },
  ],
  template: `
    <smart-sign-in-form mode="sign-in" [disabled]="false" [options]="options" />
  `,
})
export class SignInFormCustomExampleComponent {
  readonly options: ISignInFormOptions = {
    layout: 'simple',
    showLabels: true,
    forgotPasswordHref: '/forgot',
    signUpHref: '/signup',
    signInHref: '/signin',
    socialProviders: [{ id: 'google', label: 'Continue with Google' }],
  };
}

Preset

SignInFormPresetComponent (<smart-sign-in-form-preset>) is a styled drop-in replacement that extends SignInFormStandardComponent, so it inherits all form logic unchanged (email/password signals, onSubmit with the disabled guard, onSocialClick, and mode reactivity for labels/links). It realizes the four options.layout values with Tailwind (smart:-prefixed, explicit smart:dark:* twins) and consistent field styling (rounded-lg, gray borders, blue focus ring; solid blue submit; outline social buttons; blue text links).

options.layoutLook
simple (default)Single column, max-w-sm mx-auto, labeled fields
simple-no-labelsSingle column, placeholders only (labels off unless showLabels: true)
cardsimple wrapped in a bordered, rounded, shadowed white/dark card
split-screengrid lg:grid-cols-2: heroImageUrl cover image column + form column

data-role hooks for testing/styling: container, form, email, password, submit, social, forgot, alt-link, hero, extra, card.

Register it via the token so <smart-sign-in-form> renders the preset:

Gaps: heroImageUrl renders as a smart:lg:block cover image (hidden below the lg breakpoint); submitLabel, emailPlaceholder, passwordPlaceholder, and the extraTpl slot are all honored.

Reference implementation

The example application under docs/examples/app signs in through this component against a real API; its login page is the pattern to copy.

  • docs/examples/app/apps/web/src/app/auth/login.page.ts: a login page on <smart-sign-in-form> with ISignInFormOptions, the disabled input bound to a pending signal and the submit output handled as ISignInFormSubmit.
  • docs/examples/app/apps/web/src/app/auth/login.service.ts: what the submit handler calls, the OAuth password grant against /api/token with the returned token stored through AuthService.setToken.
  • docs/examples/app/apps/web/src/app/auth/login.page.spec.ts: a Jest test that drives the rendered form through its #smart-sign-in-form-email and #smart-sign-in-form-password inputs and the form submit event.
  • docs/examples/app/apps/web-e2e/src/support/app.ts: the Playwright helper that fills the same form by those ids and clicks the button.submit element.

Source

The component lives in packages/shared/angular/src/lib/components/sign-in-form and is documented for Claude Code by the angular-components-sign-in-form skill.