GenieFramework / StippleUI.jl

StippleUI is a library of reactive UI elements for Stipple.jl.
MIT License
83 stars 15 forks source link

Allow other StippleUI elements inside Tree #76

Closed Sov-trotter closed 2 years ago

Sov-trotter commented 2 years ago

Based on discussions on discord one can parse a Dict to have a tree view like the one below Sample json :

{
  "test":
  {
  "foo": false,
  "baz": "qux",
  "corge": 
    {
      "grault": 1
    }
  }
}

julia code

using Stipple, StippleUI, JSON

d = JSON.read("test.json", String)
j = JSON.parse(d)

dict(;kwargs...) = Dict{Symbol, Any}(kwargs...)
const mydiv = Genie.Renderer.Html.div

function filedict(startfile; parent = "d", name="d")
    if startfile isa Dict
        k = keys(startfile)
        dict(
            label = name,
            key = parent,
            children = [filedict(startfile[i], parent = parent*"/"*i, name=i) for i in k]
        )
    elseif startfile isa Array && !isempty(startfile)
        if startfile[1] isa Dict
            for j in startfile
                k = keys(j)
                dict(
                    label = name,
                    key = parent,
                    children = [filedict(j[i], parent = parent*"/"*i, name=i) for i in k]
                )
            end
        else
            dict(
            label = name,
            key = parent,
            children = [dict(label = i, key = parent*"/"*"i") for i in startfile])
        end    
    else
        dict(label = name,
            key = parent,
            children = [dict(label = startfile, key = parent*"/"*String(name))]
        )
    end
end

@reactive! mutable struct TreeDemo <: ReactiveModel
    name::R{String} = ""
    files::R{Vector{Dict{Symbol, Any}}} = [filedict(j)]
    files_selected::R{String} = ""
    files_expanded::R{Vector{String}} = String[]
end

# alternative definition with the new @mixin macro
# this will work with StippleUI v0.19.3 or latest master

register_mixin(@__MODULE__)
@reactive! mutable struct TreeDemo <: ReactiveModel
    name::R{String} = ""
    @mixin files::TreeSelectable([filedict(j)])
end

Genie.Router.delete!(:TreeDemo)

function ui(model)
    page(
        model,
        title = "Hello Stipple",
        row(cell( class = "st-module", [
            row([tree(var"node-key" = "key", nodes = :files,
                var"selected.sync" = :files_selected,
                var"expanded.sync" = :files_expanded
            )
        ])

            mydiv(h4("Expanded: ") * "{{ files_expanded }}")
            mydiv(h4("Selected: ") * "{{ files_selected }}")
            mydiv(h4("Ticked: ")   * "{{ files_ticked }}")
        ])),
    )
end

function handlers(model)
    on(model.isready) do isready
        isready && push!(model)
    end

    model
end

route("/") do
    # model defined gloablly for debugging and testing only
    global model
    model = init(TreeDemo) |> handlers
    model |> ui |> html
end

up()

image

It would be really useful to be able to define checkbox, textfield, numberfield (for fields like 1, qux, false)and other elements inside the tree at any desired point which is not currently possible.

cc : @hhaensel

hhaensel commented 2 years ago

Was more work than I thought, because the prop.node.key cannot be used directly. Instead I had to write getindex and setindex in javascript for the key (see below). Still improvement potential graphical-wise, but it works!

# Demo for editing a Dict by help of a tree with templates
# work in progress
# to be done:
# - move edit field into header
# - support arrays

using Stipple, StippleUI, JSON

Genie.Secrets.secret_token!()

testdict = JSON.parse("""
{
  "test":
  {
  "foo": false,
  "baz": "qux",
  "corge": 
    {
      "grault": 1
    }
  }
}
""")

dict(;kwargs...) = Dict{Symbol, Any}(kwargs...)
const mydiv = Genie.Renderer.Html.div

function dict_tree(startfile; parent = "d", name = "d")
    if startfile isa Dict
        k = keys(startfile)
        dict(
            label = name,
            key = parent,
            children = [dict_tree(startfile[i], parent = parent * "." * i, name = i) for i in k]
        )
    elseif startfile isa Array && !isempty(startfile)
        if startfile[1] isa Dict
            for j in startfile
                k = keys(j)
                dict(
                    label = name,
                    key = parent,
                    children = [dict_tree(j[i], parent = parent * "." * i, name=i) for i in k]
                )
            end
        else
            dict(
            label = name,
            key = parent,
            children = [dict(label = i, key = parent * "." * i) for i in startfile])
        end    
    else
        dict(label = name,
            key = parent,
            value = startfile,
            body = startfile isa Bool ? "bool" : startfile isa Number ? "number" : "text",
            children = []
        )
    end
end

@reactive! mutable struct TreeDemo <: ReactiveModel
    d::R{Dict{String, Any}} = deepcopy(testdict)
    tree::R{Vector{Dict{Symbol, Any}}} = [dict_tree(testdict)]

    tree_selected::R{String} = ""
    tree_ticked::R{Vector{String}} = String[]
    tree_expanded::R{Vector{String}} = String[]
end

Genie.Router.delete!(:TreeDemo)
Stipple.js_methods(::TreeDemo) = """
getindex: function(key) {
    let o = this
    kk = key.split('.')
    for(let i = 0; i < kk.length; i++){ 
        o = o[kk[i]];
    }
    return o
},

setindex: function(key, val) {
    let o = this
    kk = key.split('.')
    for(let i = 0; i < kk.length - 1; i++){ 
        o = o[kk[i]];
    }
    o[kk[kk.length-1]] = val
    return val
}
"""

function ui(model)
    page(
        model,
        title = "Dict Tree",
        row(cell( class = "st-module", [
            row([tree(var"node-key" = "key", nodes = :tree,
                var"selected.sync" = :tree_selected,
                var"expanded.sync" = :tree_expanded,
                [
                    template("", var"v-slot:body-text" = "prop", [
                        "T", textfield("", dense = true, label = R"prop.node.label", value = R"getindex(prop.node.key)", var"@input" = "newval => setindex(prop.node.key, newval)")
                    ]),
                    template("", var"v-slot:body-number" = "prop", [
                        "N", textfield("", dense = true, label = R"prop.node.label", value = R"getindex(prop.node.key)", var"@input" = "newval => setindex(prop.node.key, 1 * newval)")
                    ]),
                    template("", var"v-slot:body-bool" = "prop", [
                        checkbox("", dense = true, label = R"prop.node.label", value = R"getindex(prop.node.key)", var"@input" = "newval => setindex(prop.node.key, newval)")
                    ])
                ]
            )
        ])

            mydiv(h4("Expanded: ") * "{{ tree_expanded }}")
            mydiv(h4("Selected: ") * "{{ tree_selected }}")
            mydiv(h4("Ticked: ")   * "{{ tree_ticked }}")
        ])),
    )
end

function handlers(model)
    on(model.isready) do isready
        isready && push!(model)
    end

    model
end

route("/") do
    # model defined gloablly for debugging and testing only
    global model
    model = init(TreeDemo) |> handlers
    model |> ui |> html
end

up()

grafik

julia> model.d["test"]
Dict{String, Any} with 3 entries:
  "corge" => Dict{String, Any}("grault"=>123)
  "baz"   => "quark"
  "foo"   => true
hhaensel commented 2 years ago

It's been uploaded to StippleDemos

hhaensel commented 2 years ago

@Sov-trotter If you think this is solved, please close this issue

Sov-trotter commented 2 years ago

Sure. Thanks. :)