Skip to content
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

[Feature Proposal] Conditional fields (aka. hide/show fields depending on other fields, aka. toggle field visibility) #4158

Closed
tabacitu opened this issue Feb 5, 2022 · 35 comments

Comments

@tabacitu
Copy link
Member

tabacitu commented Feb 5, 2022

This is the most upvoted feature in our feature poll. So it's the first one we can tackle, now that we have v5 ready for launch.

Let's do this!!! 💪

The Problem

I've studied all previous PRs for this:

As I said in Laravel-Backpack/addons#11 ... this is a tricky problem to solve... because the developer needs can be so very different. In various places, people ask:

  • for the "source" to be a select, others radio, checkbox, checklist, select2_from_array, etc;
  • for the "source" to be multiple fields, not just one;
  • for the "action" to be "hiding";
  • for the "action" to be "showing";
  • for the "action" to be "hiding and disabling";
  • for the "action" to be "calculating the value";

I'm ashamed we haven't done anything to solve this until now. Literal shame. But... the reason we haven't merged any of those... is that I didn't see way for us to cover what most devs need, most of the time. We would only be covering a particular use case, which would most certainly have meant we would be adding features and niche use cases to it for years, as people report them. And that simply wasn't a good idea, to take on such a big maintenance burden... when it's not that difficult to do it in JS, for the people who needed it. You could do it in create.js and update.js and you have a solution, for your project alone, with maximum customizability. That's why I've been suggesting "creating an addon for it" to devs who have made PRs for this... because they were niche solutions, and for sure somebody would have used it... but not everybody would have been happy with it.

I didn't see how we can build this feature/field/whatever so that most people will be happy with it. But now... I think I do. And it's sort of what @pxpm suggested here... but without the AJAX calls, which I think are unproductive and bound to fail.

And thanks to the open-core split... we can start adding more and more feature, right into backpack/pro.

The Solution

What if... instead of wanting to define the JS behavior in PHP... we accept we have to write a little bit of JS for this? And have Backpack make that bit of JS so easy to write, it's a pleasure? In Backpack v5, thanks to the script widget, we can do:

Widget::add()->type('script')->content(asset('/path/to/js/file.js'));

So what if...

Step 1. We make it even easier, by providing a convenience method on CRUD, that also allows for inline content. Something like:

CRUD::addJavascript('/path/to/js/file.js');

// or even

CRUD::addJavascript("alert('what you want to be done here')");

Step 2. We make it dead-simple to write ALL of the combinations above, by providing a selector and a few actions on the crud javascript object (it's already onpage, usually used for working with DataTables). So that what JS you actually write looks like this:

// option 1 - probably needs jQuery
crud.field('agree').on("change", function() {
    crud.field('price').show();
});
crud.field('agree').onChange(function() {
    crud.field('price').show();
});

// option 2 - maybe doesn't need jQuery
crud.field('agree').addEventListener("change", function() {
    crud.field('price').show();
});

// of course, you should be able to do a few other stuff with your `fields`, but the minimum would be:
crud.field('price').show();
crud.field('price').hide();
crud.field('price').disable();
crud.field('price').enable();
crud.field('price').value(); // aka .setValue(), aka .val()

I believe this would solve all cases people have already expressed... but also fix the cases people haven't expressed yet. Which I'm sure will come up, in real apps, right after we introduce a feature with limited customizability. But by moving this logic to JS... it opens up the possibility for you to do... anything you want. It's JS, and you have complete control.


Thoughts, anyone? Am I missing something? Or is this a solution we could all get behind? I'm eager to prototype this, to see if we can launch it with Backpack v5 this week 🎉 I have a feeling it's either a lot simpler than I expect... or a lot more difficult 😅

Can't tell if this idea is incredibly good... or incredibly stupid 😅 Let me know.

@pxpm
Copy link
Contributor

pxpm commented Feb 6, 2022

Hello @tabacitu

In general I agree with your idea, it goes along with what I was already thinking with some twists, so I will try to sum up what I thought when reading it.

At first it seems to me that the only thing we are doing is "facilitating" the use of JS in page, not really adding the interactivity functionality. What I mean is:

  • you still need to know JS - it's probably good that you know some of it when working with PHP, but not totally necessary.
  • everything in Backpack is "dead-simple", I don't think this will ever be like it, what I really expect is spargetti JS in controller or spargetti JS in the .js file.
  • developers would be writting the same code over and over again for simple tasks like show/hide fields when they should just be providing the when (event) and what (action) and backpack would take care of the how (fields inside repeatable? inside inline create? etc) and execution.

One good thing I think it's worth for us doing is separating all those options (show, hide, setValue etc..) because I am sure some of them are easier to achieve than others and probably we could make the ajax optional for those who want it and leave it off for dead simple scenarios.

So starting by the most commons I think: show/hide/disable/enable

First we should establish that there are very different scenarios happening in cruds, including repeatable fields, fields inside modals, etc. So a simple .hide('field_name') may not work as desired and also, some fields uses hidden inputs that migth not be easy to target with a js selector.

That said, I think that is where backpack should shine in to help developer to don't have to think about how backpack fields are built to hide a field when other is clicked.

I am 100% against javascript in the controllers. At most I would agree with php aliases for js functions that we could translate, like.

CRUD::addfield([
    'name' => 'add_extra_cost',
    'type' => 'checkbox',
   'events' => [
        'onCheck' => function () {
            // for single form fields
            $this->hideField('field_name');
            $this->hideRepeatableField('some_field'); // we would know to search the current clicked row and hide the corresponding group field.
            $this->disableField('field_name');
       },
      'onUncheck' => function () {
                  $this->showField('field_name');
                  $this->showRepeatableField('field_name');
                  $this->enableField('field_name');
             },
]);

// events
function hideField($name) {
// return JS string to find and hide a simple field
}
function hideRepeatableField($name) {
// return JS string to find and hide a field inside a repeatable container
}

and we would translate this into JS like:

//
checkbox.on('change', function() {
    if($(this).isChecked()) {
       
        {!! $display_the_strings_we_build  !!}
    }else{
        {!! $display_the_strings_we_build  !!}
    }
}

this would not solve ALL the interactivity, but establishes a base for if/when we decide to support ajax for more complicated calculations and no need for dev to know how to search inside repeatable fields etc and repeating the code over and over again.

I also think it's worth adding that for fields that are complicated to get the containers that should be hidden etc, we could define on those fields a *fieldName*IdentifiableContainer like we have the initialize*fieldName*Field, just food for thought.

Let me know if I missed the point here @tabacitu .

@tabacitu
Copy link
Member Author

tabacitu commented Feb 7, 2022

Thanks for the feedback @pxpm 🙏 First of all, let me get the "big stuff" out of the way 😅 I have already considered and dismissed doing something like this:

CRUD::addfield([
    'name' => 'add_extra_cost',
    'type' => 'checkbox',
   'events' => [
        'onCheck' => function () {
            // for single form fields
            $this->hideField('field_name');
            $this->hideRepeatableField('some_field'); // we would know to search the current clicked row and hide the corresponding group field.
            $this->disableField('field_name');
       },
      'onUncheck' => function () {
                  $this->showField('field_name');
                  $this->showRepeatableField('field_name');
                  $this->enableField('field_name');
             },
]);

I agree with you that it will be the most dev-friendly way to do this. I do! For devs who don't know/want to code JS, this will be GOLD. And what those people want to do most of the time is show/hide fields, for which this solution would work ok.

But if you look at the bigger picture, there are more reasons NOT to like this than reasons to like this:

  • having this work will STILL need us to create a syntax/library to show/hide/disable/enable fields in JS, which would work with all field types; so it would still need the above, this would basically be a PHP layer on top of it;
  • the benefit will be for the 80% use case (agree, totally), but the other 20% use cases will find this useless, and they will need to resort to writing JS anyway;
  • what this would do is provide a way to define JS events in PHP; this is worse than writing direct JS in PHP, in my opinion; don't get me wrong, I'd hate to write JS in PHP; but at least writing JS in PHP is simple, clear and infinitely customizable: you know it's JS immediately, you can do whatever you want; creating a PHP syntax for writing JS is still "writing JS in PHP"; we're fooling ourselves if we say it's not; the only difference is that you're doing it in a different way; we'd be adding a layer that adds technical complexity, maintenance and limits available options (read: customizability), for the sake of slightly better ease-of-use; I have considered this, and decided it is NOT a tradeoff worth doing; the benefits in ease-of-use are not worth us creating a "programming language" to code JS in PHP;
  • creating a PHP syntax to code JS is a slippery slope; you proposed 4 functions, which are limiting, so there will soon be requests to "do that" and "do the other thing" etc. etc; and you know us, if they are good ideas we'll end up doing them; so it will baloon to cover more and more use cases; WE will be working on covering more and more niche use cases, for a very VERY niche feature, to make the developer save... how much time? the seconds it takes to switch to a JS file? how many developers? a few percent? Nope 😅 Our time is better spend solving bigger problems, not increasingly small problems; doing something like this is a slippery slope, and it's a categorical "no" on my end; it will end up being another 6-month-task like the relationship field, and it is NOT worth spending that much time on it; it's not something I want to build; it's not something I want to use; it's not something I want to maintain; a big role might be played by "preference" here, but it's such a very very strong preference; so no; na-ha; 😅

@tabacitu
Copy link
Member Author

tabacitu commented Feb 7, 2022

At first it seems to me that the only thing we are doing is "facilitating" the use of JS in page, not really adding the interactivity functionality. What I mean is:

  • you still need to know JS - it's probably good that you know some of it when working with PHP, but not totally necessary.

Agreed. This is one reason I'm having doubts about this. But I think if we make the syntax simple enough, and with
enough examples, this is not a real problem. People will just copy-paste the example and change the field name. Which is the exact same thing they'd do with the PHP syntax.

  • everything in Backpack is "dead-simple", I don't think this will ever be like it, what I really expect is spargetti JS in controller or spargetti JS in the .js file.

If they're doing a complicated thing and they don't know what they're doing, there will be spaghetti somewhere. I'd rather it's in JS than in their controller.

  • developers would be writting the same code over and over again for simple tasks like show/hide fields when they should just be providing the when (event) and what (action) and backpack would take care of the how (fields inside repeatable? inside inline create? etc) and execution.

True... they would be writing the same code over and over again. But it's not longer code than giving the attributes for the same thing.

Note: I will ignore the "repeatable" thing because I don't think that's something that we will ever need to do. You target a field by name. If you don't know it's name, boo-fucking-hoo, code your own selector 😅 Might change my mind about this if anybody comes up with a real situation where a selector is needed specifically for inside a certain row in a repeatable. But that will be pretty difficult to in a general way, to account for the row but also all field types. Even the PHP syntax will need the JS selector. But again. I don't think that's something we should be worrying about (or providing) from day one. So I'll ignore it.

Ok back to how much code they take up:

  • in php
        'onCheck' => function () {
            // for single form fields
            $this->hideField('field_name');
            $this->disableField('field_name');
       },
      'onUncheck' => function () {
                  $this->showField('field_name');
                  $this->enableField('field_name');
             },
  • in js
crud.field('add_extra_box').on("change", function() {
    if (this.val == 1) {
        crud.field('field_name').hide();
        crud.field('field_name').disable();
    } else {
        crud.field('field_name').show();
        crud.field('field_name').enable();
    }
});
  • which could of course become in js
crud.field('add_extra_box').on("change", function() {
    crud.field('field_name').toggleVisibility();
    crud.field('field_name').toggleEnabledness(); // horrible name, but you know what I mean
});

One good thing I think it's worth for us doing is separating all those options (show, hide, setValue etc..) because I am sure some of them are easier to achieve than others and probably we could make the ajax optional for those who want it and leave it off for dead simple scenarios.

Ok sa you said a sensitive word here... "AJAX" 😅 That is out of the question. We are not creating a baby-Livewire, when in half-a-year or a year we'll be providing support for Livewire itself. No freaking way.


Sorry to turn down your idea, Pedro. But I do not like it. Let's get back to mine, please 😅 (that's how you make friends, good job @tabacitu 😅👏👏👏) Poke holes at it. Am I missing something? Would it be bad? Why? Would it not cover enough use cases? Would it be difficult to use? Would it be difficult to understand?

@soufiene-slimi
Copy link
Contributor

Hi @tabacitu, I was reading this thread and this is really interesting, if we think outside and look at the fields, the first question that comes to my mind is, does the fields API need to be changed or enhanced? does this have anything to do with the fields? well, yes this is related to the fields, but can we if feature versions of backpack introduce another type (ex: groups, or boxes) and we have to support the show/hide/disable/enable, I think this is very possible, and regarding those thoughts, this can cover more than the fields, so what comes to my mind is something like a new wrapper over the UI crud objects that provide those features. This way we can support any field that is currently used by backpack and can ensure evolution.

What @pxpm said is very logical, especially the points about keeping backpack simple to use, (after all this is what makes backpack one of the greatest in administration panels) but the missing key here is, how to introduce this type of feature that play in the front, support all fields and feature fields, and it's easy to understand, easy to use, and of course can grow smoothly without affecting core components, the answer that comes to my mind is another type of wrapper filed, it's only job is UI state of what is inside of it, like Flutter with the GestureDetector.

So tell me what do you think about this, sometimes I overthink, I hope this is not the case 😅

@pxpm
Copy link
Contributor

pxpm commented Feb 7, 2022

@tabacitu first of all I LOVE when we disagree ❤️

Looking at your example, yeah, dead simple ... Just one or two more rows in JS than PHP but not ugly.
Now, you decided to ignore repeatable, I think that is where we are splitting opinions here.

I 100% agree that for dead dead simple tasks it would not be much overhead added using plain JS, but can you try with your example get one field in a specific repeatable row ? I will help you out:

//if element does not have a custom-selector attribute we use the name attribute
                if (typeof element.attr('data-custom-selector') == 'undefined') {
                    form.find('[name="'+$dependency+'"], [name="'+$dependency+'[]"]').change(function(el) {
                            $(element.find('option:not([value=""])')).remove();
                            element.val(null).trigger("change");
                    });
                } else {
                    // we get the row number and custom selector from where element is called
                    let rowNumber = element.attr('data-row-number');
                    let selector = element.attr('data-custom-selector');

                    // replace in the custom selector string the corresponding row and dependency name to match
                    selector = selector
                        .replaceAll('%DEPENDENCY%', $dependency)
                        .replaceAll('%ROW%', rowNumber);

                    $(selector).change(function (el) {
                        $(element.find('option:not([value=""])')).remove();
                        element.val(null).trigger("change");
                    });
                }
            }

It's not literally this, but you get the point of how that goes.

I was giving some tries hiding some fields etc etc, and I think I got to the point that @soufiene-slimi is refering. Some fields have different wrappers and different artifacts in page that need to be hidden. Hide the labels too ?

It does not seem a bad approach at all to have a StateWrapper in "html elements we want" (not referring to fields as it would work with any html inside the wrapper) .

Thanks @soufiene-slimi very nice idea I guess, need to give it some more thinking.

@tabacitu disregard the ajax part, like I said, let's focus on simple tasks (including repeatable as a simple task as is one of our main fields).

Currently we already have some kind of toggable functionality:

CRUD::addField([
'name' => 'bla',
'dependencies' => ['dep_1','dep_2'],
]);

This means that whenever one of those dependencies changes, the value of this field is cleared. What we are trying to do is inverting this dependency, and tell in dep_1 and dep_2 when they change, set the value of bla to null.

I don't agree with you that we are writting JS inside controller if we use wrappers. Eg: You are not writting bit code when coding in C, but the compiler will translate what you write into bit code.
So basically what I am proposing is a compiler (that have a PREDEFINED set of instructions) that he knows how to handle and transform you PHP code into usable JS.

To sum up the instructions that I think we need to cover 90%++) of the cases:
EVENTS:

  • text/textarea
    • change (this includes datepickers etc)
  • checkboxes / radio
    • checked, unchecked
  • selects
    • selected, unselected, cleared

ACTIONS:

  • show/hide/disable/enable/setAttribute

I can not argue about the time commitment to do this feature beeing higher than allowing people to writte JS in controllers, but I cannot see how this will really help to solve this toggable problem if we follow your route.

I can already do what you describe using widgets, for complex scenarios, I don't think there is any benefit added creating an alias for Widget::add().

For real real complex scenarios you can add your on JS functions with a Widget, and our "compiler" could also know: runJavascriptFunction(func_name). The big difference is that in that function you don't need to know what is happening, you call it from the rigth event (checked, selected, unchecked ... ) for a particular scenario.

Let the house burn! 🔥 🙃

@tabacitu
Copy link
Member Author

tabacitu commented Feb 7, 2022

Thank you @soufiene-slimi & @pxpm

Regarding the wrapper - that's exactly how I think about it too. Very difficult line to walk, in this case 😀 I saw it as a feature of the "wrapper" too. When I said "field" I was actually referring to the field's wrapper div.

I 100% agree that for dead dead simple tasks it would not be much overhead added using plain JS, but can you try with your example get one field in a specific repeatable row ? I will help you out:

Thank you for the example! I don't think the code itself is an argument for PHP API and against JS API:

  • if we decide to support subfields:
    • this bit of JS needs to be written by the maintainers EITHER WAY;
    • the only difference is if we hide it behind a JS function or a PHP function;
  • if we decide to NOT support subfields:
    • this bit of JS needs to be written by the developer anyway;

That being said, I don't see the code as complicated as that. I think in most cases what you'll want is a subfield to show/hide another subfield. So it'd be more like:

// the marketing subfield is hidden by default, only show it if the user has agreed to smth else
$("div[data-repeatable-holder=items] input[name$='[i_agree]']").change(function(){
    var row = $(this).parent('.repeatable-element');
    var marketingSubfield = row.find("name$='[would_you_like_some_spam]'").parent('form-group');
    
    marketingSubfield.toggleClass('d-none');
});

// notice we can:
// - use $= to see which inputs end with [field_name] in order to select subfields, so we can even
//   provide a selector for subfields, something like: crud.field('testimonials').subfield('i_agree') 
// - use the DOM to our advantage... we don't need to traverse everything, just the current row

But of course... for repeatable subfields there are other considerations. Like the fact that... they might not exist on pageload. Which is why I think we should not provide subfield hiding/showing. Nobody asked for this, why are we spending time thinking about it?


I was giving some tries hiding some fields etc etc, and I think I got to the point that @soufiene-slimi is refering. Some fields have different wrappers and different artifacts in page that need to be hidden. Hide the labels too ?

Yes. When you hide a field you hide the entire field wrapper. Label, input, everything. Is this questionable in any way? I don't think so... Providing granular control into what you want to hide out of a specific field would be waaaay too much.

I don't agree with you that we are writting JS inside controller if we use wrappers. Eg: You are not writting bit code when coding in C, but the compiler will translate what you write into bit code. So basically what I am proposing is a compiler (that have a PREDEFINED set of instructions) that he knows how to handle and transform you PHP code into usable JS.

Yeah, ok... I misspoke. It's not the same thing. I think that's a very good comparison... it is kind of like writing your own compiler, yes. But I think that only goes to strengthen or clarify my negative opinion on this:

  • as a dev, I don't want the small convenience a compiler would bring; I want freedom;
  • as a maintainer, I don't want to code, review, bugfix nor maintain a compiler;

I think you're heavily underestimating how long writing that compiler would take, and how bug-prone it would be.

I can not argue about the time commitment to do this feature beeing higher than allowing people to writte JS in controllers, but I cannot see how this will really help to solve this toggable problem if we follow your route.

Hmm... well... it would make it easier. The dev will no longer have to think about:

  • the proper selector to use (depending on field type);
  • the proper events to use;
  • the proper value setting or class to add, to what element in particular;

As a dev, you always do:

crud.field('agree').on("change", function() {
    crud.field('price').show();
});

doesn't matter the field type, it'd still work.


To end the PHP2JS compiler discussion, I have one word: "Livewire"

Will the compiler still make sense when you can hide the field using Livewire? No. Then why spend the months to build it? Let's build something that will work cover this need in an elegant way for half a year, maybe a year. It needs to be done in days, not months.

@tabacitu
Copy link
Member Author

tabacitu commented Feb 8, 2022

@pxpm please remember to copy-paste here the examples we created yesterday.

@pxpm
Copy link
Contributor

pxpm commented Feb 8, 2022

CRUD::field([
    'name' => 'add_extra_cost',
    'type' => 'checkbox',
    //'default' => 1
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number', // 10
    'visibleWhen' => ['add_extra_cost', 'isChecked'], // show when check is true
    //'visibleWhen' => ['add_extra_cost', '!==', ''],
    //'enabledWhen' => ['add_extra_cost', '===', 1],
    'emptyWhen' => ['add_extra_cost', 'isUnchecked']
]);

// ---------------------------------------

CRUD::field([
    'name' => 'person_type',
    'type' => 'select',
    'options' => ['person', 'business'],
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => ['person_type', '===', 'business'], // show when select value is business
]);

// ---------------------------------------

CRUD::field([
    'name' => 'person_type',
    'type' => 'select',
    'options' => ['person', 'business'],
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [
            ['person_type', '===', 'business'],
            ['start_date', '>=', 'end_date'],
        ], 
]);

// ---------------------------------------

CRUD::field([
    'name' => 'person_type',
    'type' => 'select',
    'options' => ['person', 'business'],
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [
            ['person_type', '===', 'business'],
            ['start_date', '>=', 'end_date', 'OR'], // default AND
        ], 
]);

// ---------------------------------------

CRUD::field([
    'name' => 'person_type',
    'type' => 'select',
    'options' => ['person', 'business'],
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [
            ['person_type', '===', 'business'],
            ['start_date', '>=', 'end_date', 'OR'], // default AND
        ], 
]);
sources inputs

<input>, <select>, <textarea>

available methods
  • visibleWhen
  • emptyWhen
  • enabledWhen
syntax

[FIELD, OPERATOR, VALUE, PIPE = AND]

  • Operators - <,>, === etc and: isChecked, isUnchecked

@rroblik
Copy link

rroblik commented Feb 8, 2022

For sure such (verry missing !) feature will require a Backpack Form Sate JS API and to my mind the closest thing I think about is Drupal Conditional Form Fields.
TIP : note the # naming convention for attributes 🥰 !

==> Huge re/work but zero JS line of code for most of case (while keeping possibility to add custom JS, of course).

@pxpm
Copy link
Contributor

pxpm commented Feb 8, 2022

It looks very similar to what we have prototyped in our last brainstorm about this. I like it @rroblik.

Thanks

@tabacitu
Copy link
Member Author

Thank you @pxpm & @rroblik . Pedro - let me rephrase your copy-paste, in a way that would help me remember what we talked about, determine if I still think is a good idea, and walk others through our findings, so they can understand and maybe give feedback.


We talked about the solution I proposed above (the JS syntax) as being inconvenient... and maybe not such a good idea after all 😅 Not the syntax itself... but the need to create that JS file or write JS-in-PHP.

What would a solution to problem that be? A PHP syntax. Yep, we're back to that 😅 So we said "ok, let's explore what the most common use cases are, to determine if we can actually create a PHP syntax that will cover all of them". Here's our first draft for that.


Where?

First of all, what items will we need to perform actions on? If you think about it... all Backpack fields store their value in an <input>, <select> or <textarea>, without exception. They're form fields, after all, so even the complex ones will use a hidden input or something... but an <input> nonetheless.

So yes, any solution will have to take that into consideration.

What?

What will we need to perform on Backpack fields, most of the time, to cover 90% of the use cases or more? We narrowed it down to these:

  • show/hide
  • change the value
  • enable/disable

How?

It looks like the needs above will be covered with a PHP syntax consisting of visibleWhen, emptyWhen and enabledWhen, which would receive conditions:

  • 'doSomethingWhen' => ['field_name', 'comparison_operator', 'value']

In practical terms, it would be something like:

  • 'visibleWhen' => ['agreed', '==', true]
  • 'emptyWhen' => ['category', '!=', '3']
  • 'enabledWhen' => ['subtotal', '>=', '99']

Complications

Of course, nothing's that simple. There are also a few complications worth mentioning:

  • (C1) what operators will we need to support? on most fields, you'll want to do stuff depending on another fields' value; so you can use the normal JS comparison operators:
    • == (equal to)
    • === (equal value and equal type)
    • != (not equal)
    • !== (not equal value or not equal type)
    • > (greater than)
    • < (less than)
    • >= (greater than or equal to)
    • <= (less than or equal to)
    • ? (ternary operator) - NO, this one doesn't make sense to support;
  • (C2) however, there are cases where JS comparison operators are NOT enough; for example, you can't do conditionals on checkboxes and radios based on their value, you have to do them depending on their state (checked/not); so they need some new operators, something like:
    • isChecked
    • isUnchecked
  • (C3) additionally, you might want to do show/hide/empty/enable/disable fields not when ONE other field respects a condition, but when MULTIPLE fields respect MULTIPLE conditions; and in that case... the question becomes, what's between those conditions... AND/OR? so we also need to support logical operators, which can be given as a last parameter to the condition:
'visibleWhen' => [
    ['agreed', '==', true, '&&'],
    ['category', '!=', 33]
]

So we ended up with these examples, for the most common scenarios we identified:

// *********
// show a second field when a checkbox is checked
// *********

CRUD::field([
    'name' => 'add_extra_cost',
    'type' => 'checkbox',
    //'default' => 1
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number', // 10
    'visibleWhen' => ['add_extra_cost', 'isChecked'], // show when check is true
    //'visibleWhen' => ['add_extra_cost', '!==', ''],
    //'enabledWhen' => ['add_extra_cost', '===', 1],
    'emptyWhen' => ['add_extra_cost', 'isUnchecked']
]);

// *********
// show a second field when a first field has a certain value (would work with select, input[type=text], textarea)
// *********

CRUD::field([
    'name' => 'person_type',
    'type' => 'select',
    'options' => ['person', 'business'],
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => ['person_type', '===', 'business'], // show when select value is business
]);

// *********
// show a second field when multiple conditions are met, on different fields
// *********

CRUD::field([
    'name' => 'person_type',
    'type' => 'select',
    'options' => ['person', 'business'],
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [
            ['person_type', '===', 'business'], // if a fourth parameter is missing, we assume "AND" / "&&"
            ['start_date', '>=', 'end_date'],
        ], 
]);

// *********
// show a second field when one of multiple conditions are met, on different fields 
// *********

CRUD::field([
    'name' => 'person_type',
    'type' => 'select',
    'options' => ['person', 'business'],
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [
            ['person_type', '===', 'business'],
            ['start_date', '>=', 'end_date', 'OR'], // default AND
        ], 
]);

// Questions here: should the operator be on the FIRST or SECOND condition?!

That's it. That's the rephrase. Hope it makes better sense now (to us and to others). I'll type out a second reply here with my thoughts on it.

@tabacitu
Copy link
Member Author

PROs - what I like about this PHP solution:

  • it's faster to type, after all it's right there in the Controller;
  • from the use cases that we've heard of, it will cover a lot of ground (we hope 80-90% of all common use cases);

CONs - What I don't like about this PHP solution (and proposed workarounds for some):

  • (CON 1) I think most of the time, you won't only need to show/hide some field, you will also need to disable it, in order to not have it in the request; so what you end up with, most of the time, is writing the same conditions twice; so then... most of the times you use this feature... you'll end up with duplicated code; this is even more visible when you have multiple conditions:
CRUD::field([
    'name' => 'person_type',
    'type' => 'select',
    'options' => ['person', 'business'],
]);

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [
            ['person_type', '===', 'business'],
            ['start_date', '>=', 'end_date'],
        ], 
    'disableWhen' => [
            ['person_type', '===', 'business'],
            ['start_date', '>=', 'end_date'],
        ], 
]);

// we can find ways around that, like using pipes or ampersants for the attribute name 
// (eg. `showWhen|disableWhen` / `showWhen&disableWhen`) 

CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen&disableWhen' => [
            ['person_type', '===', 'business'],
            ['start_date', '>=', 'end_date'],
        ], 
]);
// but that is a code smell if you ask me; it highlights the fact that we're reinventing the wheel here,
// we're inventing an array syntax for something that cannot be completely incapsulated 
// in an array syntax - logic; we're inventing unnatural ways around a problem, just like
// we did with isChecked and isUnchecked;
  • (CON 2) the conditions have different numbers of parameters (2/3/5), if your target field is a checkbox/radio/smth-else and whether you're using AND/OR at the end:
CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [
            ['agreed', 'isChecked', 'AND'],
            ['start_date', '>=', 'end_date', 'AND'],
            ['end_date', '<=', date('Y-m-d')],
        ], 
]);

// this seems untidy to me... and it related to two other things I don't like about this syntax:
// - the fact that you will have to define an array of arrays;
// - the fact that we have to decide if the AND/OR has to be on the first condition or the second; 
// it makes it looks dirty and complicated to me... 
// so WHAT IF... instead of requiring an array of arrays... we require one long array?
CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [ 'agreed', 'isChecked', 'AND', 'start_date', '>=', 'end_date', 'AND', 'end_date', '<=', date('Y-m-d')], 
]);

// which, if you want to split them by lines so it's easier to read, would look like
CRUD::field([
    'name' => 'extra_cost_value',
    'type' => 'number',
    'showWhen' => [ 
        'agreed', 'isChecked', 'AND', 
        'start_date', '>=', 'end_date', 'AND', 
        'end_date', '<=', date('Y-m-d')
    ], 
]);
// I think I like this one better... after all... that's how you think of them... a series of conditions...
  • (CON 3) I'm worried about is that this feature comes with customizability, but with zero extensibility; let me explain; all the possible problems we've identified above.. and the workarounds we've found... make this a pretty reasonable solution from the point of view of a developer who wants something that we already thought of; it will look like a good tradeoff to them, and it will look customizable enough; but as soon as a dev needs something that we did NOT think of... we're forced to leave him all alone, to code his own solution from scratch; it's an all-or-nothing system, where we either help the dev with everything, or help him with nothing:
    • if the available actions are not enough (showWhen/hideWhen, enableWhen/disableWhen and emptyWhen), you have to code your own everything, we don't help at all; no way of quickly creating your own action (eg. setValueWhen or makeReadOnlyWhen or setCssWhen);
    • if the selectors are not enough (you don't want to do things depending on value or checked/unchecked state, but on something else), you have to code your own everything, we don't help at all;
    • if the conditions are not enough (eg. you need a different operator... or need to chain the operators in a series of AND&OR conditionals that would require parantheses)... you have to code your own everything, we don't help at all;
  • (CON 4) because of CON 3 (zero extensibility), we have to be really REALLY careful what scenarios we support, from day one, and be firm about not adding support for new ones; we have to clearly draw the line and say "no, this is it, we're not adding more stuff to it"; we talked about showWhen/hideWhen, enableWhen/disableWhen and emptyWhen, which might cover the most common use cases, but this can easily spiral out of control; I promise you that... as soon as we introduce it, people will come up with novel actions, like setValueWhen or makeReadOnlyWhen or setCssWhen or anything else that would work for their scenario; we cannot and should not do that; we cannot expand this feature's scope, otherwise we will end up working on this our entire lives, in order to save one developer a few minutes;
  • (CON 5) cost vs benefit ratio is bad; even if we limit this feature in scope (only what we talked above), so that it doesn't become a nightmare to maintain; this feature might not be worth creating... at all; because the problem it is addressing is easily fixed with custom JS code that will only take 15-30-45 minutes to write; it would take us 2-3 months to create the above, but it would take a dev just 30min to code a JS solution, that would be PERFECT for their particular use case; even if we do create one of the solutions above (JS or PHP one)... we would actually not be saving that much of a developers' time; both the JS and PHP solutions... are complicated and costly solutions for a simple problem, that only a subset of devs have, each with their own very specific need;

Bottom line:

  • PROs - I like the 'em, I like how easy it would make the most common scenarios, so I can't dismiss this PHP solution... even though there are a lot of CONs;
  • CON 1 - 🟨 - I don't like showWhen&disableWhen very much... maybe we find a different way around this duplication;
  • CON 2 - ✅ - I like the string of conditions A LOT more than the array of arrays;
  • CON 3 - 🟥 - I hate this; I hate that we're covering what scenarios we can think of now, but for everything else... the devs are on their own;
  • CON 4 - ✅ - I think we can become strong enough to say "no", especially if our position is "no more" from the start;
  • CON 5 - 🟥 - what can we do about the cost vs benefit ratio? how about... we go and write better docs, on how to solve this problem in your own project, using custom JS/jQuery? if we had examples on all these scenarios we came up with... that would probably reduce the time for a dev to create a custom solution for their project... from 15-30-45 minutes to 5 minutes or less;
    • first of all, it would address CON 3 - we wouldn't be leaving devs all alone if they need something custom... we have examples and docs on how they can easily do this;
    • second of all, it's something devs need anyway, while we're debating a PHP vs JS solution and actually coding the above; if it takes us 3 months to build the above, during those 3 months we'll want people to have a documented way for them to do it; a temporary solution;
    • third... maybe that'll be a good-enough way to solve this problem, in itself; maybe we solve this problem using docs, instead of using code that would take us 3+ months to write, and a lot of time to maintain afterwards...

Bottom line:

  • let's continue talking about the CONs, see how/if we can get around them;
  • I'm going to work on better docs, on how you can write custom code to solve this problem, right now; this should help:
    • us better identify the scenarios we need to cover;
    • devs to reduce the time it takes them to do this, until we develop a built-in solution;

@rroblik
Copy link

rroblik commented Feb 13, 2022

Dear @tabacitu

I disagree with most of what you said 😀

Features we are talking about here require a JS API to leverage server side to client side rules "conversions". I didn't understand it when I read your comment but maybe I'm wrong? As all fields html syntax is know in advance we can easily manage front behavior server side. Also about CON3 : this JS API must/will be expendable, by design.

One other (small) thing you didn't mention is the validation : sometimes a field become mandatory depending on other field value. We can already manage it server side but not client side.

hard work in perspective but... with Pro all become possible !

@tabacitu
Copy link
Member Author

I disagree with most of what you said 😀

😅 That's why we're here, that's why we're talking about it in the open 😅 Thanks for pitching in!

Features we are talking about here require a JS API to leverage server side to client side rules "conversions".

I guess you're right. To make this feature complete, the PHP syntax would ALSO require the JS syntax I mentioned at the top of this issue. That would:

  • make it both easily usable AND extensible 👍
  • probably double the time of development 👎

Also about CON3 : this JS API must/will be expendable, by design.

That's the thing:

  • the JS API can be extensible by design; but you might not even need to extend it at all; you can just code your own JS or jQuery or Vue or whatever you want, to do whatever you want;
  • the PHP API cannot be extensible by design; we would basically need to modify all fields to account for this new feature, which is definitely not worth it; the implementation would become very complicated;

One other (small) thing you didn't mention is the validation : sometimes a field become mandatory depending on other field value. We can already manage it server side but not client side.

DAMN IT! See? That's what I was saying... with the new scenarios that will keep popping up. Yes you might want to make something required depending on what's happening in the form. Makes perfect sense.

Problem is... on the front-end you can only manipulate the front-end required status, NOT the PHP validation... So even if you add an asterisk to it and make require input... the back-end validation would still need to account for both scenarios.

This is food for thought... thank you.

hard work in perspective but... with Pro all become possible !

That's the attitude!!! 💪

@tabacitu
Copy link
Member Author

Scenarios so far:

  • MUST: when a checkbox is checked, show a second field;
  • MUST: when a checkbox is checked, show a second field AND un-disable/un-readonly it;
  • MUST: when a radio has something specific selected, show a second field;
  • MUST: when a select has something specific selected, show a second field;
  • MUST: when a checkbox is checked AND a select has a certain value, then show a third field;
  • MUST: when a checkbox is checked OR a select has a certain value, then show a third field;
  • SHOULD: when a select is a certain value, show a second field; if it's another value, show a third field;
  • SHOULD: when a checkbox is checked, automatically check a different checkbox or radio;
  • COULD: when a text input is written into, write into a second input (eg. slug);
  • COULD: when multiple inputs change, change a last input to calculate the total or smth;
  • WON'T: when a checkbox is checked, make a different field required; (can't edit PHP validation from front-end)

@tabacitu tabacitu changed the title [Feature Proposal] Toggle field visibility (hide/show fields depending on other fields) [Feature Proposal] Conditional fields (aka. hide/show fields depending on other fields, aka. toggle field visibility) Feb 13, 2022
@rroblik
Copy link

rroblik commented Feb 13, 2022

Scenarios so far:

  • MUST: when a checkbox is checked, show a second field;
  • MUST: when a checkbox is checked, show a second field AND un-disable/un-readonly it;
  • MUST: when a radio has something specific selected, show a second field;
  • MUST: when a select has something specific selected, show a second field;
  • MUST: when a checkbox is checked AND a select has a certain value, then show a third field;
  • MUST: when a checkbox is checked OR a select has a certain value, then show a third field;
  • SHOULD: when a select is a certain value, show a second field; if it's another value, show a third field;
  • SHOULD: when a checkbox is checked, automatically check a different checkbox or radio;
  • COULD: when a text input is written into, write into a second input (eg. slug);
  • COULD: when multiple inputs change, change a last input to calculate the total or smth;
  • WON'T: when a checkbox is checked, make a different field required; (can't edit PHP validation from front-end)

Thanks for sum up!

SHOULD : ➡️ Same logic as the MUST ones so yes, it's also a MUST !
WON'T : ➡️ but should be done front side, à required field look same as not required, except that the "*" appended to the label. Hope that is doable using JavaScript 😆

