SimonDanisch / Bonito.jl

Serving JS to the browser
MIT License
205 stars 30 forks source link

Update vector of DOMs #97

Open jfb-h opened 3 years ago

jfb-h commented 3 years ago

I have a carousel (using flickity) which contains a set of card elements. In principle this works really well and also plays nicely with Tailwind, which is amazing! I however would like the cards to update based on a query which I tried with onjs (among others) but only a vector of nulls is returned to the browser. Here's the code:

app = App() do session
    t = JSServe.TextField("PETase", class="rounded h-12 w-80 px-2")

    apps = Observable(application_from_topic("PETase"))

    on(t) do query
        apps[] = application_from_topic(query)
    end

    cards = lift(apps) do a
        make_card.(a)
    end

    carousel = DOM.div(
        class="carousel m-auto border-2 border-gray-200 rounded-md shadow-inner rounded", 
        dataFlickity="""{ "wrapAround": true }""",
        cards[]
    )

    JSServe.onjs(session, cards, js"""function on_update(new_value) {
        const carousel = $(carousel);
        carousel.replaceChildren(...new_value);
    }
    """)

    return DOM.div(
        JSServe.Asset("https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"),
        JSServe.Asset("https://unpkg.com/flickity@2/dist/flickity.pkgd.min.js"),
        JSServe.Asset("https://unpkg.com/flickity@2/dist/flickity.min.css"),
        JSServe.Asset("carousel.css"),        
        class="h-full bg-gray-100",

        DOM.div(t, dataTestID="text-input", class="flex justify-center py-6"),
        carousel
    )
end

How could I do this?

jfb-h commented 3 years ago

Relatedly, I tried the following which also worked in principle but because cards is an Observable of a vector of cards it doesn't get rendered as individual DOM elements. I tried to splat cards but the observable is not iterable.

app = App() do session
    t = JSServe.TextField("PETase", class="rounded h-12 w-80 px-2")

    cards = Observable(make_card.([Application()]))

    function update_cards(t)
        cards[] = make_card.(application_from_topic(t))
    end

    on(update_cards, t)

    update_cards(t[])

    return DOM.div(
        JSServe.Asset("https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"),
        JSServe.Asset("https://unpkg.com/flickity@2/dist/flickity.pkgd.min.js"),
        JSServe.Asset("https://unpkg.com/flickity@2/dist/flickity.min.css"),
        JSServe.Asset("carousel.css"),        
        class="h-full bg-gray-100",

        DOM.div(t, class="flex justify-center py-6"),
        DOM.div(
            class="carousel m-auto border-2 border-gray-200 rounded-md shadow-inner rounded", 
            dataFlickity="""{ "wrapAround": true }""",
            cards...
        )
    )
end

Application is a custom struct representing a patent application and make_card() creates a DOM element for its representation. I feel like there is probably an obvious solution which I'm just missing...

SimonDanisch commented 3 years ago

Oh, I almost missed this, since I somehow unsubscribed from the repository :-O Can you make an example that I can run and try out?

SimonDanisch commented 3 years ago

But, I guess this may work?

   cards = lift(apps) do a 
       # wrap in a  minimal DOM element
        DOM.span(make_card.(a)...)
    end
  carousel = DOM.div(
      class="carousel m-auto border-2 border-gray-200 rounded-md shadow-inner rounded", 
      dataFlickity="""{ "wrapAround": true }""",
      cards # <--- leave as observable
  )
jfb-h commented 3 years ago

Hey Simon, thanks for the pointer - I tried wrapping the cards into a minimal DOM element as you suggested but apparently the carousel div expects the carousel-cell divs as immediate children so they can't go into their own div. Here's a MWE of what it should look like:

using JSServe, Observables, WGLMakie

database = ["Cat 1", "Cat 2", "Dog 1", "Dog 2", "Dog 3"]

function data_from_query(query)
    idx = findall(contains.(database, query))
    return database[idx]
end

function make_card(data)     
    DOM.div(class="carousel-cell rounded shadow-md my-2 bg-white",
        DOM.div(class="px-6 py-4 h-5/6", data)
    )
end

app = App() do session
    t = JSServe.TextField("Dog", class="rounded h-12 w-80 px-2")
    matches = Observable(data_from_query("Dog"))

    on(t) do query
        matches[] = data_from_query(query)
    end

    cards = lift(matches) do m
        make_card.(m)
    end

    carousel = DOM.div(
        class="carousel m-auto border-2 border-gray-200 rounded-md shadow-inner rounded", 
        dataFlickity="""{ "wrapAround": true }""",
        cards[]...
    )

    return DOM.div(
        JSServe.Asset("https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"),
        JSServe.Asset("https://unpkg.com/flickity@2/dist/flickity.pkgd.min.js"),
        JSServe.Asset("https://unpkg.com/flickity@2/dist/flickity.min.css"),
        JSServe.Asset("carousel.css"),        
        class="h-full bg-gray-100",

        DOM.h1(
            "Carousel in JSServe",
            class="flex justify-center font-bold text-3xl text-gray-100 bg-green-900 py-6"
        ),
        DOM.div(t, class="flex justify-center py-6"),
        carousel
    )
end

isdefined(Main, :server) && close(server)
server = JSServe.Server(app, "127.0.0.1", 8083)

grafik

This doesn't update though because the card observable data is accessed directly in the context of the carousel via cards[]....

If I now change the cards and carousel to what you suggested:

    cards = lift(matches) do m
        DOM.span(make_card.(m)...)
    end

    carousel = DOM.div(
        class="carousel m-auto border-2 border-gray-200 rounded-md shadow-inner rounded", 
        dataFlickity="""{ "wrapAround": true }""",
        cards
    )

The card elements don't get rendered properly:

grafik