Populate a select field in vue in the panel

Hi, I am trying to populate a select field with custom options. For this I extended the k-select-field and I already have something like:

<template>
  <k-field :input="_uid" v-bind="$props" class="k-select-field">
    <k-input
        ref="input"
        :id="_uid"
        v-bind="$props"
        theme="field"
        v-on="$listeners"
        type="select"
    />
  </k-field>
</template>

<script>
export default {
  extends: "k-select-field",
  mounted() {
    this.fillSelect()
  },
  methods: {
    async fillSelect() {
      this.options = await this.$api.get('getOptions');
    }
  }
}
</script>

This is apparently not working like this :slight_smile:

I set the right entries in index.php and index.js.
getOptions returns an array of strings.
The field is usable, but fails.

If I try to pass options via the yml instead of fillSelect(),
I get an error in the console: this.options is not iterable

Where did you get this code? ChatGPT? It loads options but this is never passed to the input, so the input naturally won’t show them.

But why would you create a custom field Vue component in the first place just to feed some custom options? The default select field allows for that out of the box from a lot of different sources (incl. API): Select | Kirby CMS

Hi, thanks for the feedback @distantnative . I want to populate the field based on the selected value of another select field. As far as I understand, this is not possible with the default select field. Or is it?

I want the user to choose a page and then offer options from that page. Basically allow them to choose a block from another page. And use this as a reference. So they don’t have to copy the block.

Sorry for the code :slight_smile: I was in the middle of trying around. But my understanding of the Vue components is still very limited.

Have a look at the link I shared - that’s exactly talking about how to get dynamic options, also from other fields.

Sorry, i do not get it. The dynamic options seem all only be possible directly from another field, so for example I can split a value and display it. But can I have a query with a filter based on the value of another field in the block? I do not see an example of how to use another field in a query.

What I want is something like this to be able to select a block (id):

# this works:
query: site.find('home').layout.toLayouts.toBlocks
text: "{{ block.type }}"
value: "{{ block.id }}"
# but how to do this?
query: site.find(this.selectedPage).layout.toLayouts.toBlocks
# or
query: site.find(block.selectedPage).layout.toLayouts.toBlocks

And how would I update/refresh the values when the selectedPage field changes?

Ok, slowly I start to understand what you are trying to do. This indeed won’t work as especially unsaved changes are not available on the server yet (stored in the browser) and thus the query language has no access to what you just selected in your other field.

You will indeed need a custom component for that (say field B), which tries to get the value of field A from this.$store.getters['content/values'](), sends this a custom API, retrieves back the possible options.

Where your initial example fails is that you retrieve options form the API, but you don’t pass/bind them to the input, there you only pass the props ($props).

Ok, cool, how do I bind them to the input?

I tried adding

  props: {
    value: {
      type: String,
      default: ""
    },
    options: {
      type: Array,
      default: []
    }
  },

But in all cases I get this.options is not iterable
Is the form of the options wrong or the binding?

You might have to check out some Vue basics. You should first define your options that you use as data:

data() {
  return {
    options: []
  }
}

For when the API response isn’t loaded yet.

And you need to pass this on to the k-input component as :options="options"

Great. Thanks a lot. I will look into it.

Do you know how I can access a field within the same block?

This works:

this.$store.getters["content/values"]().layout[0].columns[0].blocks[0].content.targetpage

But this way I might select the wrong block… how to do it for a sibling field within the same block?

@distantnative I got it all working apart from my last question about getting the field value from the same block and at the moment I always get the previous value.

When filling the options on

updated() { fillBlocks() }

the value that I get from

this.$store.getters["content/values"]().layout[0].columns[0].blocks[0].content.targetpage

is the one that was there before, not the newly selected one.

Do you have an idea?

"content/values" should get you the original values combined with new changes. But you could try "content/changes" instead.

Thanks! I got it all working:

Here is the vue component.

<template>
  <k-field :input="_uid" v-bind="$props" class="k-select-field">
    <k-input
        ref="input"
        :id="_uid"
        v-bind="$props"
        theme="field"
        v-on="$listeners"
        type="select"
    />
  </k-field>
</template>

<script>
// here goes the Vue code
export default {
  extends: "k-select-field",
  updated() {
    this.fillBlocks();
  },
  mounted() {
    this.fillBlocks()
  },
  methods: {
    async fillBlocks() {
      // get the field value from the siblings within this block
      let targetPage = this.$parent.$parent.$el.querySelector('select[name="targetpage"]').value;
      // console.log('Target page: ' + targetPage);
      if (targetPage === '') {
        return;
      }

      await this.$api.get('blocks?page=' + targetPage).then((response) => {
        response = Object.keys(response).map((key) => [key, response[key]]);
        this.$withoutWatchers(() => {
          this.options = response.map((x) => x[1]);
          // console.log('Updated options: ', this.options);
          this.$forceUpdate();
        });
      });
    },
    $withoutWatchers(cb) {
      if (!this._watcher) {
        return cb();
      }
      const watcher = {
        cb: this._watcher.cb,
        sync: this._watcher.sync,
      };
      this._watcher = Object.assign(this._watcher, {cb: () => null, sync: true});
      cb();
      this._watcher = Object.assign(this._watcher, watcher);
    },
  }
}
</script>

The route:

  'api' => [
      'routes' => [
          [
              'pattern' => 'blocks',
              'action' => function () {
                  $pageId = get('page');
                  $page = site()->index()->findBy('id', $pageId);
                  if (!$page || !$page->layout()) {
                      return [];
                  }
                  $blocks = $page->layout()->toLayouts()->toBlocks()->toArray();
                  return array_map(function ($block) {
                      static $i = 0;
                      $i++;
                      return ["text" => $i . ": " . $block['type'], "value" => $block['id']];
                  }, $blocks);
              }
          ]
      ]
  ],

The event updates workaround seems a bit awkward. If there is a better way to solve this, I’d love to hear about it :slight_smile:

With this it is easy to output a referenced block:

echo $site->find($block->targetPage())->layout()->toLayouts()->toBlocks()->filterBy('id', $block->targetBlock());
1 Like