Skip to content

Conversation

@xerial
Copy link
Member

@xerial xerial commented Feb 5, 2026

Summary

  • Add composable, reactive form validation that integrates with existing value.bind() / checked.bind() two-way binding
  • Purely Rx-based: no DomNode or DomRenderer changes needed
  • Built-in validators: required, minLength, maxLength, pattern, email, plus custom rule/ruleWith

Design

  • ValidationState enum (Valid/Invalid) with isValid/errors accessors
  • FieldValidation[A] derives reactive state/isValid/errors via source.map()
  • FormValidation aggregates fields using chained Rx.join (supports any number of fields)
  • Validate object provides factory methods and built-in validators
  • Follows the self-contained module pattern established by Storage.scala

Test plan

  • 21 tests covering all validators, field validation reactivity, form aggregation, and edge cases
  • ./sbt "domTest/testOnly *ValidateTest" passes
  • ./sbt scalafmtAll clean

🤖 Generated with Claude Code

Composable, reactive form validation that integrates with
the existing value.bind() two-way binding system. Purely
Rx-based with no DomNode or DomRenderer changes needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions bot added doc Improvements or additions to documentation feature New feature labels Feb 5, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @xerial, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust and flexible form validation framework to uni-dom. The system is designed to be fully reactive, leveraging Rx to provide real-time validation feedback as user input changes. It offers a clear API for defining validation rules for individual fields and for aggregating these into a form-level validation state. This enhancement allows developers to easily implement client-side form validation without complex DOM manipulations, promoting a cleaner and more maintainable codebase.

Highlights

  • Reactive Form Validation: Introduced a new, composable, and reactive form validation system for uni-dom.
  • Seamless Integration: Integrates naturally with existing value.bind() and checked.bind() two-way binding mechanisms.
  • Rx-based Implementation: The validation logic is purely Rx-based, requiring no changes to DomNode or DomRenderer, ensuring a lightweight and non-intrusive design.
  • Comprehensive Validators: Provides a set of built-in validators for common use cases (e.g., required, minLength, maxLength, pattern, email) and supports custom validation rules.
  • Modular Design: Follows a self-contained module pattern, defining core types like ValidationState, ValidationRule, FieldValidation, and FormValidation for clear separation of concerns.
Changelog
  • plans/2026-02-05-form-validation.md
    • Added a new design document detailing the form validation feature, including its overview, API design (core types, built-in validators), file structure, and key design decisions.
  • uni-dom-test/src/test/scala/wvlet/uni/dom/ValidateTest.scala
    • Added a new test file containing 21 unit tests for the form validation system.
    • Tests cover individual validator behavior, ValidationState combination logic, reactive updates in FieldValidation, and aggregation of validation states in FormValidation.
  • uni/.js/src/main/scala/wvlet/uni/dom/Validate.scala
    • Added the core implementation of the form validation module.
    • Defined ValidationState enum (Valid/Invalid) and its utility methods.
    • Introduced ValidationRule trait for defining custom validation logic.
    • Implemented FieldValidation for reactive validation of single input fields.
    • Implemented FormValidation for aggregating validation states of multiple fields.
    • Provided the Validate object as an entry point with factory methods and built-in validators (required, minLength, maxLength, pattern, email, rule, ruleWith).
  • uni/.js/src/main/scala/wvlet/uni/dom/all.scala
    • Modified to export the newly introduced form validation types (Validate, ValidationState, ValidationRule, FieldValidation, FormValidation) for broader accessibility within the uni-dom library.
Activity
  • A new feature, form validation, has been added to uni-dom.
  • A detailed design document for the feature has been created.
  • The core logic for reactive form validation, including various validators and state management, has been implemented.
  • Comprehensive unit tests (21 in total) have been added to ensure the correctness and reactivity of the validation system.
  • The new validation components have been exported for easy use across the library.
  • The author confirmed that all tests pass and code formatting is clean.
  • The PR was generated using Claude Code.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a well-designed, reactive form validation system. The core logic is sound and the use of Rx throughout is excellent. The built-in validators cover common use cases, and the extension points for custom rules are flexible. The tests are comprehensive and cover many scenarios.

My review focuses on a few areas for improvement:

  • Enhancing the usability of the imperative validateNow() and validateAll() methods, which currently have a subtle dependency on the reactive stream being active.
  • Refactoring the implementation of FormValidation to be more concise.
  • Improving the design documentation to highlight a potential pitfall for users.

Overall, this is a great addition to uni-dom.

