By default, the relationship fieldtype lets you select entries from various collections as well as create and edit items on the fly from within the field.
You can create your own relationship fields that provide the ability to select all different sorts of items from anywhere.
Example
To illustrate that you can get items from anywhere — even remote APIs — we’ll build a field where you can select tweets from a given user.
In your blueprints, you’ll be able to use type: tweets
(whatever you name your fieldtype) and all the options that the relationship field would normally give you, like max_items
:
fields: handle: tweets field: type: tweets max_items: 3
Creating the Fieldtype
You will need to create the fieldtype – no Vue component necessary – so you can skip it with the --php
php please make:fieldtype Tweets --php
Then instead of extending Fieldtype
, you’ll extend the existing Relationship
fieldtype:
use Statamic\Fieldtypes\Relationship; class Tweets extends Relationship{ //}
There are a handful of methods and properties inside the Relationship
class, and you can override them to control how it functions.
There are three main areas you will want to customize. The index items, the selected item data, and the listing data.
Index Items
The index items are what you’ll see in the item selector stack.
You can either override the getIndexQuery
method if you’re dealing with items being retrieved through the Statamic API. You’ll need to return a QueryBuilder.
public function getIndexQuery($request){ return Entry::query()->whereIn('collection', $request->collections);}
Or, you can override getIndexItems
for full control. We’ll use this for our Twitter example.
use Carbon\Carbon; public function getIndexItems($request){ $tweets = Twitter::getUserTimeline([ 'screen_name' => $this->config('screen_name') ]); return $this->formatTweets($tweets);} protected function formatTweets($tweets){ return collect($tweets)->map(function ($tweet) { $date = Carbon::parse($tweet->created_at); return [ 'id' => $tweet->id_str, 'text' => $tweet->text, 'date' => $date->timestamp, 'date_relative' => $date->diffForHumans(), 'user' => $tweet->user->screen_name, ]; });}
You can customize which columns will be used in the selector by overriding the getColumns
method:
use Statamic\CP\Column; protected function getColumns(){ return [ Column::make('text'), Column::make('user'), Column::make('date')->value('date_relative'), ];}
Selected Item Data
Once you select items, their id
values will be used as the value for your field. If you were to hit save, you would see
something like this in your content files:
tweets: - 54376134 - 89473529
In order to convert those values into something useful, you’ll either need to override the getItemData
method or the toItemArray
method. For our example, we’ll use the former:
public function getItemData($values, $site = null){ $tweets = Twitter::getStatusesLookup(['id' => implode(',', $values)]); return $this->formatTweets($tweets);}
Listing Data
When field data is to be displayed in a listing view (eg. in the entries listing table or the entry fieldtype), you may customize the display by overwriting the preProcessIndex
method.
In our Twitter field, let’s show only the text:
public function preProcessIndex($data){ $tweets = Twitter::getStatusesLookup(['id' => implode(',', $data)]); return collect($tweets)->map(fn($tweet) => $tweet->text)->join(', ');}
Creating Items
To disable creation of items, you can add the canCreate property.
protected $canCreate = false;
Searching
By default, the search bar will be visible in the selector stack. When a user types into it, its value will be submitted in the search
query parameter. You can tweak your logic to account for searching in your getIndexItems
method. For example:
public function getIndexItems($request){ return $request->search ? $this->searchTweets($request->search) : $this->userTweets();}
To disable searching, you can add the canSearch property.
protected $canSearch = false;
Customizing the view
By default, the fieldtype will show the standard draggable block, with the title
as the text. You may provide your
own Vue component to the itemComponent
property to replace it.
protected $itemComponent = 'TwitterRelationshipItem';
Vue.component('TwitterRelationshipItem', require('./TwitterRelationshipItem.vue'));
<template> <div class="mb-1 item"> <div class="item-move"> </div> <div class="item-inner"> <div class="p-3"> <p class="mb-2 text-lg">{{ item.text }}</p> <p class="text-grey">{{ item.user }} – {{ item.date_relative }}</p> </div> </div> <dropdown-list class="pr-1"> <ul class="dropdown-menu"> <li class="warning"><a @click.prevent="$emit('removed')" v-text="__('Unlink')"></a></li> </ul> </dropdown-list> </div></template> <script>export default { props: { item: Object }}</script>
An item
prop will be passed to your component which will contain one the objects provided by the getItemData
method.
In order to allow your users to remove their selection, you should emit a removed
event, as shown above.