once again to my mind Drupal way of doing this must be double checked, it's the way to go without reinventing the wheel !

@zachweix
Copy link
Contributor

Regarding a field becoming mandatory based on other inputs, sometimes you need to duplicate code in both the frontend and backend, whether you're using a framework or not.
I like the ideas here, but I hope it doesn't become too over-complicated that nobody will be able to figure out how to use it or that it becomes too hard to maintian

@tabacitu
Copy link
Member Author

I like the ideas here, but I hope it doesn't become too over-complicated that nobody will be able to figure out how to use it or that it becomes too hard to maintian

That's my fear too...

@rroblik
Copy link

rroblik commented Feb 14, 2022

Regarding a field becoming mandatory based on other inputs, sometimes you need to duplicate code in both the frontend and backend, whether you're using a framework or not.
I like the ideas here, but I hope it doesn't become too over-complicated that nobody will be able to figure out how to use it or that it becomes too hard to maintian

For sure server side validation remain mandatory. In fact with backpack there is not client side validation so this must stay like that 😉. I only speak about mandatory field, add a span for the * must be faisable 😀

@fronbow
Copy link

fronbow commented Feb 15, 2022

This is all interesting stuff, I've been following these discussions for a while as it always crops up that I need to show certain fields when certain conditions are met.
For the past week I've been wondering if we need a more generic scaffold solution. So all the common logic is in a class, and we (the developers) write a field/column that extends this logic for specific use cases.
But I'm enjoying the current way of thinking, I remember being around when the drupal form api was in its' infancy!
One thing I would like to mention is, does BackPack need to encompass all use-cases? I know drupal does as that's what kind of beast it is (more for end users than devs) but shouldn't there be some element of development logic for us end users being as we are developers?!

