Skip to content

Commit

Permalink
Merge pull request #66 from jkuatdsc/form-inputs
Browse files Browse the repository at this point in the history
Form inputs
  • Loading branch information
joykangangi authored Sep 1, 2024
2 parents 218a2cc + 9885933 commit 9115d66
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ 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.form_builder.format.CardFormatter
import com.dsc.form_builder.format.DateFormat
import com.dsc.form_builder.format.DateFormatter
import com.dsc.formbuilder.screens.survey.components.SurveyModel

class SurveyViewmodel : ViewModel() {
Expand All @@ -24,27 +27,26 @@ class SurveyViewmodel : ViewModel() {
val formState: FormState<BaseState<*>> = FormState(
fields = listOf(
TextFieldState(
name = "username",
name = "email",
validators = listOf(
Validators.Min(
limit = 4,
message = "Username should have more than 4 characters"
),
Validators.Required()
Validators.Email(),
Validators.Required(),
),
transform = { it.trim().lowercase() },
),
TextFieldState(
name = "email",
name = "card",
formatter = CardFormatter,
validators = listOf(
Validators.Email(),
Validators.CardNumber(),
Validators.Required(),
),
transform = { it.trim().lowercase() },
),
TextFieldState(
name = "number",
name = "date",
formatter = DateFormatter(dateFormat = DateFormat.DDMMYYYY, separator = "/"),
validators = listOf(
Validators.Phone(),
Validators.Date(format = DateFormat.DDMMYYYY),
Validators.Required(),
),
),
Expand Down Expand Up @@ -105,7 +107,7 @@ class SurveyViewmodel : ViewModel() {

fun validateSurvey() {
val pages: List<List<Int>> = (0..5).chunked(3)
if (!formState.validate()){
if (!formState.validate()) {
val position = formState.fields.indexOfFirst { it.hasError }
_screen.value = pages.indexOfFirst { it.contains(position) }
} else {
Expand All @@ -116,8 +118,9 @@ class SurveyViewmodel : ViewModel() {

fun validateScreen(screen: Int) {
val fields: List<BaseState<*>> = formState.fields.chunked(3)[screen]
if (fields.map { it.validate() }.all { it }){ // map is used so we can execute validate() on all fields in that screen
if (screen == 2){
if (fields.map { it.validate() }
.all { it }) { // map is used so we can execute validate() on all fields in that screen
if (screen == 2) {
logData()
_finish.value = true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package com.dsc.formbuilder.screens.survey.components

import android.opengl.Matrix
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
package com.dsc.formbuilder.screens.survey.components

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Center
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.dsc.form_builder.BaseState
import com.dsc.form_builder.FormState
import com.dsc.form_builder.TextFieldState
import com.dsc.form_builder.format.CardFormatter
import com.dsc.form_builder.format.DateFormat
import com.dsc.form_builder.format.DateFormatter
import com.dsc.formbuilder.theme.FormBuilderTheme

@Composable
fun PersonalDetails(formState: FormState<BaseState<*>>) {
val usernameState: TextFieldState = formState.getState("username")
val emailState: TextFieldState = formState.getState("email")
val numberState: TextFieldState = formState.getState("number")
val cardState: TextFieldState = formState.getState("card")
val dateState: TextFieldState = formState.getState("date")

Column(verticalArrangement = Center, horizontalAlignment = CenterHorizontally) {
Column(
verticalArrangement = Center,
horizontalAlignment = CenterHorizontally
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Personal Details",
Expand All @@ -29,21 +44,33 @@ fun PersonalDetails(formState: FormState<BaseState<*>>) {

Spacer(modifier = Modifier.height(30.dp))

TextInput(label = "Username", state = usernameState)
TextInput(label = "Email", state = emailState)

Spacer(modifier = Modifier.height(10.dp))

TextInput(label = "Email", state = emailState)
TextInput(
label = "Card Number",
state = cardState,
visualTransformation = cardState.getTransformation()
)

Spacer(modifier = Modifier.height(10.dp))

TextInput(label = "Number", state = numberState)
TextInput(
label = "Date of birth",
state = dateState,
visualTransformation = dateState.getTransformation()
)

}
}

@Composable
fun TextInput(label: String, state: TextFieldState) {
fun TextInput(
label: String,
state: TextFieldState,
visualTransformation: VisualTransformation = VisualTransformation.None,
) {

Column {
OutlinedTextField(
Expand All @@ -58,6 +85,8 @@ fun TextInput(label: String, state: TextFieldState) {
focusedBorderColor = MaterialTheme.colors.onPrimary,
unfocusedBorderColor = MaterialTheme.colors.onPrimary
),
singleLine = true,
visualTransformation = visualTransformation,
)

if (state.hasError) {
Expand All @@ -78,9 +107,13 @@ fun TextInput(label: String, state: TextFieldState) {
fun PersonalDetailsPreview() {
val formState: FormState<BaseState<*>> = FormState(
listOf(
TextFieldState("username"),
TextFieldState("email"),
TextFieldState("number")
TextFieldState("phone"),
TextFieldState(
"date",
formatter = DateFormatter(dateFormat = DateFormat.DDMMYYYY, separator = "/"),
),
TextFieldState("card", formatter = CardFormatter),
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.dsc.formbuilder.screens.survey.components
data class SurveyModel(
// First page
val email: String,
val number: String,
val username: String,
val card: String,
val date: String,

// Second page
val ide: List<String>,
Expand Down
4 changes: 4 additions & 0 deletions form-builder/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}

kotlinOptions {
Expand Down Expand Up @@ -58,6 +59,9 @@ dependencies {

// Kotlin reflection
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

// For using modern java 8 classes with older versions of android
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.2.2"
}


Expand Down
36 changes: 34 additions & 2 deletions form-builder/src/main/java/com/dsc/form_builder/TextFieldState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ package com.dsc.form_builder

import androidx.compose.runtime.*
import androidx.compose.ui.text.input.VisualTransformation
import com.dsc.form_builder.format.DateFormat
import com.dsc.form_builder.format.DateFormatter
import com.dsc.form_builder.format.Formatter
import com.dsc.form_builder.format.toVisualTransformation
import java.time.DateTimeException
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.ResolverStyle

/**
* This class represents the state of a single form field.
Expand All @@ -24,7 +30,12 @@ open class TextFieldState(
transform: Transform<String>? = null,
validators: List<Validators> = listOf(),
private val formatter: Formatter? = null,
) : BaseState<String>(initial = initial, name = name, transform = transform, validators = validators) {
) : BaseState<String>(
initial = initial,
name = name,
transform = transform,
validators = validators
) {

/**
* A mutable value holder that reads to the initial parameter during the execution of a [Composable]
Expand Down Expand Up @@ -79,6 +90,7 @@ open class TextFieldState(
is Validators.Custom -> validateCustom(it.function, it.message)
is Validators.MinValue -> validateMinValue(it.limit, it.message)
is Validators.MaxValue -> validateMaxValue(it.limit, it.message)
is Validators.Date -> validateDate(it.message, it.format)
}
}
return validations.all { it }
Expand Down Expand Up @@ -152,7 +164,27 @@ open class TextFieldState(
checksum += if (n > 9) n - 9 else n
}

val valid = checksum%10 == 0
val valid = checksum % 10 == 0
if (!valid) showError(message)
return valid
}

/**
* This function validates a Date in [value].
* It will return true if the string value is a valid date.
* This function makes use of the [java.time.format.DateTimeFormatter] and [java.time.LocalDate] to verify the validity of the date.
* @param message the error message passed to [showError] to display if the value is not a valid date. By default we use the [DATE_MESSAGE] constant.
* @param dateFormat the format pattern that specifies the expected format of the date [value] string.
*/
internal fun validateDate(message: String, dateFormat: DateFormat): Boolean {
val formatter =
DateTimeFormatter.ofPattern(dateFormat.pattern).withResolverStyle(ResolverStyle.STRICT)
val valid = try {
LocalDate.parse(value, formatter)
true
} catch (e: DateTimeException) {
false
}
if (!valid) showError(message)
return valid
}
Expand Down
10 changes: 10 additions & 0 deletions form-builder/src/main/java/com/dsc/form_builder/Validators.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.dsc.form_builder

import com.dsc.form_builder.format.DateFormat

private const val EMAIL_MESSAGE = "Invalid email address"
private const val REQUIRED_MESSAGE = "This field is required"
private const val PHONE_MESSAGE = "Invalid phone number"
private const val WEB_URL_MESSAGE = "Invalid web url"
private const val CARD_NUMBER_MESSAGE = "Invalid card number"
private const val DATE_MESSAGE = "Invalid date"

/**
*
Expand Down Expand Up @@ -51,6 +54,13 @@ sealed interface Validators {
*/
class CardNumber(var message: String = CARD_NUMBER_MESSAGE) : Validators

/**
* This is a date validator. It will return true if the string value is a valid date.
* @param message the error message to display if the value is not a valid date. By default we use the [DATE_MESSAGE] constant.
* @param format the pattern that specifies the expected format of the date string.
*/
class Date(var message: String = DATE_MESSAGE, var format: DateFormat) : Validators

/**
* This validator is used to check for numeric values. It will return true is the value is numeric and is greater than or equal to the specified limit.
* @param limit the minimum value that the value can hold.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ package com.dsc.form_builder.format
/**
* These are the formatting options for the [DateFormatter] class.
*/
enum class DateFormat {
DDMMYYYY,
MMDDYYYY,
YYYYDDMM,
DDMMYY,
MMDDYY,
YYMMDD,
enum class DateFormat(val pattern: String) {
DDMMYYYY("ddMMuuuu"),
MMDDYYYY("MMdduuuu"),
YYYYDDMM("uuuuddMM"),
DDMMYY("ddMMuu"),
MMDDYY("MMdduu"),
YYMMDD("uuMMdd")
}

// 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 (index > 0) {
val prev = stringFormat[index - 1]

if (prev != char) {
if (indices.isNotEmpty()) indices.add(index+1)
if (indices.isNotEmpty()) indices.add(index + 1)
else indices.add(index)
}
}
Expand All @@ -38,7 +38,7 @@ private fun DateFormat.separatorIndices(): MutableList<Int> {
* Note: character limiting is not supported in the formatter.
*/

class DateFormatter(private val dateFormat: DateFormat, private val separator: String): 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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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
Expand Down Expand Up @@ -78,3 +79,14 @@ object MaxValueArgumentsProvider : ArgumentsProvider {
Arguments.of("test", 15, false),
)
}

object DateArgumentsProvider : ArgumentsProvider {
override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of(
Arguments.of("12122012", DateFormat.DDMMYYYY, true),
Arguments.of("06312023", DateFormat.MMDDYYYY, false),
Arguments.of("20231504", DateFormat.YYYYDDMM, true),
Arguments.of("290213", DateFormat.DDMMYY, false), // Non Leap Year
Arguments.of("121323", DateFormat.MMDDYY, true),
Arguments.of("070626", DateFormat.YYMMDD, true)
)
}
Loading

0 comments on commit 9115d66

Please sign in to comment.