diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt index 3b04817b..5f543dbe 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/react.kt @@ -4,6 +4,7 @@ import react.RBuilder import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty + fun RBuilder.initState(init: () -> T): ReadWriteProperty = object : ReadWriteProperty { val pair = react.useState(init) diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt index e283c92b..a360eca3 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ConfigEditorComponent.kt @@ -1,28 +1,21 @@ package hep.dataforge.vis.editor +import hep.dataforge.js.initState import hep.dataforge.meta.* import hep.dataforge.meta.descriptors.* import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.plus -import hep.dataforge.values.* -import hep.dataforge.vis.widgetType -import kotlinx.html.ButtonType -import kotlinx.html.InputType +import hep.dataforge.values.Value import kotlinx.html.classes -import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onClickFunction import org.w3c.dom.Element -import org.w3c.dom.HTMLInputElement -import org.w3c.dom.HTMLSelectElement import org.w3c.dom.events.Event -import react.RBuilder -import react.RComponent -import react.RProps +import react.* import react.dom.* -import react.setState interface ConfigEditorProps : RProps { + /** * Root config object - always non null */ @@ -42,239 +35,160 @@ interface ConfigEditorProps : RProps { * Root descriptor */ var descriptor: NodeDescriptor? - } -class ConfigEditorComponent : RComponent() { +private fun RBuilder.configEditorItem(props: ConfigEditorProps) { + var expanded: Boolean by initState { true } + val item = props.root[props.name] + val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name) + val defaultItem = props.default?.get(props.name) - override fun TreeState.init() { - expanded = true + val token = props.name.last()?.toString() ?: "Properties" + + var kostyl by initState { false } + + fun update() { + kostyl = !kostyl } - override fun componentDidMount() { + useEffectWithCleanup(listOf(props.root)) { props.root.onChange(this) { name, _, _ -> if (name == props.name) { - forceUpdate() + update() } } + return@useEffectWithCleanup { props.root.removeListener(this) } } - override fun componentWillUnmount() { - props.root.removeListener(this) + val actualItem: MetaItem? = item ?: defaultItem ?: descriptorItem?.defaultItem() + + val expanderClick: (Event) -> Unit = { + expanded = !expanded } - private val onClick: (Event) -> Unit = { - setState { - expanded = !expanded - } + val removeClick: (Event) -> Unit = { + props.root.remove(props.name) + update() } - private val onValueChange: (Event) -> Unit = { - val value = when (val t = it.target) { - // (it.target as HTMLInputElement).value - is HTMLInputElement -> if (t.type == "checkbox") { - if (t.checked) True else False - } else { - t.value.asValue() - } - is HTMLSelectElement -> t.value.asValue() - else -> error("Unknown event target: $t") - } + val valueChanged: (Value?) -> Unit = { value -> try { - props.root.setValue(props.name, value) + if (value == null) { + props.root.remove(props.name) + } else { + props.root.setValue(props.name, value) + } + update() } catch (ex: Exception) { console.error("Can't set config property ${props.name} to $value") } } - private val removeValue: (Event) -> Unit = { - props.root.remove(props.name) - } - //TODO replace by separate components - private fun RBuilder.valueChooser(value: Value, descriptor: ValueDescriptor?) { - val type = descriptor?.type?.firstOrNull() - when { - type == ValueType.BOOLEAN -> { - input(type = InputType.checkBox, classes = "float-right") { + when (actualItem) { + is MetaItem.NodeItem -> { + div { + span("tree-caret") { attrs { - defaultChecked = value.boolean - onChangeFunction = onValueChange + if (expanded) { + classes += "tree-caret-down" + } + onClickFunction = expanderClick + } + } + span("tree-label") { + +token + attrs { + if (item == null) { + classes += "tree-label-inactive" + } } } } - type == ValueType.NUMBER -> input(type = InputType.number, classes = "float-right") { - attrs { - descriptor.attributes["step"].string?.let { - step = it + if (expanded) { + ul("tree") { + val keys = buildSet { + (descriptorItem as? NodeDescriptor)?.items?.keys?.forEach { + add(NameToken(it)) + } + item?.node?.items?.keys?.let { addAll(it) } + defaultItem?.node?.items?.keys?.let { addAll(it) } } - descriptor.attributes["min"].string?.let { - min = it + + keys.forEach { token -> + li("tree-item align-middle") { + configEditor(props.root, props.name + token, props.descriptor, props.default) + } } - descriptor.attributes["max"].string?.let { - max = it - } - defaultValue = value.string - onChangeFunction = onValueChange - } - } - descriptor?.allowedValues?.isNotEmpty() ?: false -> select("float-right") { - descriptor!!.allowedValues.forEach { - option { - +it.string - } - } - attrs { - multiple = false - onChangeFunction = onValueChange - } - } - descriptor?.widgetType == "color" -> input(type = InputType.color, classes = "float-right") { - attrs { - defaultValue = value.string - onChangeFunction = onValueChange - } - } - else -> input(type = InputType.text, classes = "float-right") { - attrs { - defaultValue = value.string - onChangeFunction = onValueChange } } } - - } - - - override fun RBuilder.render() { - val item = props.root[props.name] - val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name) - val defaultItem = props.default?.get(props.name) - val actualItem = item ?: defaultItem ?: descriptorItem?.defaultItem() - val token = props.name.last()?.toString() ?: "Properties" - - when (actualItem) { - is MetaItem.NodeItem -> { - div { - span("tree-caret") { - attrs { - if (state.expanded) { - classes += "tree-caret-down" + is MetaItem.ValueItem -> { + div { + div("row") { + div("col") { + p("tree-label") { + +token + attrs { + if (item == null) { + classes += "tree-label-inactive" + } } - onClickFunction = onClick } } - span("tree-label") { - +token + div("col") { + console.log("1: Setting ${props.name} to ${actualItem.value}") + val value = actualItem.value + child(ValueChooser) { + attrs { + console.log("2: Setting ${props.name} to $value") + this.value = value + this.descriptor = descriptorItem as? ValueDescriptor + this.valueChanged = valueChanged + } + } + } + button(classes = "btn btn-link") { + +"\u00D7" attrs { if (item == null) { - classes += "tree-label-inactive" + disabled = true + } else { + onClickFunction = removeClick } } } - } - if (state.expanded) { - ul("tree") { - val keys = buildSet { - item?.node?.items?.keys?.let { addAll(it) } - defaultItem?.node?.items?.keys?.let { addAll(it) } - (descriptorItem as? NodeDescriptor)?.items?.keys?.forEach { - add(NameToken(it)) - } - } - keys.forEach { token -> - li("tree-item") { - child(ConfigEditorComponent::class) { - attrs { - this.root = props.root - this.name = props.name + token - this.default = props.default - this.descriptor = props.descriptor - } - } - } - } - } } } - is MetaItem.ValueItem -> { - div { - div("row") { - div("col") { - p("tree-label") { - +token - attrs { - if (item == null) { - classes += "tree-label-inactive" - } - } - } - } - div("col") { - valueChooser(actualItem.value, descriptorItem as? ValueDescriptor) - } - div("col-auto") { - div("dropleft p-0") { - button(classes = "btn btn-outline-primary") { - attrs { - type = ButtonType.button - attributes["data-toggle"] = "dropdown" - attributes["aria-haspopup"] = "true" - attributes["aria-expanded"] = "false" - attributes["data-boundary"] = "viewport" - } - +"\u22ee" - } - div(classes = "dropdown-menu") { - button(classes = "btn btn-outline dropdown-item") { - +"Info" - } - if (item != null) { - button(classes = "btn btn-outline dropdown-item") { - +"""Clear""" - } - attrs { - onClickFunction = removeValue - } - } + } + } +} - } - } - } - } - } - } +val ConfigEditor: FunctionalComponent = functionalComponent { configEditorItem(it) } + +fun RBuilder.configEditor( + config: Config, + name: Name = Name.EMPTY, + descriptor: NodeDescriptor? = null, + default: Meta? = null +) { + child(ConfigEditor) { + attrs { + this.root = config + this.name = name + this.descriptor = descriptor + this.default = default } } } fun Element.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null) { render(this) { - child(ConfigEditorComponent::class) { - attrs { - root = config - name = Name.EMPTY - this.descriptor = descriptor - this.default = default - } - } - } -} - -fun RBuilder.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null) { - div { - child(ConfigEditorComponent::class) { - attrs { - root = config - name = Name.EMPTY - this.descriptor = descriptor - this.default = default - } - } + configEditor(config, Name.EMPTY, descriptor, default) } } fun RBuilder.configEditor(obj: Configurable, descriptor: NodeDescriptor? = obj.descriptor, default: Meta? = null) { - configEditor(obj.config, descriptor ?: obj.descriptor, default) + configEditor(obj.config, Name.EMPTY, descriptor ?: obj.descriptor, default) } diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt index 64624f3c..bae57a29 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ObjectTree.kt @@ -34,7 +34,7 @@ private fun RBuilder.objectTree(props: ObjectTreeProps): Unit { } fun RBuilder.treeLabel(text: String) { - button(classes = "btn btn-link tree-label p-0") { + button(classes = "btn btn-link align-middle tree-label p-0") { +text attrs { if (props.name == props.selected) { diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ValueChooser.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ValueChooser.kt new file mode 100644 index 00000000..71323338 --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/ValueChooser.kt @@ -0,0 +1,99 @@ +package hep.dataforge.vis.editor + +import hep.dataforge.meta.descriptors.ValueDescriptor +import hep.dataforge.meta.get +import hep.dataforge.meta.string +import hep.dataforge.values.* +import hep.dataforge.vis.widgetType +import kotlinx.html.InputType +import kotlinx.html.js.onChangeFunction +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.HTMLSelectElement +import org.w3c.dom.events.Event +import react.RProps +import react.dom.div +import react.dom.input +import react.dom.option +import react.dom.select +import react.functionalComponent + +interface ValueChooserProps : RProps { + var value: Value + var descriptor: ValueDescriptor? + var valueChanged: (Value?) -> Unit +} + +val ValueChooser = functionalComponent { props -> +// var state by initState {props.value } + val descriptor = props.descriptor + + console.log("3: Set ${props.value}") + + val onValueChange: (Event) -> Unit = { + val res = when (val t = it.target) { + // (it.target as HTMLInputElement).value + is HTMLInputElement -> if (t.type == "checkbox") { + if (t.checked) True else False + } else { + t.value.asValue() + } + is HTMLSelectElement -> t.value.asValue() + else -> error("Unknown event target: $t") + } +// state = res + props.valueChanged(res) + } + + div { + val type = descriptor?.type?.firstOrNull() + when { + type == ValueType.BOOLEAN -> { + input(type = InputType.checkBox, classes = "float-right") { + attrs { + checked = props.value.boolean + onChangeFunction = onValueChange + } + } + } + type == ValueType.NUMBER -> input(type = InputType.number, classes = "float-right") { + attrs { + descriptor.attributes["step"].string?.let { + step = it + } + descriptor.attributes["min"].string?.let { + min = it + } + descriptor.attributes["max"].string?.let { + max = it + } + this.value = props.value.string + onChangeFunction = onValueChange + } + } + descriptor?.allowedValues?.isNotEmpty() ?: false -> select("float-right") { + descriptor!!.allowedValues.forEach { + option { + +it.string + } + } + attrs { + multiple = false + onChangeFunction = onValueChange + } + } + descriptor?.widgetType == "color" -> input(type = InputType.color, classes = "float-right") { + attrs { + this.value = props.value.string + onChangeFunction = onValueChange + } + } + else -> input(type = InputType.text, classes = "float-right") { + attrs { + this.value = props.value.string + onChangeFunction = onValueChange + } + } + } + } + +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChoosers.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChoosers.kt deleted file mode 100644 index 8cfc4381..00000000 --- a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/editor/valueChoosers.kt +++ /dev/null @@ -1,11 +0,0 @@ -package hep.dataforge.vis.editor - -//val TextValueChooser = functionalComponent { -// -// input(type = InputType.number, classes = "float-right") { -// attrs { -// defaultValue = value.string -// onChangeFunction = onValueChange -// } -// } -//} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/resources/css/main.css b/dataforge-vis-common/src/jsMain/resources/css/main.css index 3069e64e..d77f3a30 100644 --- a/dataforge-vis-common/src/jsMain/resources/css/main.css +++ b/dataforge-vis-common/src/jsMain/resources/css/main.css @@ -36,7 +36,7 @@ ul, .tree { } .tree-label-inactive { - color: gray; + color: lightgrey; } .tree-label-selected{ diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt index 463326f4..ebcb165c 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/VisualObject3D.kt @@ -24,6 +24,8 @@ interface VisualObject3D : VisualObject { var rotation: Point3D? var scale: Point3D? + override val descriptor: NodeDescriptor? get() = Companion.descriptor + companion object { val VISIBLE_KEY = "visible".asName() diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/MeshThreeFactory.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/MeshThreeFactory.kt index 76adef48..3cb54e1f 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/MeshThreeFactory.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/MeshThreeFactory.kt @@ -80,7 +80,7 @@ abstract class MeshThreeFactory( } fun Mesh.applyEdges(obj: VisualObject3D) { - children.find { it.name == "edges" }?.let { + children.find { it.name == "@edges" }?.let { remove(it) (it as LineSegments).dispose() } @@ -93,14 +93,14 @@ fun Mesh.applyEdges(obj: VisualObject3D) { EdgesGeometry(geometry as BufferGeometry), material ).apply { - name = "edges" + name = "@edges" } ) } } fun Mesh.applyWireFrame(obj: VisualObject3D) { - children.find { it.name == "wireframe" }?.let { + children.find { it.name == "@wireframe" }?.let { remove(it) (it as LineSegments).dispose() } @@ -112,7 +112,7 @@ fun Mesh.applyWireFrame(obj: VisualObject3D) { WireframeGeometry(geometry as BufferGeometry), material ).apply { - name = "wireframe" + name = "@wireframe" } ) } diff --git a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt index 9a529770..37fbc997 100644 --- a/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt +++ b/dataforge-vis-spatial/src/jsMain/kotlin/hep/dataforge/vis/spatial/three/ThreeCanvasComponent.kt @@ -43,11 +43,6 @@ class ThreeCanvasComponent : RComponent() { canvas?.render(props.obj) } -// override fun componentWillUnmount() { -// state.element?.clear() -// props.canvasCallback?.invoke(null) -// } - override fun componentDidUpdate(prevProps: ThreeCanvasProps, prevState: ThreeCanvasState, snapshot: Any) { if (prevProps.obj != props.obj) { componentDidMount() diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt index bbf1f3ee..5b77d1db 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMAppComponent.kt @@ -2,13 +2,13 @@ package ru.mipt.npm.muon.monitor import hep.dataforge.context.Context import hep.dataforge.js.card +import hep.dataforge.js.initState import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.isEmpty import hep.dataforge.vis.VisualObject import hep.dataforge.vis.editor.configEditor import hep.dataforge.vis.editor.objectTree -import hep.dataforge.vis.spatial.VisualObject3D import hep.dataforge.vis.spatial.specifications.Camera import hep.dataforge.vis.spatial.specifications.Canvas import hep.dataforge.vis.spatial.three.ThreeCanvas @@ -19,129 +19,117 @@ import io.ktor.client.request.get import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.html.js.onClickFunction -import react.* +import react.RProps import react.dom.* +import react.functionalComponent import kotlin.math.PI interface MMAppProps : RProps { var model: Model var context: Context var connection: HttpClient -} - -interface MMAppState : RState { var selected: Name? - var canvas: ThreeCanvas? } -class MMAppComponent : RComponent() { +private val canvasConfig = Canvas { + camera = Camera { + distance = 2100.0 + latitude = PI / 6 + azimuth = PI + PI / 6 + } +} - private val onSelect: (Name?) -> Unit = { - setState { - selected = it - } +val MMApp = functionalComponent { props -> + var selected by initState { props.selected } + var canvas: ThreeCanvas? by initState { null } + + val select: (Name?) -> Unit = { + selected = it } - private val canvasConfig = Canvas { - camera = Camera { - distance = 2100.0 - latitude = PI / 6 - azimuth = PI + PI / 6 + val visual = props.model.root + + div("row") { + div("col-lg-3") { + //tree + card("Object tree") { + objectTree(visual, selected, select) + } } - } - - override fun RBuilder.render() { - val visual = props.model.root - val selected = state.selected - - div("row") { - div("col-lg-3") { - //tree - card("Object tree") { - objectTree(visual, selected, onSelect) + div("col-lg-6") { + //canvas + child(ThreeCanvasComponent::class) { + attrs { + this.context = props.context + this.obj = visual + this.options = canvasConfig + this.selected = selected + this.clickCallback = select + this.canvasCallback = { + canvas = it + } } } - div("col-lg-6") { - //canvas - child(ThreeCanvasComponent::class) { - attrs { - this.context = props.context - this.obj = visual - this.options = canvasConfig - this.selected = selected - this.clickCallback = onSelect - this.canvasCallback = { - setState { - canvas = it + } + div("col-lg-3") { + div("row") { + //settings + canvas?.let { + card("Canvas configuration") { + canvasControls(it) + } + } + card("Events") { + button { + +"Next" + attrs { + onClickFunction = { + GlobalScope.launch { + val event = props.connection.get("http://localhost:8080/event") + props.model.displayEvent(event) + } + } + } + } + button { + +"Clear" + attrs { + onClickFunction = { + props.model.reset() } } } } } - div("col-lg-3") { - div("row") { - //settings - state.canvas?.let { - card("Canvas configuration") { - canvasControls(it) + div("row") { + div("container-fluid p-0") { + nav { + attrs { + attributes["aria-label"] = "breadcrumb" } - } - card("Events") { - button { - +"Next" - attrs { - onClickFunction = { - GlobalScope.launch { - val event = props.connection.get("http://localhost:8080/event") - props.model.displayEvent(event) - } - } - } - } - button { - +"Clear" - attrs { - onClickFunction = { - props.model.reset() - } - } - } - } - } - div("row") { - div("container-fluid p-0") { - nav { - attrs { - attributes["aria-label"] = "breadcrumb" - } - ol("breadcrumb") { - li("breadcrumb-item") { - button(classes = "btn btn-link p-0") { - +"World" - attrs { - onClickFunction = { - setState { - this.selected = Name.EMPTY - } - } + ol("breadcrumb") { + li("breadcrumb-item") { + button(classes = "btn btn-link p-0") { + +"World" + attrs { + onClickFunction = { + selected = hep.dataforge.names.Name.EMPTY } } } - if (selected != null) { - val tokens = ArrayList(selected.length) - selected.tokens.forEach { token -> - tokens.add(token) - val fullName = Name(tokens.toList()) - li("breadcrumb-item") { - button(classes = "btn btn-link p-0") { - +token.toString() - attrs { - onClickFunction = { - setState { - console.log("Selected = $fullName") - this.selected = fullName - } - } + } + if (selected != null) { + val tokens = ArrayList(selected?.length ?: 1) + selected?.tokens?.forEach { token -> + tokens.add(token) + val fullName = Name(tokens.toList()) + li("breadcrumb-item") { + button(classes = "btn btn-link p-0") { + +token.toString() + attrs { + onClickFunction = { + console.log("Selected = $fullName") + selected = fullName } } } @@ -151,17 +139,18 @@ class MMAppComponent : RComponent() { } } } - div("row") { - //properties - if (selected != null) { + } + div("row") { + //properties + card("Properties") { + selected.let { selected -> val selectedObject: VisualObject? = when { + selected == null -> null selected.isEmpty() -> visual else -> visual[selected] } if (selectedObject != null) { - card("Properties") { - configEditor(selectedObject, descriptor = VisualObject3D.descriptor) - } + configEditor(selectedObject, default = selectedObject.allProperties()) } } } diff --git a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt index 8346df88..86482004 100644 --- a/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt +++ b/demo/muon-monitor/src/jsMain/kotlin/ru/mipt/npm/muon/monitor/MMDemoApp.kt @@ -8,6 +8,7 @@ import io.ktor.client.HttpClient import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.serializer.KotlinxSerializer import kotlinx.serialization.json.Json +import react.child import react.dom.render import kotlin.browser.document @@ -29,7 +30,7 @@ private class MMDemoApp : Application { val element = document.getElementById("app") ?: error("Element with id 'app' not found on page") render(element) { - child(MMAppComponent::class) { + child(MMApp) { attrs { model = this@MMDemoApp.model connection = this@MMDemoApp.connection