How to create a ‘calculated field’

I’m having difficulties creating a ‘calculated field’ as a plug-in. The requirements are:

  1. User should be able to define the field in the blueprint - possibly using Kirby’s Query Language. Field value is automatically calculated based on the definition.
  2. Field should be usable in templates, like a normal field, using normal $page->field() notation.
  3. Field must update the Panel dynamically, as its value changes - bi-directionally:
    • if the value on other fields change, the value on the calculated field must also be updated, if needed
    • if the value in the calculated field changes, then any other fields that use the value - e.g., in conditional when: keys - must also be immediately updated.

Example use-cases:

  • field that gets current currency exchange rates and displays in the Panel a price entered by the user in a different currency. If the user types a new price, the value is updated immediately in the Panel.
  • field that calculates tax rates based on multiple criteria - e.g., type of item, source country, etc.
  • a ‘flag’ field to be used in when: keys of other fields, but which can respond to a much larger range of conditions - e.g. “if the page has more than one image”, or “if value of field ‘foo’ is either ‘bar’ or ‘baz’”, or “if value of field ‘foo’ is > 10”.

While it’s easy enough to create custom fields/functions in the Page Model, these are not reflected dynamically in the Panel. So, is this at all possible to create atm? If not, will the upcoming 3.6 version change anything?

I don’t think 3.6 changes anything about this. There will always be a separation between front and back end. While it might be possible to have an “immediately updating field” that derives its value from other values, those other values would have to be present in the panel somewhere.

This means for your examples:

  1. converting currencies could theoretically work.
  2. Tax rates could theoretically work.
  3. “foo > 10”: works
    “foo == ‘bar’ || foo == ‘baz’”: works
    “page.images.count > 10”: does not work, because the panel isn’t guaranteed to know all the images there are in the page. What could work would be “filesField.length > 10”.

Thank you for chipping in, @rasteiner! Indeed, the idea would be to have a field that updates its value based on other existing values - but that updates itself automatically when these values are changed in the Panel (without the user having to refresh/reload the Panel). How would we go about creating such a field?

We can just add functions to a Page Model, and then use an ‘Info’ field that displays the resulting value directly from the Model, but that is not dynamically updated when the user enters values in other fields - i.e., the user has to save these values to the content file, then refresh/reload the Panel to see the changes.

Is it possible for us to build something like a “better ‘Info’ field” - i.e, one that updates its value dynamically in the Panel whenever other fields change? And if so, then how can we make the value of the field also available on the template?..

I can think of two fundamental approaches…

If you want to do the calculations server side (in a model), there’s no way around doing network requests, which aren’t “immediate” but could be actually pretty fast. I think the most straight forward way (least complicated) would be to let people write model functions (but this means it’s not just the blueprint) and then define, in the blueprint, an extended “info field” that loads the model function and declares what the model function needs.

The model function would be something like:

function taxes() {
    return $this->price() * ($this->taxrate() / 100);

The blueprint could be something like:

  type: superinfo
  text: The taxes are {{page.taxes}} €
    - price
    - taxrate

In the template you can just use the model function, as that falls back to the saved values:

Taxes: <?= $page->taxes() ?> €

On the front end, the field would then watch for changes on the specified other fields. This should be doable by watching the getters on the store. (From the docs I couldn’t decide which would be the correct one). I doubt it, but the worst could be that you need to listen globally for all store actions and filter for “content/update”, kind of like here, but with the advantage that you actually have a field component that gets mounted and unmounted that you can use to start and stop listening for changes.
Once a value changes, you would ping an api endpoint (fields should get one automatically), and on the server you would evaluate the fields text query on a virtual page that has the updated values. Maybe debounce the call…

Another approach would be to have your own query language (kql doesn’t do math), parse it on the backend and give your frontend an abstract syntax tree that could be evaluated by the field in the panel. Eliminating thus the need for api calls. In this case, the field would have its own value that gets calculated on the frontend and then saved in the content file.

This could also be simplified by using some very bad javascript statements and functions, like with and eval. One could argue that if someone has access to the blueprints, he probably already is admin in a site and doesn’t need any kind of privilege escalation attack. With this philosophy you should get away with it by forcing yourself slowly down your city’s main street, naked, with people throwing rotten vegetables at you while repeatedly shouting “SHAME!”.
On the other hand, on a service like “kirbyzone” there might be a role with higher privileges than “admin”, don’t know about that.

Thank you for putting some thought and analysis into this, @rasteiner - truly appreciated. Whichever approach we choose, sounds like a lot more work than we had anticipated. We’re going to archive this idea to ‘revisit later’ when we have some more time, and just use a ‘quick-n-dirty’ solution for the time being. I’ll let you know when we make any progress! :slight_smile:

Option 2 seems like a fun project, if I get some free time I’ll give it a shot :slight_smile:

I’ve wanted to play with antlr anyway