GenieFramework / Stipple.jl

The reactive UI library for interactive data applications with pure Julia.
MIT License
324 stars 27 forks source link

[Question] Accessing Model sub-structures #47

Closed PhilipVinc closed 1 year ago

PhilipVinc commented 3 years ago

Hello! Let me begin by thanking you for the amazing framework, I started playing with Stipple two days ago and I find it rather pleasant to use (except for the lack of docstrings, but well, that's life).

I am trying to develop a fairly complex dashboard to monitor my simulations, and would like to split my model, which otherwise will have more than 100 fields, into several sub-structures for code clarity and maintainability/sanity.

However, I need to be able to read/set those fields from some UI elements like Selector, Checkboxes or plot them.

Is it possible to turn something like

Base.@kwdef mutable struct Model <: ReactiveModel
  group1_field1::R{Bool} = true
  group1_field2::R{Bool} = true
  group1_field3::R{Bool} = true

  group2_field1::R{Bool} = true
  group2_field2::R{Bool} = true
  group2_field3::R{Bool} = true

...
end

model = Stipple.init(Model())

function ui()
  [dashboard(vm(model),class = "container",title = "My thingy",[
        heading("Group1"),
           checkbox("Field1", : group1_field1),
           checkbox("Field2", : group1_field2),
           checkbox("Field3", : group1_field3),
        heading("Group2"),
           checkbox("Field1", : group2_field1),
           checkbox("Field2", : group2_field2),
           checkbox("Field3", : group2_field3),
  ])]
end

into something like

Base.@kwdef mutable struct ModelGroup
  field1::R{Bool} = true
  field2::R{Bool} = true
  field3::R{Bool} = true
end

Base.@kwdef mutable struct Model <: ReactiveModel
  group1::R{ModelGroup} = ModelGroup(true, true, false)
  group2::R{ModelGroup} = ModelGroup(true, true, false)
end

and still use it from the ui?

hhaensel commented 3 years ago

Couldn't you use arrays and bind array elements to the ui elements?

Vuejs accepts v-model = 'group1[0]', which can be produced by @bind("group[1]").

There are currently two caveats:

I have currently submitted a PR, that remedies the first two shortcomings, but it is still in development. If you want to give it a try, install

]add Stipple#hh-newReactive

It also imports OffsetArray, which supports user-specific indexing. (Inn the final version you might have to import OffsetArrays yourself, as we are trying to keep the number of dependencies as small as possible.) In your case a definition could look like

Stipple.@kwdef mutable struct Model <: ReactiveModel
    group1::R{OffsetArray{Bool, 1}} = OffsetArray([true, true, false], -1)
    # ...
end

The -1 shifts the lower index from 1 to 0

If you want to bind a toggle element then you have to take care that you provide a Symbol for model binding

toggle("my label", Symbol("group1[0]"))

Or you could use the @bind macro as mentioned above, which is a bit strange in this situation.

toggle("Array element", nothing, "", @bind("group1[1]", "boolean"))

I put it here rather for showing the API of stipple elements.

yakir12 commented 3 years ago

Looks amazing @hhaensel !

if you want to modify the array elements by js code be aware that changing the content of an array element does not always trigger the watcher. You may need to reassign the full array, eg. group[1]=false; group1=group1

Isn't this a known "limitation" of Observables.jl? See here. The solution is just to have a notify(obs_of_array) after updating an element. What I mean is, this isn't a shortcoming of Stipple, more of how things work with Observables.

PhilipVinc commented 3 years ago

Thanks for your response!

Indeed, this seems like a possible way to go, but I'm not satisfied 100% with the answer because that is just a workaround, it's not a solution. If my "groups" have non-homogeneous types (in my actual use-case, a group would be a bunch of dropdown and checkboxes to customise some plots) then I'm just shoving inside an untyped array a lot of different things, and it might easily lead to bugs.

Is it really not possible to use a structure? The added benefit is that I could use multiple dispatch on the Julia side and write less code, and I could more easily test it.

I noticed that even if I use structures, you correctly generate the javascript code to describe those nested structures (using dicts). What is missing is 1) the methods to allow editing directly the subfields and 2) the possibility in the Julia side to access them...

yakir12 commented 3 years ago

Maybe the solution would allow users to define their own widgets. So say I have a struct to control a thing:

struct Thing
  height::Float64
  good::Bool
  name::String
end

and you'd like to define a new widgets that is a combination of Quasar widgets (e.g. a slider, a checkbox, and input) in some specific layout (e.g. vertical?). Then it would be cool if there was a way for the user to define the connection that updates an instance of Thing.

I think GtkReactive had done something similar, or maybe it was old old Makie...

PhilipVinc commented 3 years ago

Yes indeed, that would be very nice to have...

essenciary commented 3 years ago

That can be done and it's actually how Stipple works internally. If you think about Stipple's types, eg PlotSeries, RangeData, etc there's no difference between these and Thing. The core functionality of Stipple base is to serialize a Julia data structure to a JS data structure and push it down the wire (WS, HTTP). And in reverse, receive a data payload from JavaScript, un-serialize it to recreate the proper Julia type and update the corresponding property in the Reactive model.

