GenieFramework / Stipple.jl

The reactive UI library for interactive data applications with pure Julia.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Allow to connect! app variable to an observable

yakir12 opened this issue · comments

[this is from a discussion on Discord]

Is there a way to Observable.connect! a variable from an @app body to an Observable?

Something like this:

using Observables, MyModule

@app begin
    @in variable = 0
end

connect!(MyModule.some_observable, variable)

Right now I do this:

@onchange variable begin
    MyModule.some_observable[] = variable
end

I've tried using connect! between two reactive variables but it's not working; don't know if it'd work with an observable from the outside

using GenieFramework, Observables
@genietools
@app begin
    @in var1 = 0
    @in var3 = 0
    @onchange isready begin
        model = @init
        Observables.connect!(model.var1, model.var3)
    end
    @onchange var1 begin
        println("var1 $var1")
    end
    @onchange var3 begin
        println("var3 $var3")
    end
end

ui() = slider(1:1:10, :var1)
@page("/", ui)

Another possibility is to use notify like

using Observables, MyModule

@app begin
    @in variable = 0
    @onchange variable begin
        notify(some_observable)
    end
end

This works between @in, @out variables

The question is, what are you going to achieve. You have to be aware that every refresh of the page in the browser creates a new model and upon creation of that model the model's variable needs to be connected to your Observable. That's nicely done by your

@onchange variable begin
    MyModule.some_observable[] = variable
end

Could you precisely describe what scenario you have in mind?
Maybe we find a solution then.

Otherwise, if you are not considering multi-user applications, you could work with a global model

model = @init

and connect the model's variable directly.

connect!(your_observable, model.variable)

But you have to write your own route function in that case.

This highlights how I'm "abusing" the Genie framework to my specific needs...

Yes, I have (by definition) only one user at a time, and cannot have multiple instances of the model. My use-case is a UI for controlling hardware: A user controls parameters used to control an LED strip and a Raspberry Pi camera (in a behavioral experiment with dung beetles). The RPI is connected to the user's laptop with an Ethernet cable and the RPI serves the generated website to the client. I love the fact that the user doesn't need to install anything to control the whole thing, just open a browser and navigate to a specific IP address.

Refreshing the page doesn't seem to pose any errors as of yet, perhaps because of how I set it up (you can see what it looks like for now here: https://github.com/yakir12/dancingqueen/tree/main/app).

In this case I think it is absolutely ok to work with a global model.
My current working horse for such apps is:

using GenieFramework
@genietools

@app begin
   @in x = "Enter to return"
   @out y = 0
   
   @onchange isready push!
end

model = @init

UI = Ref{Any}()

UI[] = [
    row(cell(class = "st-module", [
        h3("Input")

        row(textfield(class = "col-6", "Text Label", :x))
        row(numberfield(class = "col-6", "Number Label", :y))
    ]))

    row(cell(class = "st-module", [
        h3("Output")

        row("X: {{ x }}")
        row("Y: {{ y }}")
    ]))

]

ui() = UI[]

route("/") do
    global model

    page(model, ui) |> html
end

up(open_browser = true)

When you need faster responsiveness, consider using named apps and make the following changes:

# in this example I name the handlers function "myhandlers"
# if you don't specify the name it will be called 'handlers'
@app MyApp begin
   @in x = "Enter to return"
   @out y = 0
   
   @onchange isready push!
end myhandlers

model = init(MyApp, debounce = 0) |> myhandlers

you can see what it looks like for now here: https://github.com/yakir12/dancingqueen/tree/main/app
This has some similarity to my CameraWidget.jl Maybe you find some inspiration there how I managed the on/off problem.

Just to comment on your solution

@onchange variable begin
    MyModule.some_observable[] = variable
end

sounds perfectly fine for me. It won't use a different mechanism for triggering than connect!() does. Have a look at the following snippet:

julia> o1 = Observable(1);
julia> o2 = Observable(2);

julia> connect!(o1, o2)
ObserverFunction defined at C:\Users\helmu\.julia\packages\Observables\PHGQ8\src\Observables.jl:539 operating on Observable(2) 

julia> Observables.listeners(o1)
Pair{Int64, Any}[]

julia> Observables.listeners(o2)
1-element Vector{Pair{Int64, Any}}:
 0 => Observables.var"#11#12"{Observable{Int64}}(Observable(2))

So calling connect!() does nothing else than creating a listener that updates o1 with the value of o2 on any update or notification of o2. That's also what you do with your manual solution which you could write in a shorter form:

@onchange variable MyModule.some_observable[] = variable

But you could also do

model = init(MyApp, debounce = 0) |> myhandlers
connect!(MyModule.some_observable, model.variable)

I, personally, would opt for your proposal, because you have all callbacks in one place.

Just submitted a PR that would allow for the following usage

@page("/", ui, model = model)

There are two main issues here:

  1. the original issue of using Observable.connect! to propagate updates from a variable in a model to an observable in some module. I feel like this has been resolved, and indeed the best way is @onchange variable MyModule.some_observable[] = variable.
  2. an offshoot issue (that might warrant opening a new issue for) about using Genie mainly as GUI for a single user. I'm not exactly sure how to adapt my code as it is right now to what you suggested with the global model. I've adopted a stringent MVC framework (following your guide), and it's not clear to me how I can have a global model while at the same time maintain an MVC file and module structure. Maybe that's what you PR does...?

Let me know if you want me to open a new issue for the single-user mode.

This has some similarity to my CameraWidget.jl Maybe you find some inspiration there how I managed the on/off problem.

Wow, I wish that worked with the new GenieFrameworks (right now just get ERROR: LoadError: UndefVarError: @vars not defined). Very cool, thanks.

This has some similarity to my CameraWidget.jl Maybe you find some inspiration there how I managed the on/off problem.

Wow, I wish that worked with the new GenieFrameworks (right now just get ERROR: LoadError: UndefVarError: @vars not defined). Very cool, thanks.

That was developed with an older version of Stipple. Replace @vars with @app. Here are more details on how to convert an older codebase: #176 (comment)

Can this issue be closed?

Can this issue be closed?

Absolutely. However, it would be cool to entertain the Genie's use-case as a single-user GUI. I'll leave it at that for now, but maybe at some point in the future there can be some documentation directed at that solution.