Skip to content

Commit ba35112

Browse files
sorenoidSoren Roth
and
Soren Roth
authored
validation via the SDK (#267)
* expect a formatted value to be UTC * make BaseFieldState generic * make DateTimeFieldState vary with Instant? make DateTimeField work with Instant, not Long. * first pass at consuming core validation errors * factor out Validation States to a common file * remove merge detritus * update SDK. * cleanup * remove unused import. simplify syntax. --------- Co-authored-by: Soren Roth <[email protected]>
1 parent 8b7cd8e commit ba35112

File tree

5 files changed

+90
-77
lines changed

5 files changed

+90
-77
lines changed

gradle.properties

+2-4
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,5 @@ buildNumber=0000-snapshot
5454
ignoreBuildNumber=false
5555
# these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency
5656
# and are generally not overridden at the command line unless a special build is requested.
57-
sdkVersionNumber=200.3.0
58-
sdkBuildNumber=4051
59-
60-
57+
sdkVersionNumber=200.4.0
58+
sdkBuildNumber=4094

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/base/BaseFieldState.kt

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ internal open class BaseFieldState<T>(
5353
initialValue: T = properties.value.value,
5454
scope: CoroutineScope,
5555
protected val onEditValue: (Any?) -> Unit,
56+
val validate: () -> List<Throwable> = {listOf()}
5657
) : FormElementState(
5758
label = properties.label,
5859
description = properties.description,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* COPYRIGHT 1995-2023 ESRI
3+
*
4+
* TRADE SECRETS: ESRI PROPRIETARY AND CONFIDENTIAL
5+
* Unpublished material - all rights reserved under the
6+
* Copyright Laws of the United States.
7+
*
8+
* For additional information, contact:
9+
* Environmental Systems Research Institute, Inc.
10+
* Attn: Contracts Dept
11+
* 380 New York Street
12+
* Redlands, California, USA 92373
13+
*
14+
15+
*/
16+
17+
package com.arcgismaps.toolkit.featureforms.components.base
18+
19+
internal sealed class ValidationErrorState {
20+
object NoError: ValidationErrorState()
21+
object Required: ValidationErrorState()
22+
object MinMaxCharConstraint: ValidationErrorState()
23+
object ExactCharConstraint: ValidationErrorState()
24+
object MaxCharConstraint: ValidationErrorState()
25+
object MinNumericConstraint: ValidationErrorState()
26+
object MaxNumericConstraint: ValidationErrorState()
27+
object MinMaxNumericConstraint: ValidationErrorState()
28+
object NotANumber: ValidationErrorState()
29+
object NotAWholeNumber: ValidationErrorState()
30+
}

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/datetime/DateTimeFieldState.kt

-1
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,3 @@ internal fun rememberDateTimeFieldState(
148148
}
149149
)
150150
}
151-

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/text/FormTextFieldState.kt

