TextField plugin with generated value

Hello Kirby-community!

I’m implementing a password reset system for my site, where users can reset their passwords via URL. For this functionality, I need panel users to be able to add ‘password reset tokens’ for each user. These tokens contain fields such as token ID (a randomly generated string), expiration date, creation date etc. So from panel perspective this is a structure field which contains other fields.

Everything else is already set up, but I got stuck in making a custom field for my token id field. What I need is a modified text field, which automatically fills in a randomly generated string if the field is empty. This problem is a bit difficult to explain, but I’ll let the code speak for itself. This is what I have so far:

site/blueprints/users/default.yml

password_reset_tokens:
  type: structure
  label: Password reset tokens
  style: table
  translate: false
  fields:
    token_id:
      type: token
      label: Token ID
    created:
      type: date
      label: Created
      time: true
      disabled: true
      default: now
      time:
        format: 24
        interval: 1
    expiration:
      type: date
      label: Expiration date
    use_date:
      type: text
      time: true
      label: Use date

Note the token_id field, which is the custom field

site/plugins/tokenfield/index.php

<?php

Kirby::plugin('companyname/tokenfield', [
	'fields' => [
		'token' => [
			'props' => [
				'label' => function ($label = 'Label') {
					if (is_array($label))
						return t($label);
					else
						return $label;
				},
				'help' => function ($help = '') {
					if (is_array($help))
						return t($help);
					else
						return $help;
				},
				'value' => function (string $value = null) {
					if (empty($value))
						$value = generatePasswordResetTokenId();
					return $value;
				}
			]
		]
	]
]);

site/plugins/tokenfield/index.js

panel.plugin('companyname/tokenfield', {
	'fields': {
		'token': {
			props: {
				label: String,
				help: String,
				value: String
			},
			template: '<k-text-field v-model="value" name="token" v-bind:label="label" v-bind:help="help" />'
		}
	}
});

So the important part is the value property : If token id is empty, we call function generatePasswordResetTokenId to generate a random string, such as TO25p8hHmIkzyLDoE0Stf5DJjnS4N0g3Gue0mD2CcC8jTerTSpTM7YCeWJ7OUDRM.

if (empty($value))
    $value = generatePasswordResetTokenId();

The problem I’m facing is that when I place this new custom field inside a structure, no value is generated. When I place it as a regular field, a generated value appears correctly, but the panel does not seem to register any modifications to the field (i.e. it doesn’t suggest me to save changes, even if I edit it by hand). I guess this has something to do with Vue, of which I have zero experience. Any ideas?

The field needs to know what you want it to do on change:

panel.plugin('companyname/tokenfield', {
    'fields': {
        'token': {
            props: {
                label: String,
                help: String,
                value: String
            },
            methods: {
              input: function () {
                  this.$emit('input', this.value)
              }
            },
            template: '<k-text-field v-model="value" name="token" v-bind:label="label" v-bind:help="help" @input="input" />'
        }
    }
});

If you only want to generate a new token and no other Vue functionality is required, you could simply extend the textfield with php and no js part is required:

$parent_options = require __DIR__ . '/../../../kirby/config/fields/text.php';
$token = array_replace_recursive($parent_options, [
    'props' => [
        'type' => 'text'
    ],
    'computed' => [
        'value' => function () {
            if(empty($this->value)) {
                return generatePasswordResetTokenId();
            }
            return (string)$this->convert($this->value);
        },
        'default' => function () {
            return generatePasswordResetTokenId();
        }
    ]
]);


Kirby::plugin('companyname/tokenfield', [
    'fields' => [
        'token' => $token
    ]
]);

** Edit: Overlooked the problem with the structure field. Adding a default solves that one.

1 Like

This works well enough for me, thanks! Also, not having to redefine the whole text field component is neat as well.

There is a minor thing left unresolved for those who want to use and develop this type of field further: when adding multiple new rows to the structure, the suggested token id is always the same. I’m not big on knowing how Panel and Vue work in conjunction, but obviously the PHP-part gets evaluated only once, hence a refresh is required for getting a new token id.

Yep, then you’d have to generate the token in the vue component instead.

// index.php
Kirby::plugin('companyname/tokenfield', [
    'fields' => [
        'token' => [
            'props' => [
                'label' => function ($label = 'Label') {
                    if (is_array($label))
                        return t($label);
                    else
                        return $label;
                },
                'help' => function ($help = '') {
                    if (is_array($help))
                        return t($help);
                    else
                        return $help;
                },
                'value' => function (string $value = null) {
                    return $value;
                }
            ]
        ]
    ]
]);
// index.js
const getToken = function () {
    // token generator comes here
    return "mynewtoken";
};

panel.plugin('companyname/tokenfield', {
    'fields': {
        'token': {
            props: {
                label: String,
                help: String,
                value: String,
            },
            created: function () {
                if (!this.value || this.value === '') {
                    this.value = getToken();
                    this.input();
                }
            },
            methods: {
                input: function () {
                    this.$emit('input', this.value)
                }
            },
            template: '<k-text-field v-model="value" name="token" :label="label" :help="help" @input="input" />'
        }
    },
    components: {
        'k-token-field-preview': {
            props: {
                value: String,
            },
            template: `<p class="k-structure-table-text">{{ this.value }}</p>`
        }
    }
});
1 Like

Now it’s pretty much spot-on, thanks!