Laravel-Backpack / CRUD

Build custom admin panels. Fast!
https://backpackforlaravel.com
MIT License
3.08k stars 885 forks source link

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

Closed tabacitu closed 2 years ago

tabacitu commented 2 years ago

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 https://github.com/Laravel-Backpack/addons/issues/11 ... this is a tricky problem to solve... because the developer needs can be so very different. In various places, people ask:

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 commented 2 years ago

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:

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 commented 2 years ago

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:

tabacitu commented 2 years ago

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:

        'onCheck' => function () {
            // for single form fields
            $this->hideField('field_name');
            $this->disableField('field_name');
       },
      'onUncheck' => function () {
                  $this->showField('field_name');
                  $this->enableField('field_name');
             },

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 commented 2 years ago

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 commented 2 years ago

@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:

ACTIONS:

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 commented 2 years ago

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:

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:

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:

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 commented 2 years ago

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

pxpm commented 2 years ago
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
syntax

[FIELD, OPERATOR, VALUE, PIPE = AND]

rroblik commented 2 years ago

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 commented 2 years ago

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

Thanks

tabacitu commented 2 years ago

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:

How?

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

In practical terms, it would be something like:

Complications

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

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 commented 2 years ago

PROs - what I like about this PHP solution:

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

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:
```php

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...

Bottom line:


Bottom line:

rroblik commented 2 years ago

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 commented 2 years ago

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:

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

That's the thing:

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 commented 2 years ago

Scenarios so far:

rroblik commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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
  3. If you can avoid having the field, and the field isn't required,

  4. 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 commented 2 years ago

Is there any solution for this after all these discussions?

mamarmite commented 2 years ago

@ashek1412 I would say the plan is : https://github.com/Laravel-Backpack/CRUD/issues/4158#issuecomment-1037917117 But only iterative dev and test would tell what would be the v1.0 for this.

rroblik commented 2 years ago

๐Ÿคจ downvote this one

ashek1412 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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

firecentaur commented 2 years ago

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 commented 2 years ago

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;

https://user-images.githubusercontent.com/1838187/161063634-b3cc44fb-201e-471a-adea-7cee785f0425.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 ```js 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 ```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 ```php protected function setupCreateOperation() { Widget::add()->type('script')->content('assets/js/backpack-toggle.js'); Widget::add()->type('style')->content('assets/css/backpack-toggle.css'); ... ``` ```php // 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', ], ]); ``` ```php // 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 ```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 commented 2 years ago

Nice ! Bottom line :

That must stay what you say : a workaround only

tabacitu commented 2 years ago

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

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

Check out https://github.com/Laravel-Backpack/CRUD/pull/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 commented 2 years ago

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

tabacitu commented 2 years ago

I'm going to close this issue, in order to move the conversation to the PR here https://github.com/Laravel-Backpack/CRUD/pull/4312 ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰

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

Thanks A LOT for the feedback everyone!