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:
- Primitive Types (String, Boolean, Char, Int, Long, etc.)
- Enums
- Sealed classes
- Custom classes
- The properties (fields) must be declared as read-only variables (
val
). - All properties should have a default value
Example
@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
.
@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.
@Form
data class SignUpFormData(
...
@EmailAddress
val emailAddress: String
...
)
To build a form, you can use either one of the following functions
me.naingaungluu.formconductor.builder.form()
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
- Declarative
- Imperative
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
}
}
}
import me.naingaungluu.formconductor.builder.form
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Imperative Form Building
val formState = form(LoginForm::class)
val emailAddressState = form.registerField(LoginForm::emailAddress)
val passwordState = form.registerField(LoginForm::password)
etLogin.doAfterTextChanged {
emailAddressState.setField(it)
}
etPassword.doAfterTextChanged {
passwordState.setField(it)
}
emailAddresState.resultStream.collectLatest {
if (it is FieldResult.Error) {
// Handle Error
}
}
formState.valueStream.collectLatest { result ->
btnLogin.enabled = (result is FormResult.Success)
}
btnLogin.setOnClickListener {
viewModel.login(formState.value)
}
}
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:
FormResult.Success
FormResult.Error
FormResult.NoInput
FormResult.Success
FormResult.Success
is a sealed data class containing data
property with filled with form data.
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.
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:
FieldResult.Success
FieldResult.Error
FieldResult.NoInput
FieldResult.Success
FieldResult.Success
is a sealed data class returned when all validations pass and there's no error.
val nameField = form.registerField(SignUpFormData::name)
nameField.resultStream.collectLatest {
if (it is FieldResult.Success) {
// Validation Success
}
}
FieldResult.Eror
FieldResult.Error
is 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.
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.
NoInput
is only a state and not an error indicating an empty mandatory field.