Sharing Google Maps Data across Separate Livewire Components

Sharing a glowing Google Maps Marker between two green hands.
Image by Annie Ruygt

Deploy now on Fly.io, and get your Laravel app running in a jiffy!

Let’s say we have separate Livewire components for:

  1. A Google Map Element - an interactive map for adding and deleting location markers
  2. A Search box Element - to re-focus the Google Map element to a user given location
  3. A Drop Down Element - to filter markers shown in the Google Map element
  4. Finally, a Form - to submit user provided data, from all the components above

How exactly, do we share data between these separate Livewire components?

Sharing Possibilities

In this article, we’ll chance a glimpse on different possibilities for sharing data across Livewire components:

  1. Sharing Data through Livewire's dispatchBrowserEvent: useful when data from a component A in the server is needed to make changes to a component B’s UI
  2. Sharing Data through JavaScript Variables: useful when data gathered from component A’s UI is needed to make changes to a component B’s UI
  3. Sharing Data through Livewire's emit events: useful when processed data from a component A in the server is needed in a component B’s component in the server
  4. Sharing Data through Html Query Selectors: useful when we need to submit data from user input provided in separate components’ elements in parent component

We’ll go through all these different possibilities by integrating these Livewire components together: a Google Maps Element, a Search Box, a Drop Down Filter, and a Form.

Creating the Google Map Component

Let’s start with the center stage of our article today: A Google Map Component. We’ll use this to allow our users to add markers to different locations in the world.

First, create the component with php artisan make:livewire map. Make sure to add in its view a div element with a specified width and height to show the map:

<!--app\resources\views\livewire\map.blade.php-->
<div>
    <div 
    wire:ignore id="map" 
    style="width:500px;height:400px;">
    </div>

Then add the script to initialize a Google Maps element into this div. Make sure to authorize access to Google Maps api by either using the inline bootstrap loader or the legacy script loading tag.

    <script>
        /* Add Inline Google Auth Boostrapper here */

        /* How to initialize the map */
        let map;
        async function initMap() {
            const { Map } = await google.maps.importLibrary("maps");
            map = new Map(document.getElementById("map"), {
                    zoom: 4,
                    center: { lat: @js( $lat ), lng: @js( $lng ) },
                    mapId: "DEMO_MAP_ID",
            });
        }

        /* Initialize map when Livewire has loaded */
        document.addEventListener('livewire:load', function () { 
            initMap();
        });
    </script>
</div>

Notice the @js( $lat ) and @js( $lng ) in the code snippet above? That’s Livewire’s helper that let’s us use PHP attributes $lat and $lng in our view’s JavaScript. We’ll have to declare those from the Map component:

/* App\Http\Livewire\Map.php */

class Map extends Component{

      public $lat = -25.344;
      public $lng = 131.031;

Above, we have declared default location coordinates. This would show a default location in our maps component. In the next section below, we’ll add in a search box component to allow users to easily relocate focus to their desired location. And from there, we’ll implement the first way to share data across components.

Creating the Search Box Component

Create a separate component with: php artisan make:livewire map-search-box. It’s view will have two elements, an input text field and a button:

<!--app\resources\views\livewire\map-search-box.blade.php-->
<div>
    <input type="text" wire:model.defer="address" />
    <button wire:click="search">Search</button>
</div>

The text element is wired to an $address attribute of the component, and uses model.defer to make sure Livewire doesn’t send its default requests whenever a change occurs on the element. On the other hand, the button element is wired to a search() method in the Livewire component through a click listener.

This search() method converts the string value of the input element( wired to $address ) into location coordinates:

/* App\Http\Livewire\MapSearchBox.php */

class MapSearchBox extends Component{

      public $address;

      public function search()
      {
          // Use a custom service to get address' lat-long coordinates
          // Either through Google GeoCoder or some other translator
          $coordinates = new \App\Http\Services\GoogleLocationEncoder( 
            $this->address 
          );
      }

After retrieving our coordinates from the MapSearchBox component, we’ll have to re-center the location visible from our Map component’s view.

Hmmmm. But. The MapSearchBox component is a separate component from the Map component, so—how exactly do we share data between two separate components?

Sharing Data with Browser Events

A usual way to share event data between components is through Livewire’s emit feature. However, this approach actually makes two immediate requests for every emit: the first request a call to the component the event was emitted from, and the second request a call to the component listening for the emitted event.

In our use case( recentering the map based on the coordinates from the search box ), we don’t actually need the second request. We only need the resulting coordinates to be shared to our Map‘s UI to change the location it’s map is centered on.

So, instead of emit, let’s use Livewire’s dispatchBrowserEvent:

/* \App\Http\Livewire\MapSearchBox */
public function search()
{
    /* Get coordinates */

    // Dispatch event to the page
+  $this->dispatchBrowserEvent( 'updatedMapLocation',[
+    'lat' => $coordinates->getLatitude(),
+    'lng' => $coordinates->getLongitude()
+  ]);
}

The browser window event, updatedMapLocation from the MapSearchBox, is fired to the current page, where all available components can listen in on. Since our Map component’s view is also in this page, it can easily listen to the dispatched event and re-center the location based on the received coordinates:

/* \app\resources\views\livewire\map.blade.php */
<script>
// Listen to location update from search box
window.addEventListener('updatedMapLocation',function(e){
    // Defer set lat long values of component
    @this.set( 'lat', e.detail.lat, true);
    @this.set( 'lng', e.detail.lng, true);

    // Translate to Google coord
    let coord = new google.maps.LatLng(e.detail.lat, e.detail.lng);

    // Re-center map
    map.setCenter( coord );
});

This takes just one request to the MapSearchBox component to process coordinates from the user given string and update the Map component’s view to re-center its location—neat!

Fly.io ❤️ Laravel

Fly your servers close to your users—and marvel at the speed of close proximity. Deploy globally on Fly in minutes!

Deploy your Laravel app!

Revising the Maps Component for Pin Marking

An important functionality of map elements is their “Pin Dropping” feature, allowing users to mark locations in the map. For this, we’ll need a click event listener in our map element:

/* \app\resources\views\livewire\map.blade.php */
let map;

// Dictionary of markers, each marker identified by its lat lng string
+  let markers = {}; 

async function initMap() {
  /* Map initialization logic here... */

  // Add marker listener
+  map.addListener("click", (mapsMouseEvent) => {
      // Get coordinates 
      let coord = mapsMouseEvent.latLng.toJSON();

      // Generate id based on lat lng to record marker
      let id = coord.lat.toString()+coord.lng.toString();

      // Add Marker to coordinate clicked on, identified by id
      markers[id] = new google.maps.Marker({
          position: mapsMouseEvent.latLng,
          map,
          title: "Re-Click to Delete",
      });

      // Delete marker on re-click
      markers[id].addListener("click", () => {
          markers[id].setMap(null);
          delete markers.id;
      });
  });
});  

Step by step, what’s happening above? First, we add a JavaScript dictionary called markers to hold reference to uniquely identified location markers in the Map component’s view. Then, we revise the initMap() functionality to intercept click events on the map.

For each location clicked on, we get the coordinates, generate a unique id based on these coordinates, and assign this unique id as a key in our markers dictionary, setting its value as a reference to a new Google Map marker. We also add an on-click listener to each new marker to optionally delete them on re-clicking.

In the next section, we’ll move on to our second method of sharing data, by filtering the markers above through a separate filter component.

Creating the Filter Component

Let’s say we want a way to bulk remove map markers in the Map component based on filter conditions from the server. We can add a component for this: php artisan make:livewire map-marker-filter.

In this example, we’ll remove markers that are not within the confines of an “area”. We’ll use these “area” options to filter the map markers. To get these options, we can declare them from the MapMarkerFilter component:

/* \App\Http\Livewire\MapMarkerFilter */

class MapMarkerFilter extends Component
{
    public $options = [
        ['id'=>1, 'label'=>'Area1'],
        ['id'=>2, 'label'=>'Area2'],
    ];

We can provide these “area filters” as $options in a select element in the view:

/* app\resources\views\livewire\map-marker-filter.blade.php */
<div>
  <select id="area" onchange="filterChange( this )">
    <option>Select an Area</option>
    @foreach( $options as $opt )
        <option value="{{ $opt['id'] }}">
        {{ $opt['label'] }}
        </option>
    @endforeach
  </select>
</div>

When a new filter is selected, we’ll send two things to the MapMarkerFilter component in the server: 1️⃣the value of the selected option, and, 2️⃣the coordinate list from the JavaScript markers dictionary.

Sharing Data via JavaScript Variables

We can easily get the 1️⃣selected option on change, since it’s in our current MapMarkerFilter component. But how about the JS variable 2️⃣markers holding our pins that’s from the Map’s view? It’s declared in a different component, so how do we share this to the current MapMarkerFilter’s view?

To share the markers list from Map to MapMarkerFilter, let’s try a straightforward approach; let’s look into the scope the markers variable is available in. Being declared in the JavaScript of the Map component’s view, let’s see if we can get this value from the JavaScript of MapMarkerFilter.

/* app\resources\views\livewire\map-marker-filter.blade.php */
<script>
  function filterChange( objVal ){
      let filterId = objVal.value;
  +   let coords = Object.keys(markers);
  +   console.log( coords );
  +   @this.filterMarkers( filterId, coords );
  }
</script>

Save the changes, and select an option from the filter…and. And—it works! We actually have access to Map’s JS variable marker from MapMarkerFilter! No shocker here. These are JavaScript variables scoped and available to other scripts within the same page:

Notice the @this.filterMarkers() statement. It’s an inline way for calling a Component method from the view’s JavaScript. In the case above, two variables, filterId and coords from the JavaScript view is sent to a filterMarkers() method in the component:

/* \App\Http\Livewire\MapMarkerFilter.php */
public function filterMarkers( $filterId, $coords )
{
    // Using filterId, get marker ids that should be removed from the map
    // $toRemove sample: ["-19.19356730928235_125.40645731663705", "..."]
    $toRemove = (new \App\Http\Services\MapMarkerFilter)
    ->getCoordsToRemove( $filterId, $coords );

    // Send this back to the view
    $this->dispatchBrowserEvent( 'removeMarkers', [
        'coords' => $toRemove
    ]);
}   

From this method, we can use the selected $filterId to determine which markers in the $coordinates list is to be removed from the map view.

Once we’ve determined the marker coordinates to remove, we can use another dispatchBrowserEvent call to fire a removeMarkers browser event back to the client page. Our markers are in the Map component, and so, from its view’s JavaScript, let’s add a listener to this event, and remove the specified markers id sent from the event:

/* app\resources\views\livewire\map.blade.php */

/* Listen to location update from search box */
window.addEventListener('removeMarkers',function(e){
    // Delete each coordinate by id
    for( i in e.detail.coords ){
        let id = e.detail.coords[i];
        markers[id].setMap(null);
        delete markers[id];
    }
});

The Form Finale

For the finale, let’s submit all user input, from each components we’ve declared above: 1️⃣the search keyword from the Search component, 2️⃣the filter option selected from the Filter component, and finally, 3️⃣the map markers selected in the Map component.

Let’s create a form component with php artisan make:livewire map-form. For its view, we simply include all other components we’ve created, with an additional button:

<!--app\resources\views\livewire\map-form.blade.php-->
<div> 
  <h1>Form</form>
  <form wire:submit.prevent="submit">
      <div class="flex flex-row justify-between">
          <livewire:map-search-box />
          <livewire:map-territory-filter />
      </div>