The Julia -> JS serialization is done with Stipple.render and you can specialize it to suit your Thing. However, you probably won't need it, as by default Stipple will produce a JavaScript dictionary.

The JS -> Julia un-serialization is done by specializing Base.parse for your Thing type.

Actual code for RangeData:

function Stipple.render(rd::RangeData{T}, fieldname::Union{Symbol,Nothing} = nothing) where {T,R}
  Dict(:min => rd.range.start, :max => rd.range.stop)
end

function Base.parse(::Type{RangeData{T}}, d::Dict{X,Y}) where {T,X,Y}
  RangeData(d["min"]:d["max"])
end

https://github.com/GenieFramework/StippleUI.jl/blob/d4fde584bb291826a444e3fdea303adb5ee11042/src/Range.jl#L48

essenciary commented 3 years ago

Other thoughts: 1 - probably going as far as having custom types is too much. A Reactive{Dict} should probably work as well. 2 - use @bind to target nested properties in the data structure (dict) 3 - it's a great point, thanks for bringing it up. As Stipple is very new we're now observing how design patterns emerge and what kind of problems we need to solve. Large UIs are definitely an important problem that we want to solve right. Please keep us in the loop with how things go and I'll be happy to support you through development and find the best solutions. If you're able to build the UI in the open and share the codebase it would be interesting to collaborate on this. 4 - docs are slowly being added - happy to say that now Stipple.jl is fully commented on master and the rest of the packages in the ecosystem will follow. Also working on adding tests and generating docs.

PhilipVinc commented 3 years ago

1 - Maybe so, but I find that in many cases multiple dispatch helps reduce the amount of code and makes reasoning around it simpler... But maybe not in this case.

2 - How does bind work in this case? @bind(parent.field) ?

3 - will do. I'm simply building a front-end for sacred, a bit like Omniboard, though only for my simulations and not for arbitrary data, and a bit simpler...

By the way, is there a way to have observer function print errors to the REPL?

essenciary commented 3 years ago

1 - yes, I also believe that types are more expressive, vs dicts (eg a Car struct vs a dict with values). But is here the case that you can define structs that map to business objects? From your examples, a group a fields really looks like a dict :) Also, if you go the struct way, there will be more code to write. Up to you though, both approaches should work.

2 - I haven't actually tried it but it should work with @bind(R"parent.field") as you basically need to point to the reference on the JS side (R"..." is just a shortcut for Symbol("...") btw). If it doesn't work lmk and I'll take a look.

3 - nice, thanks!

5 - errors in REPL - I think this came up in the past as well, can't remember how/if it has been solved. That part is handled by Observables.jl and I suspect it's because the observable runs in a different thread. It's worth checking if this is solved in Observables. If not, we need to find a way of doing it ourselves.

yakir12 commented 3 years ago

btw, if Stipple would use NamedTuples instead of Dict{String, Any}s then I think you'd get interoperability with structs for free.

essenciary commented 3 years ago

@yakir12 what part of the stack do you have in mind? Do you have an example?

yakir12 commented 3 years ago

I admit I thought this was more prevalent, but one example is btn__toggle where the buttons is a vector of dicts, and a pressed button is of course a dict. I think this came to my attention else where, but I can't find it right now. I can also admit that I might be too trigger happy to replace a dict where a namedtuple would do.

hhaensel commented 3 years ago

There is one major difficulty in using NamedTuples, although I really like the approach; the set of allowed characters is smaller. In particular, you cannot easily code dicts that contain {:width: 30}. Currently this is handled by a Dict(R":width" => 30). If we wanted to support the same content by NamedTuples we would need translators on both sides.

I have currently introduced a translator (reviver) for js functions, which is under testing in hh-newReactive. We could, of course, define special characters/sequences, and replace them by the reviver. I remember that usage of NTs instead of Dicts{Symbol=>Any} was also discussed in terms of performance for Plots.jl.

If we feel that performance starts to be a problem, we might invite some compiler people for discussion. BTW, @essenciary , have you registered for a talk during JuliaCon?

hhaensel commented 3 years ago

Isn't this a known "limitation" of Observables.jl? See here. The solution is just to have a notify(obs_of_array) after updating an element. What I mean is, this isn't a shortcoming of Stipple, more of how things work with Observables.

Well, it is a similar thing to what you refer to, but on the front-end side. To address the problem on the Stipple side I already proposed to use setindex! for reactive types and rename the current setindex!() to something like set withoutwatcher!() that would nicely match with the watcher mixin that does the same job on the frontend side. I'm still waiting for @essenciary 's comment 😉

essenciary commented 3 years ago

@hhaensel I don't fully understand the impact of the proposed changes at https://github.com/GenieFramework/Stipple.jl/pull/43#issuecomment-810318917 - for start, what is "the current setindex!" that you mention? The only reference to setindex! I found in the codebase specializes the one in Observables and has a different signature:

image

essenciary commented 3 years ago

I did not submit a Stipple talk proposal for this year as I didn't feel that it would've brought enough benefits/novelty compared to the 2020 Stipple talk. But one about Stipple v1 in 2022, with a stable API, docs, a mature packages ecosystem, UI builder, etc would be a killer! :D

PhilipVinc commented 3 years ago

There is one major difficulty in using NamedTuples, although I really like the approach; the set of allowed characters is smaller. In particular, you cannot easily code dicts that contain {:width: 30}. Currently this is handled by a Dict(R":width" => 30).

I don't know enough of Stipple's internal to talk about it, so if what I say below makes no sense please ignore it, but I guess that you are referring to the fact that you modify those objects differently, right?

However, Why can't you support NamedTuples and Dicts? For example, if you define

stipple_getproperty(x, key) = getproperty(x, key) 
stipple_getproperty(x, key::AbstractString) = stipple_getproperty(x, Symbol(key))
stipple_getproperty(x::Dict, key::Symbol) = x[key]

and similar stipple_setproperty, specialising it for dictionaries, and use those methods internally, then you might be able to support both with little effort?

essenciary commented 3 years ago

For me it's still not clear what Dicts are we talking about. Is this about Stipple.render?

I believe it's useful to brainstorm features and improvements as they pop-up in different contexts, but to address them properly we should set separate issues for each individual topic with a well defined problem and a minimal working example.

hhaensel commented 3 years ago

The only reference to setindex! I found in the codebase specializes the one in Observables and has a different signature:

That's the one, I'm talking of.

If I understand it correctly, the aim of this definition is to update the content of the observable while suppressing the listeners that are listed in keys .... I found only one usage of this routine in update!(). My proposal is to rename that function to setindex_withoutwatchers! or something similar and to change the function call in update!().

We could then define

function setindex!(r::Reactive{T}, val, arg1, args...) where {T}
    setindex!(r.o, val, arg1, args...)
    notify!(r)
end

which would make it possible to change elements by

r = Reactive([1, 2, 3])
r[3] = 4

It would hold for any type that exposes setindex!

essenciary commented 3 years ago

The only reference to setindex! I found in the codebase specializes the one in Observables and has a different signature:

That's the one, I'm talking of.

If I understand it correctly, the aim of this definition is to update the content of the observable while suppressing the listeners that are listed in keys .... I found only one usage of this routine in update!(). My proposal is to rename that function to setindex_withoutwatchers! or something similar and to change the function call in update!().

We could then define

function setindex!(r::Reactive{T}, val, arg1, args...) where {T}
    setindex!(r.o, val, arg1, args...)
    notify!(r)
end

which would make it possible to change elements by

r = Reactive([1, 2, 3])
r[3] = 4

It would hold for any type that exposes setindex!

Ah, OK, yes, that makes sense! :D Sure!

The idea there is that the first observable handler is always the Stipple one. But the users can attach additional handlers using on(...). We want to be able to separate between Stipple's handler (used for data sync) and other user defined handlers and invoke the right ones.

We use it here to skip the handler number 1 and trigger the handlers at index >= 2: image

essenciary commented 3 years ago

Hmmm... however, the important part here is that this overwrites/specializes the original Observables.setindex! method. And if I remember correctly this was necessary in order to avoid that the data gets synced (Stipple basically hijacks the default behavior of Observables to skip the sync handler). So you'd have to skip handler 1 in the implementation that you propose, to keep the current behavior. I can't remember exactly, but this was necessary to make Stipple work.

essenciary commented 3 years ago

@hhaensel ok I remembered. When a value is pushed from the front we need to update it in our model and we need to trigger all its user defined observers/handlers with the exception of the Stipple data sync handler (to avoid that the value received from the front is pushed to the front again). Makes sense?

hhaensel commented 3 years ago

That's how I understood it and that's what I implemented with withoutwatchers mixing (EDIT: for the frontend). I think, if we split the set index! for zero keys and with keys (arg1, args...) we should get along. I'll give it a try then

hhaensel commented 3 years ago

It's done and, as far as I can see, works like a charm. See also my post in PR 43. So now we have

Reactive(v::T) where T = convert(Reactive{T}, v)
r = Reactive([1, 2, 3])
on(r) do r
  @show(r)
end
#non-notifying
r[][3] = 4
#notifying
r[3] = 4
essenciary commented 3 years ago

@hhaensel You rock! I'll switch to the branch and run our demos. Hopefully we'll have CI and automated UI tests in a few weeks time too :D

hhaensel commented 3 years ago

It's never too late for a post-deadline talk 😉

BTW, just added the constructor Reactive(v::T, args...) where T = convert(Reactive{T}, (v, args...))

hhaensel commented 2 years ago

I link here my latest implementation of nested indexing (#79, #81).

This allows to change elements of dictionaries and arrays just by adding consecutive indices, e.g.

julia> model.plot_options[:chart, :type] = :line
Dict{Symbol, Any} with 1 entry:
  :type => :line

The element is changed correctly and only the that very element is updated via webchannel. We soon might also support object fields.