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.
The Statamic 3 Open Beta has begun! 🎉
— Statamic (@statamic) November 21, 2019
The docs: https://t.co/D3MupV0ssH
The repo: https://t.co/JFDKodjvqH
Huge thanks to our 100 Alpha testers, we can’t fit them in all in a tweet, but you know who you are.
Super excited to see where this next phase brings us. 😊
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
I had fun building this custom location autocomplete fieldtype for @statamic. Would folks get value out of a short blog post and/or screencast walking through the development process? pic.twitter.com/tKceLsA0ex
— matt rothenberg 🍕 (@mattrothenberg) November 23, 2019
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 😈.
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! 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);
});
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 😎.
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.
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.
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.';
}
🍻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.
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)
Very interesting, I'm looking for information on statamic, but not much is found, this is one of the best I've found.