forked from kscience/visionforge
Fix html input renderers
This commit is contained in:
parent
469655092e
commit
7561ddad36
@ -2,10 +2,8 @@ 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
|
||||
|
||||
/**
|
||||
* A feedback client that communicates with a server and provides ability to propagate events and changes back to the model
|
||||
@ -15,25 +13,25 @@ public interface VisionClient: Plugin {
|
||||
|
||||
public suspend fun sendEvent(targetName: Name, event: VisionEvent)
|
||||
|
||||
public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?)
|
||||
// public fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?)
|
||||
}
|
||||
|
||||
|
||||
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) {
|
||||
notifyPropertyChanged(visionName, propertyName.parseAsName(true), item)
|
||||
}
|
||||
|
||||
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Number) {
|
||||
notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
|
||||
}
|
||||
|
||||
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: String) {
|
||||
notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
|
||||
}
|
||||
|
||||
public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Boolean) {
|
||||
notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
|
||||
}
|
||||
//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Meta?) {
|
||||
// notifyPropertyChanged(visionName, propertyName.parseAsName(true), item)
|
||||
//}
|
||||
//
|
||||
//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Number) {
|
||||
// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
|
||||
//}
|
||||
//
|
||||
//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: String) {
|
||||
// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
|
||||
//}
|
||||
//
|
||||
//public fun VisionClient.notifyPropertyChanged(visionName: Name, propertyName: String, item: Boolean) {
|
||||
// notifyPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
|
||||
//}
|
||||
|
||||
public fun VisionClient.sendEvent(targetName: Name, payload: MetaRepr): Unit {
|
||||
context.launch {
|
||||
|
@ -13,10 +13,7 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
||||
import space.kscience.dataforge.meta.toJson
|
||||
import space.kscience.dataforge.meta.toMeta
|
||||
import space.kscience.dataforge.names.Name
|
||||
import space.kscience.visionforge.html.VisionOfCheckbox
|
||||
import space.kscience.visionforge.html.VisionOfHtmlForm
|
||||
import space.kscience.visionforge.html.VisionOfNumberField
|
||||
import space.kscience.visionforge.html.VisionOfTextField
|
||||
import space.kscience.visionforge.html.*
|
||||
|
||||
public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionContainer<Vision> {
|
||||
override val tag: PluginTag get() = Companion.tag
|
||||
@ -72,9 +69,11 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta), MutableVisionCont
|
||||
defaultDeserializer { SimpleVisionGroup.serializer() }
|
||||
subclass(NullVision.serializer())
|
||||
subclass(SimpleVisionGroup.serializer())
|
||||
subclass(VisionOfHtmlInput.serializer())
|
||||
subclass(VisionOfNumberField.serializer())
|
||||
subclass(VisionOfTextField.serializer())
|
||||
subclass(VisionOfCheckbox.serializer())
|
||||
subclass(VisionOfRangeField.serializer())
|
||||
subclass(VisionOfHtmlForm.serializer())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package space.kscience.visionforge.html
|
||||
|
||||
import kotlinx.html.InputType
|
||||
import kotlinx.html.TagConsumer
|
||||
import kotlinx.html.stream.createHTML
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import space.kscience.dataforge.meta.*
|
||||
@ -13,10 +15,39 @@ public abstract class VisionOfHtml: AbstractVision(){
|
||||
public var classes: List<String> by properties.stringList(*emptyArray())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("html.plain")
|
||||
public class VisionOfPlainHtml : VisionOfHtml() {
|
||||
public var content: String? by properties.string()
|
||||
}
|
||||
|
||||
public inline fun VisionOfPlainHtml.content(block: TagConsumer<*>.() -> Unit) {
|
||||
content = createHTML().apply(block).finalize()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
public enum class InputFeedbackMode{
|
||||
/**
|
||||
* Fire feedback event on `onchange` event
|
||||
*/
|
||||
ONCHANGE,
|
||||
|
||||
/**
|
||||
* Fire feedback event on `oninput` event
|
||||
*/
|
||||
ONINPUT,
|
||||
|
||||
/**
|
||||
* provide only manual feedback
|
||||
*/
|
||||
NONE
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("html.input")
|
||||
public open class VisionOfHtmlInput(
|
||||
public val inputType: String,
|
||||
public val feedbackMode: InputFeedbackMode = InputFeedbackMode.ONCHANGE
|
||||
) : VisionOfHtml() {
|
||||
public var value: Value? by properties.value()
|
||||
public var disabled: Boolean by properties.boolean { false }
|
||||
|
@ -39,9 +39,22 @@ public fun Vision.useProperty(
|
||||
callback: (Meta) -> Unit,
|
||||
): Job = useProperty(propertyName.parseAsName(), inherit, includeStyles, scope, callback)
|
||||
|
||||
/**
|
||||
* Observe changes to the specific property without passing the initial value.
|
||||
*/
|
||||
public fun <V : Vision, T> V.onPropertyChange(
|
||||
property: KProperty1<V, T>,
|
||||
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
|
||||
callback: suspend V.(T) -> Unit,
|
||||
): Job = properties.changes.onEach { name ->
|
||||
if (name.startsWith(property.name.asName())) {
|
||||
callback(property.get(this))
|
||||
}
|
||||
}.launchIn(scope)
|
||||
|
||||
public fun <V : Vision, T> V.useProperty(
|
||||
property: KProperty1<V, T>,
|
||||
scope: CoroutineScope? = manager?.context,
|
||||
scope: CoroutineScope = manager?.context ?: error("Orphan Vision can't observe properties"),
|
||||
callback: V.(T) -> Unit,
|
||||
): Job {
|
||||
//Pass initial value.
|
||||
@ -50,5 +63,5 @@ public fun <V : Vision, T> V.useProperty(
|
||||
if (name.startsWith(property.name.asName())) {
|
||||
callback(property.get(this@useProperty))
|
||||
}
|
||||
}.launchIn(scope ?: error("Orphan Vision can't observe properties"))
|
||||
}.launchIn(scope)
|
||||
}
|
@ -26,7 +26,7 @@ public interface ElementVisionRenderer : Named {
|
||||
/**
|
||||
* Give a [vision] integer rating based on this renderer capabilities. [ZERO_RATING] or negative values means that this renderer
|
||||
* can't process a vision. The value of [DEFAULT_RATING] used for default renderer. Specialized renderers could specify
|
||||
* higher value in order to "steal" rendering job
|
||||
* higher value to "steal" rendering job
|
||||
*/
|
||||
public fun rateVision(vision: Vision): Int
|
||||
|
||||
|
@ -65,20 +65,20 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
||||
|
||||
private fun Element.getFlag(attribute: String): Boolean = attributes[attribute]?.value != null
|
||||
|
||||
private val mutex = Mutex()
|
||||
// private val mutex = Mutex()
|
||||
|
||||
private val changeCollector = VisionChangeBuilder()
|
||||
|
||||
/**
|
||||
* Communicate vision property changed from rendering engine to model
|
||||
*/
|
||||
override fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) {
|
||||
context.launch {
|
||||
mutex.withLock {
|
||||
changeCollector.propertyChanged(visionName, propertyName, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Communicate vision property changed from rendering engine to model
|
||||
// */
|
||||
// private fun notifyPropertyChanged(visionName: Name, propertyName: Name, item: Meta?) {
|
||||
// context.launch {
|
||||
// mutex.withLock {
|
||||
// changeCollector.propertyChanged(visionName, propertyName, item)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private val eventCollector by lazy {
|
||||
MutableSharedFlow<Pair<Name, VisionEvent>>(meta["feedback.eventCache"].int ?: 100)
|
||||
@ -97,7 +97,7 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
||||
renderer.render(element, name, vision, outputMeta)
|
||||
}
|
||||
|
||||
private fun startVisionUpdate(element: Element, name: Name, vision: Vision?, outputMeta: Meta) {
|
||||
private fun startVisionUpdate(element: Element, visionName: Name, vision: Vision, outputMeta: Meta) {
|
||||
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
|
||||
val wsUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) {
|
||||
val endpoint = resolveEndpoint(element)
|
||||
@ -109,9 +109,10 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
||||
URL(attr.value)
|
||||
}.apply {
|
||||
protocol = "ws"
|
||||
searchParams.append("name", name.toString())
|
||||
searchParams.append("name", visionName.toString())
|
||||
}
|
||||
|
||||
|
||||
logger.info { "Updating vision data from $wsUrl" }
|
||||
|
||||
//Individual websocket for this vision
|
||||
@ -127,12 +128,11 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
||||
// If change contains root vision replacement, do it
|
||||
if (event is VisionChange) {
|
||||
event.vision?.let { vision ->
|
||||
renderVision(element, name, vision, outputMeta)
|
||||
renderVision(element, visionName, vision, outputMeta)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug { "Got $event for output with name $name" }
|
||||
if (vision == null) error("Can't update vision because it is not loaded.")
|
||||
logger.debug { "Got $event for output with name $visionName" }
|
||||
vision.receiveEvent(event)
|
||||
} else {
|
||||
logger.error { "WebSocket message data is not a string" }
|
||||
@ -147,32 +147,44 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
||||
val feedbackAggregationTime = meta["feedback.aggregationTime"]?.int ?: 300
|
||||
|
||||
onopen = {
|
||||
|
||||
|
||||
val mutex = Mutex()
|
||||
|
||||
val changeCollector = VisionChangeBuilder()
|
||||
|
||||
feedbackJob = visionManager.context.launch {
|
||||
eventCollector.filter { it.first == name }.onEach {
|
||||
//launch a separate coroutine to send events to the backend
|
||||
eventCollector.filter { it.first == visionName }.onEach {
|
||||
send(visionManager.jsonFormat.encodeToString(VisionEvent.serializer(), it.second))
|
||||
}.launchIn(this)
|
||||
|
||||
//launch backward property propagation
|
||||
vision.properties.changes.onEach { propertyName: Name ->
|
||||
changeCollector.propertyChanged(visionName, propertyName, vision.properties.getMeta(propertyName))
|
||||
}.launchIn(this)
|
||||
|
||||
//aggregate atomic changes
|
||||
while (isActive) {
|
||||
delay(feedbackAggregationTime.milliseconds)
|
||||
val change = changeCollector[name] ?: continue
|
||||
if (!change.isEmpty()) {
|
||||
if (!changeCollector.isEmpty()) {
|
||||
mutex.withLock {
|
||||
eventCollector.emit(name to change.deepCopy(visionManager))
|
||||
change.reset()
|
||||
eventCollector.emit(visionName to changeCollector.deepCopy(visionManager))
|
||||
changeCollector.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info { "WebSocket feedback channel established for output '$name'" }
|
||||
logger.info { "WebSocket feedback channel established for output '$visionName'" }
|
||||
}
|
||||
|
||||
onclose = {
|
||||
feedbackJob?.cancel()
|
||||
logger.info { "WebSocket feedback channel closed for output '$name'" }
|
||||
logger.info { "WebSocket feedback channel closed for output '$visionName'" }
|
||||
}
|
||||
onerror = {
|
||||
feedbackJob?.cancel()
|
||||
logger.error { "WebSocket feedback channel error for output '$name'" }
|
||||
logger.error { "WebSocket feedback channel error for output '$visionName'" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,9 +253,9 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
||||
}
|
||||
|
||||
//Try to load vision via websocket
|
||||
element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> {
|
||||
startVisionUpdate(element, name, null, outputMeta)
|
||||
}
|
||||
// element.attributes[OUTPUT_CONNECT_ATTRIBUTE] != null -> {
|
||||
// startVisionUpdate(element, name, null, outputMeta)
|
||||
// }
|
||||
|
||||
else -> error("No embedded vision data / fetch url for $name")
|
||||
}
|
||||
@ -252,9 +264,12 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
|
||||
|
||||
override fun content(target: String): Map<Name, Any> = if (target == ElementVisionRenderer.TYPE) {
|
||||
listOf(
|
||||
numberVisionRenderer(),
|
||||
textVisionRenderer(),
|
||||
formVisionRenderer()
|
||||
inputVisionRenderer,
|
||||
checkboxVisionRenderer,
|
||||
numberVisionRenderer,
|
||||
textVisionRenderer,
|
||||
rangeVisionRenderer,
|
||||
formVisionRenderer
|
||||
).associateByName()
|
||||
} else super<AbstractPlugin>.content(target)
|
||||
|
||||
|
@ -3,10 +3,10 @@ package space.kscience.visionforge
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.html.InputType
|
||||
import kotlinx.html.js.input
|
||||
import kotlinx.html.js.onChangeFunction
|
||||
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
|
||||
@ -14,7 +14,11 @@ import space.kscience.dataforge.context.logger
|
||||
import space.kscience.dataforge.meta.*
|
||||
import space.kscience.visionforge.html.*
|
||||
|
||||
|
||||
/**
|
||||
* Subscribes the HTML element to a given vision.
|
||||
*
|
||||
* @param vision The vision to subscribe to.
|
||||
*/
|
||||
private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
|
||||
vision.useProperty(VisionOfHtml::classes) {
|
||||
classList.value = classes.joinToString(separator = " ")
|
||||
@ -22,6 +26,11 @@ private fun HTMLElement.subscribeToVision(vision: VisionOfHtml) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Subscribes the HTML input element to a given vision.
|
||||
*
|
||||
* @param inputVision The input vision to subscribe to.
|
||||
*/
|
||||
private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
|
||||
subscribeToVision(inputVision)
|
||||
inputVision.useProperty(VisionOfHtmlInput::disabled) {
|
||||
@ -29,33 +38,123 @@ private fun HTMLInputElement.subscribeToInput(inputVision: VisionOfHtmlInput) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun JsVisionClient.textVisionRenderer(): ElementVisionRenderer =
|
||||
ElementVisionRenderer<VisionOfTextField> { visionName, vision, _ ->
|
||||
internal val inputVisionRenderer: ElementVisionRenderer =
|
||||
ElementVisionRenderer<VisionOfHtmlInput>(acceptRating = ElementVisionRenderer.DEFAULT_RATING - 1) { _, vision, _ ->
|
||||
input {
|
||||
type = InputType.text
|
||||
onChangeFunction = {
|
||||
notifyPropertyChanged(visionName, VisionOfTextField::text.name, value)
|
||||
}
|
||||
}.apply {
|
||||
val onEvent: (Event) -> Unit = {
|
||||
vision.value = value.asValue()
|
||||
}
|
||||
|
||||
|
||||
when (vision.feedbackMode) {
|
||||
InputFeedbackMode.ONCHANGE -> onchange = onEvent
|
||||
|
||||
InputFeedbackMode.ONINPUT -> oninput = onEvent
|
||||
InputFeedbackMode.NONE -> {}
|
||||
}
|
||||
|
||||
subscribeToInput(vision)
|
||||
vision.useProperty(VisionOfHtmlInput::value) {
|
||||
this@apply.value = it?.string ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val checkboxVisionRenderer: ElementVisionRenderer =
|
||||
ElementVisionRenderer<VisionOfCheckbox> { _, vision, _ ->
|
||||
input {
|
||||
type = InputType.checkBox
|
||||
}.apply {
|
||||
val onEvent: (Event) -> Unit = {
|
||||
vision.checked = checked
|
||||
}
|
||||
|
||||
|
||||
when (vision.feedbackMode) {
|
||||
InputFeedbackMode.ONCHANGE -> onchange = onEvent
|
||||
|
||||
InputFeedbackMode.ONINPUT -> oninput = onEvent
|
||||
InputFeedbackMode.NONE -> {}
|
||||
}
|
||||
|
||||
subscribeToInput(vision)
|
||||
vision.useProperty(VisionOfCheckbox::checked) {
|
||||
this@apply.checked = it ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val textVisionRenderer: ElementVisionRenderer =
|
||||
ElementVisionRenderer<VisionOfTextField> { _, vision, _ ->
|
||||
input {
|
||||
type = InputType.text
|
||||
}.apply {
|
||||
val onEvent: (Event) -> Unit = {
|
||||
vision.text = value
|
||||
}
|
||||
|
||||
|
||||
when (vision.feedbackMode) {
|
||||
InputFeedbackMode.ONCHANGE -> onchange = onEvent
|
||||
|
||||
InputFeedbackMode.ONINPUT -> oninput = onEvent
|
||||
InputFeedbackMode.NONE -> {}
|
||||
}
|
||||
|
||||
subscribeToInput(vision)
|
||||
vision.useProperty(VisionOfTextField::text) {
|
||||
value = (it ?: "").asValue()
|
||||
this@apply.value = it ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun JsVisionClient.numberVisionRenderer(): ElementVisionRenderer =
|
||||
ElementVisionRenderer<VisionOfNumberField> { visionName, vision, _ ->
|
||||
internal val numberVisionRenderer: ElementVisionRenderer =
|
||||
ElementVisionRenderer<VisionOfNumberField> { _, vision, _ ->
|
||||
input {
|
||||
type = InputType.text
|
||||
onChangeFunction = {
|
||||
notifyPropertyChanged(visionName, VisionOfNumberField::value.name, value)
|
||||
}
|
||||
}.apply {
|
||||
|
||||
val onEvent: (Event) -> Unit = {
|
||||
value.toDoubleOrNull()?.let { vision.number = it }
|
||||
}
|
||||
|
||||
when (vision.feedbackMode) {
|
||||
InputFeedbackMode.ONCHANGE -> onchange = onEvent
|
||||
|
||||
InputFeedbackMode.ONINPUT -> oninput = onEvent
|
||||
InputFeedbackMode.NONE -> {}
|
||||
}
|
||||
subscribeToInput(vision)
|
||||
vision.useProperty(VisionOfNumberField::value) {
|
||||
value = (it?.double ?: 0.0).asValue()
|
||||
this@apply.valueAsNumber = it?.double ?: 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val rangeVisionRenderer: ElementVisionRenderer =
|
||||
ElementVisionRenderer<VisionOfRangeField> { _, vision, _ ->
|
||||
input {
|
||||
type = InputType.text
|
||||
min = vision.min.toString()
|
||||
max = vision.max.toString()
|
||||
step = vision.step.toString()
|
||||
}.apply {
|
||||
|
||||
val onEvent: (Event) -> Unit = {
|
||||
value.toDoubleOrNull()?.let { vision.number = it }
|
||||
}
|
||||
|
||||
when (vision.feedbackMode) {
|
||||
InputFeedbackMode.ONCHANGE -> onchange = onEvent
|
||||
|
||||
InputFeedbackMode.ONINPUT -> oninput = onEvent
|
||||
InputFeedbackMode.NONE -> {}
|
||||
}
|
||||
subscribeToInput(vision)
|
||||
vision.useProperty(VisionOfRangeField::value) {
|
||||
this@apply.valueAsNumber = it?.double ?: 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -83,7 +182,7 @@ internal fun FormData.toMeta(): Meta {
|
||||
return DynamicMeta(`object`)
|
||||
}
|
||||
|
||||
internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer =
|
||||
internal val formVisionRenderer: ElementVisionRenderer =
|
||||
ElementVisionRenderer<VisionOfHtmlForm> { visionName, vision, _ ->
|
||||
|
||||
val form = document.getElementById(vision.formId) as? HTMLFormElement
|
||||
@ -91,10 +190,10 @@ internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer =
|
||||
|
||||
form.subscribeToVision(vision)
|
||||
|
||||
logger.debug { "Adding hooks to form with id = '$vision.formId'" }
|
||||
vision.manager?.logger?.debug { "Adding hooks to form with id = '$vision.formId'" }
|
||||
|
||||
vision.useProperty(VisionOfHtmlForm::values) { values ->
|
||||
logger.debug { "Updating form '${vision.formId}' with 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()
|
||||
@ -104,7 +203,7 @@ internal fun JsVisionClient.formVisionRenderer(): ElementVisionRenderer =
|
||||
form.onsubmit = { event ->
|
||||
event.preventDefault()
|
||||
val formData = FormData(form).toMeta()
|
||||
notifyPropertyChanged(visionName, VisionOfHtmlForm::values.name, formData)
|
||||
vision.values = formData
|
||||
console.info("Sent: ${formData.toMap()}")
|
||||
false
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user