What's the lifecycle of a custom field's value?

Due to lack of documentation, I go through Kirby’s source code to figure out how native fields work so I can create mine as correctly as possible. So far, I was doing great, but I can’t understand one thing - when is a field’s value prop called?

I’m trying to create a field in which you can select a page, among other things. I have reused most of the functionality of the native pages field. I need to store just the selected page id under the key page, and send the necessary meta data for the given page before giving it to the front-end and the k-pages-field there.

Essentially, I want to replicate the native pages field behavior. It stores an array of page ids and returns an array with data for each of these pages to the front-end. I want to store a single id under the page key of my field value, and return that page’s meta data under the same page field to the front-end.

Here’s what I currently have in my index.php file of the plugin:

$fieldsSource = $kirby->root('kirby') . DS . 'config' . DS . 'fields';
$pagesField = include $fieldsSource . DS . 'pages.php';

Kirby::plugin('me/plugin', [
  'fields' => [
    'myfield' => [
      'props' => [
        'value' => function ($value = null) {
          $data = Yaml::decode($value);

          // if (isset($data['page']) && is_string($data['page'])) {
          if (!empty($data['page'])) {
            $page = kirby()->page($data['page']);
            $data['page'] = [$this->pageResponse($page)];
          }

          return $data;
        }
      ],
      'methods' => [
        'pageResponse' => $pagesField['methods']['pageResponse'],
      ],
      'api' => function () use ($pagesField) {
        return [
          [
            'pattern' => '/pages',
            'method' => 'GET',
            'action' => $pagesField['api']()[0]['action']
          ]
        ];
      },
      'save' => function ($value = null) {
        if (!empty($value['page'])) {
          $value['page'] = $value['page'][0]['id'];
        }

        return $value;
      }
    ]
  ]
]);

…and index.js:

panel.plugin("me/plugin", {
  fields: {
    myfield: {
      props: {
        value: Object,
        endpoints: Object,
        label: String
      },
      data: function() {
        return {
          data: this.value
        }
      },
      methods: {
        input: function (data) {
          this.$emit('input', {
            page: data
          })
        }
      },
      watch: {
        value: function (value) {
          this.data = value
        }
      },
      template: `
        <k-pages-field
          v-model="data.page"
          :endpoints="{
            field: this.endpoints.field + '/pages'
          }"
          @input="input"
        ></k-pages-field>
      `
    }
  }
})

So…

What I expect is that the value prop is called whenever the stored Field value must be parsed before being sent to the front-end. This means parsing the YAML and fetching the page data. When save is called, I expect it to receive the data sent by the front-end and filter it so that only the necessary parts are saved.

Well, this kind of happens. If I manually set the field value to page: home for example, then value is indeed called, it fetches the page, the front-end receives it, and the page is displayed. However, if I change the page and click Save, the value is not saved. In fact, it’s removed from the page and there’s nothing in the txt file.

After looking at the native pages field and playing around with my own field, I noticed that if I change:

if (!empty($data['page'])) {

…to:

if (isset($data['page']) && is_string($data['page'])) {

…then everything starts to work perfectly. The page id is saved, the meta data is displayed on the front-end.

My question is…

Why does that work? Why should I check if the page key is a string? Why can’t I check whether it merely exists? I mean, that value comes from the parsed YAML and if the YAML has a page key, it should always be a single string, since the save function leaves only that. If I have to make the string check, this means that at some point, that value is not a string, therefore the value prop is called in more than one scenario. What is it?

Since this is a pretty core-related question, I’ll tag @distantnative and @bastianallgeier since I doubt someone else can help me.


By the way, this lack of documentation is a great pain. Kirby has such a powerful and well-designed API. It’s a pity that you have to shoot in the dark to figure out how to use it… The last plugin I made would have been so much easier to make if I only knew about this save setting of fields…

Edit: After further digging, I think that save simply isn’t called with the value from the front-end, at least not always. I think the front-end value goes through the value prop function before reaching save for some reason? Someone please explain what the value prop and save actually do.