Skip to main content

Validation

How it works​

Form conductor performs validations based on the annotations on the form class. The annotations (together with the options) are converted into individual field validators under the hood, modifying the form's state as input events occur.

Form Construction​

Form construction with formconductor is as easy as annotating the form data class with @Form annotation. Every instance of a Form is associated to a Form Data Class. It is recommend to create a new data class for each type of form. Once the data class is associated to a Form, the library will automatically create validators for every declared property in the class.

Field Types​

The library can accept every type of properties as long as it is a sub-class of Any.

This includes data types such as:

  1. Primitive Types (String, Boolean, Char, Int, Long, etc.)
  2. Enums
  3. Sealed classes
  4. Custom classes
⛏️Requirements
  • The properties (fields) must be declared as read-only variables (val).
  • All properties should have a default value

Example​

SignUpForm.kt
@Form
data class SignUpForm(
val name: String = "",
val age: Int = 0,
val emailAddress: String = "",
val bio: String = "",
val gender: Gender = Gender.Male,
val address: Address? = null
val termsAndConditionAgreed: Boolean = false
)

enum class Gender {
Male,
Female
}

data class Address(
val streeet: String,
val postalCode: String,
val state: String
)

Form Data Input​

You can input data in various ways using the library. Entered data entry will be validated in real-time.

Using Form.setField​

val form = form(SignUpFormData::class)

form.setField(SignUpFormData::name, "Harry")

Using FormField.setField​

val form = form(SignUpFormData::class)

val genderField = form.registerField(SignUpFormData::gender) // FormField<Gender>
val addressField = form.registerField(SignUpFormData::address) // FormField<Address>

genderField.setField(Gender.Male)

addressField.setField(
Address(
street = "128 Bird Street",
postalCode = "11221",
state = "Yangon"
)
)

Using FormFieldScope.setField​

You can also set field value inside the field composable function, which is under the FormFieldScope.

SignUpForm.kt

@Composable
fun SignUpForm() {

form(SignUpForm::class) {
field(SignUpForm::name) { // FormFieldScope<String>
TextField(
onValueChange = {
setField(it)
}
)
}
}

}

Using Form.submit​

val form = form(SignUpFormData::class)

form.submit(
SignUpFormData(
name = "Harry",
age = 1`,
emailAddress = "hello@example.com",
bio = "I'm a pet lover",
gender = Gender.Male,
address = null
termsAndConditionAgreed = false
)
) // FormResult.Success or FormResult.Error

Validation​

Form validations are executed by the Form object automatically based on the annotations. All the validations are performed in real-time as you submit field values.

Example Form
@Form
data class SignUpFormData(
...
@EmailAddress
val emailAddress: String
...
)

To build a form, you can use either one of the following functions

  1. me.naingaungluu.formconductor.builder.form()
  2. me.naingaungluu.formconductor.composeui.form()

me.naingaungluu.formconductor.builder.form() is included in :core module and can be used as a normal function to build the form.

me.naingaungluu.formconductor.composeui.form() is a @Composable function and is included in the :compose-ui module to be used in composable ui's.

Examples​

Traditional Android UI​

import me.naingaungluu.formconductor.builder.form

...

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Declarative Form Building
val formState = form(LoginForm::class) {

field(LoginForm::emailAddress) { // FormFieldScope<String>
etEmailAddress.doAfterTextChanged {
this.setField(it)
}
this.resultStream.collectLatest {...}
}

field(LoginForm::name) { // FormFieldScope<String>
etName.doAfterTextChanged {
this.setField(it)
}
}

this.formDataStream.collectLatest {
btnSignUp.isEnabled = it is FormResult.Success
}
}
}

Jetpack Compose UI​

import me.naingaungluu.formconductor.composeui.form

...

@Composable
fun SignUpFormScreen() {
form(SignUpFormData::class) {

field(SignUpFormData::name) {
TextField(
value = this.state.observeAsState(),
onValueChange = this::setField,
)
}

field(SignUpFormData::emailAddress) {
TextField(
value = this.state.observeAsState(),
onValueChange = this::setField
)
}

val formResult = this.resultStream.observeAsState(FormResult.NoInput)

Button(
text = "Sign Up",
enabled = formResult is FormResult.Success
)
}
}

Validation Rules​

You can check out the various ways to apply validation rules here

Validation Results​

Form validations result in two different type of results: FormResult and FieldResult. You can observe the FieldResult for individual field result states or the FormResult for the entire form validation result.

FormResult​

FormResult value can be either one of the followings:

  1. FormResult.Success
  2. FormResult.Error
  3. FormResult.NoInput

FormResult.Success​

FormResult.Success is a sealed data class containing data property with filled with form data.

Example
val formResult: FormResult<SignUpFormData> = form.submit(...)

if (formResult is FormResult.Success) {
val formData: SignUpFormData = formResult.data
}

FormResult.Error​

FormResult.Error is a sealed data class returned when any of the validations fail for a property in the form. The class contains a failedRules property that holds the list of failed rules for the property.

Example
val formResult: FormResult<SignUpFormData> = form.submit(...)

if (formResult is FormResult.Error) {
formResult.failedRules.forEach { // VaildationRule
// handle error
}
}

FormResult.NoInput​

FormResult.NoInput is a special case or state when the form has no interaction at all. The form's state will always be in FormResult.NoInput when it's just iniitalized and there's no interaction yet.

val form = form(SignUpFormData::class)

val state = form.formDataStream.first() // returns FormResult.NoInput

FieldResult​

Quite Similarly to FormResult, FieldResult has three states nearly identical. However, the FieldResult represents the state of each field in the form.

FieldResult value can be either one of the followings:

  1. FieldResult.Success
  2. FieldResult.Error
  3. FieldResult.NoInput

FieldResult.Success​

FieldResult.Success is a sealed data class returned when all validations pass and there's no error.

Example
val nameField = form.registerField(SignUpFormData::name)

nameField.resultStream.collectLatest {
if (it is FieldResult.Success) {
// Validation Success
}
}

FieldResult.Eror​

FieldResult.Erroris a sealed data class returned when one of the validations failed for the field. The class contains a failedRule property that holds the failed ValidationRule instance. If multiple validation rules are applied, the failedRule will hold the very first rule that failed.

Example
val emailAddressField = form.registerField(SignUpFormData::emailAddress)

emailAddressField.resultStream.collectLatest {
if (it is FieldResult.Error) {
if (it.failedRule is EmailAddressRule) {
// Show Error
}
}
}

FieldResult.NoInput​

FieldResult.NoInput is a special case or state where the field has no data input yet. It is usually the initial state of the field after it's just initialized. You'll also see this NoInput state when user clears the text input.

caution

NoInput is only a state and not an error indicating an empty mandatory field.