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.reflect.KProperty
fun <T> RBuilder.initState(init: () -> T): ReadWriteProperty<Any?, T> =
object : ReadWriteProperty<Any?, T> {
val pair = react.useState(init)

View File

@ -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<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() {
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<Meta>? = 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<NameToken> {
(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<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) {
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)
}

View File

@ -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) {

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 {
color: gray;
color: lightgrey;
}
.tree-label-selected{

View File

@ -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()

View File

@ -80,7 +80,7 @@ abstract class MeshThreeFactory<in T : VisualObject3D>(
}
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"
}
)
}

View File

@ -43,11 +43,6 @@ class ThreeCanvasComponent : RComponent<ThreeCanvasProps, ThreeCanvasState>() {
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()

View File

@ -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<MMAppProps, MMAppState>() {
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<MMAppProps> { 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<Event>("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<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
}
}
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<NameToken>(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<NameToken>(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<MMAppProps, MMAppState>() {
}
}
}
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())
}
}
}

View File

@ -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