@rroblik
Copy link

rroblik commented Feb 15, 2022

@fronbow Drupal Form API is not for end users but for developers and Drupal admin UI rely on it. Here with backpack no admin ui, only in code possibility ; main reason why we need an real Form API like that in Backpack !

@mamarmite
Copy link
Contributor

mamarmite commented Mar 7, 2022

I finally finished the reading! It's awesome to read process like that, always constructive.

I like a lot of the backend/php solution with the wheres-alike conditions, seem really versatile, from my point of view.

I'm not sure I'm adding something constructive, but for theses :

CON 1 - 🟨 - I don't like showWhen&disableWhen very much... maybe we find a different way around this duplication;

CONs - What I don't like about this PHP solution (and proposed workarounds for some):

(CON 1) I think most of the time, you won't only need to show/hide some field, you will also need to disable it, in order to not have it in the request; so what you end up with, most of the time, is writing the same conditions twice; so then... most of the time you use this feature... you'll end up with duplicated code; this is even more visible when you have multiple conditions:

It seems to me like the disable and hiding should be set based on the field's setting.

If I try to explain my pov here :

  1. If you needed the field to always have a value (in the request and in the validation)
  2. The showWhen conditions will hide the field but keep it available, to have its value in the request
  • This would use the default value of the field
  • But I feel this opens more cases and the need of a : default-value-when-hidden
  1. If you can avoid having the field, and the field isn't required,
  2. The show/hide feature disable and hide the field

Need more brainstorming :

I think hiding the field completely helps the reading and decluttered forms in general.
It's more a UX question, but I would like to set this in a more general way, aside/on-top from each field.
Maybe in the config file : conditional_field_hidding_behavior : hide, grayed out. And I would implement only the hiding first. And see if it's a thing.

Anyhow, I hoped this makes sense a bit. I might be to late.
Love this process and thanks for inviting me to comment on it.

@ashek1412
Copy link

Is there any solution for this after all these discussions?

@mamarmite
Copy link
Contributor

@ashek1412 I would say the plan is : #4158 (comment)
But only iterative dev and test would tell what would be the v1.0 for this.

@rroblik
Copy link

rroblik commented Mar 28, 2022

🤨 downvote this one

@ashek1412
Copy link

It's a real shame that the issue is still not solved after all these discussions. My client bought the Backpack PRO and I can't give him a solution. It's a real shame for the Backapak team.

@mamarmite
Copy link
Contributor

@ashek1412 Everything is gonna be OK, you'll find a solution for your problem. Don't push down an entire package on one missing features. That is planned btw.
I think that is not the place to ask that and say that.
It's awesome that the backpack team asks the community and brainstorm with them for the implementations of the feature.

@ashek1412
Copy link

however, I have to do it using javascript for the time being. but when you guys put your plan into action?

@firecentaur
Copy link

however, I have to do it using javascript for the time being. but when you guys put your plan into action?

Hi, yes please put a plan I to action

@promatik
Copy link
Contributor

Hi everyone!
I've done this in the past, using Javascript, I created a simple solution for what I needed at that time, take a look at the result;

2022-03-31.12-47-53.mp4

You can show/hide based on select and input[type="checkbox"].

Until Backpack provides a solution for this, you can use this code. It uses wrapperAttributes to add attributes to fields (select, checboxs) so it hides and shows based on that

It's super easy to implement, here is the code;

public/assets/js/backpack-toggle.js

JavaScript
let toggleInputFields = elem => {
  const targetsIds = elem.dataset.toggles.split(' ');
  targetsIds.forEach(id => {
    const targetElem = document.querySelector(
      `#${id}, [data-field-name=${id}]`
    );

    const checkbox = elem.querySelector('input[type="checkbox"]');
    targetElem.classList.toggle('d-none', !checkbox.checked);
  });
};

let toggleSelectFields = elem => {
  const { value } = elem;
  const { dataset: { toggles }} = elem.closest('[data-toggles]');

  document.querySelectorAll(`[${toggles}]`).forEach(field => {
    let r = false;

    // Check if json or single value
    try {
      r = !JSON.parse(field.attributes[toggles].value).includes(value);
    } catch (e) {
      r = field.attributes[toggles].value !== value;
    }

    // Toggle visibility
    field.classList.toggle('d-none', r);

    // Clear any child selects
    field.querySelectorAll('[data-toggles] select').forEach(select => {
      select.value = '';
      toggleSelectFields(select);
    });
  });
};

let initTogglers = () => {
  document.querySelectorAll('[data-toggles]').forEach(elem => {
    const field = elem.querySelector('input[type="checkbox"], select');
    // eslint-disable-next-line default-case
    switch (field.type) {
      case 'checkbox':
        // On click
        elem.addEventListener('click', () => {
          toggleInputFields(elem);
        });

        // Check initial status
        if (elem.querySelector('[type=checkbox]').checked) {
          toggleInputFields(elem);
        }
        break;
      case 'select-one':
        // On change
        $(elem).on('change', () => {
          toggleSelectFields(field);
        });

        // Check initial status
        toggleSelectFields(field);
        break;
    }
  });
};

document.addEventListener('DOMContentLoaded', initTogglers);

public/assets/css/backpack-toggle.css

CSS
.form-quote {
  border-left: 1px solid #e7e8ea;
  margin-left: 2rem;
  max-width: calc(100% - 4rem);
  margin-bottom: 0;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}

app\Http\Controllers\Admin\EntityCrudController.php

