From 7c528257a89d3ed15452c2167f344bb12ffe46ff Mon Sep 17 00:00:00 2001 From: iHoonter Date: Mon, 4 Aug 2025 13:58:26 -0600 Subject: [PATCH 1/3] Moved Draft into an interface so that ValidatedDraft can exist --- .../com/lightningkite/reactive/core/Draft.kt | 85 ++++++++++--------- .../lensing/validation/ValidatedDraft.kt | 21 +++++ 2 files changed, 68 insertions(+), 38 deletions(-) create mode 100644 src/commonMain/kotlin/com/lightningkite/reactive/lensing/validation/ValidatedDraft.kt diff --git a/src/commonMain/kotlin/com/lightningkite/reactive/core/Draft.kt b/src/commonMain/kotlin/com/lightningkite/reactive/core/Draft.kt index 6758d6c..80c1ea3 100644 --- a/src/commonMain/kotlin/com/lightningkite/reactive/core/Draft.kt +++ b/src/commonMain/kotlin/com/lightningkite/reactive/core/Draft.kt @@ -4,57 +4,66 @@ import com.lightningkite.reactive.context.ReactiveContext import com.lightningkite.reactive.context.awaitOnce /** - * A mutable reactive value that supports draft editing and publishing. Essentially, this provides an input buffer for a [MutableReactive]. + * A mutable reactive value that supports draft editing and publishing. Essentially, + * this represents an input buffer for a [MutableReactive]. * - * This class wraps a [MutableReactive] value and provides a buffer layer using [MutableRemember]. - * Changes can be made to the draft without affecting the published value until explicitly published. - * - * - The draft value is calculated and updated reactively, but can be manually set. - * - The draft is lazy: if there are no listeners, it will not calculate a value. - * - Listeners are only notified if the draft value actually changes. - * - Use [publish] to commit draft changes to the published value, or [cancel] to discard changes and reset the draft. + * A [Draft] copies its [published] value until you set a new value in the draft. After + * you set a new value, the draft keeps this change separate from the published value. + * The draft will keep your change until you call [publish] (to save it) or [cancel] + * (to discard it and revert to the published value). * * Example: * ```kotlin * val published = Signal(0) * val draft = Draft(published) * - * draft.set(42) // changes the draft, not the published value + * draft.value = 42 // changes the draft, not the published value * draft.publish() // commits the draft to published. published now holds the value '42' * - * draft.set(43) // draft now holds '43', while published holds '42' + * draft.value = 43 // draft now holds '43', while published holds '42' * draft.cancel() // discards changes and resets the draft. draft now reads '42' again. * ``` - * - * @param T The type of value being edited and published. - * @property published The underlying published value. - * @property draft The mutable draft value. - * @property changesMade A reactive value indicating if the draft differs from the published value. - * - * - * @see MutableRemember */ -class Draft private constructor( - val published: MutableReactive, - private val draft: MutableRemember -): ReactiveWithMutableValue by draft { +interface Draft : ReactiveWithMutableValue { + /** + * The current saved value that this [Draft] is buffering. + * + * NOTE: Manually setting values for [published] will not by-default update values in the draft buffer. + * */ + val published: MutableReactive + + /** + * Saves all changes made to this [Draft] to the published [MutableReactive] + * */ + suspend fun publish(): T + + /** + * Discards all changes made to this [Draft] and reverts back to the [published] state + * */ + fun cancel() + + /** + * Reads `true` if there are any differences between the [published] value and the value stored in the draft buffer. + * */ + val changesMade: Reactive +} + +private class BaseDraft private constructor( + override val published: MutableReactive, + val buffer: MutableRemember +): Draft, ReactiveWithMutableValue by buffer { constructor(published: MutableReactive) : this(published, MutableRemember(stopListeningWhenOverridden = false) { published() }) - constructor(initialValue: ReactiveContext.() -> T) : this( - MutableRemember( - useLastWhileLoading = true, - initialValue = initialValue - ) - ) - constructor(initialValue: T) : this(Signal(initialValue)) - - val changesMade = remember { draft() != published() } - - suspend fun publish(): T { - published.set(draft.awaitOnce()) - draft.reset() + + override val changesMade = remember { buffer() != published() } + + override suspend fun publish(): T { + published.set(buffer.awaitOnce()) + buffer.reset() return awaitOnce() } - fun cancel() { draft.reset() } + override fun cancel() { buffer.reset() } +} - override suspend fun set(value: T) { draft.valueSet(value) } -} \ No newline at end of file +fun Draft(published: MutableReactive): Draft = BaseDraft(published) +fun Draft(initialValue: T): Draft = BaseDraft(Signal(initialValue)) +fun Draft(initialValue: ReactiveContext.() -> T): Draft = BaseDraft(MutableRemember(useLastWhileLoading = true, initialValue = initialValue)) \ No newline at end of file diff --git a/src/commonMain/kotlin/com/lightningkite/reactive/lensing/validation/ValidatedDraft.kt b/src/commonMain/kotlin/com/lightningkite/reactive/lensing/validation/ValidatedDraft.kt new file mode 100644 index 0000000..4f973ca --- /dev/null +++ b/src/commonMain/kotlin/com/lightningkite/reactive/lensing/validation/ValidatedDraft.kt @@ -0,0 +1,21 @@ +package com.lightningkite.reactive.lensing.validation + +import com.lightningkite.reactive.core.Draft +import com.lightningkite.reactive.core.MutableReactive + +/** + * A [Draft] with a validation tree. Useful for buffering and validating user input at the same time. + * + * @see Draft + * @see MutableValidated + * */ +interface ValidatedDraft : Draft, MutableValidated + +private class RootValidatedDraft(val draft: Draft) : ValidatedDraft, Draft by draft { + override val node: IssueNode = IssueNode(null) + + override fun lens(get: (T) -> L, set: (L) -> T): MutableValidated = ValidatedSetLens(this, get, set) + override fun lens(get: (T) -> L, modify: (T, L) -> T): MutableValidated = ValidatedModifyLens(this, get, modify) +} + +fun Draft.validated(): ValidatedDraft = RootValidatedDraft(this) \ No newline at end of file From 7e4bdb6efbaee5e0a23beaafdc38c6c974e6fccc Mon Sep 17 00:00:00 2001 From: iHoonter Date: Mon, 4 Aug 2025 14:12:07 -0600 Subject: [PATCH 2/3] Draft factory function documentation --- .../kotlin/com/lightningkite/reactive/core/Draft.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/com/lightningkite/reactive/core/Draft.kt b/src/commonMain/kotlin/com/lightningkite/reactive/core/Draft.kt index 80c1ea3..8d759d8 100644 --- a/src/commonMain/kotlin/com/lightningkite/reactive/core/Draft.kt +++ b/src/commonMain/kotlin/com/lightningkite/reactive/core/Draft.kt @@ -5,7 +5,7 @@ import com.lightningkite.reactive.context.awaitOnce /** * A mutable reactive value that supports draft editing and publishing. Essentially, - * this represents an input buffer for a [MutableReactive]. + * this represents an input buffer for a "published" [MutableReactive]. * * A [Draft] copies its [published] value until you set a new value in the draft. After * you set a new value, the draft keeps this change separate from the published value. @@ -64,6 +64,17 @@ private class BaseDraft private constructor( override fun cancel() { buffer.reset() } } +/** + * Creates a [Draft] using the specified [MutableReactive] as the published value. + * */ fun Draft(published: MutableReactive): Draft = BaseDraft(published) + +/** + * Creates a [Draft] where the published value is the provided [initialValue] + * */ fun Draft(initialValue: T): Draft = BaseDraft(Signal(initialValue)) + +/** + * Creates a [Draft] where the published value is calculated based off the provided [initialValue] calculation. + * */ fun Draft(initialValue: ReactiveContext.() -> T): Draft = BaseDraft(MutableRemember(useLastWhileLoading = true, initialValue = initialValue)) \ No newline at end of file From 8ad2044fef8e6ddcea735afc81f8de69e56997d1 Mon Sep 17 00:00:00 2001 From: iHoonter Date: Mon, 4 Aug 2025 14:16:48 -0600 Subject: [PATCH 3/3] removed unneeded print statement --- .../kotlin/com/lightningkite/reactive/extensions/helpers.kt | 1 - .../lightningkite/reactive/lensing/validation/ValidatedDraft.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/src/commonMain/kotlin/com/lightningkite/reactive/extensions/helpers.kt b/src/commonMain/kotlin/com/lightningkite/reactive/extensions/helpers.kt index a683f68..7dc8c37 100644 --- a/src/commonMain/kotlin/com/lightningkite/reactive/extensions/helpers.kt +++ b/src/commonMain/kotlin/com/lightningkite/reactive/extensions/helpers.kt @@ -37,7 +37,6 @@ var MutableValue.value: T get() = throw IllegalStateException("Attempted to retrieve value for set-only property") @JvmName("setValue2") set(value) { - println("Setting outer value: $value") valueSet(value) } diff --git a/src/commonMain/kotlin/com/lightningkite/reactive/lensing/validation/ValidatedDraft.kt b/src/commonMain/kotlin/com/lightningkite/reactive/lensing/validation/ValidatedDraft.kt index 4f973ca..8be40bc 100644 --- a/src/commonMain/kotlin/com/lightningkite/reactive/lensing/validation/ValidatedDraft.kt +++ b/src/commonMain/kotlin/com/lightningkite/reactive/lensing/validation/ValidatedDraft.kt @@ -1,7 +1,6 @@ package com.lightningkite.reactive.lensing.validation import com.lightningkite.reactive.core.Draft -import com.lightningkite.reactive.core.MutableReactive /** * A [Draft] with a validation tree. Useful for buffering and validating user input at the same time.