Skip to content

Commit

Permalink
Merge pull request #65 from jkuatdsc/feature/card-number-formatting
Browse files Browse the repository at this point in the history
Formatting feature
  • Loading branch information
joykangangi authored Jun 12, 2023
2 parents dcde3c4 + e292fe7 commit 218a2cc
Show file tree
Hide file tree
Showing 20 changed files with 211 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -26,22 +31,22 @@ class SurveyViewmodel : ViewModel() {
message = "Username should have more than 4 characters"
),
Validators.Required()
)
),
),
TextFieldState(
name = "email",
validators = listOf(
Validators.Email(),
Validators.Required(),
),
transform = { it.trim().lowercase() }
transform = { it.trim().lowercase() },
),
TextFieldState(
name = "number",
validators = listOf(
Validators.Phone(),
Validators.Required(),
)
),
),
SelectState(
name = "platform",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 1 addition & 3 deletions form-builder/src/main/java/com/dsc/form_builder/BaseState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
val initial: T,
Expand Down Expand Up @@ -76,4 +74,4 @@ abstract class BaseState<T>(
this.value = initial
this.hideError()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -40,4 +39,4 @@ class ChoiceState(
}
return validations.all { it }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package com.dsc.form_builder

fun String.isNumeric(): Boolean {
return this.toIntOrNull()?.let { true } ?: false
}
}
4 changes: 1 addition & 3 deletions form-builder/src/main/java/com/dsc/form_builder/FormState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<T : BaseState<*>>(val fields: List<T>) {

Expand Down Expand Up @@ -56,4 +54,4 @@ open class FormState<T : BaseState<*>>(val fields: List<T>) {
it.reset()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -127,5 +126,4 @@ class SelectState(
override fun getData(): Any? {
return if (transform == null) value.toList() else transform.transform(value)
}

}
}
28 changes: 22 additions & 6 deletions form-builder/src/main/java/com/dsc/form_builder/TextFieldState.kt
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<String>? = null,
validators: List<Validators> = listOf(),
private val formatter: Formatter? = null,
) : BaseState<String>(initial = initial, name = name, transform = transform, validators = validators) {

/**
Expand All @@ -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
Expand Down Expand Up @@ -200,4 +217,3 @@ open class TextFieldState(
return valid
}
}

5 changes: 1 addition & 4 deletions form-builder/src/main/java/com/dsc/form_builder/Transform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Expand All @@ -14,4 +11,4 @@ fun interface Transform<T> {
* For example to read 'age' value from a field [TextFieldState], it will be transformed from [String] to [Int].
*/
fun transform(value: T): Any?
}
}
7 changes: 2 additions & 5 deletions form-builder/src/main/java/com/dsc/form_builder/Validators.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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") }
* )
* ```
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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.chunked(4).joinToString(" ")
}
}
Original file line number Diff line number Diff line change
@@ -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<Int> {
val indices = mutableListOf<Int>()
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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ internal class FormStateTest {
assert(ageState.value == "34" && !ageState.hasError)
}
}
}
}
42 changes: 42 additions & 0 deletions form-builder/src/test/java/com/dsc/form_builder/FormatterTest.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading

0 comments on commit 218a2cc

Please sign in to comment.