PHP
protected function setupCreateOperation() {
    Widget::add()->type('script')->content('assets/js/backpack-toggle.js');
    Widget::add()->type('style')->content('assets/css/backpack-toggle.css');
    ...
// Select Toggler
CRUD::addField([
    ...
    'wrapperAttributes' => [
        'data-toggles' => 'toggler-device-type',
    ],
]);

CRUD::addField([
    ...
    'wrapperAttributes' => [
        'class' => 'd-none form-group col-sm-12 form-quote',
        'toggler-device-type' => 'win',
    ],
]);
// Checkbox Toggler
CRUD::addField([
    ...
    'wrapperAttributes' => [
        'class' => 'field-toggler form-group col-sm-12',
        'data-toggles' => 'hours minutes',
    ],
]);

CRUD::addField([
    ...
    'wrapperAttributes' => [
        'class' => 'd-none col-12 form-quote',
        'id' => 'hours',
    ],
]);

CRUD::addField([
    'name' => 'minutes',
    ...
    'wrapperAttributes' => [
        'class' => 'd-none col-12 form-quote',
        'id' => 'minutes',
    ],
]);

The example video above uses the following code;

app\Http\Controllers\Admin\EntityCrudController.php

Example PHP
protected function setupCreateOperation() {
    Widget::add()->type('script')->content('assets/js/backpack-toggle.js');
    Widget::add()->type('style')->content('assets/css/backpack-toggle.css');
    
    // Toggler Device Type
    CRUD::addField([
        'name' => 'device_type',
        'label' => 'Device type',
        'type' => 'select_from_array',
        'options' => [
            'win' => 'Windows',
            'linux' => 'Linux',
            'mac' => 'Mac',
            'unix' => 'Unix',
        ],
        'default' => 'win',
        'wrapperAttributes' => [
            'data-toggles' => 'toggler-device-type',
        ],
    ]);

    CRUD::addField([
        'name' => 'win_script',
        'label' => 'Windows script',
        'wrapperAttributes' => [
            'class' => 'd-none form-group col-sm-12 form-quote',
            'toggler-device-type' => 'win',
        ],
    ]);

    CRUD::addField([
        'name' => 'win_type',
        'label' => 'Windows type',
        'type' => 'select_from_array',
        'options' => [
            'win10' => 'Win 10',
            'win11' => 'Win 11',
        ],
        'default' => 'win',
        'wrapperAttributes' => [
            'class' => 'd-none form-group col-sm-12 form-quote',
            'toggler-device-type' => 'win',
            'data-toggles' => 'toggler-win-type',
        ],
    ]);

    CRUD::addField([
        'name' => 'win11',
        'label' => 'Windows 11 special function',
        'wrapperAttributes' => [
            'class' => 'd-none col-12 form-quote',
            'toggler-win-type' => 'win11',
        ],
    ]);

    // ---

    CRUD::addField([
        'name' => 'linux_title',
        'label' => 'Linux title (unix / linux)',
        'wrapperAttributes' => [
            'class' => 'd-none col-12 form-quote',
            'toggler-device-type' => json_encode(['linux', 'unix']),
        ],
    ]);

    CRUD::addField([
        'name' => 'mac_title',
        'label' => 'Mac title (unix / mac)',
        'wrapperAttributes' => [
            'class' => 'd-none col-12 form-quote',
            'toggler-device-type' => json_encode(['mac', 'unix']),
        ],
    ]);

    CRUD::addField([
        'name' => 'unix_script',
        'label' => 'Unix script (unix / (linux / mac))',
        'wrapperAttributes' => [
            'class' => 'd-none col-12 form-quote',
            'toggler-device-type' => json_encode(['unix', 'linux', 'mac']),
        ],
    ]);

    // Toggler time
    CRUD::addField([
        'name' => 'scheduled_time',
        'label' => 'Scheduling time',
        'type' => 'checkbox',
        'wrapperAttributes' => [
            'class' => 'field-toggler form-group col-sm-12',
            'data-toggles' => 'hours minutes',
        ],
    ]);

    CRUD::addField([
        'name' => 'hours',
        'label' => 'Hours',
        'type' => 'select2_from_array',
        'options' => range(0, 23),
        'wrapperAttributes' => [
            'class' => 'd-none col-12 form-quote',
            'id' => 'hours',
        ],
    ]);

    CRUD::addField([
        'name' => 'minutes',
        'label' => 'Minutes',
        'type' => 'select2_from_array',
        'options' => range(0, 60),
        'wrapperAttributes' => [
            'class' => 'd-none col-12 form-quote',
            'id' => 'minutes',
        ],
    ]);

    // Toggler weekdays
    CRUD::addField([
        'name' => 'scheduled_weekdays',
        'label' => 'Scheduling weekdays',
        'type' => 'checkbox',
        'wrapperAttributes' => [
            'class' => 'field-toggler form-group col-sm-12',
            'data-toggles' => 'weekdays',
        ],
    ]);

    CRUD::addField([
        'name' => 'weekdays',
        'label' => 'Weekdays',
        'type' => 'select2_from_array',
        'allows_multiple' => true,
        'options' => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturay'],
        'wrapperAttributes' => [
            'class' => 'd-none col-12 form-quote',
            'id' => 'weekdays',
        ],
    ]);
}

@rroblik
Copy link

rroblik commented Apr 1, 2022

Nice ! Bottom line :

  • you cannot handle required fields properly with that
  • Lot of code repetition (wrapper base class for example)

That must stay what you say : a workaround only

@tabacitu
Copy link
Member Author

tabacitu commented Apr 5, 2022

I'm back with good news. Like I said, I think the first steps towards a good solution are:

  • having a documented way to easily do the top 10 most common scenarios; even if it's verbose / vanilla JS / jQuery;
  • having a JS library to interact with fields, with examples for simple scenarios;
  • having a JS library to interact with fields, so that when you get to do complicated stuff, you only think about your custom stuff, not field selectors and other boilerplate like that;

And the above are... pretty much done! At least the working version, that I can show (v7 in my prototypes though 😅).

Check out #4312 - where I've created a small JS library to help devs interact with fields. It's got only ~90 lines of JS, but it covers ALL the scenarios we talked about. I even included the proof 😅 It still needs feedback but... I kind of like it. It's a simple solution to a complicated problem. And I love it when that happens.

Let me know what you guys think.
Cheers!

@zachweix
Copy link
Contributor

zachweix commented Apr 5, 2022

Nice! I love simple solutions to complicated problems. I'll have to take a look at it later

@tabacitu
Copy link
Member Author

tabacitu commented Apr 6, 2022

I'm going to close this issue, in order to move the conversation to the PR here #4312 🎉🎉🎉

It would be super-difficult for anybody to read all this thread anyway.

Thanks A LOT for the feedback everyone!

@tabacitu tabacitu closed this as completed Apr 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

11 participants