diff --git a/demo/playground/src/jvmMain/kotlin/controlVision.kt b/demo/playground/src/jvmMain/kotlin/controlVision.kt new file mode 100644 index 00000000..f7a9c753 --- /dev/null +++ b/demo/playground/src/jvmMain/kotlin/controlVision.kt @@ -0,0 +1,59 @@ +package space.kscience.visionforge.examples + +import kotlinx.html.p +import space.kscience.visionforge.VisionControlEvent +import space.kscience.visionforge.html.* +import space.kscience.visionforge.onClick + + +fun main() = serve { + + val events = ArrayDeque(10) + + val html = VisionOfPlainHtml() + + fun pushEvent(event: VisionControlEvent) { + events.addFirst(event) + if (events.size >= 10) { + events.removeLast() + } + html.content { + events.forEach { event -> + p { + text(event.toString()) + } + } + } + } + + vision { + htmlCheckBox { + checked = true + onValueChange(context) { + pushEvent(this) + } + } + } + + vision { + htmlRangeField(1, 10) { + numberValue = 4 + onValueChange(context) { + pushEvent(this) + } + } + } + + + vision { + button("Click me"){ + onClick(context){ + pushEvent(this) + } + } + } + + + + vision(html) +} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt index 20525b90..fec77548 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/ControlVision.kt @@ -8,14 +8,13 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MetaRepr -import space.kscience.dataforge.meta.MutableMeta -import space.kscience.dataforge.meta.Value +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.parseAsName @Serializable @SerialName("control") -public abstract class VisionControlEvent : VisionEvent, MetaRepr { +public sealed class VisionControlEvent : VisionEvent, MetaRepr { public abstract val meta: Meta override fun toMeta(): Meta = meta @@ -24,30 +23,41 @@ public abstract class VisionControlEvent : VisionEvent, MetaRepr { public interface ControlVision : Vision { public val controlEventFlow: SharedFlow - public fun dispatchControlEvent(event: VisionControlEvent) + public suspend fun dispatchControlEvent(event: VisionControlEvent) - override fun receiveEvent(event: VisionEvent) { + override suspend fun receiveEvent(event: VisionEvent) { if (event is VisionControlEvent) { dispatchControlEvent(event) } else super.receiveEvent(event) } } + /** * @param payload The optional payload associated with the click event. */ @Serializable @SerialName("control.click") -public class VisionClickEvent(public val payload: Meta = Meta.EMPTY) : VisionControlEvent() { - override val meta: Meta get() = Meta { ::payload.name put payload } +public class VisionClickEvent(override val meta: Meta) : VisionControlEvent() { + public val payload: Meta? by meta.node() + public val name: Name? get() = meta["name"].string?.parseAsName() + + override fun toString(): String = meta.toString() } +public fun VisionClickEvent(payload: Meta = Meta.EMPTY, name: Name? = null): VisionClickEvent = VisionClickEvent( + Meta { + VisionClickEvent::payload.name put payload + VisionClickEvent::name.name put name.toString() + } +) + public interface ClickControl : ControlVision { /** * Create and dispatch a click event */ - public fun click(builder: MutableMeta.() -> Unit = {}) { + public suspend fun click(builder: MutableMeta.() -> Unit = {}) { dispatchControlEvent(VisionClickEvent(Meta(builder))) } } @@ -64,7 +74,18 @@ public fun ClickControl.onClick(scope: CoroutineScope, block: suspend VisionClic public class VisionValueChangeEvent(override val meta: Meta) : VisionControlEvent() { public val value: Value? get() = meta.value + + /** + * The name of a control that fired the event + */ + public val name: Name? get() = meta["name"]?.string?.parseAsName() + + override fun toString(): String = meta.toString() } -public fun VisionValueChangeEvent(value: Value?): VisionValueChangeEvent = - VisionValueChangeEvent(Meta { this.value = value }) +public fun VisionValueChangeEvent(value: Value?, name: Name? = null): VisionValueChangeEvent = VisionValueChangeEvent( + Meta { + this.value = value + name?.let { set("name", it.toString()) } + } +) diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt index 78f917a9..ece71edd 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/Vision.kt @@ -50,7 +50,7 @@ public interface Vision : Described { /** * Receive and process a generic [VisionEvent]. */ - public fun receiveEvent(event: VisionEvent) { + public suspend fun receiveEvent(event: VisionEvent) { if(event is VisionChange) update(event) else manager?.logger?.warn { "Undispatched event: $event" } } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt index e337fea0..7a62de7d 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionClient.kt @@ -1,9 +1,7 @@ package space.kscience.visionforge -import kotlinx.coroutines.launch import space.kscience.dataforge.context.Plugin import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.MetaRepr import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.parseAsName @@ -32,10 +30,4 @@ public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: St public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Boolean) { notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item)) -} - -public fun VisionClient.sendMetaEvent(targetName: Name, payload: MetaRepr): Unit { - context.launch { - sendEvent(targetName, VisionMetaEvent(payload.toMeta())) - } } \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt index 8799a73f..2d765a35 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/VisionManager.kt @@ -69,12 +69,14 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont defaultDeserializer { SimpleVisionGroup.serializer() } subclass(NullVision.serializer()) subclass(SimpleVisionGroup.serializer()) + subclass(VisionOfPlainHtml.serializer()) subclass(VisionOfHtmlInput.serializer()) subclass(VisionOfNumberField.serializer()) subclass(VisionOfTextField.serializer()) subclass(VisionOfCheckbox.serializer()) subclass(VisionOfRangeField.serializer()) subclass(VisionOfHtmlForm.serializer()) + subclass(VisionOfHtmlButton.serializer()) } } diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt index 5e597702..0738ddc6 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtml.kt @@ -1,9 +1,11 @@ package space.kscience.visionforge.html -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.html.DIV import kotlinx.html.InputType -import kotlinx.html.TagConsumer +import kotlinx.html.div import kotlinx.html.stream.createHTML import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -27,8 +29,10 @@ public class VisionOfPlainHtml : VisionOfHtml() { public var content: String? by properties.string() } -public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) { - content = createHTML().apply(block).finalize() +public fun VisionOfPlainHtml.content(block: DIV.() -> Unit) { + content = createHTML().apply { + div(block = block) + }.finalize() } @Suppress("UnusedReceiverParameter") @@ -54,15 +58,7 @@ public enum class InputFeedbackMode { NONE } -@Serializable -@SerialName("html.input") -public open class VisionOfHtmlInput( - public val inputType: String, - public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE, -) : VisionOfHtml(), ControlVision { - public var value: Value? by properties.value() - public var disabled: Boolean by properties.boolean { false } - public var fieldName: String? by properties.string() +public abstract class VisionOfHtmlControl: VisionOfHtml(), ControlVision{ @Transient private val mutableControlEventFlow = MutableSharedFlow() @@ -70,14 +66,32 @@ public open class VisionOfHtmlInput( override val controlEventFlow: SharedFlow get() = mutableControlEventFlow - override fun dispatchControlEvent(event: VisionControlEvent) { - if(event is VisionValueChangeEvent){ - this.value = event.value - } - mutableControlEventFlow.tryEmit(event) + override suspend fun dispatchControlEvent(event: VisionControlEvent) { + mutableControlEventFlow.emit(event) } } + +@Serializable +@SerialName("html.input") +public open class VisionOfHtmlInput( + public val inputType: String, + public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE, +) : VisionOfHtmlControl() { + public var value: Value? by properties.value() + public var disabled: Boolean by properties.boolean { false } + public var fieldName: String? by properties.string() + +} + +/** + * Trigger [callback] on each value change + */ +public fun VisionOfHtmlInput.onValueChange( + scope: CoroutineScope = manager?.context ?: error("Coroutine context is not resolved for $this"), + callback: suspend VisionValueChangeEvent.() -> Unit, +): Job = controlEventFlow.filterIsInstance().onEach(callback).launchIn(scope) + @Suppress("UnusedReceiverParameter") public inline fun VisionOutput.htmlInput( inputType: String, @@ -110,7 +124,7 @@ public inline fun VisionOutput.htmlCheckBox( @Serializable @SerialName("html.number") public class VisionOfNumberField : VisionOfHtmlInput(InputType.number.realValue) { - public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) + public var numberValue: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) } @Suppress("UnusedReceiverParameter") @@ -125,14 +139,14 @@ public class VisionOfRangeField( public val max: Double, public val step: Double = 1.0, ) : VisionOfHtmlInput(InputType.range.realValue) { - public var number: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) + public var numberValue: Number? by properties.number(key = VisionOfHtmlInput::value.name.asName()) } @Suppress("UnusedReceiverParameter") public inline fun VisionOutput.htmlRangeField( - min: Double, - max: Double, - step: Double = 1.0, + min: Number, + max: Number, + step: Number = 1.0, block: VisionOfRangeField.() -> Unit = {}, -): VisionOfRangeField = VisionOfRangeField(min, max, step).apply(block) +): VisionOfRangeField = VisionOfRangeField(min.toDouble(), max.toDouble(), step.toDouble()).apply(block) diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt index e56af874..c128ec6f 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionOfHtmlForm.kt @@ -8,6 +8,8 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.node +import space.kscience.dataforge.meta.string +import space.kscience.visionforge.ClickControl /** * @param formId an id of the element in rendered DOM, this form is bound to @@ -16,7 +18,7 @@ import space.kscience.dataforge.meta.node @SerialName("html.form") public class VisionOfHtmlForm( public val formId: String, -) : VisionOfHtml() { +) : VisionOfHtmlControl() { public var values: Meta? by properties.node() } @@ -26,4 +28,21 @@ public fun TagConsumer.bindForm( ): R = form { this.id = visionOfForm.formId builder() +} + + +@Serializable +@SerialName("html.button") +public class VisionOfHtmlButton : VisionOfHtmlControl(), ClickControl { + public var label: String? by properties.string() +} + + +@Suppress("UnusedReceiverParameter") +public inline fun VisionOutput.button( + text: String, + block: VisionOfHtmlButton.() -> Unit = {}, +): VisionOfHtmlButton = VisionOfHtmlButton().apply { + label = text + block() } \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt index c24ab493..e0dc975a 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/JsVisionClient.kt @@ -135,7 +135,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { } logger.debug { "Got $event for output with name $visionName" } - vision.receiveEvent(event) + context.launch { + vision.receiveEvent(event) + } } else { logger.error { "WebSocket message data is not a string" } } @@ -262,7 +264,8 @@ public class JsVisionClient : AbstractPlugin(), VisionClient { numberVisionRenderer, textVisionRenderer, rangeVisionRenderer, - formVisionRenderer + formVisionRenderer, + buttonVisionRenderer ).associateByName() } else super.content(target) diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt new file mode 100644 index 00000000..6a360af8 --- /dev/null +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/formRenderers.kt @@ -0,0 +1,90 @@ +package space.kscience.visionforge + +import kotlinx.browser.document +import kotlinx.coroutines.launch +import kotlinx.html.ButtonType +import kotlinx.html.js.button +import org.w3c.dom.HTMLFormElement +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.get +import org.w3c.xhr.FormData +import space.kscience.dataforge.context.debug +import space.kscience.dataforge.context.logger +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.names.Name +import space.kscience.visionforge.html.VisionOfHtmlButton +import space.kscience.visionforge.html.VisionOfHtmlForm + + +internal fun FormData.toMeta(): Meta { + @Suppress("UNUSED_VARIABLE") val formData = this + //val res = js("Object.fromEntries(formData);") + val `object` = js("{}") + //language=JavaScript + js( + """ + formData.forEach(function(value, key){ + // Reflect.has in favor of: object.hasOwnProperty(key) + if(!Reflect.has(object, key)){ + object[key] = value; + return; + } + if(!Array.isArray(object[key])){ + object[key] = [object[key]]; + } + object[key].push(value); + }); + """ + ) + return DynamicMeta(`object`) +} + + +public fun VisionClient.sendMetaEvent(targetName: Name, payload: MetaRepr): Unit { + context.launch { + sendEvent(targetName, VisionMetaEvent(payload.toMeta())) + } +} + +internal val formVisionRenderer: ElementVisionRenderer = + ElementVisionRenderer { name, client, vision, _ -> + + val form = document.getElementById(vision.formId) as? HTMLFormElement + ?: error("An element with id = '${vision.formId} is not a form") + + form.subscribeToVision(vision) + + vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" } + + vision.useProperty(VisionOfHtmlForm::values) { values -> + vision.manager?.logger?.debug { "Updating form '${vision.formId}' with values $values" } + val inputs = form.getElementsByTagName("input") + values?.valueSequence()?.forEach { (token, value) -> + (inputs[token.toString()] as? HTMLInputElement)?.value = value.toString() + } + } + + form.onsubmit = { event -> + event.preventDefault() + val formData = FormData(form).toMeta() + client.sendMetaEvent(name, formData) + console.info("Sent: ${formData.toMap()}") + false + } + } + +internal val buttonVisionRenderer: ElementVisionRenderer = + ElementVisionRenderer { name, client, vision, _ -> + button(type = ButtonType.button).also { button -> + button.subscribeToVision(vision) + button.onclick = { + client.context.launch { + client.sendEvent(name, VisionClickEvent(name = name)) + } + } + vision.useProperty(VisionOfHtmlButton::label) { + button.innerHTML = it ?: "" + } + + } + } diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt index a43a99f1..4112370a 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/inputRenderers.kt @@ -1,21 +1,17 @@ package space.kscience.visionforge -import kotlinx.browser.document import kotlinx.coroutines.launch import kotlinx.dom.clear import kotlinx.html.InputType import kotlinx.html.div -import kotlinx.html.dom.append import kotlinx.html.js.input import org.w3c.dom.HTMLElement -import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLInputElement import org.w3c.dom.events.Event -import org.w3c.dom.get -import org.w3c.xhr.FormData -import space.kscience.dataforge.context.debug -import space.kscience.dataforge.context.logger -import space.kscience.dataforge.meta.* +import space.kscience.dataforge.meta.Value +import space.kscience.dataforge.meta.asValue +import space.kscience.dataforge.meta.double +import space.kscience.dataforge.meta.string import space.kscience.dataforge.names.Name import space.kscience.visionforge.html.* @@ -24,7 +20,7 @@ import space.kscience.visionforge.html.* * * @param vision The vision to subscribe to. */ -private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { +internal fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { vision.useProperty(VisionOfHtml::classes) { classList.value = classes.joinToString(separator = " ") } @@ -33,7 +29,7 @@ private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) { private fun VisionClient.sendInputEvent(name: Name, value: Value?) { context.launch { - sendEvent(name, VisionValueChangeEvent(value)) + sendEvent(name, VisionValueChangeEvent(value, name)) } } @@ -51,43 +47,39 @@ private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) { internal val htmlVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { _, _, vision, _ -> - div {}.also { div -> + div().also { div -> div.subscribeToVision(vision) vision.useProperty(VisionOfPlainHtml::content) { div.clear() - div.append { - - } - div.textContent = it + if (it != null) div.innerHTML = it } } } -internal val inputVisionRenderer: ElementVisionRenderer = - ElementVisionRenderer( - acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1 - ) { name, client, vision, _ -> - input { - type = InputType.text - }.also { htmlInputElement -> - val onEvent: (Event) -> Unit = { - client.sendInputEvent(name, htmlInputElement.value.asValue()) - } +internal val inputVisionRenderer: ElementVisionRenderer = ElementVisionRenderer( + acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1 +) { name, client, vision, _ -> + input { + type = InputType.text + }.also { htmlInputElement -> + val onEvent: (Event) -> Unit = { + client.sendInputEvent(name, htmlInputElement.value.asValue()) + } - when (vision.feedbackMode) { - InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent + when (vision.feedbackMode) { + InputFeedbackMode.ONCHANGE -> htmlInputElement.onchange = onEvent - InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent - InputFeedbackMode.NONE -> {} - } + InputFeedbackMode.ONINPUT -> htmlInputElement.oninput = onEvent + InputFeedbackMode.NONE -> {} + } - htmlInputElement.subscribeToInput(vision) - vision.useProperty(VisionOfHtmlInput::value) { - htmlInputElement.value = it?.string ?: "" - } + htmlInputElement.subscribeToInput(vision) + vision.useProperty(VisionOfHtmlInput::value) { + htmlInputElement.value = it?.string ?: "" } } +} internal val checkboxVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { name, client, vision, _ -> @@ -95,7 +87,7 @@ internal val checkboxVisionRenderer: ElementVisionRenderer = type = InputType.checkBox }.also { htmlInputElement -> val onEvent: (Event) -> Unit = { - client.sendInputEvent(name, htmlInputElement.value.asValue()) + client.sendInputEvent(name, htmlInputElement.checked.asValue()) } @@ -140,7 +132,7 @@ internal val textVisionRenderer: ElementVisionRenderer = internal val numberVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { name, client, vision, _ -> input { - type = InputType.text + type = InputType.number }.also { htmlInputElement -> val onEvent: (Event) -> Unit = { @@ -165,7 +157,7 @@ internal val numberVisionRenderer: ElementVisionRenderer = internal val rangeVisionRenderer: ElementVisionRenderer = ElementVisionRenderer { name, client, vision, _ -> input { - type = InputType.text + type = InputType.range min = vision.min.toString() max = vision.max.toString() step = vision.step.toString() @@ -189,53 +181,3 @@ internal val rangeVisionRenderer: ElementVisionRenderer = } } } - -internal fun FormData.toMeta(): Meta { - @Suppress("UNUSED_VARIABLE") val formData = this - //val res = js("Object.fromEntries(formData);") - val `object` = js("{}") - //language=JavaScript - js( - """ - formData.forEach(function(value, key){ - // Reflect.has in favor of: object.hasOwnProperty(key) - if(!Reflect.has(object, key)){ - object[key] = value; - return; - } - if(!Array.isArray(object[key])){ - object[key] = [object[key]]; - } - object[key].push(value); - }); - """ - ) - return DynamicMeta(`object`) -} - -internal val formVisionRenderer: ElementVisionRenderer = - ElementVisionRenderer { name, client, vision, _ -> - - val form = document.getElementById(vision.formId) as? HTMLFormElement - ?: error("An element with id = '${vision.formId} is not a form") - - form.subscribeToVision(vision) - - vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" } - - vision.useProperty(VisionOfHtmlForm::values) { values -> - vision.manager?.logger?.debug { "Updating form '${vision.formId}' with values $values" } - val inputs = form.getElementsByTagName("input") - values?.valueSequence()?.forEach { (token, value) -> - (inputs[token.toString()] as? HTMLInputElement)?.value = value.toString() - } - } - - form.onsubmit = { event -> - event.preventDefault() - val formData = FormData(form).toMeta() - client.sendMetaEvent(name, formData) - console.info("Sent: ${formData.toMap()}") - false - } - } \ No newline at end of file