Comment on lines +84 to +89
def validateNow(): ValidationState =
lastValue match
case Some(v) =>
runRules(v)
case None =>
ValidationState.Invalid(Seq("No value available"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The validateNow() method can be tricky to use correctly. It relies on lastValue, which is only populated if the source Rx has been subscribed to and has emitted a value. If validateNow() is called before this, it will incorrectly return Invalid(Seq("No value available")). This is a potential footgun for users of the library.

To improve usability, especially for the common case where the source is an RxVar, you could try to get the value from the RxVar directly if lastValue is None.

  def validateNow(): ValidationState = {
    val v = lastValue.orElse(source match {
      case rxv: RxVar[A] => Some(rxv.get)
      case _ => None
    })
    v.map(runRules).getOrElse(ValidationState.Invalid(Seq("No value available")))
  }

- Purely Rx-based: validation state is plain `Rx` values consumed by existing reactive DOM binding
- Validation runs on every change by default (reacts to all updates from `value.bind()`)
- FormValidation uses chained pairwise `Rx.join` via `foldLeft` to support any number of fields
- Tracks last value internally for imperative `validateNow()`/`validateAll()`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The design decision to track the last value for imperative validation is good, but it has a subtle requirement: the reactive stream must be active (subscribed to) for validateNow()/validateAll() to work correctly. This is a potential pitfall for users. It would be beneficial to document this requirement here, for example by adding a note:

- Tracks last value internally for imperative `validateNow()`/`validateAll()`
  - **Note**: These imperative methods require the reactive chain to be active (i.e., subscribed to) to have access to the latest value.

Comment on lines +100 to +117
val isValid: Rx[Boolean] =
fields.size match
case 0 =>
Rx.variable(true)
case 1 =>
fields(0).isValid
case n =>
// Chain pairwise joins for any number of fields
val first: Rx[Boolean] = fields(0).isValid
fields
.tail
.foldLeft(first) { (acc, field) =>
acc
.join(field.isValid)
.map { case (a, b) =>
a && b
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of isValid can be simplified by using foldLeft with an initial value, which removes the need for the fields.size match and special casing for 0 or 1 fields. This makes the code more concise.

  val isValid: Rx[Boolean] =
    fields.map(_.isValid).foldLeft(Rx.variable(true)) { (acc, fieldIsValid) =>
      acc.join(fieldIsValid).map { case (a, b) => a && b }
    }

Comment on lines +119 to +135
val errors: Rx[Seq[String]] =
fields.size match
case 0 =>
Rx.variable(Seq.empty)
case 1 =>
fields(0).errors
case n =>
val first: Rx[Seq[String]] = fields(0).errors
fields
.tail
.foldLeft(first) { (acc, field) =>
acc
.join(field.errors)
.map { case (a, b) =>
a ++ b
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to isValid, the implementation of errors can be simplified by using foldLeft with an initial value. This makes the code more concise and consistent.

  val errors: Rx[Seq[String]] =
    fields.map(_.errors).foldLeft(Rx.variable(Seq.empty[String])) { (acc, fieldErrors) =>
      acc.join(fieldErrors).map { case (a, b) => a ++ b }
    }

xerial and others added 2 commits February 5, 2026 17:31
Remove unused imports, drop @volatile (single-threaded Scala.js),
add scaladoc for pattern's full-match behavior, and add test for
validateNow() before subscription.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- validateNow() reads RxVar directly for reliable imperative use
- Simplify FormValidation.isValid/errors with foldLeft from initial value
- Update plan doc with subscription requirement note

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@xerial
Copy link
Member Author

xerial commented Feb 6, 2026

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust and reactive form validation system to uni-dom, leveraging Rx for composable field and form validations. The design is clean, integrating well with existing two-way binding, and includes a good set of built-in validators. The accompanying test suite is comprehensive, covering various scenarios and edge cases, which is excellent for ensuring reliability. The documentation in the plans file is also clear and well-structured.

Comment on lines +211 to +213
def email(message: String = "Invalid email address"): ValidationRule[String] = pattern(
"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current email regex ^[^@\s]+@[^@\s]+\.[^@\s]+$ is quite basic and might not cover all valid email formats or might allow some invalid ones. While the comment mentions it's a "simple practical regex", for a validation library, a more robust regex or integration with a dedicated email validation utility could enhance its reliability and user experience. Consider using a more comprehensive regex or a library if strict email validation is critical.

xerial and others added 2 commits February 5, 2026 18:11
Merge main (Geolocation feature) and document email regex's
intentional simplicity per Gemini review feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep validateNow() simple with lastValue tracking, restore
explicit FormValidation size matching, and trim verbose scaladoc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@xerial xerial merged commit f1df291 into main Feb 6, 2026
13 checks passed
@xerial xerial deleted the feature/form-validation branch February 6, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Improvements or additions to documentation feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant