Fix react properties update bug

This commit is contained in:
Alexander Nozik 2020-04-19 22:33:51 +03:00
parent 38db0e30ed
commit e0346b7db5
11 changed files with 311 additions and 321 deletions

View File

@ -4,6 +4,7 @@ import react.RBuilder
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
fun <T> RBuilder.initState(init: () -> T): ReadWriteProperty<Any?, T> = fun <T> RBuilder.initState(init: () -> T): ReadWriteProperty<Any?, T> =
object : ReadWriteProperty<Any?, T> { object : ReadWriteProperty<Any?, T> {
val pair = react.useState(init) val pair = react.useState(init)

View File

@ -1,28 +1,21 @@
package hep.dataforge.vis.editor package hep.dataforge.vis.editor
import hep.dataforge.js.initState
import hep.dataforge.meta.* import hep.dataforge.meta.*
import hep.dataforge.meta.descriptors.* import hep.dataforge.meta.descriptors.*
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.NameToken import hep.dataforge.names.NameToken
import hep.dataforge.names.plus import hep.dataforge.names.plus
import hep.dataforge.values.* import hep.dataforge.values.Value
import hep.dataforge.vis.widgetType
import kotlinx.html.ButtonType
import kotlinx.html.InputType
import kotlinx.html.classes import kotlinx.html.classes
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSelectElement
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
import react.RBuilder import react.*
import react.RComponent
import react.RProps
import react.dom.* import react.dom.*
import react.setState
interface ConfigEditorProps : RProps { interface ConfigEditorProps : RProps {
/** /**
* Root config object - always non null * Root config object - always non null
*/ */
@ -42,239 +35,160 @@ interface ConfigEditorProps : RProps {
* Root descriptor * Root descriptor
*/ */
var descriptor: NodeDescriptor? var descriptor: NodeDescriptor?
} }
class ConfigEditorComponent : RComponent<ConfigEditorProps, TreeState>() { 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() { val token = props.name.last()?.toString() ?: "Properties"
expanded = true
var kostyl by initState { false }
fun update() {
kostyl = !kostyl
} }
override fun componentDidMount() { useEffectWithCleanup(listOf(props.root)) {
props.root.onChange(this) { name, _, _ -> props.root.onChange(this) { name, _, _ ->
if (name == props.name) { if (name == props.name) {
forceUpdate() update()
} }
} }
return@useEffectWithCleanup { props.root.removeListener(this) }
} }
override fun componentWillUnmount() { val actualItem: MetaItem<Meta>? = item ?: defaultItem ?: descriptorItem?.defaultItem()
props.root.removeListener(this)
val expanderClick: (Event) -> Unit = {
expanded = !expanded
} }
private val onClick: (Event) -> Unit = { val removeClick: (Event) -> Unit = {
setState { props.root.remove(props.name)
expanded = !expanded update()
}
} }
private val onValueChange: (Event) -> Unit = { val valueChanged: (Value?) -> Unit = { value ->
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")
}
try { 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) { } catch (ex: Exception) {
console.error("Can't set config property ${props.name} to $value") 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 when (actualItem) {
private fun RBuilder.valueChooser(value: Value, descriptor: ValueDescriptor?) { is MetaItem.NodeItem -> {
val type = descriptor?.type?.firstOrNull() div {
when { span("tree-caret") {
type == ValueType.BOOLEAN -> {
input(type = InputType.checkBox, classes = "float-right") {
attrs { attrs {
defaultChecked = value.boolean if (expanded) {
onChangeFunction = onValueChange 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") { if (expanded) {
attrs { ul("tree") {
descriptor.attributes["step"].string?.let { val keys = buildSet<NameToken> {
step = it (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
} }
} }
} }
is MetaItem.ValueItem -> {
} div {
div("row") {
div("col") {
override fun RBuilder.render() { p("tree-label") {
val item = props.root[props.name] +token
val descriptorItem: ItemDescriptor? = props.descriptor?.get(props.name) attrs {
val defaultItem = props.default?.get(props.name) if (item == null) {
val actualItem = item ?: defaultItem ?: descriptorItem?.defaultItem() classes += "tree-label-inactive"
val token = props.name.last()?.toString() ?: "Properties" }
when (actualItem) {
is MetaItem.NodeItem -> {
div {
span("tree-caret") {
attrs {
if (state.expanded) {
classes += "tree-caret-down"
} }
onClickFunction = onClick
} }
} }
span("tree-label") { div("col") {
+token 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 { attrs {
if (item == null) { if (item == null) {
classes += "tree-label-inactive" disabled = true
} else {
onClickFunction = removeClick
} }
} }
} }
}
if (state.expanded) {
ul("tree") {
val keys = buildSet<NameToken> {
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<ConfigEditorProps> = 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) { fun Element.configEditor(config: Config, descriptor: NodeDescriptor? = null, default: Meta? = null) {
render(this) { render(this) {
child(ConfigEditorComponent::class) { configEditor(config, Name.EMPTY, descriptor, default)
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
}
}
} }
} }
fun RBuilder.configEditor(obj: Configurable, descriptor: NodeDescriptor? = obj.descriptor, default: Meta? = null) { 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)
} }

View File

@ -34,7 +34,7 @@ private fun RBuilder.objectTree(props: ObjectTreeProps): Unit {
} }
fun RBuilder.treeLabel(text: String) { 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 +text
attrs { attrs {
if (props.name == props.selected) { if (props.name == props.selected) {

View File

@ -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<ValueChooserProps> { 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
}
}
}
}
}

View File

@ -1,11 +0,0 @@
package hep.dataforge.vis.editor
//val TextValueChooser = functionalComponent<ConfigEditorProps> {
//
// input(type = InputType.number, classes = "float-right") {
// attrs {
// defaultValue = value.string
// onChangeFunction = onValueChange
// }
// }
//}

View File

@ -36,7 +36,7 @@ ul, .tree {
} }
.tree-label-inactive { .tree-label-inactive {
color: gray; color: lightgrey;
} }
.tree-label-selected{ .tree-label-selected{

View File

@ -24,6 +24,8 @@ interface VisualObject3D : VisualObject {
var rotation: Point3D? var rotation: Point3D?
var scale: Point3D? var scale: Point3D?
override val descriptor: NodeDescriptor? get() = Companion.descriptor
companion object { companion object {
val VISIBLE_KEY = "visible".asName() val VISIBLE_KEY = "visible".asName()

View File

@ -80,7 +80,7 @@ abstract class MeshThreeFactory<in T : VisualObject3D>(
} }
fun Mesh.applyEdges(obj: VisualObject3D) { fun Mesh.applyEdges(obj: VisualObject3D) {
children.find { it.name == "edges" }?.let { children.find { it.name == "@edges" }?.let {
remove(it) remove(it)
(it as LineSegments).dispose() (it as LineSegments).dispose()
} }
@ -93,14 +93,14 @@ fun Mesh.applyEdges(obj: VisualObject3D) {
EdgesGeometry(geometry as BufferGeometry), EdgesGeometry(geometry as BufferGeometry),
material material
).apply { ).apply {
name = "edges" name = "@edges"
} }
) )
} }
} }
fun Mesh.applyWireFrame(obj: VisualObject3D) { fun Mesh.applyWireFrame(obj: VisualObject3D) {
children.find { it.name == "wireframe" }?.let { children.find { it.name == "@wireframe" }?.let {
remove(it) remove(it)
(it as LineSegments).dispose() (it as LineSegments).dispose()
} }
@ -112,7 +112,7 @@ fun Mesh.applyWireFrame(obj: VisualObject3D) {
WireframeGeometry(geometry as BufferGeometry), WireframeGeometry(geometry as BufferGeometry),
material material
).apply { ).apply {
name = "wireframe" name = "@wireframe"
} }
) )
} }

View File

@ -43,11 +43,6 @@ class ThreeCanvasComponent : RComponent<ThreeCanvasProps, ThreeCanvasState>() {
canvas?.render(props.obj) canvas?.render(props.obj)
} }
// override fun componentWillUnmount() {
// state.element?.clear()
// props.canvasCallback?.invoke(null)
// }
override fun componentDidUpdate(prevProps: ThreeCanvasProps, prevState: ThreeCanvasState, snapshot: Any) { override fun componentDidUpdate(prevProps: ThreeCanvasProps, prevState: ThreeCanvasState, snapshot: Any) {
if (prevProps.obj != props.obj) { if (prevProps.obj != props.obj) {
componentDidMount() componentDidMount()

View File

@ -2,13 +2,13 @@ package ru.mipt.npm.muon.monitor
import hep.dataforge.context.Context import hep.dataforge.context.Context
import hep.dataforge.js.card import hep.dataforge.js.card
import hep.dataforge.js.initState
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.NameToken import hep.dataforge.names.NameToken
import hep.dataforge.names.isEmpty import hep.dataforge.names.isEmpty
import hep.dataforge.vis.VisualObject import hep.dataforge.vis.VisualObject
import hep.dataforge.vis.editor.configEditor import hep.dataforge.vis.editor.configEditor
import hep.dataforge.vis.editor.objectTree 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.Camera
import hep.dataforge.vis.spatial.specifications.Canvas import hep.dataforge.vis.spatial.specifications.Canvas
import hep.dataforge.vis.spatial.three.ThreeCanvas import hep.dataforge.vis.spatial.three.ThreeCanvas
@ -19,129 +19,117 @@ import io.ktor.client.request.get
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import react.* import react.RProps
import react.dom.* import react.dom.*
import react.functionalComponent
import kotlin.math.PI import kotlin.math.PI
interface MMAppProps : RProps { interface MMAppProps : RProps {
var model: Model var model: Model
var context: Context var context: Context
var connection: HttpClient var connection: HttpClient
}
interface MMAppState : RState {
var selected: Name? var selected: Name?
var canvas: ThreeCanvas?
} }
class MMAppComponent : RComponent<MMAppProps, MMAppState>() { private val canvasConfig = Canvas {
camera = Camera {
distance = 2100.0
latitude = PI / 6
azimuth = PI + PI / 6
}
}
private val onSelect: (Name?) -> Unit = { val MMApp = functionalComponent<MMAppProps> { props ->
setState { var selected by initState { props.selected }
selected = it var canvas: ThreeCanvas? by initState { null }
}
val select: (Name?) -> Unit = {
selected = it
} }
private val canvasConfig = Canvas { val visual = props.model.root
camera = Camera {
distance = 2100.0 div("row") {
latitude = PI / 6 div("col-lg-3") {
azimuth = PI + PI / 6 //tree
card("Object tree") {
objectTree(visual, selected, select)
}
} }
} div("col-lg-6") {
//canvas
override fun RBuilder.render() { child(ThreeCanvasComponent::class) {
val visual = props.model.root attrs {
val selected = state.selected this.context = props.context
this.obj = visual
div("row") { this.options = canvasConfig
div("col-lg-3") { this.selected = selected
//tree this.clickCallback = select
card("Object tree") { this.canvasCallback = {
objectTree(visual, selected, onSelect) canvas = it
}
} }
} }
div("col-lg-6") { }
//canvas div("col-lg-3") {
child(ThreeCanvasComponent::class) { div("row") {
attrs { //settings
this.context = props.context canvas?.let {
this.obj = visual card("Canvas configuration") {
this.options = canvasConfig canvasControls(it)
this.selected = selected }
this.clickCallback = onSelect }
this.canvasCallback = { card("Events") {
setState { button {
canvas = it +"Next"
attrs {
onClickFunction = {
GlobalScope.launch {
val event = props.connection.get<Event>("http://localhost:8080/event")
props.model.displayEvent(event)
}
}
}
}
button {
+"Clear"
attrs {
onClickFunction = {
props.model.reset()
} }
} }
} }
} }
} }
div("col-lg-3") { div("row") {
div("row") { div("container-fluid p-0") {
//settings nav {
state.canvas?.let { attrs {
card("Canvas configuration") { attributes["aria-label"] = "breadcrumb"
canvasControls(it)
} }
} ol("breadcrumb") {
card("Events") { li("breadcrumb-item") {
button { button(classes = "btn btn-link p-0") {
+"Next" +"World"
attrs { attrs {
onClickFunction = { onClickFunction = {
GlobalScope.launch { selected = hep.dataforge.names.Name.EMPTY
val event = props.connection.get<Event>("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
}
}
} }
} }
} }
if (selected != null) { }
val tokens = ArrayList<NameToken>(selected.length) if (selected != null) {
selected.tokens.forEach { token -> val tokens = ArrayList<NameToken>(selected?.length ?: 1)
tokens.add(token) selected?.tokens?.forEach { token ->
val fullName = Name(tokens.toList()) tokens.add(token)
li("breadcrumb-item") { val fullName = Name(tokens.toList())
button(classes = "btn btn-link p-0") { li("breadcrumb-item") {
+token.toString() button(classes = "btn btn-link p-0") {
attrs { +token.toString()
onClickFunction = { attrs {
setState { onClickFunction = {
console.log("Selected = $fullName") console.log("Selected = $fullName")
this.selected = fullName selected = fullName
}
}
} }
} }
} }
@ -151,17 +139,18 @@ class MMAppComponent : RComponent<MMAppProps, MMAppState>() {
} }
} }
} }
div("row") { }
//properties div("row") {
if (selected != null) { //properties
card("Properties") {
selected.let { selected ->
val selectedObject: VisualObject? = when { val selectedObject: VisualObject? = when {
selected == null -> null
selected.isEmpty() -> visual selected.isEmpty() -> visual
else -> visual[selected] else -> visual[selected]
} }
if (selectedObject != null) { if (selectedObject != null) {
card("Properties") { configEditor(selectedObject, default = selectedObject.allProperties())
configEditor(selectedObject, descriptor = VisualObject3D.descriptor)
}
} }
} }
} }

View File

@ -8,6 +8,7 @@ import io.ktor.client.HttpClient
import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer import io.ktor.client.features.json.serializer.KotlinxSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import react.child
import react.dom.render import react.dom.render
import kotlin.browser.document 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") val element = document.getElementById("app") ?: error("Element with id 'app' not found on page")
render(element) { render(element) {
child(MMAppComponent::class) { child(MMApp) {
attrs { attrs {
model = this@MMDemoApp.model model = this@MMDemoApp.model
connection = this@MMDemoApp.connection connection = this@MMDemoApp.connection