Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proliferation of redundant containers #283

Open
sprig opened this issue Jan 21, 2025 · 4 comments
Open

Proliferation of redundant containers #283

sprig opened this issue Jan 21, 2025 · 4 comments

Comments

@sprig
Copy link

sprig commented Jan 21, 2025

Hello,
I recently discovered bonito and have been really enjoying the general approach. However;

When playing around with Bonito, especially considering it for various projects, I'm especially wary of past experiences with various frameworks in regard to visual layout, and particularly in browsers, where it was always a hot mess. Considering myself a non-expert in layout design, I still think it is possible to get reasonably precise layout fairly easily for a range of use cases in many scenarios, if whatever UI kit I'm using does not make it cumbersome.

In this regard, the primary hindrance IMHO to simple design is unexpected containers, especially anonymous classless ones, since every additional container is an opportunity for styling to break. This especially when mixing in other framework that rely on specific assumptions regarding their layout, and in particular to anything that uses Canvas such as WGLMakie (but likewise for Plotly and others) where sizing to parent container can break.

As a simple example, consider

using WGLMakie
using Bonito

WGLMakie.activate!(resize_to=:parent)
fullpage=CSS(
    "background"=>"Gray",
    "height"=>"100%",
    "width"=>"100%",
    "position"=>"fixed",
    "margin"=>"0",
    "padding"=>"0",
    "top"=>"0",
    "left"=>"0",
  )

## Doesn't work
App() do; scatter(rand(10),rand(10)) end
## Does work
App() do; DOM.div(scatter(rand(10),rand(10)),style=fullpage) end
## Doesn't work
App() do; DOM.div(Observable(scatter(rand(10),rand(10))),style=fullpage) end

In my browser this does not resize to the full height of the page, despite my expectations that it should in all three cases. This may be a bit contrived since supposedly one could/should make a specific type with overloaded jsrender to wrap the observable. However, if even frameworks that are deeply integrated with Bonito can't do this right, this is a bad sign.

I propose to reduce to a bare minimum any containers that Bonito creates beyond those that the user explicitly specifies (specifically in regards to Observables, but any other types that do not have an explicit jsrender methos). For those that are absolutely necessary, i propose to make them classed for simple styling, and to explicitly enumerate cases where they appear to reduce the element of surprise. I would happily participate in making the changes, as I really like Bonito otherwise and would find it useful - but I might need some guidance.

Is there support for this?

@sprig
Copy link
Author

sprig commented Jan 21, 2025

I would add to the above, it is not just that example where something specifically breaks;

I looked at the dom produced and it is littered with containers upon containers, divs with nothing in them, or nothing but a style and a script, or a span, nested deep inside what should have been user dom.

@SimonDanisch
Copy link
Owner

It would be great to have this more predictable ;)

@sprig
Copy link
Author

sprig commented Jan 26, 2025

Great! i poked around in the code a bit and at least from the julia side roughly understand what is happening.

As a first step i would replace the span with a div in the generic observable jsrender, and add an explicit class to any utility dom that is inserted from the julia side.

Im not sure the entire process is clear to me though, esp from the js side where i had less opportunity to look at the code.

as far as i understand at a high level, and please correct me - the basic flow is that wherever there is some observable depending leaf in the dom tree generated by julia, a container C with a piece of js code is inserted instead, that pulls a binary blob with some dom that replaces the contents of C, and sets up a subscription to further updates. Then it seems like what happens is that julia creates a special container to be rendered in a “subsession”, which - im guessing a bit here since that would explain the behavior - sets up a temporary container into which it injects the html to be rendered, then depopulates it and sends it back to julia, which sends it back where it is inserted into the “main” subcontainer, with a subscription to repeat this process whenever the observable changes on the julia side.

Is that the actual cause of the grandparent “bonito fragment”, parent and uncle “data-jscall-id” containers with empty uncle, child as the ided root container created in the beginning of the observable jsrender method, and grandchild finally the contents?

As noted in MakieOrg/Makie.jl#1318 I found kind of a workaround. That essentially forced one to use in a mode where there is a single top container managing everything instead of each piece having its own connection etc. This is mostly a usage pattern change so that wouldn’t be that much to do in terms of code changes, though i still would recommend better defaults per above. What do you think of such a usage pattern? I can see there is potential for poor performance in redrawing. How do you think other interactions would be affected?

@sprig
Copy link
Author

sprig commented Jan 28, 2025

I hacked around something like this, for a generic container that updates on its contents and a generic input element. Seem to work reasonably well. What do you think?

struct View{T}
  dom::T
  id::String
  args::Observable{Any}
end

function view(dom,args...;kwargs...)
  ID=:id in keys(kwargs) ? kwargs.id : string(uuid4())
  View(dom,ID,Observable{Any}((args,kwargs)))
end

function set!(d::View,args...;kwargs...)
  d.args[]=(args,kwargs)
end

function Bonito.jsrender(session::Session,d::View)
  ID=d.id
  node=@lift d.dom($(d.args)[1]...;id=ID,class="view",$(d.args)[2]...)

  onjs(session,node,js"""(v)=>{
  el=document.getElementById($ID);
  console.log("Updating:",el,v);
  el.replaceWith(v);
  }""")

  return Bonito.jsrender(session, node[])
end

struct Value{T}
  dom::T
  id::String
  args
  value::Observable{Any}
  _value::Observable{Any}
end

function value(dom,val,args...;kwargs...)
  ID=:id in keys(kwargs) ? kwargs.id : string(uuid4())

  value=Observable{Any}(val)
  _value=Observable{Any}(val)

  v=Value(dom,ID,(args,kwargs),_value,value)
end

function set!(d::Value,val,args...;kwargs...)
  d.args=(args,kwargs)
  d._value[]=val
end

Bonito.is_widget(::Value) = true
Bonito.update_value!(v::Value, val) = (@info "updating" v.value[] val; v._value[] = val)

function Bonito.jsrender(session::Session,v::Value)
  ID=v.id

  node=v.dom(v.args[1]...,
    ;id=ID
    ,class="value"
    ,value=v.value[]
    ,v.args[2]...
  )
  t=string(now())
  attached=get!(session.session_objects,ID,false)

  Bonito.onload(session,node,js"""function onload(nd) {
  el=document.getElementById($ID);
  console.log($t,'Adding listeners to',el,el?.listening);
  // Shit seems to run twice
  if (!el) {
  console.log("hasn't loaded yet");
  return;
  }
  if (el?.listening) {
  console.log("already listening");
  return;
  }
  el.addEventListener('input', function (evt) {
    val=this.value;
    console.log("Value Input:", el, evt, val);
    $(v.value).notify(val);
  });
  el.addEventListener('change', function (evt) {
    el=document.getElementById($ID);
    val=this.value;
    console.log("Value change:", el, evt, val);
    $(v.value).notify(val);
  });

  el.listening=true;

  $(v.value).on(function set_value(val) {
    el=document.getElementById($ID);
    console.log("Got a value update:",el,val, $t);
    el.value=val;
  });
  }
  """)

  return Bonito.jsrender(session, node)
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants