Mapbox & LiveView - map immediately disappears after the liveview updates the DOM with autocomplete search results

Hey all,

I recently implemented an autocomplete search input for my application which displays a map along side. I’m having some difficulty though, because the map immediately disappears after the liveview updates the DOM with the autocomplete search results. I’m using a hook to load the map and additional data layers. The search box uses a normal phx-change tag with a pretty generic handle_event/3 function that just assigns a list of results to display. The markup for the search results is below:

<div id="results" class="...">
<%= for result <- @search_results do %>
  <div class="..." phx-click={search_click(result)}><%= result.title %></div>
<% end %>
</div>

The only other unusual thing is the phx-click tag which uses the new LiveView.JS module, but that doesn’t seem to be the root cause. (I tried removing it and still have the same effect)

Finally, I’ve also tried adding phx-update="ignore" to my map tag, but that produces worse results?

Any suggestions appreciated, thanks!

Current behavior:
ezgif-1-979c600ea0

With phx-change=“ignore”:
ezgif-1-7e95e5e75a

1 Like

Hard to tell without all the code, but my hunch is your map display is not separated from the autosuggest results.
Here’s a nice example that helped me implement an autosuggest: phoenix_live_view_example/search_live.ex at master · chrismccord/phoenix_live_view_example · GitHub
Also you might want to look at bindings: Bindings — Phoenix LiveView v0.17.7
Specifically phx-change vs phx-submit

1 Like

Thanks for the suggestion - I don’t think this solves my issue though. I had originally attempted the datalist method for autosuggest that you linked, but I didn’t like how the browser rendered it so I went for my own markup ¯\_(ツ)_/¯

Side note: this line in the heex is really useful! I had no idea you could add a binary flag with the {elixir code} syntax (I thought it had to be attr={elixir code})

<input type="text" name="q" value={@query} list="matches" placeholder="Search..." {%{readonly: @loading}}/>

I can provide more code though, which probably is helpful :smiley:

FullMap hook:

FullMap: {
        mounted() {
            this.map = new Mapper();
            this.map.initMap('full-map');
            this.handleEvent('street-data', (payload) => {
                geojson = {
                    'type': 'FeatureCollection',
                    'features': payload.data.map(street => {
                        const { geometry, ...properties } = street
                        return {
                            type: 'Feature',
                            geometry: geometry,
                            properties: properties
                        }
                    })
                }
                this.map.addStreets(geojson, payload.sector_id)
            });
            this.handleEvent('update-streets', (_payload) => {
                this.map.clearStreets()
            })
            window.addEventListener('zoom-to', (e) => {
                this.map.zoom(e.detail)
            })
            this.map.registerMapEvent('load', () => { this.pushEvent('load-data', { bounds: this.map.mapBounds() }) });
            this.map.registerMapEvent('moveend', () => { this.pushEvent('load-data', { bounds: this.map.mapBounds() }) });
        }
    },

and the Mapper class:

class Mapper {

    constructor() {
        this.map = null;
    }

    initMap(container_id) {
        mapboxgl.accessToken = document.getElementById(container_id).dataset.apiKey;
        this.map = new mapboxgl.Map({
            container: container_id,
            style: 'mapbox://styles/mapbox/streets-v11',
            center: [-122.438434, 37.769397],
            zoom: 15,
            maxBounds: [
                [-122.66336, 37.652987], // Southwest coordinates
                [-122.250481, 37.851651] // Northeast coordinates
            ],
            minZoom: 13
        });
    }

    registerMapEvent(event, fun) {
        this.map.on(event, fun);
    }

    mapBounds() {
        return this.map.getBounds().toArray();
    }

    clearStreets() {
        if (this.map.getSource('sweep-streets') != undefined) {
            this.map.removeLayer('sweep-streets')
            this.map.removeSource('sweep-streets')
        }
    }

    zoom(geometry) {
        const bounds = new mapboxgl.LngLatBounds(geometry.coordinates[0], geometry.coordinates[0]);
        for (const coord of geometry.coordinates) {
            bounds.extend(coord);
        }
        this.map.fitBounds(bounds, { padding: 20 });
    }

    addStreets(geojson) {
        var geojson_source = this.map.getSource('sweep-streets')

        if (geojson_source == undefined) {
            this.map.addSource('sweep-streets', {
                type: 'geojson',
                data: geojson
            });

            this.map.addLayer({
                id: 'sweep-streets',
                type: 'line',
                source: `sweep-streets`,
                layout: {
                    'line-join': 'round',
                    'line-cap': 'round'
                },
                paint: {
                    'line-color': ['get', 'color', ['at', 0, ['get', 'sweeps']]],
                    'line-width': 3,
                }
            });

            const popup = new mapboxgl.Popup({
                closeButton: false,
                closeOnClick: false
            });

            this.map.on('mouseenter', 'sweep-streets', (e) => {
                this.map.getCanvas().style.cursor = 'pointer';

                const coordinates = e.lngLat;
                const properties = e.features[0].properties;
                const sweeps = JSON.parse(properties.sweeps)

                const html = `<div>
                                <h1 class="font-bold text-lg">${properties.corridor}</h1>
                                <div class="flex"><p class="flex-1">${properties.limits}</p><p class="mr-2">${properties.block_side}</p></div>
                                <p>Next street sweeping is <b>${formatDay(sweeps[0].next)}</b> from <b>${formatHour(sweeps[0].from_hour)}</b> to <b>${formatHour(sweeps[0].to_hour)}</b>.</p>
                            </div>`

                popup.setLngLat(coordinates).setHTML(html).addTo(this.map);
            });

            this.map.on('mouseleave', 'sweep-streets', () => {
                this.map.getCanvas().style.cursor = '';
                popup.remove();
            });

        } else {
            var new_features = geojson_source._data.features.concat(geojson.features);
            geojson_source.setData({ type: "FeatureCollection", features: new_features });
        }
    }
};

Oh and the heex for the view is as follows (with all tailwind classes removed for brevity - though most of the plain divs are flexbox if it makes any difference):

~H"""
    <section>
      <div>
        <div>
          <h1>Saved Streets</h1>
        </div>
        <.form let={f} for={:search} phx-change="search" phx-submit="search">
          <div>
            <%= search_input f, :params, list: "streets", autocomplete: "off", phx_debounce: 200, phx_focus: %JS{} |> JS.show(to: "#results"), phx_blur: %JS{} |> JS.hide(to: "#results") %>
            <%= submit do %>
              <svg>icon</svg>
            <% end %>
          </div>
          <div id="results">
          <%= for result <- @search_results do %>
            <div phx-click={search_click(result)}><%= "#{result.corridor}, #{result.limits}" %></div>
          <% end %>
          </div>
        </.form>

        ...
    
      </div>
      <div id="full-map" phx-hook="FullMap" data-api-key={mapbox_api_token()}/>
    </section>
    """

I assume you’re adding the phx-update="ignore" in here?

Correct - I took it out because the map goes to the wrong position as shown in the gif

Hello. Maybe it won’t change anything but I think I had kind of the same problem with leaflet. If I remember correctly I fixed it by just adding a wrapper div with phx-ignore (instead of putting phx-ignore directly on the map div, add a div as a parent with the phx-ignore).

I hope it will work :crossed_fingers:

2 Likes

Thanks! This did the trick!

Hope this helps anyone else with this maps issue :smiley:

3 Likes

Nice. Glad I could help I remember I tried everything I could think of before trying this simple thing (but I don’t know if it is normal : maybe it is a bug or maybe it should be explicitly said somewhere in the phx-ignore docs?)

1 Like

Well, that solves that problem of mine!

3 Likes