DEV Community

Cover image for Build a location autocomplete field for Statamic V3
Matt Rothenberg
Matt Rothenberg

Posted on • Updated on

Build a location autocomplete field for Statamic V3

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.

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. 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 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

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

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

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.

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.

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.

$ php please make:fieldtype location

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

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 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.

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" something called FieldType.

<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 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!

😱 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.

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

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, 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 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. 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.

<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.

<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 😎.

Sweet sweet autocomplete.

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.

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
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.

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.

<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).

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 πŸ’― 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...

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.

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

Custom Index View

🍻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.

<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 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

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!

❀️

Top comments (1)

Collapse
 
gonzalo2683 profile image
Gonzalo Guevara

Very interesting, I'm looking for information on statamic, but not much is found, this is one of the best I've found.