Skip to content

Commit

Permalink
Add property enabled to the form state:
Browse files Browse the repository at this point in the history
- When enabled, validation is bypassed and errors are cleared.
- FormGroupControl has an empty map value when disabled.
  • Loading branch information
desweemerl committed Jan 18, 2023
1 parent 7c84556 commit b43e114
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ abstract class AbstractFormControl<S, V>(initialState: S) :
@Suppress("UNCHECKED_CAST")
suspend fun markAsDirty(dirty: Boolean = true): FormState<V> =
transform { state -> state.markAsDirty(dirty) as S }

@Suppress("UNCHECKED_CAST")
suspend fun enable(enabled: Boolean = true): FormState<V> =
transform { state -> state.enable(enabled) as S }
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ open class FormFieldControl<V>(

private suspend fun liveValidate(validationRequested: Boolean = false): FormFieldState<V> {
validationJob?.cancelAndJoin()

if (!state.enabled) {
validationJob = null
return updateAndNotify { state ->
state.copy(errors = listOf())
}
}

validationJob = scope.launch {
val initialState = updateAndNotify { state ->
state.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class FormFieldState<V>(
override val errors: ValidationErrors = listOf(),
override val dirty: Boolean = false,
override val touched: Boolean = false,
override val enabled: Boolean = true,
override val validating: Boolean = false,
override val validationRequested: Boolean = false,
) : FormState<V> {
Expand All @@ -26,6 +27,9 @@ class FormFieldState<V>(
override fun markAsDirty(dirty: Boolean): FormState<V> =
copy(dirty = dirty)

override fun enable(enabled: Boolean): FormState<V> =
copy(enabled = enabled)

fun <O> convert(converter: (value: V) -> O): FormFieldState<O> =
FormFieldState(
value = converter(value),
Expand All @@ -41,13 +45,15 @@ class FormFieldState<V>(
errors: ValidationErrors = this.errors,
dirty: Boolean = this.dirty,
touched: Boolean = this.touched,
enabled: Boolean = this.enabled,
validating: Boolean = this.validating,
validationRequested: Boolean = this.validationRequested,
): FormFieldState<V> = FormFieldState(
value = value,
errors = errors,
dirty = dirty,
touched = touched,
enabled = enabled,
validating = validating,
validationRequested = validationRequested,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

fun Map<String, Control<FormState<Any?>>>.getValues(): Map<String, Any?> =
entries.associate { entry -> Pair(entry.key, entry.value.state.value) }
entries
.filter { entry -> entry.value.state.enabled }
.associate { entry -> Pair(entry.key, entry.value.state.value) }

fun Map<String, Control<FormState<Any?>>>.getErrors(): ValidationErrors =
entries
Expand All @@ -21,6 +23,9 @@ fun Map<String, Control<FormState<Any?>>>.touched(): Boolean =
fun Map<String, Control<FormState<Any?>>>.dirty(): Boolean =
values.any { control -> control.state.dirty }

fun Map<String, Control<FormState<Any?>>>.enabled(): Boolean =
values.any { control -> control.state.enabled }

class FormGroupControl(
private val controls: Map<String, AbstractFormControl<FormState<Any?>, Any?>> = mapOf(),
override val validators: Validators<FormGroupState> = arrayOf(),
Expand Down Expand Up @@ -87,13 +92,19 @@ class FormGroupControl(
filter(
{ newState.formDirty != null },
{ controlState -> controlState.markAsDirty(newState.formDirty!!) }),
filter(
{ newState.formEnabled != null },
{ controlState -> controlState.enable(newState.formEnabled!!) }),
)
)
}
}
.joinAll()

updateAndNotify { newState.copy(formTouched = null, formDirty = null) }
updateAndNotify {
newState.copy(formTouched = null, formDirty = null, formEnabled = null)
}

liveValidate()
}
transformJob?.join()
Expand All @@ -106,6 +117,14 @@ class FormGroupControl(

private suspend fun liveValidate(validationRequested: Boolean = false): FormGroupState {
validationJob?.cancelAndJoin()

if (!state.enabled) {
validationJob = null
return updateAndNotify { state ->
state.copy(formErrors = listOf())
}
}

validationJob = scope.launch {
updateAndNotify { state ->
state.copy(validating = true, validationRequested = validationRequested)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ class FormGroupState(
private val formErrors: ValidationErrors = listOf(),
val formTouched: Boolean? = null,
val formDirty: Boolean? = null,
val formEnabled: Boolean? = null,
private var controlsDirty: Boolean = true,
override val validating: Boolean = false,
override val validationRequested: Boolean = false,
) : FormState<Map<String, Any?>> {
private var _errors: ValidationErrors? = null

override val value: Map<String, Any?>
get() = formValue.plus(controls.getValues())
get() = if (formEnabled == false) mapOf() else formValue.plus(controls.getValues())

override val errors: ValidationErrors
get() {
Expand All @@ -38,11 +39,14 @@ class FormGroupState(
override val dirty: Boolean
get() = controls.dirty()

override val enabled: Boolean
get() = controls.enabled()

override fun toString(): String = """
FormGroupState{value=$value errors=$errors
dirty=$dirty touched=$touched
validating=$validating validationRequested=$validationRequested
formValue=$formValue formErrors=$formErrors controlsDirty=$controlsDirty}""".trimIndent()
formValue=$formValue formErrors=$formErrors formDisabled=$formEnabled controlsDirty=$controlsDirty}""".trimIndent()

override fun setValue(transformer: Transformer<Map<String, Any?>>): FormGroupState =
copy(formValue = transformer(value))
Expand All @@ -53,11 +57,15 @@ class FormGroupState(
override fun markAsDirty(dirty: Boolean): FormGroupState =
copy(formDirty = dirty)

override fun enable(enabled: Boolean): FormState<Map<String, Any?>> =
copy(formEnabled = enabled)

fun copy(
formValue: Map<String, Any?> = this.formValue,
formErrors: ValidationErrors = this.formErrors,
formTouched: Boolean? = this.formTouched,
formDirty: Boolean? = this.formDirty,
formEnabled: Boolean? = this.formEnabled,
controlsDirty: Boolean = this.controlsDirty,
validating: Boolean = this.validating,
validationRequested: Boolean = this.validationRequested,
Expand All @@ -68,13 +76,14 @@ class FormGroupState(
formErrors = formErrors,
formTouched = formTouched,
formDirty = formDirty,
formEnabled = formEnabled,
controlsDirty = controlsDirty,
validating = validating,
validationRequested = validationRequested,
)
}

fun FormGroupState.valueToJson(encoder: JsonElementEncoder = ::valueJsonEncoder): Result<JsonElement> =
kotlin.runCatching {
runCatching {
encoder(value)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ interface FormState<V> {
val errors: ValidationErrors
val dirty: Boolean
val touched: Boolean
val enabled: Boolean
val validating: Boolean
val validationRequested: Boolean

fun setValue(transformer: Transformer<V>): FormState<V>
fun markAsTouched(touched: Boolean = true): FormState<V>
fun markAsDirty(dirty: Boolean = true): FormState<V>
fun enable(enabled: Boolean = true): FormState<V>

fun matches(other: FormState<V>?): Boolean =
if (other == null) {
Expand All @@ -25,6 +27,7 @@ interface FormState<V> {
&& errors.matches(other.errors)
&& dirty == other.dirty
&& touched == other.touched
&& enabled == other.enabled
&& validating == other.validating
&& validationRequested == other.validationRequested
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ fun FormCheckbox(
} else if (focused && !focusState.isFocused && !state.touched) {
onStateChanged(state.copy(touched = true))
}
}
},
enabled = state.enabled,
)

if (label != null && align == FormCheckBoxAlign.RIGHT) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ fun FormTextField(
onStateChanged(state.copy(touched = true))
}
},
enabled = state.enabled,
label = label?.let { { Text(text = it) } },
placeholder = placeholder?.let { { Text(text = it) } },
supportingText = {
Expand Down Expand Up @@ -107,6 +108,7 @@ fun FormTextField(
onStateChanged(state.copy(touched = true))
}
},
enabled = state.enabled,
label = label?.let { { Text(text = it) } },
placeholder = placeholder?.let { { Text(text = it) } },
supportingText = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,12 @@ class FormFieldControlValidationTest :
runTest {
assertMatchErrors(requiredError, control.validate().errors)
}

@Test
@ExperimentalCoroutinesApi
fun `When a disabled control is initialized with a wrong value expect validation return no error`() =
runTest {
control.enable(false)
assertMatchErrors(listOf(), control.validate().errors)
}
}
41 changes: 41 additions & 0 deletions lib/src/test/kotlin/com/desweemerl/compose/form/FormGroupTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ class NestedFormGroupTest : FormGroupControlTest(
assertMatch(expectation, state.value)
}

@Test
@ExperimentalCoroutinesApi
fun `When a disabled form is initialized expect state is empty`() = runTest {
control.enable(false)

assertMatch(mapOf(), state.value)
}

@Test
@ExperimentalCoroutinesApi
fun `When the child form is updated expect state has the new value`() = runTest {
Expand Down Expand Up @@ -176,6 +184,16 @@ class FormGroupValidationTest : FormGroupControlTest(
assertMatchErrors(formErrors, state.errors)
}

@Test
@ExperimentalCoroutinesApi
fun `When a disabled control has errors and validation is done on the form expect states on form and control have no errors`() =
runTest {
control.enable(false)

assertMatchErrors(listOf(), control.validate().errors)
assertMatchErrors(listOf(), state.errors)
}

@Test
@ExperimentalCoroutinesApi
fun `When a control is updated with wrong value expect states on form and control have errors of the control`() =
Expand All @@ -195,6 +213,19 @@ class FormGroupValidationTest : FormGroupControlTest(
assertMatchErrors(formErrors, state.errors)
}

@Test
@ExperimentalCoroutinesApi
fun `When a disabled control is updated with wrong value expect states on form and control have no errors`() =
runTest {
control.enable(false)

assertMatchErrors(
listOf(),
getTextField("first_name").setValue { "héllo!>" }.errors
)
assertMatchErrors(listOf(), state.errors)
}

@Test
@ExperimentalCoroutinesApi
fun `When a control is updated with wrong value and validation is done on the form expect form state has all errors`() =
Expand All @@ -208,6 +239,16 @@ class FormGroupValidationTest : FormGroupControlTest(
assertMatchErrors(formErrors, control.validate().errors)
assertMatchErrors(formErrors, state.errors)
}

@Test
@ExperimentalCoroutinesApi
fun `When a disabled control is updated with wrong value and validation is done on the form expect form state has no errors`() =
runTest {
control.enable(false)
getTextField("first_name").setValue { "héllo!>" }
assertMatchErrors(listOf(), control.validate().errors)
assertMatchErrors(listOf(), state.errors)
}
}

class FormGroupValidationRequestedTest : FormGroupControlTest(
Expand Down

0 comments on commit b43e114

Please # to comment.