Deploy now on Fly.io, and get your Laravel app running in a jiffy!
Let’s say we have separate Livewire components for:
- A Google Map Element - an interactive map for adding and deleting location markers
- A Search box Element - to re-focus the Google Map element to a user given location
- A Drop Down Element - to filter markers shown in the Google Map element
- 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:
- 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 - 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 - 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 - 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!
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 $coords
values 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!