Build a location autocomplete field for Statamic V3
Tags: php, tutorial, vue
Published: 2019-11-24 02:43:57

**UPDATE**: An open sourced version of this add-on can be found here: https://github.com/mattrothenberg/location

If you're anything like me, you were totally psyched to hear about the Open Beta of Statamic V3.

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">The Statamic 3 Open Beta has begun! 🎉<br><br>The docs: <a href="https://t.co/D3MupV0ssH">https://t.co/D3MupV0ssH</a><br><br>The repo: <a href="https://t.co/JFDKodjvqH">https://t.co/JFDKodjvqH</a><br><br>Huge thanks to our 100 Alpha testers, we can’t fit them in all in a tweet, but you know who you are. <br><br>Super excited to see where this next phase brings us. 😊</p>&mdash; Statamic (@statamic) <a href="https://twitter.com/statamic/status/1197536602878283778?ref_src=twsrc%5Etfw">November 21, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

In the days since the beta opened, I've been exploring this new version and would love to share with you all my process for building a custom [Fieldtype](https://statamic.dev/extending/fieldtypes). In this case, we'll be building a `location` fieldtype which allows content authors to select their street address with the assistance of a typeahead, autocomplete search widget.

Keep in mind that Statamic V3 is still very much in beta, which means that APIs and documentation are subject to (breaking) change(s).


## Assumptions & Prerequisites
- You have a [fresh installation of Statamic V3](https://statamic.dev/installation) ready to play with.
- You have Yarn or NPM installed globally.
- You're ready to give me feedback on how good/bad this tutorial is 😅

## The Goal

As you might be able to tell from the article's cover photo, we're trying to build a fancy new fieldtype that autocompletes users' address as they type. I recently had to implement a similar fieldtype for a Craft CMS website which functions as a directory for local equipment manufacturers. With this information in the site's database, we can build all sorts of cool things on the front-end (which are out of scope for this tutorial, but worth mentioning).

- An interactive map showing the location of various businesses

- A search experience where users can find manufacturers closest to their ZIP code

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">I had fun building this custom location autocomplete fieldtype for <a href="https://twitter.com/statamic?ref_src=twsrc%5Etfw">@statamic</a>. Would folks get value out of a short blog post and/or screencast walking through the development process? <a href="https://t.co/tKceLsA0ex">pic.twitter.com/tKceLsA0ex</a></p>&mdash; matt rothenberg 🍕 (@mattrothenberg) <a href="https://twitter.com/mattrothenberg/status/1198345865825574914?ref_src=twsrc%5Etfw">November 23, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## Getting Started

If you haven't yet run `yarn` or `npm install` in your project's root directory, now would be a good time to do so. We're going to be making use of Statamic's development scripts (namely `yarn watch`) as we build out our custom fieldtype.

In order to add custom Javascript to our site's control panel, we need to [follow the steps outlined in the Statamic docs](https://statamic.dev/extending/control-panel#adding-css-and-js-assets)

In `AppServiceProvider`, located in `app/Providers/AppServiceProvider.php`, we need to add the following incantations.

```php
use Statamic\Statamic;

class AppServiceProvider
{
    public function boot
    {
        Statamic::script('app', 'cp.js');
    }
}
```

Then, from the command line, let's create the aforementioned `cp.js` file.

```bash
echo "console.log('sup')" > resources/js/cp.js
```

And finally, let's tell Webpack Mix – Laravel's mechanism for compiling our CSS/JS assets – to compile this file. Add the following line to your `webpack.mix.js`.

```js
mix.js("resources/js/cp.js", "public/vendor/app/js");
```

We're good to go. Run `yarn watch`, open up your browser and navigate to your control panel, and you should see `sup` logged in your browser console.

## Custom Fieldtypes

Lucky for us, Statamic makes it super easy to add custom fieldtypes to your site. In your terminal, run the following command.

```bash
$ php please make:fieldtype location
```

If all goes well, you should see the following output.

```bash
Fieldtype created successfully.
Your fieldtype class awaits at: app/Fieldtypes/Location.php
Your fieldtype vue component awaits at: resources/js/components/fieldtypes/Location.vue
```

Now is as good a time as any to create a [Blueprint](https://statamic.dev/blueprints) which uses this custom `location` fieldtype. Keep in mind that in its current state, the fieldtype will be totally unusable. Not for long, though 😈.


![Blueprint field selector. Note the Location field type.](https://thepracticaldev.s3.amazonaws.com/i/ewga0l620haoytn4v19l.png)

Our `make:fieldtype` command generated two files for us – one PHP file and one Vue file. The PHP file acts as a sort of "controller" for our Vue component, doing whatever business logic / data transformation is necessary to ensure that our Vue component has the data it needs for its presentational concerns. Specifically, this PHP file exposes a handful of functions we can take advantage of 

- `public function blank() {}` – What should the blank/default value of our field be?
- `public function preProcess() {}` – How should we transform the data that lives inside of our entry _before_ it gets to the Vue component?
- `public function process() {}` – How should we transform the data the our Vue component emits _after_ a user hits the "Save & Publish" button?

Our Vue component, on the other hand, has some interesting boilerplate to look at.

Note that by default, our component is rendering the following component (provided to us by Statamic) and ["mixing in"](https://vuejs.org/v2/guide/mixins.html) something called `FieldType`.

```vue
<template>
  <div>
    <text-input :value="value" @input="update" />
  </div>
</template>

<script>
export default {
  mixins: [Fieldtype],
  data() {
    return {}
  }
};
</script>

```

As a quick refresher, Vue mixins are one (of many) ways to share functionality across Vue components. In our case, the mixin is giving us access to a field called `value` (which corresponds to the literal value of our location field), as well as a function called `update` (which is a callback for persisting a new value for our location field.)

I'm personally not a huge fan of mixins, for the simple reason that our component's dependencies – `value` and `update` – are totally _implicit_. You just "have to know" what exactly `FieldType` mixes in to our `Location.vue` component in order to use it effectively. I encourage the Statamic devs to consider something like a higher-order component / [scoped slots](https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots) to make this "mixing in" of Statamic-specific behavior more explicit.

Apologies for the sidebar, let's get back to work.

Assuming you've created a blueprint for a specific collection/structure with our new `Location` field, let's head over and try to create an entry.

![Uh oh, nothing's working!](https://thepracticaldev.s3.amazonaws.com/i/bzu2us3zyis769yp8zbt.png)

:scream: Uh oh! We have a blank spot where our custom field should go!

This is because despite scaffolding our custom fieldtype, we never _registered_ it such that our Control Panel can use it. Inside of `cp.js`, let's go ahead and import our Vue component and register it accordingly.

```js
import Location from "./components/fieldtypes/Location";

Statamic.booting(() => {
  // NOTE: We need to add `-fieldtype` to the end of our
  // component's name in order for the CP to recognize it.
  Statamic.$components.register("location-fieldtype", Location);
});

```

![Voila](https://thepracticaldev.s3.amazonaws.com/i/e0wj8508etq82s70tv2m.png)

And there you have it. We've got a simple-yet-custom Vue component for specifying our `location` value.

Run `yarn watch` from your terminal to fire up the development server and get ready for the next steps!

## Address Autocompletion

There's no shortage of wonderful geolocation / address autocompletion services out there. A personal favorite of mine is [Algolia Places](https://community.algolia.com/places/), primarily because they have a generous free tier and a kick-ass Javascript library for turning a plain old HTML5 input into a fancy autocomplete widget.

[Sign up for a free account](https://www.algolia.com/users/sign_up/places) and procure yourself an `APP_ID` and an `API_KEY`. You're going to need them in a second.

As mentioned before, Algolia offers a wonderful Javascript library for "turning any input into an address autocomplete," [`places.js`](https://github.com/algolia/places). Let's go ahead and add it to our project.

```
yarn add places.js
```

In our `Location.vue` component, let's go ahead and bring `places.js` into the mix. First things first, let's replace the `text-input` component with a plain `input`.

```vue
<template>
  <div>
    <input placeholder="Start typing your address" :value="value" ref="inputRef" />
  </div>
</template>
```

Then, in our component's script tag, let's import `places.js` and use it in the `mounted` lifecycle hook (a common procedure, by the way, for using a third-party Javascript library inside of a Vue component). Be sure to use your `APP_ID` and `API_KEY` from your Algolia account.

```js
<script>
import places from "places.js";
export default {
  mixins: [Fieldtype],
  mounted() {
    const placesInstance = places({
      appId: YOUR_APP_ID,
      apiKey: YOUR_API_KEY,
      container: this.$refs.inputRef
    });
  }
};
</script>
```

Save your changes, head back to the Control Panel, and take your new autocomplete for a test drive :sunglasses:.

![Sweet sweet autocomplete](https://thepracticaldev.s3.amazonaws.com/i/e98j3yygagz86tvsbi2z.gif).

But wait, there's one *major* problem. When we hit "Save & Publish" and then refresh, the input has a blank value? How could this be, you may ask? Well, we forgot the most important part here – persisting the autocompleted address to our database.

Let's hook into the `change` event on our instance of `places.js`, binding the event to a method on our Vue component called `handleAddressSelect`.

```js
mounted () {
  // below plugin initialization
  placesInstance.on("change", this.handleAddressSelect);
},
methods: {
  handleAddressSelect(e) {
    this.update(e);
  }
}
```

Once more, back to the browser for a test drive. We're getting warmer, but this time after we refresh, our input is pre-populated with some less-than-helpful data.

![Object object](https://thepracticaldev.s3.amazonaws.com/i/h17rkjuzhfe4vmkjb1yg.png)
_Ah yes, I love the town of [Object object]_...

Believe it or not, this is actually a _good_ thing that we're seeing. What this tells us is that our backend has persisted the correct data – in this case, a serialized version of a [gnarly location object that `places.js` spits out](https://community.algolia.com/places/documentation.html#api-events-change). 

What we need to do now is _translate_ this serialized object into a format that our `input` can use as its `value` prop. To that end, let's update our template code as follows.

```vue
<div>
  <input
    placeholder="Start typing your address"
    :value="inputValue"
    ref="inputRef"
  />
</div>
```

And let's add a computed property, `inputValue`, which plucks the correct field off our gnarly, serialized `location` data (if it's available, otherwise returns an empty string).

```js
computed: {
  inputValue() {
    // If we've got a value, let's take `suggestion.value` off it.
    return this.value ? this.value.suggestion.value : "";
  }
}
```

Head back to your browser, refresh the page, and give it a spin. Everything should be looking :100: now. Our input should be pre-populated with a string (instead of 'Object object') and subsequent updates such persist the correct data to the backend.

## Custom Index Views

Please don't kill me, but we _do_ have another problem. Go back to the collection index view and feast your eyes on the massive blob of data that's being shown in the `Location` column.

![The horror...](https://thepracticaldev.s3.amazonaws.com/i/eul16iqoq3dbo5zfwk9u.png)

By default, Statamic will try to display the contents of our serialized location data in this table. But that's _definitely_ not what we want.

Lucky for us, Statamic provides two ways for us to customize the presentation of our location data inside of a collection index view.

### The "simple" way

Remember how I told you that the PHP file we generated when running `make:fieldtype` exposed a bunch of functions we could use to transform our location data? Well, I forgot to mention that one of those functions is called `preProcessIndex` and we can use it to change how our location data is presented on index views accordingly.

Let's write a naïve function returns the nested property `suggestion.value` _if_ our location exists. Otherwise, let's return some boilerplate text letting users know that this entry doesn't have a location.

```php
public function preProcessIndex($value)
{
    return $value ? $value['suggestion']['value'] : 'No location specified.';
}
```

![Custom Index View](https://thepracticaldev.s3.amazonaws.com/i/lrdggcnjwfzr5aqa93q9.png)

🍻Congratulations on building your very first custom fieldtype!

### The "harder" way

Let's say that you wanted to add some pizzazz to the index view. Simple text is _so_ 2018.

Lucky for us, Statamic offers a Vue-based API for customizing the presentation of our collection index views.

In `resources/js/components/fieldtypes`, let's add a component called `LocationIndex.vue` and add the following code to it.

```vue
<template>
  <div>
    <div class="flex items-center" v-if="value">
      {{ value.suggestion.name }}
      <a class="ml-1" :href="mapLink">→</a>
    </div>
    <span class="text-red" v-else>Yikes, no location yet!</span>
  </div>
</template>

<script>
export default {
  mixins: [IndexFieldtype],
  computed: {
    mapLink() {
      return `https://www.openstreetmap.org/search?query=${this.value.suggestion.value}`;
    }
  }
};
</script>

```

Note that we're mixing in `IndexFieldType` which affords us a `value` object that we can use in our Vue template. This is indeed our gnarly serialized location data, so we can pluck off `suggestion.value` like we did above, as well as other data like longitude and latitude, etc.

One of the amazing things about Statamic – and I'm honestly not sure whether this is accidental or on-purpose – is that it uses [TailwindCSS](https://tailwindcss.com/) for styling the control panel. What this means is that _we_ can use Tailwind classes in our custom control panel Vue components, as shown above. Here, we're showing the address name inline with a little arrow which, when clicked, takes users to an OpenStreetMap view of the location. Otherwise, we're showing some helper text to let users know that no location has been selected.

![Vue component](https://thepracticaldev.s3.amazonaws.com/i/65psivnbxf39ro05khie.png)

And there you have it! We've built a fancy autocomplete widget which helps users enter location data, and we've explored how to customize how that data is displayed on the backend.

Believe me, we've only scratched the surface here – there are tons of interesting avenues of exploration from here, and I hope this post gives you the confidence you need to embark upon your custom fieldtype journey!


Please reach out on twitter @mattrothenberg if you find any typos or issues, or if you have questions!

:heart: