Skip to content

validation via the SDK #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 4, 2024
6 changes: 2 additions & 4 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,5 @@ buildNumber=0000-snapshot
ignoreBuildNumber=false
# these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency
# and are generally not overridden at the command line unless a special build is requested.
sdkVersionNumber=200.3.0
sdkBuildNumber=4051


sdkVersionNumber=200.4.0
sdkBuildNumber=4094
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ internal open class BaseFieldState<T>(
initialValue: T = properties.value.value,
scope: CoroutineScope,
protected val onEditValue: (Any?) -> Unit,
val validate: () -> List<Throwable> = {listOf()}
) : FormElementState(
label = properties.label,
description = properties.description,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* COPYRIGHT 1995-2023 ESRI
*
* TRADE SECRETS: ESRI PROPRIETARY AND CONFIDENTIAL
* Unpublished material - all rights reserved under the
* Copyright Laws of the United States.
*
* For additional information, contact:
* Environmental Systems Research Institute, Inc.
* Attn: Contracts Dept
* 380 New York Street
* Redlands, California, USA 92373
*
* email: [email protected]
*/

package com.arcgismaps.toolkit.featureforms.components.base

internal sealed class ValidationErrorState {
object NoError: ValidationErrorState()
object Required: ValidationErrorState()
object MinMaxCharConstraint: ValidationErrorState()
object ExactCharConstraint: ValidationErrorState()
object MaxCharConstraint: ValidationErrorState()
object MinNumericConstraint: ValidationErrorState()
object MaxNumericConstraint: ValidationErrorState()
object MinMaxNumericConstraint: ValidationErrorState()
object NotANumber: ValidationErrorState()
object NotAWholeNumber: ValidationErrorState()
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,3 @@ internal fun rememberDateTimeFieldState(
}
)
}

Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,26 @@ import androidx.compose.runtime.saveable.rememberSaveable
import com.arcgismaps.data.Domain
import com.arcgismaps.data.FieldType
import com.arcgismaps.data.RangeDomain
import com.arcgismaps.exceptions.FeatureFormValidationException
import com.arcgismaps.mapping.featureforms.FeatureForm
import com.arcgismaps.mapping.featureforms.FieldFormElement
import com.arcgismaps.mapping.featureforms.TextAreaFormInput
import com.arcgismaps.mapping.featureforms.TextBoxFormInput
import com.arcgismaps.toolkit.featureforms.R
import com.arcgismaps.toolkit.featureforms.components.base.BaseFieldState
import com.arcgismaps.toolkit.featureforms.components.base.FieldProperties
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.ExactCharConstraint
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MaxCharConstraint
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MaxNumericConstraint
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MinMaxCharConstraint
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MinMaxNumericConstraint
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MinNumericConstraint
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.NoError
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.NotANumber
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.NotAWholeNumber
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.Required
import com.arcgismaps.toolkit.featureforms.components.formelement.FieldElement
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.ExactCharConstraint
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MaxCharConstraint
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MaxNumericConstraint
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MinMaxCharConstraint
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MinMaxNumericConstraint
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MinNumericConstraint
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.NoError
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.NotANumber
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.NotAWholeNumber
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.Required
import com.arcgismaps.toolkit.featureforms.utils.asDoubleTuple
import com.arcgismaps.toolkit.featureforms.utils.asLongTuple
import com.arcgismaps.toolkit.featureforms.utils.domain
Expand Down Expand Up @@ -95,12 +97,14 @@ internal class FormTextFieldState(
initialValue: String = properties.value.value,
scope: CoroutineScope,
private val context: Context,
onEditValue: (Any?) -> Unit
onEditValue: (Any?) -> Unit,
validate: () -> List<Throwable>
) : BaseFieldState<String>(
properties = properties,
initialValue = initialValue,
scope = scope,
onEditValue = onEditValue
onEditValue = onEditValue,
validate = validate
) {
// indicates singleLine only if TextBoxFeatureFormInput
val singleLine = properties.singleLine
Expand All @@ -125,7 +129,6 @@ internal class FormTextFieldState(
private val _hasError = mutableStateOf(false)
private val _supportingTextIsErrorMessage = mutableStateOf(false)
val supportingTextIsErrorMessage: State<Boolean> = _supportingTextIsErrorMessage

/**
* The domain of the element's field.
*/
Expand Down Expand Up @@ -236,38 +239,6 @@ internal class FormTextFieldState(
_hasError.value = errors.isNotEmpty()
}

private fun validateTextRange(value: String): ValidationErrorState =
if (value.length !in minLength..maxLength) {
if (minLength > 0 && maxLength > 0) {
if (minLength == maxLength) {
ExactCharConstraint
} else {
MinMaxCharConstraint
}
} else {
MaxCharConstraint
}
} else {
NoError
}

private fun validateNumber(value: String): ValidationErrorState =
if (fieldType.isIntegerType) {
val numberVal = value.toIntOrNull()
if (numberVal == null) {
NotAWholeNumber
} else {
validateNumericRange(numberVal)
}
} else {
val numberVal = value.toDoubleOrNull()
if (numberVal == null) {
NotANumber
} else {
validateNumericRange(numberVal)
}
}

private fun validateNumericRange(numberVal: Int): ValidationErrorState {
require(fieldType.isIntegerType)
return if (domain != null && domain is RangeDomain) {
Expand Down Expand Up @@ -340,7 +311,6 @@ internal class FormTextFieldState(
validationErrors.firstOrNull { it != Required && it != NotANumber && it != NotAWholeNumber } ?: NoError
} else {
// if non empty, focused, show any error other than required (the Required error shouldn't be in the list)
check (!validationErrors.contains(Required))
validationErrors.first()
}
} else if (hasBeenFocused) {
Expand All @@ -354,32 +324,58 @@ internal class FormTextFieldState(
}
} else {
// if non empty, unfocused, show any error other than required (the Required error shouldn't be in the list)
check (!validationErrors.contains(Required))
validationErrors.first()
}
} else {
// never been focused
NoError
}

private fun validate(value: String): MutableList<ValidationErrorState> {
private fun validate(value: String): List<ValidationErrorState> {
val errors = validate()
val ret = mutableListOf<ValidationErrorState>()
if (isRequired.value && value.isEmpty()) {
if (errors.any { it is FeatureFormValidationException.RequiredException }) {
ret += Required
}

if (!fieldType.isNumeric) {
val rangeError = validateTextRange(value)
if (rangeError != NoError) {
ret += rangeError
if (errors.any { it is FeatureFormValidationException.MinCharConstraintException }
|| errors.any { it is FeatureFormValidationException.MaxCharConstraintException }
) {
if (minLength > 0 && maxLength > 0) {
if (minLength == maxLength) {
ret += ExactCharConstraint
} else {
ret += MinMaxCharConstraint
}
} else {
ret += MaxCharConstraint
}
}
} else {
val error = validateNumber(value)
if (error != NoError) {
ret += error
if (fieldType.isIntegerType) {
val numberVal = value.toIntOrNull()
if (numberVal == null) {
ret += NotAWholeNumber
} else {
val error = validateNumericRange(numberVal)
if (error != NoError) {
ret += error
}
}
} else {
val numberVal = value.toDoubleOrNull()
if (numberVal == null) {
ret += NotANumber
} else {
val error = validateNumericRange(numberVal)
if (error != NoError) {
ret += error
}
}
}
}

return ret
}

Expand Down Expand Up @@ -425,6 +421,7 @@ internal class FormTextFieldState(
form.editValue(formElement, newValue)
scope.launch { form.evaluateExpressions() }
},
validate = { formElement.getValidationErrors() }
).apply {
// focus is lost on rotation. https://devtopia.esri.com/runtime/apollo/issues/230
hasBeenFocused = list[1] as Boolean
Expand Down Expand Up @@ -459,30 +456,18 @@ internal fun rememberFormTextFieldState(
fieldType = form.fieldType(field),
domain = form.domain(field) as? RangeDomain,
minLength = minLength,
maxLength = maxLength,
maxLength = maxLength
),
scope = scope,
context = context,
onEditValue = {
form.editValue(field, it)
scope.launch { form.evaluateExpressions() }
}
},
validate = { field.getValidationErrors() }
)
}

private sealed class ValidationErrorState {
object NoError: ValidationErrorState()
object Required: ValidationErrorState()
object MinMaxCharConstraint: ValidationErrorState()
object ExactCharConstraint: ValidationErrorState()
object MaxCharConstraint: ValidationErrorState()
object MinNumericConstraint: ValidationErrorState()
object MaxNumericConstraint: ValidationErrorState()
object MinMaxNumericConstraint: ValidationErrorState()
object NotANumber: ValidationErrorState()
object NotAWholeNumber: ValidationErrorState()
}

/**
* Provide a format string for any numeric type.
*
Expand Down