+57-72
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,26 @@ import androidx.compose.runtime.saveable.rememberSaveable
2828
import com.arcgismaps.data.Domain
2929
import com.arcgismaps.data.FieldType
3030
import com.arcgismaps.data.RangeDomain
31+
import com.arcgismaps.exceptions.FeatureFormValidationException
3132
import com.arcgismaps.mapping.featureforms.FeatureForm
3233
import com.arcgismaps.mapping.featureforms.FieldFormElement
3334
import com.arcgismaps.mapping.featureforms.TextAreaFormInput
3435
import com.arcgismaps.mapping.featureforms.TextBoxFormInput
3536
import com.arcgismaps.toolkit.featureforms.R
3637
import com.arcgismaps.toolkit.featureforms.components.base.BaseFieldState
3738
import com.arcgismaps.toolkit.featureforms.components.base.FieldProperties
39+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState
40+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.ExactCharConstraint
41+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MaxCharConstraint
42+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MaxNumericConstraint
43+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MinMaxCharConstraint
44+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MinMaxNumericConstraint
45+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.MinNumericConstraint
46+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.NoError
47+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.NotANumber
48+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.NotAWholeNumber
49+
import com.arcgismaps.toolkit.featureforms.components.base.ValidationErrorState.Required
3850
import com.arcgismaps.toolkit.featureforms.components.formelement.FieldElement
39-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.ExactCharConstraint
40-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MaxCharConstraint
41-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MaxNumericConstraint
42-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MinMaxCharConstraint
43-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MinMaxNumericConstraint
44-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.MinNumericConstraint
45-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.NoError
46-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.NotANumber
47-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.NotAWholeNumber
48-
import com.arcgismaps.toolkit.featureforms.components.text.ValidationErrorState.Required
4951
import com.arcgismaps.toolkit.featureforms.utils.asDoubleTuple
5052
import com.arcgismaps.toolkit.featureforms.utils.asLongTuple
5153
import com.arcgismaps.toolkit.featureforms.utils.domain
@@ -95,12 +97,14 @@ internal class FormTextFieldState(
9597
initialValue: String = properties.value.value,
9698
scope: CoroutineScope,
9799
private val context: Context,
98-
onEditValue: (Any?) -> Unit
100+
onEditValue: (Any?) -> Unit,
101+
validate: () -> List<Throwable>
99102
) : BaseFieldState<String>(
100103
properties = properties,
101104
initialValue = initialValue,
102105
scope = scope,
103-
onEditValue = onEditValue
106+
onEditValue = onEditValue,
107+
validate = validate
104108
) {
105109
// indicates singleLine only if TextBoxFeatureFormInput
106110
val singleLine = properties.singleLine
@@ -125,7 +129,6 @@ internal class FormTextFieldState(
125129
private val _hasError = mutableStateOf(false)
126130
private val _supportingTextIsErrorMessage = mutableStateOf(false)
127131
val supportingTextIsErrorMessage: State<Boolean> = _supportingTextIsErrorMessage
128-
129132
/**
130133
* The domain of the element's field.
131134
*/
@@ -236,38 +239,6 @@ internal class FormTextFieldState(
236239
_hasError.value = errors.isNotEmpty()
237240
}
238241

239-
private fun validateTextRange(value: String): ValidationErrorState =
240-
if (value.length !in minLength..maxLength) {
241-
if (minLength > 0 && maxLength > 0) {
242-
if (minLength == maxLength) {
243-
ExactCharConstraint
244-
} else {
245-
MinMaxCharConstraint
246-
}
247-
} else {
248-
MaxCharConstraint
249-
}
250-
} else {
251-
NoError
252-
}
253-
254-
private fun validateNumber(value: String): ValidationErrorState =
255-
if (fieldType.isIntegerType) {
256-
val numberVal = value.toIntOrNull()
257-
if (numberVal == null) {
258-
NotAWholeNumber
259-
} else {
260-
validateNumericRange(numberVal)
261-
}
262-
} else {
263-
val numberVal = value.toDoubleOrNull()
264-
if (numberVal == null) {
265-
NotANumber
266-
} else {
267-
validateNumericRange(numberVal)
268-
}
269-
}
270-
271242
private fun validateNumericRange(numberVal: Int): ValidationErrorState {
272243
require(fieldType.isIntegerType)
273244
return if (domain != null && domain is RangeDomain) {
@@ -340,7 +311,6 @@ internal class FormTextFieldState(
340311
validationErrors.firstOrNull { it != Required && it != NotANumber && it != NotAWholeNumber } ?: NoError
341312
} else {
342313
// if non empty, focused, show any error other than required (the Required error shouldn't be in the list)
343-
check (!validationErrors.contains(Required))
344314
validationErrors.first()
345315
}
346316
} else if (hasBeenFocused) {
@@ -354,32 +324,58 @@ internal class FormTextFieldState(
354324
}
355325
} else {
356326
// if non empty, unfocused, show any error other than required (the Required error shouldn't be in the list)
357-
check (!validationErrors.contains(Required))
358327
validationErrors.first()
359328
}
360329
} else {
361330
// never been focused
362331
NoError
363332
}
364333

365-
private fun validate(value: String): MutableList<ValidationErrorState> {
334+
private fun validate(value: String): List<ValidationErrorState> {
335+
val errors = validate()
366336
val ret = mutableListOf<ValidationErrorState>()
367-
if (isRequired.value && value.isEmpty()) {
337+
if (errors.any { it is FeatureFormValidationException.RequiredException }) {
368338
ret += Required
369339
}
370-
340+
371341
if (!fieldType.isNumeric) {
372-
val rangeError = validateTextRange(value)
373-
if (rangeError != NoError) {
374-
ret += rangeError
342+
if (errors.any { it is FeatureFormValidationException.MinCharConstraintException }
343+
|| errors.any { it is FeatureFormValidationException.MaxCharConstraintException }
344+
) {
345+
if (minLength > 0 && maxLength > 0) {
346+
if (minLength == maxLength) {
347+
ret += ExactCharConstraint
348+
} else {
349+
ret += MinMaxCharConstraint
350+
}
351+
} else {
352+
ret += MaxCharConstraint
353+
}
375354
}
376355
} else {
377-
val error = validateNumber(value)
378-
if (error != NoError) {
379-
ret += error
356+
if (fieldType.isIntegerType) {
357+
val numberVal = value.toIntOrNull()
358+
if (numberVal == null) {
359+
ret += NotAWholeNumber
360+
} else {
361+
val error = validateNumericRange(numberVal)
362+
if (error != NoError) {
363+
ret += error
364+
}
365+
}
366+
} else {
367+
val numberVal = value.toDoubleOrNull()
368+
if (numberVal == null) {
369+
ret += NotANumber
370+
} else {
371+
val error = validateNumericRange(numberVal)
372+
if (error != NoError) {
373+
ret += error
374+
}
375+
}
380376
}
381377
}
382-
378+
383379
return ret
384380
}
385381

@@ -425,6 +421,7 @@ internal class FormTextFieldState(
425421
form.editValue(formElement, newValue)
426422
scope.launch { form.evaluateExpressions() }
427423
},
424+
validate = { formElement.getValidationErrors() }
428425
).apply {
429426
// focus is lost on rotation. https://devtopia.esri.com/runtime/apollo/issues/230
430427
hasBeenFocused = list[1] as Boolean
@@ -459,30 +456,18 @@ internal fun rememberFormTextFieldState(
459456
fieldType = form.fieldType(field),
460457
domain = form.domain(field) as? RangeDomain,
461458
minLength = minLength,
462-
maxLength = maxLength,
459+
maxLength = maxLength
463460
),
464461
scope = scope,
465462
context = context,
466463
onEditValue = {
467464
form.editValue(field, it)
468465
scope.launch { form.evaluateExpressions() }
469-
}
466+
},
467+
validate = { field.getValidationErrors() }
470468
)
471469
}
472470

473-
private sealed class ValidationErrorState {
474-
object NoError: ValidationErrorState()
475-
object Required: ValidationErrorState()
476-
object MinMaxCharConstraint: ValidationErrorState()
477-
object ExactCharConstraint: ValidationErrorState()
478-
object MaxCharConstraint: ValidationErrorState()
479-
object MinNumericConstraint: ValidationErrorState()
480-
object MaxNumericConstraint: ValidationErrorState()
481-
object MinMaxNumericConstraint: ValidationErrorState()
482-
object NotANumber: ValidationErrorState()
483-
object NotAWholeNumber: ValidationErrorState()
484-
}
485-
486471
/**
487472
* Provide a format string for any numeric type.
488473
*

0 commit comments

Comments
 (0)