From 3c6bf4765712a74159e774acf705fcd0ddc5e004 Mon Sep 17 00:00:00 2001 From: linusmuema Date: Mon, 12 Jun 2023 06:31:26 +0200 Subject: [PATCH 1/2] - added the formatting classes --- .../screens/survey/SurveyViewmodel.kt | 13 +++-- .../survey/components/PersonalDetails.kt | 4 +- .../java/com/dsc/form_builder/BaseState.kt | 4 +- .../java/com/dsc/form_builder/ChoiceState.kt | 3 +- .../java/com/dsc/form_builder/Extensions.kt | 2 +- .../java/com/dsc/form_builder/FormState.kt | 4 +- .../java/com/dsc/form_builder/SelectState.kt | 4 +- .../com/dsc/form_builder/TextFieldState.kt | 28 ++++++++-- .../java/com/dsc/form_builder/Transform.kt | 5 +- .../java/com/dsc/form_builder/Validators.kt | 7 +-- .../dsc/form_builder/format/CardFormatter.kt | 15 +++++ .../com/dsc/form_builder/format/DateFormat.kt | 56 +++++++++++++++++++ .../com/dsc/form_builder/format/Formatter.kt | 29 ++++++++++ .../com/dsc/form_builder/FormStateTest.kt | 2 +- .../com/dsc/form_builder/FormatterTest.kt | 42 ++++++++++++++ .../form_builder/FormatterTestProviders.kt | 22 ++++++++ .../dsc/form_builder/SelectStateProviders.kt | 2 +- .../com/dsc/form_builder/SelectStateTest.kt | 2 +- .../dsc/form_builder/TextFieldProviders.kt | 2 +- .../dsc/form_builder/TextFieldStateTest.kt | 4 +- 20 files changed, 211 insertions(+), 39 deletions(-) create mode 100644 form-builder/src/main/java/com/dsc/form_builder/format/CardFormatter.kt create mode 100644 form-builder/src/main/java/com/dsc/form_builder/format/DateFormat.kt create mode 100644 form-builder/src/main/java/com/dsc/form_builder/format/Formatter.kt create mode 100644 form-builder/src/test/java/com/dsc/form_builder/FormatterTest.kt create mode 100644 form-builder/src/test/java/com/dsc/form_builder/FormatterTestProviders.kt diff --git a/example/src/main/java/com/dsc/formbuilder/screens/survey/SurveyViewmodel.kt b/example/src/main/java/com/dsc/formbuilder/screens/survey/SurveyViewmodel.kt index 4aca60c..8005b6c 100644 --- a/example/src/main/java/com/dsc/formbuilder/screens/survey/SurveyViewmodel.kt +++ b/example/src/main/java/com/dsc/formbuilder/screens/survey/SurveyViewmodel.kt @@ -5,7 +5,12 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import com.dsc.form_builder.* +import com.dsc.form_builder.BaseState +import com.dsc.form_builder.ChoiceState +import com.dsc.form_builder.FormState +import com.dsc.form_builder.SelectState +import com.dsc.form_builder.TextFieldState +import com.dsc.form_builder.Validators import com.dsc.formbuilder.screens.survey.components.SurveyModel class SurveyViewmodel : ViewModel() { @@ -26,7 +31,7 @@ class SurveyViewmodel : ViewModel() { message = "Username should have more than 4 characters" ), Validators.Required() - ) + ), ), TextFieldState( name = "email", @@ -34,14 +39,14 @@ class SurveyViewmodel : ViewModel() { Validators.Email(), Validators.Required(), ), - transform = { it.trim().lowercase() } + transform = { it.trim().lowercase() }, ), TextFieldState( name = "number", validators = listOf( Validators.Phone(), Validators.Required(), - ) + ), ), SelectState( name = "platform", diff --git a/example/src/main/java/com/dsc/formbuilder/screens/survey/components/PersonalDetails.kt b/example/src/main/java/com/dsc/formbuilder/screens/survey/components/PersonalDetails.kt index be3eb9b..3adf793 100644 --- a/example/src/main/java/com/dsc/formbuilder/screens/survey/components/PersonalDetails.kt +++ b/example/src/main/java/com/dsc/formbuilder/screens/survey/components/PersonalDetails.kt @@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.Arrangement.Center import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.dsc.form_builder.BaseState @@ -59,7 +57,7 @@ fun TextInput(label: String, state: TextFieldState) { errorBorderColor = MaterialTheme.colors.error, focusedBorderColor = MaterialTheme.colors.onPrimary, unfocusedBorderColor = MaterialTheme.colors.onPrimary - ) + ), ) if (state.hasError) { diff --git a/form-builder/src/main/java/com/dsc/form_builder/BaseState.kt b/form-builder/src/main/java/com/dsc/form_builder/BaseState.kt index 0d4d879..b28d575 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/BaseState.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/BaseState.kt @@ -13,8 +13,6 @@ import androidx.compose.runtime.setValue * @param transform this function is used to change the data type in the field state. You can use it to convert the data in the field to your preferred type e.g [String] to [Int] * @param validators this is the list of [Validators] that are used to validate the field state. By default most states will have an empty list. You can override this and provide your own list of validators. * - * @author [Linus Muema](https://github.com/linusmuema) - * @created 05/04/2022 - 10:00 AM */ abstract class BaseState( val initial: T, @@ -76,4 +74,4 @@ abstract class BaseState( this.value = initial this.hideError() } -} \ No newline at end of file +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/ChoiceState.kt b/form-builder/src/main/java/com/dsc/form_builder/ChoiceState.kt index 40bd966..2ec1f2b 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/ChoiceState.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/ChoiceState.kt @@ -15,7 +15,6 @@ package com.dsc.form_builder * * @param validators a list of [Validators] applied to the state's value. * - * @author [Samwel Otieno](https://github.com/otienosamwel) */ class ChoiceState( name: String, @@ -40,4 +39,4 @@ class ChoiceState( } return validations.all { it } } -} \ No newline at end of file +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/Extensions.kt b/form-builder/src/main/java/com/dsc/form_builder/Extensions.kt index 8f1d32f..dc9c851 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/Extensions.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/Extensions.kt @@ -2,4 +2,4 @@ package com.dsc.form_builder fun String.isNumeric(): Boolean { return this.toIntOrNull()?.let { true } ?: false -} \ No newline at end of file +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/FormState.kt b/form-builder/src/main/java/com/dsc/form_builder/FormState.kt index 499e0b0..edb8f98 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/FormState.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/FormState.kt @@ -7,8 +7,6 @@ import kotlin.reflect.KParameter * This class represents the state of the whole form, i.e, the whole collection of fields. It is used to manage all of the states in terms of accessing data and validations. * @param fields this is a list of all fields in the form. We pass them as a parameter to the constructor for ease of management and access. * - * @author [Linus Muema](https://github.com/linusmuema) - * @created 05/04/2022 - 10:00 AM */ open class FormState>(val fields: List) { @@ -56,4 +54,4 @@ open class FormState>(val fields: List) { it.reset() } } -} \ No newline at end of file +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/SelectState.kt b/form-builder/src/main/java/com/dsc/form_builder/SelectState.kt index 8dffc5c..7ee18d7 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/SelectState.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/SelectState.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.* * * @param validators a list of [Validators] applied to the state's value. * - * @author [Samwel Otieno](https://github.com/otienosamwel) */ class SelectState( name: String, @@ -127,5 +126,4 @@ class SelectState( override fun getData(): Any? { return if (transform == null) value.toList() else transform.transform(value) } - -} \ No newline at end of file +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/TextFieldState.kt b/form-builder/src/main/java/com/dsc/form_builder/TextFieldState.kt index f7965d6..d8c6390 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/TextFieldState.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/TextFieldState.kt @@ -1,6 +1,9 @@ package com.dsc.form_builder import androidx.compose.runtime.* +import androidx.compose.ui.text.input.VisualTransformation +import com.dsc.form_builder.format.Formatter +import com.dsc.form_builder.format.toVisualTransformation /** * This class represents the state of a single form field. @@ -10,18 +13,17 @@ import androidx.compose.runtime.* * * @param name The name of the field used to access the state when required in the form * @param initial The initial value/state of the field. By default it is an empty string. + * @param formatter The formatting option for the field. * @param transform The function used to change the [String] data type on the text field to a suitable type e.g [String] to [Int]. * @param validators This is the list of [Validators] that are used to validate the field state. By default the field states will have an empty list. You can override this and provide your own list of validators. * - * @author [Joy Kangangi](https://github.com/joykangangi) - * @created 06/04/2022 - 2:50 p.m. - * */ open class TextFieldState( name: String, initial: String = "", transform: Transform? = null, validators: List = listOf(), + private val formatter: Formatter? = null, ) : BaseState(initial = initial, name = name, transform = transform, validators = validators) { /** @@ -40,9 +42,24 @@ open class TextFieldState( this.value = update } + + /** + * This function is used to get a value transformation for a specified formatter. + * You need to first provide a [Formatter]. As the input value changes, the value is formatted. + */ + fun getTransformation(): VisualTransformation { + checkNotNull(this.formatter) { + """ + Missing formatter in the class. + You need to specify a formatter to use the getFormattedValue function. + """.trimIndent() + } + return formatter.toVisualTransformation() + } + /** - *This function is used to validate all text field inputs by checking against - *the corresponding validator from the list of [validators]. + * This function is used to validate all text field inputs by checking against + * the corresponding validator from the list of [validators]. * The validation checks are functions to validate the field values. * and returns true only if all fields are valid. * It is @@ -206,4 +223,3 @@ open class TextFieldState( return valid } } - diff --git a/form-builder/src/main/java/com/dsc/form_builder/Transform.kt b/form-builder/src/main/java/com/dsc/form_builder/Transform.kt index 330f0e5..38e3e23 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/Transform.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/Transform.kt @@ -2,9 +2,6 @@ package com.dsc.form_builder /** * This interface is used to allow change of data types to a suitable type when necessary. - * - *@author [Joy Kangangi](https://github.com/joykangangi) - * @created 06/04/2022 - 2:35 p.m. */ fun interface Transform { @@ -14,4 +11,4 @@ fun interface Transform { * For example to read 'age' value from a field [TextFieldState], it will be transformed from [String] to [Int]. */ fun transform(value: T): Any? -} \ No newline at end of file +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/Validators.kt b/form-builder/src/main/java/com/dsc/form_builder/Validators.kt index a01a112..c24963f 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/Validators.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/Validators.kt @@ -10,9 +10,6 @@ private const val CARD_NUMBER_MESSAGE = "Invalid card number" * * These are the types of validators available in the form builder library. * They all have the _message_ parameter to allow the developer to set their own custom error message. - * - * @author [Linus Muema](https://github.com/linusmuema) - * @created 05/04/2022 - 10:00 AM */ sealed interface Validators { @@ -80,7 +77,7 @@ sealed interface Validators { * Example: check if a string contains the word hello * ```kt * Validators.Custom( - * message = "value must have hello" + * message = "value must have hello", * function = { it.contains("hello") } * ) * ``` @@ -92,4 +89,4 @@ sealed interface Validators { * @param message the error message to display if the validation fails. */ class Custom(var message: String, var function: (value: Any) -> Boolean) : Validators -} \ No newline at end of file +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/format/CardFormatter.kt b/form-builder/src/main/java/com/dsc/form_builder/format/CardFormatter.kt new file mode 100644 index 0000000..518d168 --- /dev/null +++ b/form-builder/src/main/java/com/dsc/form_builder/format/CardFormatter.kt @@ -0,0 +1,15 @@ +package com.dsc.form_builder.format + +/** + * + * This formatter is used for credit/debit card numbers. Can also be used for IBAN numbers. + * It groups the input value into chunks of four characters. + * The separator is an empty space as this is the most common option. + * + * Note: character limiting is not supported in the formatter. + */ +object CardFormatter: Formatter { + override fun format(value: String): String { + return value.replace(" ", "").chunked(4).joinToString(" ") + } +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/format/DateFormat.kt b/form-builder/src/main/java/com/dsc/form_builder/format/DateFormat.kt new file mode 100644 index 0000000..a5b5314 --- /dev/null +++ b/form-builder/src/main/java/com/dsc/form_builder/format/DateFormat.kt @@ -0,0 +1,56 @@ +package com.dsc.form_builder.format + +/** + * These are the formatting options for the [DateFormatter] class. + */ +enum class DateFormat { + DDMMYYYY, + MMDDYYYY, + YYYYDDMM, + DDMMYY, + MMDDYY, + YYMMDD, +} + +// Get the index where to place the separator +private fun DateFormat.separatorIndices(): MutableList { + val indices = mutableListOf() + val stringFormat = this.toString() + stringFormat.forEachIndexed { index, char -> + if (index > 0){ + val prev = stringFormat[index-1] + + if (prev != char) { + if (indices.isNotEmpty()) indices.add(index+1) + else indices.add(index) + } + } + } + return indices +} + + +/** + * + * This formatter is used for date inputs. You need to specify a [DateFormat] and a separator. + * The formatting function places the separator in the respective index as the user types. + * + * Note: character limiting is not supported in the formatter. + */ + +class DateFormatter(private val dateFormat: DateFormat, private val separator: String): Formatter { + override fun format(value: String): String { + var formatted = value + val indices = dateFormat.separatorIndices() + + // add first separator if user has exceeded that index + val first = indices.first() + if (value.length > first) formatted = formatted.replaceRange(first, first, separator) + + // add last separator if user has exceeded that index + val last = indices.last() + if (value.length >= last) formatted = formatted.replaceRange(last, last, separator) + + return formatted + } +} diff --git a/form-builder/src/main/java/com/dsc/form_builder/format/Formatter.kt b/form-builder/src/main/java/com/dsc/form_builder/format/Formatter.kt new file mode 100644 index 0000000..3054854 --- /dev/null +++ b/form-builder/src/main/java/com/dsc/form_builder/format/Formatter.kt @@ -0,0 +1,29 @@ +package com.dsc.form_builder.format + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + + +/** + * + * These are the formatting interface for the [TextFieldState]. + * You can get the visual transformation to apply in your text input. + */ +interface Formatter { + fun format(value: String): String +} + +internal fun Formatter.toVisualTransformation(): VisualTransformation { + return VisualTransformation { + val output = format(it.text) + TransformedText( + AnnotatedString(output), + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int = output.length + override fun transformedToOriginal(offset: Int): Int = it.text.length + } + ) + } +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt b/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt index e545594..f95a377 100644 --- a/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/FormStateTest.kt @@ -39,4 +39,4 @@ internal class FormStateTest { assert(ageState.value == "34" && !ageState.hasError) } } -} \ No newline at end of file +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/FormatterTest.kt b/form-builder/src/test/java/com/dsc/form_builder/FormatterTest.kt new file mode 100644 index 0000000..98190c2 --- /dev/null +++ b/form-builder/src/test/java/com/dsc/form_builder/FormatterTest.kt @@ -0,0 +1,42 @@ +package com.dsc.form_builder + +import com.dsc.form_builder.format.CardFormatter +import com.dsc.form_builder.format.DateFormat +import com.dsc.form_builder.format.DateFormatter +import com.dsc.form_builder.format.Formatter +import org.junit.jupiter.api.Nested +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +internal class FormatterTest { + + @Nested + inner class DescribingFormatting { + + @ParameterizedTest + @ArgumentsSource(CreditCardFormatterProvider::class) + fun `credit card numbers are formatted correctly`(input: String, expected: String){ + // Given a formatting class + val classToTest: Formatter = CardFormatter + + // When formatting is applied + val formatted = classToTest.format(input) + + // then the value should be formatted correctly + assert(formatted == expected) + } + + @ParameterizedTest + @ArgumentsSource(DateFormatterProvider::class) + fun `dater inputs are formatted correctly`(format: DateFormat, separator: String, input: String, expected: String){ + // Given a formatting class + val classToTest: Formatter = DateFormatter(dateFormat = format, separator = separator) + + // When formatting is applied + val formatted = classToTest.format(input) + + // then the value should be formatted correctly + assert(formatted == expected) + } + } +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/FormatterTestProviders.kt b/form-builder/src/test/java/com/dsc/form_builder/FormatterTestProviders.kt new file mode 100644 index 0000000..6f59718 --- /dev/null +++ b/form-builder/src/test/java/com/dsc/form_builder/FormatterTestProviders.kt @@ -0,0 +1,22 @@ +package com.dsc.form_builder + +import com.dsc.form_builder.format.DateFormat +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +object CreditCardFormatterProvider: ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + Arguments.of("40128888888818", "4012 8888 8888 18"), + Arguments.of("5555555555554444", "5555 5555 5555 4444"), + Arguments.of("DE89370400440532013000", "DE89 3704 0044 0532 0130 00") + ) +} + +object DateFormatterProvider: ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream = Stream.of( + Arguments.of(DateFormat.DDMMYY, ".", "010101", "01.01.01"), + Arguments.of(DateFormat.DDMMYYYY, "-", "01012001", "01-01-2001") + ) +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/SelectStateProviders.kt b/form-builder/src/test/java/com/dsc/form_builder/SelectStateProviders.kt index 0905451..e0ae4f4 100644 --- a/form-builder/src/test/java/com/dsc/form_builder/SelectStateProviders.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/SelectStateProviders.kt @@ -19,4 +19,4 @@ object MaxArgumentsProvider: ArgumentsProvider { Arguments.of(mutableListOf("item 1", "item 2"), 2, true), Arguments.of(mutableListOf("item 1", "item 2", "item 3"), 2, false), ) -} \ No newline at end of file +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/SelectStateTest.kt b/form-builder/src/test/java/com/dsc/form_builder/SelectStateTest.kt index fef7037..7382f25 100644 --- a/form-builder/src/test/java/com/dsc/form_builder/SelectStateTest.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/SelectStateTest.kt @@ -69,4 +69,4 @@ internal class SelectStateTest { assert(actual == expected) } } -} \ No newline at end of file +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/TextFieldProviders.kt b/form-builder/src/test/java/com/dsc/form_builder/TextFieldProviders.kt index d4bcea1..cb0b323 100644 --- a/form-builder/src/test/java/com/dsc/form_builder/TextFieldProviders.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/TextFieldProviders.kt @@ -75,4 +75,4 @@ object MaxValueArgumentsProvider : ArgumentsProvider { Arguments.of("20", 15, false), Arguments.of("test", 15, false), ) -} \ No newline at end of file +} diff --git a/form-builder/src/test/java/com/dsc/form_builder/TextFieldStateTest.kt b/form-builder/src/test/java/com/dsc/form_builder/TextFieldStateTest.kt index aba835d..61bafa4 100644 --- a/form-builder/src/test/java/com/dsc/form_builder/TextFieldStateTest.kt +++ b/form-builder/src/test/java/com/dsc/form_builder/TextFieldStateTest.kt @@ -1,5 +1,7 @@ package com.dsc.form_builder +import com.dsc.form_builder.format.CardFormatter +import com.dsc.form_builder.format.Formatter import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -125,4 +127,4 @@ internal class TextFieldStateTest { assert(actual == expected) } } -} \ No newline at end of file +} From e292fe72ab71fe1aefced7b1c7169fb2b6a4b10e Mon Sep 17 00:00:00 2001 From: linusmuema Date: Mon, 12 Jun 2023 09:09:51 +0200 Subject: [PATCH 2/2] - remove unnecessary empty string replacement --- .../src/main/java/com/dsc/form_builder/format/CardFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/form-builder/src/main/java/com/dsc/form_builder/format/CardFormatter.kt b/form-builder/src/main/java/com/dsc/form_builder/format/CardFormatter.kt index 518d168..839672a 100644 --- a/form-builder/src/main/java/com/dsc/form_builder/format/CardFormatter.kt +++ b/form-builder/src/main/java/com/dsc/form_builder/format/CardFormatter.kt @@ -10,6 +10,6 @@ package com.dsc.form_builder.format */ object CardFormatter: Formatter { override fun format(value: String): String { - return value.replace(" ", "").chunked(4).joinToString(" ") + return value.chunked(4).joinToString(" ") } }