      <livewire:map />
      <button type="submit" class="btn btn-primary">Submit</button>
  </form>
</div>

Now, we can certainly do an emit submit signal from parent that all child components will listen to, and emit up their values in response to:

/* App\Http\Livewire\MapForm.php */
public function submit(): void
{
    $this->emit('submit');
}

/* App\Http\Livewire\<somechildcomponenthere> */
// Listen to submit from parent
public $listeners = [
    'submit' 
];

public function submit(): void
{
    $this->emitUp('map-form', $this->valueNeedToPass);
}

But. Thats gonna be a ton of request, one request from the form, and two requests to-and-fro each listening component (1. to receive the parent emit event, and 2. to send value to parent component ). Ah-yikes.

A'ight, since the above ain’t so thrifty, let’s try a different approach on sharing, shall we?

Sharing Data through HTML Element Query Selector

Similar to how js variables are available from one component to another component in the same page, so are html elements! So, we simply get the values of the input elements from the MapSearch and MapMarkerFilter components in our MapForm’s JavaScript.

First, revise the form element to call a JS function:

<!--app\resources\views\livewire\map-form.blade.php-->

- <form wire:submit.prevent="submit">
+ <form onsubmit="return process()">

Then, make sure to add an identifier to their input elements:

<!--app\resources\views\livewire\map-search-box.blade.php-->
<input id="searchString" type="text" placeholder="Location" wire:model.defer="address" />

<!--app\resources\views\livewire\map-marker-filter.blade.php-->
<select id="filterId" onchange="filterChange( this )">

Since markers is available as a variable in the current page, we can simply call it from MapForm’s JavaScript as well:

function process()
{
  // Get Search keyword
  @this.set('searchKey', document.getElementById('searchId').value, true);

  // Get Selected Filter option
  @this.set('filterId', document.getElementById('filterId').value, true);

  // Get markers keys list, as the key themselves are the coordinates
  @this.set('coords', Object.keys(markerList), true);

  // Trigger Submit
  @this.submit();

  return false;
}

Then simply define the submit method in our MapForm component:

/* \App\Http\Livewire\MapForm.php*/

public $searchKey;
public $filterId;
public $coords;

public function submit()
{
    // Validate entries
    $this->validate([
      'searchKey' => 'string',
      'filterId'  => 'numeric',
      'coords'    => 'required',
    ]);

    // Do processing

    // Redirect 
}

Before calling the submit() method in the server, we first set the $searchKey,$filterId, and $coordsvalues from different components through query selection, and JavaScript variables. And now, we all have these values from separate components in our parent MapForm component!

Learning Possibilities

So, what did we learn today?

—Possibilities!

We learned about four, different, use-case-helpful possibilities in sharing data amongst Livewire components.

And although, they’re not all pure Livewire specific approaches, they do the job pretty nicely in their specific use case domains. Try them out sometime!