-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Logic tag to build and evaluate conditional rules #115
Comments
This is turning out to be an interesting feature. I found a library called JSON Logic, a cross-language specification for building complex rules that can be serialized as JSON and evaluated in the browser or server side. It was the perfect starting point - I rewrote their implementation in TypeScript and PHP to fully customize for our needs. To confirm they're correct and compatible with each other, there's a suite of test cases written in JSON (tests.json). Logic rulesThe main concept is a "logic rule", which is always in this shape. type Rule = {
[key: Operator]: Value | Value[]
}
type Operator = 'and' | 'or' | 'not' | ...
type Value = Rule | any It's a plain object (associative array) with a single key, which is an operator. It has one or more values, which themselves can be a logic rule. Here's the list of supported operators, like Example of logic rules{
'==': [1, 2]
} ..is equivalent to the condition, {
and: [
{ '>': [1, 2] },
{ '<': [3, 4] }
]
} ..is Logic tagThe new <Logic name=example action=hide>
<Rule control=text_1 value="some value" />
</Logic> The above creates an object like: {
name: 'example',
action: 'hide',
logic: {
and: [
{ rule: { control: 'text_1', value: 'some value' } }
]
}
}
|
The function function ruleEvaluator(rule) {
const {
control,
value,
compare = '==' // Default: equal
} = rule
const actual = data[ control ]
return evaluate({
[compare]: [ actual, value ]
})
} This might be useful for converting the current rule evaluator in the Fields module, For now, the |
Hi @eliot-akira, thank you for all the examples! I would have some questions to be sure I understood everything correctly before starting making the changes in fields and blocks
Does that mean we are harmonizing the syntax used in fields to be inline with what we do in L&L/blocks, and switching from this syntax: $fields->render_fields('field-name', [
// ...
'condition' => [
'action' => 'show',
'condition' => [
'_and' => [
'another-field' => [ '_eq' => 'something' ],
'and-another' => [ '_neq' => 'something else' ],
]
],
],
]); To this: $fields->render_fields('field-name', [
// ...
'condition' => [
'action' => 'show',
'logic' => [
'and' => [
'another-field' => [ 'equal' => 'something' ],
'and-another' => [ 'not_equal' => 'something else' ],
],
],
],
]); Both are very similar so if that's the case it shouldn't be too hard to keep backward compatibility
We also have a rule evaluation on the PHP side in fields (here) that uses the same syntax I imagine we also want to update that part to Is the logic folder also going to be a its own composer module/github repository like for the framework? (Or maybe part of the framework?) I'm a little worried that it will still be confusing for users to know which kind or rules can be used together. For example I will probably assume that something like this will work: <Logic name="logic_name" action=hide>
<Rule control=text_2 value="something" />
<!-- I'm not sure it's a real rule but let's pretend it's a rule that exists in L&L -->
<Rule user=current role="admin" />
</Logic>
<Control type="text" name="text_1" label="Text 1" logic="logic_name" />
<Control type="text" name="text_2" label="Text 2" /> Maybe we should try to evaluate both back-end and front-end conditions in blocks I'm not sure what would be the best way to do it, but I'm thinking something like this could work (sorry the code is not correct but I hope it's clear enough to understand): /**
* PHP array for:
* <Control type="text" name="text_1" label="Text 1" logic="logic_name" />
*/
$control;
if( ! empty($control['logic']) && is_string($control['logic']) ) {
/**
* Returned value according to <Logic name="logic_name" /> tag:
* [
* 'name' => 'logic_name'
* 'action' => 'hide',
* 'logic' => [
* 'and' => [
* [ 'rule' => [ 'control' => 'text_2', 'value' => 'something' ],
* [ 'rule' => [ 'user' => 'current', 'role' => 'admin' ]
* ]
* ]
* ]
*/
$item['logic'] = template_system\get_logic_by_name( $item['logic'] );
foreach( $item['logic']['logic'] as $rule_group ) {
foreach( $rule_group as $rule ) {
/**
* It's a control rule, so we know we will evaluate this one on the frontend side
*/
if( ! empty($rule['control']) ) continue;
$rule = [
'server_side_evaluated' => true,
'result' => logic\evaluate_rule( $rule ) // We will need a way to access the evaluation callback from template_system
];
}
}
} And then we could do something like this on the client-side to combine both client-side and server-side rules: import { evaluate } from '@tangible/logic'
const result = evaluate( control.logic, data => {
if( data.server_side_evaluated ) return data.result
return /** evaluate control value dynamically */
}) |
Or, if it's easier, maybe the two syntaxes to co-exist with In your first example, the condition in the new syntax would look like this. [
'action' => 'show',
'logic' => [
'and' => [
[
'rule' => [
'control' => 'another-field',
'value' => 'something',
'compare' => 'equal'
]
],
[
'rule' => [
'control' => 'and-another',
'value' => 'something else',
'compare' => 'not_equal'
]
],
],
],
] It's longer because the rule syntax is more generic, and You're right, this syntax is very similar in structure to what already exists in the Fields module. That's one of the things I liked about the JSON Logic specs. Well, it's up to you if you want to convert the existing functions, or have them co-exist somehow with the new ones. I suppose whichever is better for long-term maintenance and further development of the feature.
Oh that's a good point. It's best to publish it as its own module so the Fields module can include only the Logic module instead of the whole Framework. OK, so the Logic module will be an NPM package (JS) and an independent Composer package (PHP). Then the interface would be: use tangible\logic;
$result = logic\evaluate( $condition, $evaluator ); That aligns well with the JS side: import * as logic from '@tangible/logic'
const result = logic.evaluate( condition, evaluator ) About supporting a mixture of server and browser-side logic rules.. Can we replace all server-side rules with Then the transformed rules can be passed to the frontend, with only the control value comparisons. The same reduced set of rules can be evaluated every time any control value changes, which I imagine can be frequent. OK, I'll let you know when this is done: There's actually an existing Logic module (v1) that's entwined with the internals of the template system. This part I always felt needed a clearer design. The new version based on JSON Logic is more elegant conceptually, and convenient how it has compatible evaluators on frontend and server side. It brings me back to the Logic UI concept, I can picture a visual user interface for building logic rules for various purposes: location rules for templates, or visibility conditions for templates, blocks, block controls. It would be great to have a standard schema of logic rules for all these situations, and even UI components. |
I was hoping to take this for a more thorough test drive, but I don't think I fully understand the proposed solution yet and how it's going to affect the rest of the language. It seems the main thing being discussed here is a replacement to logic variables. How does L&L determine whether the logic should be evaluated by the server or browser? Is there going to be a unique set of browser-side compatible subjects or will there be some overlap? In any case, here are a few comments on the discussion in this thread. Overall, I like the ideas in this thread and will try to contribute something more solution-oriented once this has percolated a bit more. The
|
Thanks for the feedback! About not linking logic with action, the Logic tag does not do anything with There is no link between logic and action, it's up to the evaluator what to do with condition properties that are passed to it. The example for About the Otherwise, I feel the You're right the ..That reminds me, I was wondering whether it's possible to use a logic variable inside another one: <Logic name=weekend compare=or>
<Rule field=day value=saturday />
<Rule field=day value=sunday />
</Logic>
<Logic name=weekday>
<Rule not logic=weekend />
</Logic> Theoretically that should work, when the rules are evaluated by the However, for the frontend evaluator being planned for block control visibility, so far we've only considered rules like I agree with not passing <Loop type=event logic=weekend_webinar count=3> That looks like it should return 3 weekend seminars, but instead will get 3 events then filter them based on the logic. If we don't give users the ability to apply filtering logic directly on the loop, they will have to use the <Loop type=event count=3>
<If logic=weekend_webinar> That's clear that there will be 3 or less events that match. It would be great, however, to consider how we can let users achieve the "natural" course of action, like: "Loop through 3 events that match this logic." Currently, I don't think there's a simple way to do it. |
@nicolas-jaussaud The Logic module is now published as its own Git repository. https://github.com/TangibleInc/logic It's a subrepo of the template system. npm run subrepo init logic -r [email protected]:tangibleinc/logic -b main
npm run subrepo push logic Its documentation includes a section on how to use from WordPress plugin or module. To summarize:
{
"repositories": [
{
"type": "vcs",
"url": "[email protected]:tangibleinc/logic"
}
],
"require": {
"tangible/logic": "dev-main"
},
"minimum-stability": "dev"
}
require_once __DIR__ . '/vendor/tangible/logic/module.php';
import * as logic from './vendor/tangible/logic/index.ts' Instead of importing it as an NPM package, I think it's better if the Fields module uses the same version of the Logic module on both frontend/backend, by loading from the |
Thanks for the responses!
Wouldn't it only get passed to the frontend evaluator when it gets used, such as One other bit of feedback: in the first post you mention that logic will be built up with Sorry if I'm derailing the conversation, I just want to make sure we're thinking a few steps ahead here. |
True. The An advantage of having an "action" is that it's extensible, if we want to support any other frontend (or backend) actions in the future. But for our current purpose, I agree it's unnecessary - the default action can be
The purpose of the It would also make it impossible to use the <If something>
<Rule some rule />
<Else />
<Rule another rule />
</If>
Currently, that's determined by the user by passing the built rules to the As Nicolas mentioned, for this latter use case it seems we need to support a mixture of server-side rules. Theoretically, I think it can be achieved by pre-evaluating such rules and replacing them with true/false, before passing the transformed rules to the frontend. But I'm not too comfortable with the idea - it will complicate the implementation as well as explaining to the user how it works.
This is an open question to discuss and explore. So far, the only browser-side subject is With this example of a proposed way to mix frontend/backend rules: <Logic name=example>
<Rule control=text_1 value=something />
<Rule user_role=admin />
</Logic>
<Control name=control logic=example /> The rule for user role is evaluated once on the server side. But the rule for control value is supposed to be evaluated dynamically when it changes on the frontend. The difference in behavior is not clear from the syntax. Perhaps it should be made more explicit, like <Rule dynamic control=text_1 value=something /> That would allow controlling where the rule gets evaluated. In this case, the following without <Rule control=text_1 value=something /> Apart from this context of visibility conditions, for browser-side logic in general, I'm considering a special HTML attribute with a different function and syntax than the |
About logical operators to combine rules, it may be more intuitive to have |
About applying logic to loops, I think it's actually possible to achieve this: <Loop type=event logic=weekend_webinar count=3> There are only a few attributes ( Then the above code will work as expected, "Loop through 3 events that match this logic." Otherwise, to get the same result, the user will have to figure out a trick to count posts inside the |
About browser-side conditions, it would be great if we can leverage the Interactivity API that is now built into WordPress core. Here's the reference page with a list of supported HTML directives. I'm having difficulty picturing how I would use any of the features though. Maybe we can come up with some concrete examples of browser-side conditions that would be useful for our purposes. When we have an idea of specific use cases, we can explore if/how logic rules will fit into such dynamic frontend behavior. How they could be used together with |
Perfect, thanks for the clarifications, I was mainly tripping up over visualizing how front-end and back-end conditions would be mixed, but I now see that this approach doesn't preclude that.
If the
That's a good point. I was thinking earlier about legibility and realized that the word "and" is generally placed between items in a list, which is what's done in most logic UIs: the This also presents a less confusing alternative to the <Not>
<Or>
<Rule ... />
<Rule ... />
</Or>
</Not> It could instead be: <All false>
<Rule ... />
<Rule ... />
</All> |
As an aside, I learned that "readability" is a real word.
The latter is about being able to recognize letters, for example the style of handwriting or printed font that's more or less legible. And the former is about how the use of words or flow of sentences can affect the reader's understanding. The word was fresh in my mind because I was recently looking at a popular JavaScript library called readability that is used for Firefox Reader View, which takes an HTML document and extracts only the title and text. Apparently, even the word "writability" exists, meaning "the ease of writing a program, given the problem it must solve". I think
<All>
<Rule check=1 value=1 />
<Rule check=2 value=3 />
<Rule check=3 value=3 />
</All> The above results in When <Not>
<Rule check=1 value=1 />
<Rule check=2 value=3 />
<Rule check=3 value=3 />
</Not> This results in That's different from all rules are false. <All false>
<Rule check=1 value=1 />
<Rule check=2 value=3 />
<Rule check=3 value=3 />
</All> This should result in Looking at the JSON Logic evaluator, I see there's a logical operator called So I will implement:
These cover what are called Boolean operators (and, or, not).
|
Well.. I'm still wondering how we can cleanly achieve mixing frontend and backend logic. JSON Logic is a good start because its data structure is cross-platform, with evaluators in multiple languages. But it was not designed for partially evaluating some rules on the backend, then the rest of the rules on the frontend - that sounds complicated. It reminds me of a famous talk called "Simple Made Easy" by the creator of the Clojure language, where he explains the meaning of the word "complect" and how it relates to conceptual simplicity in software. When more than one thing is complected (braided/folded/twisted) together, complexity arises. Interestingly, this question of mixing client/server logic is very relevant these days in the world of React, where they released a feature for server-side components (blog announcement and docs). Traditionally, all React components were client side, rendered in the browser; or, rendered on the server then "hydrated" (rendered again) in the client to make them dynamic. With the new feature, developers can make some components render entirely on the server, shipping only the resulting HTML to the browser. That's much more efficient in terms of JS bundle size and performance. Newer frameworks that evolved after (or on top of) React usually include such a feature already, such as server components in Next.js, client/server modules in Remix, or client directives in Astro. With L&L, we're working in the opposite direction. By default all templates are rendered on the server, and gradually we've been introducing more client-side behavior like pagination, where parts of the template are dynamically rendered. There's a new internal project called Elandel.
Originally the HTML and CSS engines were created to solve a need in the template code editor, to better integrate with the extended syntax we're using. (The new HTML formatter is better designed and more flexible to customize than the old one based on Prettier/Angular's HTML formatter. I remember you pointed out a small but very significant detail regarding the spaces between tags, so if you're up for it, I'd love to work together on refining the details.) These features were organized into an independent module (separate from WordPress and PHP-specific code) because I realized it has the potential to become the foundation of a cross-platform template language that can run entirely in the browser, or on server-side JavaScript runtimes such as Node and Bun. As it evolves, it could use headless WordPress (REST API) as a data source, among others like SQLite, SQLocal (SQLite WASM), or in-memory objects for testing. The language will be compatible with L&L, so we can use it for instant live preview in the editor, or runnable code examples in the docs. It will be developed to first prioritize practical benefits for L&L and Tangible Blocks. But the long-term vision gives me as a fresh perspective, particularly in looking at all the frontend features that exist in the Template System, and considering if/how they can be organized into general-purpose functions that are useful for a template engine that runs on the frontend. For example, the TypeScript evaluator for JSON Logic will be a suitable basis for How that relates to mixing frontend/backend logic, or the possible use of HTML directives.. I think we'll need to explore/experiment, discuss and develop it according to our practical needs. So far, it seems that whatever solution we create for block control visibility has a specific context that might not apply to a general solution for all frontend logic. We don't have enough concrete use cases for the latter, so the solution is not clear yet. In contrast, there is an existing implementation for visibility logic, so it's clear what needs to be achieved as the end result. Looking at the available HTML directives in the Interactivity API, there is no Even if it did, we probably won't be able to use it to show/hide block controls, because of how controls are rendered in the page builder settings form. Even if that's possible, every control would need to be wrapped in a |
The They accept the attribute
I've also updated the To rewrite the examples from my comment above (which I plan to use as rough draft for a docs page about the All rules must be true<All>
<Rule ... />
<Rule ... />
</All> All rules must be false<All false>
<Rule ... />
<Rule ... />
</All> At least one rule must be true<Any>
<Rule ... />
<Rule ... />
</Any> At least one rule must be false<Any false>
<Rule ... />
<Rule ... />
</Not> Not all rules are true<Not>
<Rule ... />
<Rule ... />
</Not> This is the negation of I find this more intuitive, but that's subjective and can depend on what kind of condition is being expressed. For example, given a complex condition like the "weekend webinar": <All>
<Rule taxonomy=event_type term=webinar />
<Any>
<Rule field=event_date value=Saturday />
<Rule field=event_date value=Sunday />
</Any>
</All> It's easier to get the opposite logic, "weekday seminar", by replacing <Not>
<Rule taxonomy=event_type term=webinar />
<Any>
<Rule field=event_date value=Saturday />
<Rule field=event_date value=Sunday />
</Any>
</Not> Compared to using "any false", even though the result is the same. <Any false>
<Rule taxonomy=event_type term=webinar />
<Any>
<Rule field=event_date value=Saturday />
<Rule field=event_date value=Sunday />
</Any>
</Any> Of course in this case it's better to use the Logic tag's attribute <Logic name=weekday_seminar compare=not>
...
</Logic> Or use a previously defined logic as a rule and negate it. <Logic name=weekday_seminar>
<Rule not logic=weekend_seminar />
</Logic> This last example reminds me.. When passing a logic variable to the frontend, any other logic variable used inside must also be passed so that the evaluator can refer to it. ..OK, so I'll close this issue as done, since the foundation of the Logic tag is now prepared. We can discuss further about specific aspects: |
If
Loop
From Tangible Blocks: Visibility conditions - Syntax change:
Build logic
Use with
Loop
Use with
If
Use with
Control
The text was updated successfully, but these errors were encountered: