Skip to content

Functionality to decouple HTML and JS from Julia while keeping them still connected using Bonito's WebSockets - #384

Draft
Niels1006 wants to merge 2 commits into
SimonDanisch:masterfrom
Niels1006:master
Draft

Niels1006 wants to merge 2 commits into
SimonDanisch:masterfrom
Niels1006:master

Conversation

@Niels1006

Copy link
Copy Markdown

Hello,

I find it quite convenient to decouple HTML and JavaScript from Julia, so I have made a proof of concept PR.
This makes it easier to create bigger and well designed frontends using UI coponent libraries, CSS libaries or even whole frameworks like Vue while keeping the core functionality in Julia.

This PR adds the functionality to write HTML code in index.html, JS code in JS files, place Bonito plots in index.html and expose observables to window.observables, using Bonito's WebSockets to update observables.

An example using this PR can be found on https://github.com/Niels1006/exBonito, using custom HTML components to change observables:
image

This is what a snippet of a Bonito app looks like:

app = App() do session
  # Dictionary to hold all observables to be exposed to JS
  all_observables::Dict{Symbol,Observable} = Dict()
  
  # Custom functions that create observables and plots
  probdistr_k_ob, f1, slider_p1_1, slider_p1_2 = distributionanddplot(Gamma, 0.8, 9.0; scalefactor=1.0, xmax=10.0)
  probdistr_a_ob, f2, slider_p2_1, slider_p2_2 = distributionanddplot(Gamma, 2, 166; scalefactor=1.0, xmax=700.0)
  f3, slider_p3_1 = reciprplot(probdistr_k_ob, probdistr_a_ob)
  _, f4, slider_p4_1, slider_p4_2 = distributionanddplot(InverseGamma, 2, 40,
      [2.110157885527154, 0.11797309429206473, 0.255508880565279, 1.3856769558849402, 0.8077752824065648, 0.4787811048884323, 0.02487445489654902, 2.6186723568911687, 1.5055123709384435, 0.3359926847041475, 0.5955436663980697, 0.4550186638731471, 0.84446149558237, 3.507812284548887, 1.1989341800136433];
      scalefactor=1.0, xmax=300)
  
  # Expose observables we want to use in JS to `window.observables`
  expose_observables_script = add_observables_script!(
      all_observables,
      Dict(
          :slider_p1_1 => slider_p1_1,
          :slider_p1_2 => slider_p1_2,
          :slider_p2_1 => slider_p2_1,
          :slider_p2_2 => slider_p2_2,
          :slider_p3_1 => slider_p3_1,
          :slider_p4_1 => slider_p4_1,
          :slider_p4_2 => slider_p4_2,
      )
  )
  
  # See if the observables are getting updated from our custom JS slider
  on(all_observables[:slider_p1_1]) do newVal
      println(newVal)
  end
  
  return DOM.html(
      index_html_inject_script(joinpath(@__DIR__, "..", "static", "index.html")), # inject the HTML template `index.html`
      DOM.script(src=Bonito.Asset("static/script.js")), # inject custom JS script
      expose_observables_script..., # inject the observables to JS
      plot_to_html_script([f1 => "plot1", f2 => "plot2", f3 => "plot3", f4 => "plot4"])..., # inject plots and move them to divs created in `index.html`
  )
end

In this snippet, plots get created that depend on observables that then get exposed to window.observables. Furthermore, index.html and script.js get injected into the document.

index.html is just a regular html file. Note, that scripts or stylesheets can be included in the head as usual, because the head of index.html gets appended to document.head. Important here is the creation of the divs with id=plot-*, because the Bonito plots get placed in these:

<html>
  <head>
    <title>exSep Example</title>
    <link
      href="https://cdn.jsdelivr.net/npm/daisyui@5"
      rel="stylesheet"
      type="text/css"
    />
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
    <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
  </head>

  <body>
    <div class="w-screen h-screen bg-slate-950 text-slate-200">
      <div class="h-screen grid grid-cols-3 grid-rows-2 gap-4 p-4">
        <div
          class="grid grid-cols-2 gap-4 border-slate-700 bg-slate-900 rounded-lg p-4"
        >
          <p class="col-span-2 text-2xl">
            \[ k \sim \text{Gamma}(x; \alpha, \theta) = \frac{x^{\alpha-1}
            e^{-x/\theta}}{\Gamma(\alpha) \theta^\alpha} \]
          </p>
          <div>
            <label
              for="sliderp1_1"
              class="block text-center text-sm font-medium text-heading"
            >
              \[ \text{shape parameter}~\text{rec_k} \alpha \]
            </label>
            <input
              id="slider_p1_1"
              type="range"
              class="w-full rounded-full range range-sm"
            />
          </div>
          <div>
            <label
              for="slider_p1_2"
              class="block text-center text-sm font-medium text-heading"
            >
              \[ \text{scale parameter}~\text{rec_k} \theta \]
            </label>
            <input
              id="slider_p1_2"
              type="range"
              class="w-full range range-sm"
            />
          </div>
          <div class="col-span-2" id="plot1"></div>
        </div>

        <div
          class="grid grid-cols-2 gap-4 border-slate-700 bg-slate-900 rounded-lg p-4"
        >
          <p class="col-span-2 text-2xl">
            \[ a \sim \text{Gamma}(x; \alpha, \theta) = \frac{x^{\alpha-1}
            e^{-x/\theta}}{\Gamma(\alpha) \theta^\alpha} \]
          </p>
          <div>
            <label
              for="slider_p2_1"
              class="block text-center mb-2.5 text-sm font-medium text-heading"
            >
              \[ \text{shape parameter}~\text{rec_a} \alpha \]
            </label>
            <input
              id="slider_p2_1"
              type="range"
              class="w-full range range-sm"
            />
          </div>
          <div>
            <label
              for="slider_p2_2"
              class="block text-center mb-2.5 text-sm font-medium text-heading"
            >
              \[ \text{scale parameter}~\text{rec_a} \theta \]
            </label>
            <input
              id="slider_p2_2"
              type="range"
              class="w-full range range-sm"
            />
          </div>
          <div class="col-span-2" id="plot2"></div>
        </div>

        <div
          class="grid grid-cols-2 gap-4 border-slate-700 bg-slate-900 rounded-lg p-4"
        >
          <p class="col-span-2 text-2xl">
            \[ \text{MTTR} \sim \text{InverseGamma}(x; \alpha, \theta) =
            \frac{\theta^\alpha x^{-(\alpha + 1)}}{\Gamma(\alpha)}
            e^{-\frac{\theta}{x}} \]
          </p>
          <div>
            <label
              for="slider_p4_1"
              class="block text-center mb-2.5 text-sm font-medium text-heading"
            >
              \[ \text{shape parameter}~\text{mttr_} \alpha \]
            </label>
            <input
              id="slider_p4_1"
              type="range"
              class="w-full range range-sm"
            />
          </div>
          <div>
            <label
              for="slider_p4_2"
              class="block text-center mb-2.5 text-sm font-medium text-heading"
            >
              \[ \text{scale parameter}~\text{mttr_} \theta \]
            </label>
            <input
              id="slider_p4_2"
              type="range"
              class="w-full range range-sm"
            />
          </div>
          <div class="col-span-2" id="plot4"></div>
        </div>

        <div
          class="grid grid-cols-2 col-span-3 border-slate-700 bg-slate-900 rounded-lg p-4"
        >
          <p class="col-span-2 text-2xl">\[ r(x;k,a) = k + \frac{a}{x} \]</p>
          <label
            for="slider_p3_1"
            class="col-span-2 block text-center mb-2.5 text-sm font-medium text-heading"
          >
            number of samples
          </label>
          <input
            class="col-span-2 w-full range range-sm"
            id="slider_p3_1"
            type="range"
          />
          <div class="col-span-2" id="plot3"></div>
        </div>
      </div>
    </div>
  </body>
</html>

To now make those sliders use the observables defined in Julia, JS can be used:

const slidersObs = ["slider_p1_1", "slider_p1_2", "slider_p2_1", "slider_p2_2", "slider_p3_1", "slider_p4_1", "slider_p4_2"]; 
const limits = {
            "slider_p1_1": { min: 0.1, max: 20, step: 0.1 },
            "slider_p1_2": { min: 0.1, max: 20, step: 0.1 },
            "slider_p2_1": { min: 0.1, max: 20, step: 0.1 },
            "slider_p2_2": { min: 30, max: 200, step: 1 },
            "slider_p3_1": { min: 1, max: 100, step: 1 },
            "slider_p4_1": { min: 0.1, max: 20, step: 1 },
            "slider_p4_2": { min: 20, max: 50, step: 1 },
        }

        //initialize sliders
        slidersObs.forEach((obsName) => {
            const sliderOb = window.observables[obsName];
            const sliderElement = document.getElementById(`${obsName}`);

            if (sliderOb && sliderElement) {
                //set slider limits
                const limitsForSlider = limits[obsName];
                if (limitsForSlider) {
                    sliderElement.min = limitsForSlider.min;
                    sliderElement.max = limitsForSlider.max;
                    sliderElement.step = limitsForSlider.step;
                }

                // Initialize the slider value
                sliderElement.value = sliderOb.value;

                //update slider on observable change
                sliderOb.on((value => {
                    sliderElement.value = value;
                }));

                // Update observable on slider change
                sliderElement.addEventListener("input", (event) => {
                    sliderOb.notify(parseFloat(event.target.value));
                });
            }
        });

What do you think about the idea? The approach can be changed, right now it's just a proof of concept.

@SimonDanisch

SimonDanisch commented Dec 22, 2025

Copy link
Copy Markdown
Owner

Thank you for the PR :)
I'm not entirely sure which problems this solves, though.
Bonito already does put a heavy emphasize on being able to use any existing JS/HTML/CSS libraries.
The main advantage I see here is directly writing an HTML file - but I'm not sure what advantage you get by this compared to using DOM.div etc. Especially with AI nowadays, its super easy to convert existing HTML to the bonito DOM.div structure.
There's nothing holding you back from separating your file containing the Bonito DOM, from any other JS/CSS/etc file.
So I also don't see how this allows you to more cleanly separate concerns.

@lazarusA

Copy link
Copy Markdown
Collaborator

If it's compatible with Vue, then it should work with vitepress? It would be nice to test that.

@SimonDanisch

Copy link
Copy Markdown
Owner

If it's compatible with Vue, then it should work with vitepress? It would be nice to test that.

Well, Vitepress does the complete building from start to end in their pipeline, so that's different from using a specific JS library like Vue inside Bonito and letting Bonito handle everything.

@SimonDanisch

Copy link
Copy Markdown
Owner

What you want for Vitepress is a Bonito integration into Vitepress, not Vue integration into Bonito ;)

@Niels1006

Copy link
Copy Markdown
Author

The main advantage I see here is directly writing an HTML file - but I'm not sure what advantage you get by this compared to using DOM.div etc. Especially with AI nowadays, its super easy to convert existing HTML to the bonito DOM.div structure. There's nothing holding you back from separating your file containing the Bonito DOM, from any other JS/CSS/etc file. So I also don't see how this allows you to more cleanly separate concerns.

It indeed is very easy to convert the HTML file to Julia, but maintaining it will get harder, since it can get pretty messy for bigger projects. Furthermore, writing actual HTML rather than HTML in Julia feels more natural and makes splitting work easier.

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

Successfully merging this pull request may close these issues.

3 participants