ux_kit_hooks

ux_kit_hooks builds on top of ux_kit_core and provides Flutter Hooks support for managing form state, validation, and inputs.

It makes working with forms declarative, reactive, and simple, while keeping behavior separate from styling.

Getting started

Prerequisites:

  • Flutter 3.0 or higher
  • Dart 3.8 or higher

Usage


class SignUpForm {
  const SignUpForm({
    this.firstName = '',
    this.lastName = '',
    this.email = '',
    this.password = '',
  });

  final String firstName;
  final String lastName;
  final String email;
  final String password;

  factory SignUpForm.formatter(List values) {
    return SignUpForm(
      firstName: values[0],
      lastName: values[1],
      email: values[2],
      password: values[3],
    );
  }

  static const FormConfig<SignUpForm> config = FormConfig(
    list: [
      TextConfig(
        label: 'First Name',
        hint: 'first name',
        validation: Validation(
          validator: Validator.required(),
          message: 'First name is required',
        ),
      ),
      TextConfig(
        label: 'Last Name',
        hint: 'last name',
        validation: Validation(
          validator: Validator.required(),
          message: 'Last name is required',
        ),
      ),
      EmailConfig(
        label: 'Email',
        hint: 'email',
        validation: Validation.queue([
          Validation(
            validator: Validator.required(),
            message: 'Email is required',
          ),
          Validation(
            validator: Validator.email,
            message: 'Email is invalid',
          ),
        ]),
      ),
      PasswordConfig(
        label: 'Password',
        hint: 'password',
        validation: Validation.queue([
          Validation(
            key: 'password',
            validator: Validator.required(),
            message: 'Password is required',
          ),
          Validation(
            key: 'password',
            validator: Validator.minLength(8),
            message: 'Password must be at least 8 characters long',
          ),
        ]),
      ),
      PasswordConfig(
        label: 'Confirm Password',
        hint: 'confirm password',
        validation: Validation.relatedTo('password',
          validatorBuilder: IsSameAsValidator.new,
          message: 'Passwords do not match',
        ),
      ),
    ],
    formatter: SignUpForm.formatter,
  );
}

class SignUpFormWidget extends StatelessWidget {
  const SignUpFormWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return MyForm(
      config: SignUpForm.config,
      onSubmit: (SignUpForm form) {
        // TODO: submit form
      },
    );
  }
}

class MyForm<T> extends HookWidget {
  const MyForm({
    super.key,
    required this.config,
    required this.onSubmit,
    this.value,
  });

  final FormConfig<T> config;
  final void Function(T formattedValue) onSubmit;
  final FutureOr<List>? value;

  @override
  Widget build(BuildContext context) {
    final form = useInput.form(
      config: config,
      value: value,
    );
    return ListView(
      children: [
        for (final input in form.list)
          if (input case FieldInput())
            MyTextField(input: input),
        ElevatedButton(
          onPressed: () {
            if (form.validate()) {
              onSubmit(form.formatted);
            }
          },
          child: const Text('Submit'),
        ),
      ],
    );
  }
}


class MyTextField extends HookWidget {
  const MyTextField({super.key, required this.input});

  final FieldInput input;

  @override
  Widget build(BuildContext context) {

    String? errorText;
    final validationResult = useListenable(input.validationResult).value;
    if (validationResult case HasMessage(isValid: final isValid, message: final message) when !isValid && message is String) {
      errorText = message;
    }

    bool obscureText = false;
    if (input case PasswordInput(obscure: final obscure)) {
      obscureText = useListenable(obscure).value;
    }

    return TextField(
      controller: input.textEditing,
      focusNode: input.focusNode,
      autocorrect: input is! PasswordInput,
      obscureText: obscureText,
      keyboardType: input.keyboardType,
      inputFormatters: input.inputFormatters,
      autofillHints: input.autofillHints,
      decoration: InputDecoration(
        labelText: input.config.label,
        hintText: input.config.hint,
        errorText: errorText,
      ),
    );
  }
}

extension on FieldInput {
  TextInputType get keyboardType {
    return switch (this) {
      PasswordInput() => TextInputType.visiblePassword,
      PhoneInput() => TextInputType.phone,
      TextInput() => TextInputType.text,
      NumberInput() => TextInputType.number,
      EmailInput() => TextInputType.emailAddress,
      DateInput() => TextInputType.datetime,
      AddressInput() => TextInputType.streetAddress,
    };
  }

  List<TextInputFormatter>? get inputFormatters {
    return switch (this) {
      PhoneInput() ||
      NumberInput() => [FilteringTextInputFormatter.digitsOnly],
      _ => null,
    };
  }

  List<String>? get autofillHints {
    return switch (this) {
      PasswordInput() => [AutofillHints.password],
      PhoneInput() => [
        AutofillHints.telephoneNumberNational,
        AutofillHints.telephoneNumber,
      ],
      EmailInput() => [AutofillHints.email],
      DateInput() => [AutofillHints.birthday],
      AddressInput() => [AutofillHints.fullStreetAddress],
      _ => null,
    };
  }
}

Dependencies

This package depends on:

Libraries

ux_kit_hooks