Conditional fields: `then` option
Since Kirby 3.1.0, we have the option to conditionally show and hide fields using when. Would it be possible to also have a then option in the blueprints that takes field settings that are applied as soon as the when condition is met? Iβm thinking of different placeholder texts based on conditions or of different textarea sizes depending on the layout needed for a condition.
Pseudo blueprint:
when:
test:
type: interview
then:
show: false
when:
test:
type: project
then:
show: true
placeholder: hello
I think such a feature would be very useful :+1:
then:
anyFieldProperty: value
Unfortunately itβs not possible to have two or multiple different whens in the same YAML structure as the when keys would override each other. But this syntax could work:
when:
- test:
type: interview
then:
show: false
- test:
type: project
then:
show: true
placeholder: hello
required: true
While we are at it, we could also add testOr and testNot tests. Only one of them could be used at a time though.
Regarding negations, it would also be possible to think of it like this:
when:
- test:
type: interview
then:
show: false
otherwise:
placeholder: type here
The then entry could be optional for full negations.
It would also be possible to incorporate AND/OR comparisons directly:
when:
- test:
- type: interview
- category: article
comparison: or
then:
show: false
comparison: and could be the default (or vice versa) so that you don't have to type it everytime.
@nilshoerrmann that solves the and/or problem quite nicely!
What the suggested syntax probably wouldn't cover yet is a (A or B) and C condition. Maybe we could make it
when:
tests:
-
conditions:
- type: interview
- category: article
comparison: or
then:
show: false
comparison: and
then:
required: true
So possibly having comparison and then on both levels? Or too complicated?
As the issue gets longer, the queries get longer and more complicated π
How do we solve complex conditions with query and keep when simple? https://github.com/getkirby/ideas/issues/231
The difficulty of queries is, that they can't be evaluated in the Panel (in JS/Vue) themselves. But we would need constant API requests to update the value of the query.
But yes, I see your point... this is getting very complicated and long.
As the issue gets longer, the queries get longer and more complicated π
That's true. Looking at this as user, I'd prefer to have different syntaxes for different tasks. So I only have to use one of these complex syntax constructs when it's actually needed. So keeping the current simple syntax as a base.
What if we support only a simple data structure for now (like Nils proposed above) that is still much more powerful than the current implementation and add query or query-like when option later when we have a good solution for that?
As queries are strings, it would be very simple to determine whether the test option is a query (that could be evaluated in the frontend if we write a parser) or if it's a list of fields that will be evaluated with an operator (which is what I would call the and/or option in Nils' example).
I'd like to propose a slightly different option for the expression portion of the when property that I think would simplify both the implementation and visual syntax.
I suggest starting with a full expression AST and optimizing from there. That way we know that the system is capable of expressing just about anything from the start, and can make some special syntax for the common cases.
test:
kind: and
terms:
- kind: field
field: type
value: interview
- kind: field
field: category
value: article
- kind: or
terms:
- kind: field
field: shippingMethod
value: UPS
- kind: field
field: shippingMethod
value: FedEx
This expression can be evaluated with a few lines of JavaScript:
let evalExpression = page => expression =>
expression.kind === "and"
? expression.terms.every(evalExpression(page))
: expression.kind === "or"
? expression.terms.some(evalExpression(page))
: expression.kind === "not"
? !evalExpression(expression.term)
: expression.kind === "field"
? page[expression.field] === expression.value
: null;
This system is capable of expressing arbitrary boolean expressions of any complexity. But it is kind of hard to read. A few syntax transforms can make it easier:
- Expression objects with no
kindfield are evaluated like this: a. Having anandfield denotes anandcondition b. Having anorfield denotes anorcondition c. Having anotfield denots anotcondition d. Otherwise it is a field condition
Which lets us write the condition like this:
test:
and:
- type: interview
- category: article
- or:
- shippingMethod: UPS
- shippingMethod: FedEx
- Term lists that are objects (not lists) are expanded to an array of objects each containing one key/value pair from the original object
Which lets us write the condition like this:
test:
and:
type: interview
category: article
or:
- shippingMethod: UPS
- shippingMethod: FedEx
Note that the or condition still needs to be a list since it has two terms for the same field. Otherwise it could be written the same as the and condition, without the -s.
The expansions can be performed in a few lines of JavaScript:
// Expansion #2
const expandTerms = list =>
Array.isArray(list)
? list
: Object.entries(list).map(x => Object.fromEntries([x]));
// Expansion #1
const expandCondition = test =>
test.kind // Base, verbose case
? { kind: test.kind, terms: expandTerms(test.terms).map(expandCondition) }
: test.and // Case a
? { kind: "and", terms: expandTerms(test.and).map(expandCondition) }
: test.or // Case b
? { kind: "or", terms: expandTerms(test.or).map(expandCondition) }
: test.not // Case c
? { kind: "not", term: expandCondition(test.not) }
// Case d
: Object.entries(test).map(([field, value]) => ({ kind: "field", field, value }))[0];
A few tests:
// Converted directly from the YAML above
let test = {
"and": {
"type": "interview",
"category": "article",
"or": [
{ "shippingMethod": "UPS" },
{ "shippingMethod": "FedEx" }
]
}
};
let page = {"type": "interview", "category": "article", "shippingMethod": "UPS"};
evalExpression(page)(expandCondition(test));
// => true
let page = {"type": "report", "category": "article", "shippingMethod": "UPS"};
evalExpression(page)(expandCondition(test));
// => false
let page = {"type": "interview", "category": "article", "shippingMethod": "FedEx"};
evalExpression(page)(expandCondition(test));
// => true
let page = {"type": "interview", "category": "article", "shippingMethod": "DHL"};
evalExpression(page)(expandCondition(test));
// => false
Pro: Simple, yet powerful enough to express arbitrary expressions
Con: Fields called "and", "or", "not" in the blueprint cannot be referenced. Using them requires falling back to the verbose syntax. Seems rather unlikely someone would be using fields with those names though.
Pro: You can fall back to the verbose syntax if necessary
Pro: The expression evaluation is small and simple, so can trivially be implemented in both PHP and JS. (Necessary if when ever supports indicating whether a field is required.)
Pro: Easily expanded to support conditions other than equality by specifying kinds like 'greaterThan' or 'regex'. Could also expand it to support reusable fragments by using kind: 'reference', reference: 'predeclaredCondition'.
Pro: Easily expanded to include other boolean operations like implies. (I've found implication to be very useful when deciding whether a field is required. (type === 'interview' || type === 'meeting') implies (category === 'report' || category === 'book') means category can be anything if type isn't interview or meeting. The equivalent expression using and/or/not is more complex.
Con: This requires a bit of logic around the handling of the when property so as to avoid breaking existing blueprints. (I don't think it would be too much though.)
Pro: This is independent of how the "then/otherwise" bit ends up working. It deals only with the conditional part of the when.
Here's what it would look like in a blueprint:
type:
label: Type
type: select
options:
- interview
- report
category:
label: Category
type: select
options:
- article
- editorial
shippingMethod:
label: Shipping Method
type: select
options:
- UPS
- USPS
- FedEx
- DHL
conditionalField:
label: Conditional Field
type: text
when:
and: # Could default to "and", but this would be handled outside of the expression evaluation I wrote above.
type: interview
category: article
or:
- shippingMethod: UPS
- shippingMethod: FedEx
# Still works if when/then/otherwise is implemented
anotherConditionalField:
label: Another Conditional Field
type: text
when:
test:
and: # Again, could default to "and", but that would not be part of the expression evaluation
type: interview
category: article
or:
- shippingMethod: UPS
- shippingMethod: FedEx
then:
required: true
placeholder: "Placeholder Here"
otherwise:
show: false
This should be moved to https://github.com/getkirby/kirby in my eyes.
In https://github.com/getkirby/kirby we really would like to try limiting it to bugs - I know sometimes we put enhancements or features in there as well but especially since Nolt will also serve for creating the roadmap, this should go on Nolt still (in my view).
Okay π I would leave summarizing the topic to one of you others.