Closed PhilipVinc closed 1 year 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:
group1::R{Vector{Any}} = Any[true, false, false]
group[1]=false; group1=group1
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.
nothing
for the binding in the first placeLooks 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.
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...
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...
Yes indeed, that would be very nice to have...
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
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.
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?
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.
btw, if Stipple would use NamedTuple
s instead of Dict{String, Any}
s then I think you'd get interoperability with structs for free.
@yakir12 what part of the stack do you have in mind? Do you have an example?
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.
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?
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 😉
@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:
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
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?
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.
The only reference to
setindex!
I found in the codebase specializes the one inObservables
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!
The only reference to
setindex!
I found in the codebase specializes the one inObservables
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 inupdate!()
. My proposal is to rename that function tosetindex_withoutwatchers!
or something similar and to change the function call inupdate!()
.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:
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.
@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?
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
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
@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
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...))
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.
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
into something like
and still use it from the ui?