statamic / docs

Statamic Documentation
https://statamic.dev
Other
117 stars 383 forks source link

https://statamic.dev/extending/fieldtypes #1525

Closed AtmoFX closed 4 weeks ago

AtmoFX commented 4 weeks ago

Hello,

I have recently tried my hand on https://statamic.dev/extending/fieldtypes, to see if I was able to extend statamic.
To people like me who are having their very first web development on Statamic, some of the obvious things to you guys are not obvious at all. I felt being a little bit more verbose for the password introductory example could have saved me from hours of cargo cult programming.

Below is my humble suggestion to improve the help page, with:

... hopefully nothing too crazy.

Let me know if you think that is useful.


Registering

Any fieldtype classes inside the App\Fieldtypes namespace will be automatically registered.

To store them elsewhere, manually register an action in a service provider by calling the static register method on your action class.

public function boot()
{
    Your\Fieldtype::register();
}

Creating

Fieldtypes have two pieces:

For this example we will create a password field with a "show" toggle control:

An example fieldtype that reveals a password field
Follow along and you could make this!

Prerequisites

If this is the first fieldtype you are creating, you will need to:

  1. Add the following to the devDependencies in package.json.
    "devDependencies": {
        "@tailwindcss/typography": "^0.5.12",
        "autoprefixer": "^10.4.19",
        "laravel-vite-plugin": "^1.0.2",
        "postcss": "^8.4.38",
        "tailwindcss": "^3.4.3",
        "@vitejs/plugin-vue2": "^2.3.1", // [tl! ++]
        "vite": "^5.2.8"
    }
  2. Install the new dependency.
  3. Create the files: resources/js/cp.js and resources/css/cp.css.
  4. Inside app/Providers/AppServiceProvider.php, add the path to both files in the public function boot(): void (see the "Using vite" help section). In most situations, you may only need to uncomment the function's body.
  5. Inside vite.config.js uncomment the lines as highlighted in the "Adding assets to your build process" help section.

Creating the field

Create a fieldtype PHP class and Vue component by running the following command:

php please make:fieldtype <Your fieldtype class name>

This creates the following:

Your component has two requirements:

Statamic provides you with a Fieldtype mixin that does this automatically to reduce boilerplate code.

At this point, coding will take place in the following 4 files, the first 2 of which are shared across all the fieldtypes you create:

Refer to the language written in each of the code snippets below to find out what file to edit.

Example Vue component

As the first example, the TogglePassword class will not require editing all 4 of the above files.

Create it using the command below:

php please make:fieldtype TogglePassword

Register your Vue component as [handle]-fieldtype:

import Fieldtype from './components/fieldtypes/TogglePassword.vue';

Statamic.booting(() => { 
  // Should be named [snake_case_handle]-fieldtype
  Statamic.$components.register('toggle_password-fieldtype', Fieldtype);
});

The component in this example is defined by the code:

<template>
    <div>
        <text-input :type="inputType" :value="value" @input="updateDebounced" />
        <label><input type="checkbox" v-model="show" /> Show Password</label>
    </div>
</template>

<script>
export default {
    mixins: [Fieldtype],
    data() {
        return {
            show: false
        };
    },
    computed: {
        inputType() {
            return this.show ? 'text' : 'password';
        }
    }
};
</script>

Finally, build the component:

npm run build

Example walk-through:

:::warning Do not modify the value prop directly. Instead, call this.update(value) (or this.updateDebounced(value)) and let the Vuex store handle the update appropriately. :::

Fieldtype Icon

You can use an existing SVG icon from Statamic's resources/svg directory by passing it's name into an $icon class variable, by returning a full SVG as a string, or returning it as a string from the icon() method.

<?php

class CustomFieldtype extends Fieldtype
{
    protected $icon = 'tags';
    // or
    protected $icon = '<svg> ... </svg>';
    // or
    function icon()
    {
        return file_get_contents(__DIR__ . 'resources/svg/left_shark.svg');
    }
}

Fieldtype Categories

When using the blueprint builder inside the control panel, your fieldtype will be listed under the special category by default. To move your fieldtype into a different category, define the $categories property on your class:

