starfederation / datastar

A real-time hypermedia framework.
https://data-star.dev/
MIT License
732 stars 41 forks source link

Possible loss of target selectors within applyToTargets function during docWithViewTransitionAPI.startViewTransition callback #133

Closed nzanghi-tst closed 2 months ago

nzanghi-tst commented 2 months ago

I am attempting to build out an interactive dashboard using multiple fragments, wherein the first fragment is setting up the layout for the rest of the fragments to be merged into. I noticed that this block of code:

https://github.com/delaneyj/datastar/blob/72680008f0e25b9223a35179d0db7c3835cd6315/packages/library/src/lib/plugins/backend.ts#L402-L406

Is referencing the applyToTargets function, which holds a reference to targets and when going down the path of calling applyToTargets within the startViewTransition callback the changes are not applied to the first fragment, instead missing it altogether. When I move applyToTargets outside of the if and comment it out, I get the expected behavior of merging the fragments together. Wondering if this is expected or a bug related to the view transition

Attaching relevant fragments and screenshots below

Before:

before

After:

after

Fragments (let me know if it would be easier to attach as text!)

fragments
delaneyj commented 2 months ago

I'm not sure. You can send a data: vt off to bypass the view transitions all together. Please send a minimal reproducible Go or TS project. That or try to add a Datastar example. Maybe a http.Handler that just sends the exact fragments your app generates (cleaned up to the minimum)?

nzanghi-tst commented 2 months ago

Attaching a minimum recreation: ds-133.bundle.gz

Unzip and then git clone ds-133.bundle will create the project directory

Should just be able to run go mod tidy and go run main in there, and then comment in / out L69 to see the issue in action at port 3000

delaneyj commented 2 months ago

This is a better minimal reproduction of the issue...

package main

import (
    "fmt"
    "net/http"

    "github.com/delaneyj/datastar"
    "github.com/go-chi/chi/v5"
)

const port = 3000

func main() {
    router := chi.NewRouter()

    router.Get("/", func(w http.ResponseWriter, r *http.Request) {
        page := []byte(`
<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"></script>
  </head>
  <body>
    <div id="id-root" data-on-load="$$get('/dashboard')"></div>
  </body>
</html>
        `)
        w.Write(page)
    })

    router.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
        sse := datastar.NewSSE(w, r)

        fragments := []string{
            `
                    <div id="id-root">
                        <div id="tile-1">Tile1</div>
                        <div id="tile-2">Tile2</div>
                    </div>
                `,
            `<div id="tile-1" style="padding:1rem;color:white;background-color: blue;">List View</div>`,
            `<div id="tile-2" style="padding:1rem;color:white;background-color: green;">Map View</div>`,
        }

        for _, fragment := range fragments {
            datastar.RenderFragmentString(sse, fragment)
        }
    })

    fmt.Printf("Server starting on port %d...\n", port)
    err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", port), router)
    if err != nil {
        fmt.Println("Error starting server:", err)
    }
}

will look into

nzanghi-tst commented 2 months ago

Improved minimal repro all in one main.go:

package main

import (
    "fmt"
    "net/http"

    "github.com/delaneyj/datastar"
    "github.com/go-chi/chi/v5"
)

const port = 3000

var fragments = []string{
    `<div id="id-root" class="flex h-full m-2"><style type="text/css">.basis_81c2{flex-basis:50%;}</style><div class="flex flex-col border border-solid basis_81c2"><div role="tablist" class="tabs tabs-bordered"><a id="id-map" role="tab" class="tab" onclick="handleTabClick(event)">Map Tab</a></div><div id="id-tile-1" class="flex-1">Tile ID: 1</div></div><div class="flex flex-col border border-solid basis_81c2"><div role="tablist" class="tabs tabs-bordered"><a id="id-list-tab-1" role="tab" class="tab" onclick="handleTabClick(event)">List 1</a><a id="id-list-tab-2" role="tab" class="tab" onclick="handleTabClick(event)">List Tab 2</a></div><div id="id-tile-2" class="flex-1">Tile ID: 2</div></div></div>`,
    `<div id="id-tile-2" class="flex-1 bg-teal-900">List View</div>`,
    `<div id="id-tile-1" class="flex-1 bg-sky-700">Geo Map View</div>`,
    `<div id="id-tile-2" class="flex-1 bg-teal-900">List View</div>`,
    `<div id="id-tile-1" class="flex-1 bg-sky-700">Geo Map View</div>`,
}

func main() {
    router := chi.NewRouter()

    router.Get("/", func(w http.ResponseWriter, r *http.Request) {
        page := []byte(`
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
    <link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" type="text/css" />
        <script src="https://cdn.tailwindcss.com"></script>
    <script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar@0.18.2"></script>
  </head>
  <body class="flex flex-col h-screen">
    <div id="id-root" class="flex h-full m-2" data-on-load="$$get('/dashboard')"></div>
  </body>
</html>
        `)
        w.Write(page)
    })

    router.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
        sse := datastar.NewSSE(w, r)
        for i, fragment := range fragments {
            mergeType := datastar.WithMergeOuterElement()
            if i == 0 {
                mergeType = datastar.WithMergeInnerElement()
            }

            datastar.RenderFragmentString(
                sse,
                fragment,
                mergeType,
                // TODO: toggle the next line to see the issue, expected behavior is to see one blue tile and one teal tile
                // datastar.WithoutViewTransitions(),
            )
        }
    })

    fmt.Printf("Server starting on port %d...\n", port)
    err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", port), router)
    if err != nil {
        fmt.Println("Error starting server:", err)
    }
}

I am still seeing the behavior in 0.18.2