diff --git a/content/collections/extending-docs/dictionaries.md b/content/collections/extending-docs/dictionaries.md new file mode 100644 index 000000000..4948ecbde --- /dev/null +++ b/content/collections/extending-docs/dictionaries.md @@ -0,0 +1,253 @@ +--- +id: d0668b6e-915b-46da-863e-51fec54b02e2 +blueprint: page +title: Dictionaries +template: page +intro: 'Dictionaries add options to the [Dictionary](/fieldtypes/dictionary) fieldtype.' +--- +## Overview + +Dictionaries come in two "flavors" depending on which class you extend from. + +With a `BasicDictionary`, you only really need to define the items. The options, searching, and GraphQL behavior is all handled automatically. + +If you need more control, you can either override methods, or extend the base `Dictionary` class and do it yourself. + +:::tip +You might not even need a custom dictionary. The native [File dictionary](/fieldtypes/dictionary#file) allows you simply provide a JSON, YAML, or CSV file to use a source of options. +::: + + +## Basic Dictionaries + +You may create a dictionary using the following command, which will generate a class in the `App\Dictionaries` namespace. + +```shell +php please make:dictionary +``` + +You may generate it into an addon using the `--addon=vendor/package` option, which will generate it into your addon's `Dictionaries` namespace. + +```php + 'Alabama', 'value' => 'AL', 'capital' => 'Montgomery'], + ['label' => 'Alaska', 'value' => 'AK', 'capital' => 'Juneau'], + ['label' => 'Arizona', 'value' => 'AZ', 'capital' => 'Phoenix'], + // ... + ]; + } +} +``` + +### Item data + +In the example above, you can see that each item has a `label` and `value`. These will be used in the dropdown field. Any additional keys will be available within templates. + +Here we are returning a hardcoded array. But in reality you may be getting options from somewhere like a file, database, or an API: + +```php +protected function getItems(): array +{ + return Product::all()->toArray(); +} +``` + + +### Values and Labels + +By default, the `value` and `label` keys will be used. However, you may remap them: + +```php +protected function getItems(): array +{ + protected string $valueKey = 'abbr'; + protected string $labelKey = 'name'; + + return [ + ['name' => 'Alabama', 'abbr' => 'AL', 'capital' => 'Montgomery'], + // ... + ]; +} +``` + + +If you require more logic, you can override the `getItemValue` and/or `getItemLabel` methods: + +```php +protected function getItemLabel(array $item): string +{ + return $item['name'] . ' (' . $item['label'] . ')'; // "Alabama (AL)" +} +``` + +### Basic Search + +By default, when a user searches the field, a basic search will be performed by checking against each item's values. + +You may use the `searchable` property to narrow down which fields should be searched. + +```php +protected array $searchable = ['name', 'abbr']; +``` + +Alternatively, you may customize how the match is performed by overriding the `matchesSearchQuery` method: + +```php +protected function matchesSearchQuery(string $query, Item $item): bool +{ + return str_contains($item['name'], $query); +} +``` + +## Options + +The `options` method controls what is selectable within the fieldtype. This method should return an array of value/label pairs. + +```php +public function options(?string $search = null): array +{ + return [ + 'one' => 'Option One', + 'two' => 'Option Two', + ]; +} +``` + +This array's keys define what will be stored in the content. + +### Search + +The `options` method will be passed a `$search` string if the user is searching within the fieldtype. You should filter your options based on this search term. + +## Items + +The `get` method accepts a value (one of the option's keys) and should return an `Item` instance. + +An `Item` requires the value, label, and optionally an array of any additional data. + +In the following example we assume a product ID was saved to the content, the product name is the label, and price/sku is extra. + +```php +public function get(string $key): ?Item +{ + $product = Product::find($key); + + return new Item($key, $product->name, [ + 'price' => $product->price, + 'sku' => $product->sku, + ]); +} +``` + +## Config + +You may define config fields in order for the user to customize functionality of your dictionary. For example, if you are providing products, you may want to allow the user to select a category to narrow down the options. + +```php +protected function fieldItems() +{ + return [ + 'category' => [ + 'type' => 'select', + 'options' => ['clothing', 'accessories'] + ] + ]; +} +``` + +The user's configuration values will be available in your class within the `config` property. + +```php +$this->config['category']; +``` + +## GraphQL + +A dictionary will automatically get a GraphQL type named `Dictionary_YourClass`. Within it, you're able to query the item's fields, like so: + +```graphql +your_dictionary_field { + id + price +} +``` + +By default, the base `Dictionary` class will provide the GraphQL schema for nested fields automatically. It does this by looking up the first item. You may wish to override this and provide your own schema. + +```php +protected function getGqlFields(): array +{ + return [ + 'id' => [ + 'type' => GraphQL::nonNull(GraphQL::string()), + 'resolve' => fn (Item $item, $args, $context, $info) => $item['id']; + ], + 'price' => [ + 'type' => GraphQL::nonNull(GraphQL::int()), + 'resolve' => fn (Item $item, $args, $context, $info) => $item['price']; + ], + // ... + ]; +} +``` + +## Full Example + +Here is an example dictionary class that will use the [MusicBrainz](https://musicbrainz.org/) API to create a dictionary of musicians/artists. + +```php +json(); + + return collect($response['artists'])->mapWithKeys(function ($artist) { + $label = $artist['name']; + + if ($disambiguation = $artist['disambiguation'] ?? null) { + $label .= ' ('.$disambiguation.')'; + } + + return [$artist['id'] => $label]; + })->all(); + } + + public function get(string $key): ?Item + { + return Cache::rememberForever('artist-'.$key, function () use ($key) { + $response = Http::get('https://musicbrainz.org/ws/2/artist/'.$key.'?fmt=json')->json(); + + return new Item($key, $response['name'], [ + 'name' => $response['name'], + 'disambiguation' => $response['disambiguation'] ?? null, + 'type' => $response['type'], + 'country' => $response['country'], + ]); + }); + } +} +``` diff --git a/content/collections/fieldtypes/dictionary.md b/content/collections/fieldtypes/dictionary.md new file mode 100644 index 000000000..f8d6132c1 --- /dev/null +++ b/content/collections/fieldtypes/dictionary.md @@ -0,0 +1,213 @@ +--- +title: Dictionary +description: Choose from options provided by dictionaries. +intro: Give your users a list of options to choose from. Similar to the Select field, but allows you to read options from YAML or JSON files, or even hit external APIs. +screenshot: fieldtypes/screenshots/dictionary.png +options: + - + name: dictionary + type: array + description: | + Configure the dictionary to be used. You may also define any config values which should be passed along to the dictionary. The `dictionary` option accepts both string & array values: + + ```yaml + # When it's a dictionary without any config fields... + dictionary: countries + + # When it's a dictionary with config fields... + dictionary: + type: countries + region: Europe + ``` + - + name: placeholder + type: string + description: | + Set the non-selectable placeholder text. Default: none. + - + name: default + type: string + description: | + Set the default option key. Default: none. + - + name: max_items + type: integer + description: > + Cap the number of selections. Setting this to 1 will change the UI. Default: null (unlimited). +id: 9b14b5b8-6a7a-4db2-8533-9c78faa0e054 +--- +## Overview +At a glance, the Dictionary fieldtype is similar to the [Select fieldtype](/fieldtypes/select). However, with the Dictionary fieldtype, options aren't manually defined in a field's config, but rather returned from a PHP class (called a "dictionary"). + +This can prove to be pretty powerful, since it means you can read options from YAML or JSON files, or even hit an external API. It also makes it easier to share common select options between projects. + +## Data Storage +Dictionary fields will store the "key" of the chosen option or options. + +For example, a dictionary might have items such as: + +```php +'jan' => 'January', +'feb' => 'February', +'mar' => 'March', +``` + +Your saved data will be: + +``` yaml +select: jan +``` + +## Templating +Dictionary fields will return the "option data" returned by the dictionary's `get` method. The shape of this data differs between dictionaries and is outlined below. + +For example, using the built-in Countries dictionary, your template might look like this: + +```yaml +past_vacations: + - USA + - AUS + - CAN + - DEU + - GBR +``` + +``` + +``` + +```html + +``` + +## Available Dictionaries +Statamic includes a few dictionaries straight out of the box. + +### File +This allows you point to a file located in your `resources/dictionaries` directory to populate the options. The file can be `json`, `yaml`, or `csv`. + +Each option array should have `label` and `value` keys at the minimum. Any additional keys will be available when templating. + +You may redefine which keys are used for the labels and values by providing them to your fieldtype config. In the following example, `name` is the label and `id` is the value. + +```json +[ + {"name": "Apple", "id": "apple", "emoji": "🍎"}, + {"name": "Banana", "id": "banana", "emoji": "🍌"}, + {"name": "Cherry", "id": "cherry", "emoji": "πŸ’"}, + ... +] +``` + +```yaml +- + handle: fruit + field: + type: dictionary + dictionary: + type: file + filename: fruit.json + label: name # optional, defaults to "label" + value: id # optional, defaults to "value" +``` + +You may provide enhanced labels using basic Antlers syntax. For example, to include the emoji before the fruit name, you can do this: + +```yaml +label: '{{ emoji }} {{ name }}' +``` + +### Countries +This provides a list of countries with their ISO codes, region, subregion, and flag emoji. +```yaml +- + handle: countries + field: + type: dictionary + dictionary: + type: countries + region: 'oceania' # Optionally filter the countries by a region. + # Supported options are: africa, americas, asia, europe, oceania, polar +``` +```yaml +countries: + - USA + - AUS +``` +``` +{{ countries }} + {{ emoji }} {{ name }}, {{ iso2 }}, {{ iso3 }}, {{ region }}, {{ subregion }} +{{ /countries }} +``` +``` +πŸ‡ΊπŸ‡Έ United States, US, USA, Americas, Northern America +πŸ‡¦πŸ‡Ί Australia, AU, AUS, Oceania, Australia and New Zealand +``` + +### Timezones +This provides a list of timezones and their UTC offsets. + +```yaml +- + handle: timezones + field: + type: dictionary + dictionary: + type: timezones +``` +```yaml +timezones: + - America/New_York + - Australia/Sydney +``` +``` +{{ timezones }} + {{ name }} {{ offset }} +{{ /timezones }} +``` +``` +America/New_York -04:00 +Australia/Sydney +10:00 +``` + +### Currencies +This provides a list of currencies, with their codes, symbols, and decimals. + +```yaml +- + handle: currencies + field: + type: dictionary + dictionary: + type: currencies +``` +```yaml +currencies: + - USD + - HUF +``` +``` +{{ currencies }} + {{ name }}, {{ code }}, {{ symbol }}, {{ decimals }} +{{ /currencies }} +``` +``` +US Dollar, USD, $, 2 +Hungarian Forint, HUF, Ft, 0 +``` + +## Custom Dictionaries + +In many cases, using the native [File](#file) dictionary can be all you need for something custom. However, it's possible to create an entirely custom dictionary that could read from files, APIs, or whatever you can think of. + +[Find out how to create a custom dictionary](/extending/dictionaries) diff --git a/content/trees/navigation/extending_docs.yaml b/content/trees/navigation/extending_docs.yaml index fa518d8c8..194856feb 100644 --- a/content/trees/navigation/extending_docs.yaml +++ b/content/trees/navigation/extending_docs.yaml @@ -47,6 +47,9 @@ tree: - id: 7e4f5154-4499-40a4-929a-92fb81f10bb8 entry: aa6e0a79-9d3f-493b-92c9-df4d2257bc64 + - + id: 270cf2ba-ceb6-499d-b1f0-e548a1aed282 + entry: d0668b6e-915b-46da-863e-51fec54b02e2 - id: 044b4ef5-5809-4bde-8988-ca680199926d entry: b4b46ceb-9feb-4587-8f0d-2080511bf9e3 diff --git a/public/img/fieldtypes/icons/dictionary.svg b/public/img/fieldtypes/icons/dictionary.svg new file mode 100644 index 000000000..414738237 --- /dev/null +++ b/public/img/fieldtypes/icons/dictionary.svg @@ -0,0 +1 @@ + diff --git a/public/img/fieldtypes/screenshots/dictionary.png b/public/img/fieldtypes/screenshots/dictionary.png new file mode 100644 index 000000000..7ce65df73 Binary files /dev/null and b/public/img/fieldtypes/screenshots/dictionary.png differ