Skip to content

Commit

Permalink
- Updated deps
Browse files Browse the repository at this point in the history
- Build against SDK35
- Support passing in state for validation
- Cleaned up some rules
- Added secondary constructors for when embedded in a model
- Updated README.MD
  • Loading branch information
chrisjenx committed Jan 9, 2025
1 parent 8911972 commit 922e011
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 36 deletions.
4 changes: 3 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

`TextField` validation is a pain, hopefully this is a bit easier

![](sample/assets/YakcovDemo.gif)

```kotlin
val emailValidator = TextFieldValueValidator(
val emailValidator = rememberTextFieldValueValidator(
rules = listOf(Required, Email)
)
with(emailValidator) {
Expand Down
20 changes: 10 additions & 10 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
[versions]

kotlin = "2.0.20"
compose = "1.7.0"
agp = "8.7.1"
androidx-activityCompose = "1.9.2"
androidx-uiTest = "1.7.3"
kotlin = "2.1.0"
compose = "1.7.3"
agp = "8.7.3"
androidx-activityCompose = "1.9.3"
androidx-uiTest = "1.7.6"
kotlinx-datetime = "0.6.1"
libphonenumberJvm = "8.13.47"
libphonenumberJvm = "8.13.52"
libphonenumberAndroid = "8.13.35"
coreKtx = "1.13.1"
coreKtx = "1.15.0"
junit = "4.13.2"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.6"
lifecycleRuntimeKtx = "2.8.7"
# https://developer.android.com/develop/ui/compose/bom/bom-mapping
composeBom = "2024.09.03"
composeBom = "2024.12.01"
startupRuntime = "1.2.0"

[libraries]
Expand Down Expand Up @@ -43,4 +43,4 @@ compose = { id = "org.jetbrains.compose", version.ref = "compose" }
android-library = { id = "com.android.library", version.ref = "agp" }
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
maven-publish = { id = "com.vanniktech.maven.publish", version = "0.29.0" }
maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
4 changes: 2 additions & 2 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ kotlin {

android {
namespace = "com.chrisjenx.yakcov"
compileSdk = 34
compileSdk = 35

defaultConfig {
minSdk = 23
Expand All @@ -141,7 +141,7 @@ android {
//https://developer.android.com/studio/test/gradle-managed-devices
@Suppress("UnstableApiUsage")
testOptions {
targetSdk = 34
targetSdk = 35
unitTests {
isIncludeAndroidResources = true
}
Expand Down
23 changes: 23 additions & 0 deletions library/src/commonMain/kotlin/com/chrisjenx/yakcov/States.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.chrisjenx.yakcov

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import kotlinx.datetime.LocalDate

@Immutable
internal class ImmutableBooleanState(override val value: Boolean) : State<Boolean>

@Immutable
internal class ImmutableIntState(override val value: Int) : State<Int>

@Immutable
internal class ImmutableNumberState(override val value: Number) : State<Number>

@Immutable
internal class ImmutableStringState(override val value: String) : State<String>

@Immutable
internal class ImmutableLocalDateState(override val value: LocalDate) : State<LocalDate>

@Immutable
internal class ImmutableListState<T>(override val value: List<T>) : State<List<T>>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.chrisjenx.yakcov.generic
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import com.chrisjenx.yakcov.ImmutableListState
import com.chrisjenx.yakcov.RegularValidationResult
import com.chrisjenx.yakcov.ResourceValidationResult
import com.chrisjenx.yakcov.ValidationResult
Expand All @@ -24,6 +25,8 @@ class Required<T> : ValueValidatorRule<T?> {

@Stable
class InList<T>(list: State<List<T>>) : ValueValidatorRule<T?> {
constructor(list: List<T>) : this(ImmutableListState(list))

private val list: List<T> by list
override fun validate(value: T?): ValidationResult {
return if (value in list) ResourceValidationResult.error(Res.string.ruleInList)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ package com.chrisjenx.yakcov.strings

import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.input.TextFieldValue
import com.chrisjenx.yakcov.ImmutableBooleanState
import com.chrisjenx.yakcov.ImmutableIntState
import com.chrisjenx.yakcov.ImmutableLocalDateState
import com.chrisjenx.yakcov.ImmutableNumberState
import com.chrisjenx.yakcov.ImmutableStringState
import com.chrisjenx.yakcov.ResourceValidationResult
import com.chrisjenx.yakcov.ValidationResult
import com.chrisjenx.yakcov.ValueValidator
Expand Down Expand Up @@ -64,25 +70,31 @@ data object Decimal : ValueValidatorRule<String> {
}

@Stable
data class MinValue(val minValue: Number) : ValueValidatorRule<String> {
data class MinValue(val minValue: State<Number>) : ValueValidatorRule<String> {
constructor(minValue: Number) : this(ImmutableNumberState(minValue))

private val _minValue by minValue
override fun validate(value: String): ValidationResult {
if (value.isBlank()) return ResourceValidationResult.success()
val number = value.toDoubleOrNull()
return if (number == null || number < minValue.toDouble()) {
ResourceValidationResult.error(Res.string.ruleMinValue, minValue)
return if (number == null || number < _minValue.toDouble()) {
ResourceValidationResult.error(Res.string.ruleMinValue, _minValue)
} else {
ResourceValidationResult.success()
}
}
}

@Stable
data class MaxValue(val maxValue: Float) : ValueValidatorRule<String> {
data class MaxValue(val maxValue: State<Number>) : ValueValidatorRule<String> {
constructor(maxValue: Number) : this(ImmutableNumberState(maxValue))

private val _maxValue by maxValue
override fun validate(value: String): ValidationResult {
if (value.isBlank()) return ResourceValidationResult.success()
val number = value.toFloatOrNull()
return if (value.isNotBlank() && (number == null || number > maxValue)) {
ResourceValidationResult.error(Res.string.ruleMaxValue, maxValue)
val number = value.toDoubleOrNull()
return if (value.isNotBlank() && (number == null || number > _maxValue.toDouble())) {
ResourceValidationResult.error(Res.string.ruleMaxValue, _maxValue)
} else {
ResourceValidationResult.success()
}
Expand All @@ -96,31 +108,50 @@ data class MaxValue(val maxValue: Float) : ValueValidatorRule<String> {
*/
@Stable
data class MinLength(
val minLength: Int,
val trim: Boolean = true,
val includeWhiteSpace: Boolean = true,
val minLength: State<Int>,
val trim: State<Boolean> = ImmutableBooleanState(value = true),
val includeWhiteSpace: State<Boolean> = ImmutableBooleanState(value = true),
) : ValueValidatorRule<String> {
constructor(minLength: Int, trim: Boolean = true, includeWhiteSpace: Boolean = true) : this(
ImmutableIntState(minLength),
ImmutableBooleanState(trim),
ImmutableBooleanState(includeWhiteSpace)
)

private val _minLength by minLength
private val _trim by trim
private val _includeWhiteSpace by includeWhiteSpace
override fun validate(value: String): ValidationResult {
val trimmed = value.let { if (trim) it.trim() else it }
val trimmed = value.let { if (_trim) it.trim() else it }
return when {
includeWhiteSpace && trimmed.length < minLength -> {
ResourceValidationResult.error(Res.string.ruleMinLength, minLength)
_includeWhiteSpace && trimmed.length < _minLength -> {
ResourceValidationResult.error(Res.string.ruleMinLength, _minLength)
}

!includeWhiteSpace && trimmed.count { !it.isWhitespace() } < minLength -> {
ResourceValidationResult.error(Res.string.ruleMinLengthNoWhitespace, minLength)
!_includeWhiteSpace && trimmed.count { !it.isWhitespace() } < _minLength -> {
ResourceValidationResult.error(Res.string.ruleMinLengthNoWhitespace, _minLength)
}

else -> ResourceValidationResult.success()
}
}
}

/**
* Max length of a string
*
* @param maxLength the max length of the string
*/
@Stable
data class MaxLength(val maxLength: Int) : ValueValidatorRule<String> {
data class MaxLength(
val maxLength: State<Int>
) : ValueValidatorRule<String> {
constructor(maxLength: Int) : this(ImmutableIntState(maxLength))

private val _maxLength by maxLength
override fun validate(value: String): ValidationResult {
return if (value.length > maxLength) {
ResourceValidationResult.error(Res.string.ruleMaxLength, maxLength)
return if (value.length > _maxLength) {
ResourceValidationResult.error(Res.string.ruleMaxLength, _maxLength)
} else {
ResourceValidationResult.success()
}
Expand All @@ -146,11 +177,16 @@ data object Email : ValueValidatorRule<String> {
* @param defaultRegion the default region to use for phone number validation, ISO 3166-1 alpha-2 code US, GB, ES, etc
*/
@Stable
data class Phone(val defaultRegion: String = "US") : ValueValidatorRule<String> {
data class Phone(
val defaultRegion: State<String> = ImmutableStringState(value = "US")
) : ValueValidatorRule<String> {
constructor(defaultRegion: String) : this(ImmutableStringState(defaultRegion))

private val _defaultRegion by defaultRegion
override fun validate(value: String): ValidationResult {
// only validate if not empty as Required will check if not empty
if (value.isBlank()) return ResourceValidationResult.success()
return if (!value.isPhoneNumber(defaultRegion)) {
return if (!value.isPhoneNumber(_defaultRegion)) {
ResourceValidationResult.error(Res.string.rulePhone)
} else {
ResourceValidationResult.success()
Expand All @@ -160,6 +196,8 @@ data class Phone(val defaultRegion: String = "US") : ValueValidatorRule<String>

@Stable
data class DayValidation(val localDate: State<LocalDate>) : ValueValidatorRule<String> {
constructor(localDate: LocalDate) : this(ImmutableLocalDateState(localDate))

override fun validate(value: String): ValidationResult {
val current = localDate.value
val dayOfMonth = value.toIntOrNull()
Expand All @@ -175,6 +213,8 @@ data class DayValidation(val localDate: State<LocalDate>) : ValueValidatorRule<S

@Stable
data class MonthValidation(val localDate: State<LocalDate>) : ValueValidatorRule<String> {
constructor(localDate: LocalDate) : this(ImmutableLocalDateState(localDate))

override fun validate(value: String): ValidationResult {
val current = localDate.value
val monthOfYear = value.toIntOrNull()
Expand All @@ -190,6 +230,8 @@ data class MonthValidation(val localDate: State<LocalDate>) : ValueValidatorRule

@Stable
data class YearValidation(val localDate: State<LocalDate>) : ValueValidatorRule<String> {
constructor(localDate: LocalDate) : this(ImmutableLocalDateState(localDate))

override fun validate(value: String): ValidationResult {
val current = localDate.value
val year = value.toIntOrNull() ?: return ResourceValidationResult.error(Res.string.ruleYear)
Expand Down Expand Up @@ -238,3 +280,5 @@ fun PasswordMatches(stringField: ValueValidator<String, *>): ValueValidatorRule<
fun PasswordMatches(textFieldValueField: ValueValidator<TextFieldValue, *>): ValueValidatorRule<String> {
return PasswordMatchesTextFieldValue(textFieldValueField)
}


Binary file added sample/assets/YakcovDemo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ plugins {

android {
namespace = "com.chrisjenx.yakcov.sample"
compileSdk = 34
compileSdk = 35

defaultConfig {
applicationId = "com.chrisjenx.yakcov.sample"
minSdk = 23
targetSdk = 34
targetSdk = 35
versionCode = 1
versionName = "1.0"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.KeyboardOptions
Expand All @@ -29,12 +31,18 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.derivedStateOf
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.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import com.chrisjenx.yakcov.generic.IsChecked
import com.chrisjenx.yakcov.generic.ListNotEmpty
Expand All @@ -54,7 +62,10 @@ class SampleActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
YakcovTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.safeDrawing,
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
Expand Down Expand Up @@ -112,6 +123,42 @@ class SampleActivity : ComponentActivity() {

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

var firstTextField by remember { mutableStateOf(TextFieldValue()) }
val minLength = remember { derivedStateOf { firstTextField.text.length } }
val matchLength = rememberTextFieldValueValidator(
rules = listOf(MinLength(minLength)), alwaysShowRule = true,
)

OutlinedTextField(
value = firstTextField,
onValueChange = { firstTextField = it },
label = { Text("Use this length") },
modifier = Modifier.fillMaxWidth()
)

with(matchLength) {
OutlinedTextField(
value = value,
label = { Text("Match Above length") },
modifier = Modifier
.validationConfig(
validateOnFocusLost = true,
shakeOnInvalid = true
)
.fillMaxWidth(),
onValueChange = ::onValueChange,
isError = isError(),
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Number,
),
singleLine = true,
supportingText = supportingText()
)
}

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

Text(text = "Password", style = MaterialTheme.typography.headlineSmall)
// Password example
val passwordValidator = rememberTextFieldValueValidator(
Expand Down

0 comments on commit 922e011

Please sign in to comment.