<?php

class CustomFieldtype extends Fieldtype
{
    public $categories = ['number'];
}

You can select from any of the keys available in the FieldtypeSelector:

Configuration Fields

You can make your fieldtype configurable with configuration fields. These fields are defined by adding a configFieldItems() method on your PHP class that returns an array of fields.

protected function configFieldItems(): array
{
    return [
        'mode' => [
            'display' => 'Mode',
            'instructions' => 'Choose which mode you want to use',
            'type' => 'select',
            'default' => 'regular',
            'options' => [
                'regular' => __('Regular'),
                'enhanced' => __('Enhanced'),
            ],
            'width' => 50
        ],
        'secret_agent_features' => [
            'display' => 'Enable super secret agent features',
            'instructions' => 'Can you even handle these features?',
            'type' => 'toggle',
            'default' => false,
            'width' => 50
        ],
    ];
}

The configuration values can be accessed in the Vue component using the config property.

return this.config.mode; // regular

Options

Key Definition
display The field's display label
instructions Text shown underneath the display label. Supports Markdown.
type Name of the fieldtype used to manage the config option.
default An optional default value.
*width
other Some fieldtypes have additional configuration options available.

:::tip A little code diving will reveal all the possible config options for each field type. Look for the configFieldItems() method in each class here: https://github.com/statamic/cms/tree/3.3/src/Fieldtypes :::

Adding configuration fields to existing fieldtypes

Sometimes you may want to add a config field to another fieldtype rather than creating a completely new one.

You can do this using the appendConfigField or appendConfigFields methods on the respective fieldtype.

use Statamic\Fieldtypes\Text;

// One field...
Text::appendConfigField('group', [
  'type' => 'text',
  'display' => 'Group',
]);

// Multiple fields...
Text::appendConfigFields([
  'group' => ['type' => 'text', 'display' => '...',],
  'another' => ['type' => 'text', 'display' => '...',],
]);

Meta Data

Fieldtypes can preload additional "meta" data from PHP into JavaScript. This can be anything you want, from settings to eager loaded data.

public function preload()
{
    return ['foo' => 'bar'];
}

This can be accessed in the Vue component using the meta property.

return this.meta; // { foo: bar }

If you have a need to update this meta data on the JavaScript side, use the updateMeta method. This will persist the value back to Vuex store and communicate the update to the appropriate places.

this.updateMeta({ foo: 'baz' });
this.meta; // { foo: 'baz' }

Example use cases -

Here are some reasons why you might want to use this feature:

Replicator Preview

When Replicator (or Bard) sets are collapsed, Statamic will display a preview of the values within it.

By default, Statamic will do its best to display your fields value. However, if you have a value more complex than a simple string or array, you may want to customize it.

You may customize the preview text by adding a replicatorPreview computed value to your Vue component. For example:

computed: {
    replicatorPreview() {
        return this.value.join('+');
    }
}

:::tip This does support returning an HTML string so you could display image tags for a thumbnail, etc. Just be aware of the limited space. :::

Index Fieldtypes

In listings (collection indexes in the Control Panel, for example), string values will be displayed as a truncated string and arrays will be displayed as JSON.

You can adjust the value before it gets sent to the listing with the preProcessIndex method:

public function preProcessIndex($value)
{
    return str_repeat('*', strlen($value));
}

If you need extra control or functionality, fieldtypes may have an additional "index" Vue component.

import Fieldtype from './TogglePasswordIndexFieldtype.vue';

// Should be named [snake_case_handle]-fieldtype-index
Statamic.$components.register('toggle_password-fieldtype-index', Fieldtype);
<template>
    <div v-html="bullets" />
</template>

<script>
export default {
    mixins: [IndexFieldtype],
    computed: {
        bullets() {
            return '&bull;'.repeat(this.value.length);
        }
    }
}
</script>

The IndexFieldtype mixin will provide you with a value prop so you can display it however you'd like. Continuing our example above, we will replace the value with bullets.

Augmentation

By default, a fieldtype will not perform any augmentation. It will just return the value as-is.

You can customize how it gets augmented with an augment method:

public function augment($value)
{
    return strtoupper($value);
}

Read more about augmentation

Accessing Other Fields

If you find yourself needing to access other form field values, configs, etc., you can reach into the publish form store from within your Vue component:

inject: ['storeName'],

computed: {
    formValues() {
        return this.$store.state.publish[this.storeName].values;
    },
},

Updating from v2

In Statamic v2 we pass a data prop that can be directly modified. You might be see something like this:

<input type="text" v-model="data" />

In v3 you need to pass the value down in a prop (call it value), and likewise pass the modified value up by emitting an input event. This change is the result of architectural changes in Vue.js 2.

<!-- Using a standard HTML input field: -->
<input type="text" :value="value" @input="$emit('input', $event.target.value)">

<!-- Using the "Fieldtype" mixin's `update` method to emit the event for you: -->
<input type="text" :value="value" @input="update($event.target.value)">

<!-- Using a Statamic input component to clean it up even further: -->
<text-input :value="value" @input="update">

An alternate solution could be to add a data property, initialize it from the new value prop, then emit the event whenever the data changes. By doing this, you won't need to modify your template or the rest of your JavaScript logic. You can just continue to modify data.

<template>
    <input type="text" v-model="data" />
</template>
<script>
export default {
    mixins: [Fieldtype],
    data() {
        return {
            data: this.value,
        }
    },
    watch: {
        data(data) {
            this.update(data);
        }
    }
}
</script>

If you had a replicatorPreviewText method, it should be renamed to replicatorPreview and moved to a computed.

The PHP file should require no changes.

jasonvarga commented 4 weeks ago

Thanks for this.

As far as I can tell, the confusing part that you've tried to address is that the fieldtype page itself doesn't really go into detail about setting up your JS+build process. Is that right?

AtmoFX commented 3 weeks ago

Well... there certainly was confusion on my end but I am not sure this is going to be useful now that you updated the article. However, I would like to discuss about documentation for beginners from a more general perspective.

I am sure this will come as pedantic (despite my effort) so feel free to ignore it.


From the type of tutorial it is, I would say this page is typically directed at beginners. Actually, this is for first-timers as anyone, even if still beginners, who already created their own field and confirmed it works have no use for copy-our-example-to-see-how-it-works instructions anymore. I think it misses its mark though (even now).

As someone whose job used to include the redaction of documentation, I classify information into what is useful, distracting or undetermined (for lack of a better word). The "undetermined " appears as neither useful nor distracting or rather, in the grand signal-to-noise scheme of things, different people classify it as signal or as noise. I generally consider beginners have fewer memory items, a bigger need for an exhaustive checklist and will see more of these undetermined points as signal, so they should not be excluded. In fact, only the blattantly off-topic (and hardly anything else) should be put in the "disctracting" category and excluded from the beginners' help.

One telling example: between my suggestion and your version, I see you removed a link in the "Prerequisites" section, supposedly to decrease the noise (let me know if I am wrong assuming that).

I (and I think everyone trying to create their first fieldtype) was prepared to invest time to read detailed, very technical documentation but I sure would have traded the hours of browsing through entire webpages for the exhaustive list of relevant sections, linked directly from the procedure.

That was the goal of my "Prerequisites" section suggestion: include it but let people know it may be skipped unless "this is the first fieldtype [they] are creating".

BTW, the same can be said about editing package.json in the same section. Maybe you are like: "of course you have to edit it, duh!" but:

-> I leave that to your evaluation.

PS: In case you wonder how it could possibly have taken me hours, I can explain easily:

AtmoFX commented 3 weeks ago

Hey @jasonvarga

Just one tiny additional comment since I am actually using what I learned starting today: php please make:fieldtype ABC shows this message:

  ⇂ Don't forget to import and register your fieldtype's Vue component in resources/js/addon.js
  ⇂ For more information, see the documentation: https://statamic.dev/fieldtypes#vue-components

Just wanted to highlight addon.js is not consistent with the cp.js from the documentation.