orchidsoftware / platform

Orchid is a @laravel package that allows for rapid application development of back-office applications, admin/user panels, and dashboards.
https://orchid.software
MIT License
4.44k stars 652 forks source link

Unify Field usage across CRUD screens #1560

Open pqr opened 3 years ago

pqr commented 3 years ago

Is your feature request related to a problem? Please describe. For typical CRUD interface I'am usually create 3 screens: List screen, View scree, Edit screen.

Currently I have to describe each model field 3 times:

  1. as TD with custom render function for List screen
  2. as ViewField or Label for View screen
  3. as Field for Edit screen

It would be helpful if each type of Field (e.g. Input, TextArea, DateTimer, ...) can be easily rendered as readonly markup for List screen (as TD) and for View screen.

Describe the solution you'd like Good example is Laravel Nova - fields are described only once and then Nova renders them in different forms (in table cell, on view screen, on edit screen)

tabuna commented 3 years ago

Hi, I was thinking about creating something like a lookup table.

image

Where the syntax will not be bound to the field, instead, we will create small components. Something similar to, https://github.com/orchidsoftware/platform/issues/1489

Description::make('name')->component(ShortString::class);
Description::make('price')->component(Numeric::class);
Description::make('body')->component(Html::class);

// img/html/etc

This way, it will be easy for us to add this to the screen. At the same time, maintaining the independence of the fields. What do you think about it?

tabuna commented 3 years ago

Why is it so? Because, for example, the Textarea field can have different values for display. First 100 characters, 300 characters. Or for example, @mention support as soon as I did in this post @pqr

Also, with others. Whether a reference to an object should be specified in the Relation / Select field or just display text. And many, many other things. Such simple Laravel components, according to the idea, should help us.

pqr commented 3 years ago

On the one hand we could have many, many other things to be be customized on "view" screen - agree.

On the the other hand in most cases there is a "default" behavior that satisfy:

So for every type of Field we could define reasonable and sensible way to display it on "view"/"list" screen - this would cover most default cases.

When customization is needed, then Field class could provide a few options:

By the way sometimes it also helpful to customize "edit" representation of field without creating a new extends Field class.

tabuna commented 3 years ago

@pqr Did you mean exactly CRUD (https://github.com/orchidsoftware/crud) or just use in general?

tabuna commented 3 years ago

I just don't really understand the idea.

We'll have to specify fields everywhere, but depending on the context (the result will be different for the layers)?

class ListLayout extends Table
{
    protected function columns() : array
    {
      return [
          TextArea::make('description'),
        ]
    }
}

Result: simple text

class Edit extends Row
{
    protected function fields() : array
    {
      return [
          TextArea::make('description'),
        ]
    }
}

Result: <textarea> tag.

Is this how it should work?

pqr commented 3 years ago

Is this how it should work?

Exactly!

The main idea: define field once, use everywhere (in table layout, in view layout, in edit layout...)

More robust example:

(note: Money is not a real class from Orchid, imagine it just for example)

class FinanceFields
{
    public static function invoiceAmount()
    {
        return Money::make('invoice_amount', 'Invoice Amount')
             ->align(TD::ALIGN_RIGHT)
             ->sort()
             ->currency('USD')
             ->placeholder('Enter Invoice Amount')
             ->required()
    }
}

class ListLayout extends Table
{
    protected function columns() : array
    {
      return [
          FinanceFields:: invoiceAmount(),
        ]
    }
}

class Edit extends Row
{
    protected function fields() : array
    {
      return [
          FinanceFields:: invoiceAmount(),
        ]
    }
}

Here we mix methods and properties from Field and from TD:

Philosophically it sounds bad: we mix different responsibilities in one class, we all know low cohesion is anti-pattern.

On the other side me personally like it: I like to define all field properties for all possible environments (table/view/edit) in one place.

pqr commented 3 years ago

Did you mean exactly CRUD (https://github.com/orchidsoftware/crud) or just use in general?

First I thought about CRUD, but then I decided to place this issue into platform repository to propose such unification in general in the core of Orchid.

pqr commented 3 years ago

One more thing: this concept of "unified field" can be implemented as class which holds only meta information about field properties and settings, but no actual methods to render.

abstract class UniField {} // keeps only basic properties like $sort, $align, $required
class UniMoney extends UniField {} // add money specific fields like $currency

Then we could have factories to produce good old orchids Field or TD:

class UniMoney extends UniField
{
     protected string $currency = '';
      // ... other pros related to money functionality

     public function asField(): Fieldable
    {
        return Input::make(...); // converts meta information about this field into good old Orchids Input field
    }

     public function asTD(): TD
    {
        return TD::make(...); // converts meta information about this field into good old Orchids TD
    }
}

I think I can try to build it as separate package and battle test in my current projects.

tabuna commented 3 years ago

In theory, we don't need to link them at a low level. Just make a facade over them. Something like:

namespace Orchid\Support;

use Orchid\Screen\Fields\Input;
use Orchid\Screen\TD;

/**
 * Class ExampleCombinator
 *
 * @mixin Input|TD
 */
class ExampleCombinator
{
    /**
     * @var Input
     */
    protected $field;

    /**
     * @var TD
     */
    protected $td;

    /**
     * Combinator constructor.
     *
     * @param string|null $name
     */
    public function __construct(string $name = null)
    {
        $this->field = Input::make($name);
        $this->td = TD::make($name);
    }

    /**
     * Create a new Field element.
     *
     * @param string|null $name
     *
     * @return static
     */
    public static function make(string $name = null): self
    {
        return new static($name);
    }

    /**
     * @return Input
     */
    public function getField()
    {
        return $this->field;
    }

    /**
     * @return TD
     */
    public function getTd(): TD
    {
        return $this->td;
    }

    /**
     * @param string $name
     * @param array  $arguments
     *
     * @return Combinator
     */
    public function __call(string $name, array $arguments)
    {
        $this->fill($name, $arguments);

        return $this;
    }

    /**
     * @param string     $name
     * @param array|null $arguments
     */
    private function fill(string $name, ?array $arguments = [])
    {
        rescue(function () use ($name, $arguments) {
            $this->getField()->$name(...$arguments);
            $this->getTd()->$name(...$arguments);
        }, null, false);
    }
}

Then the result of using will be something like what you want:

$combinator = ExampleCombinator::make('name')->required();

dd($combinator);

It would also work right now, without the major version. For example, we defined fields somewhere (for example, in a model)

public static function fields(): Collection
{
    return collect([
        ExampleCombinator::make('name')->required()
    ]);
}

Then in rows and tables, we only needed to call:

class ListLayout extends Table
{
    protected function columns() : array
    {
      return YourModel::fields()->map->getTd()->toArray(); 
    }
}

class Edit extends Row
{
    protected function fields() : array
    {
      return YourModel::fields()->map->getField()->toArray();
    }
}
tabuna commented 3 years ago

I think we have many options, from a facade to a simple DTO with a set of getter and setter.

VDF::make()
    ->setField(Input::make()->required())
    ->setTd(Td::make()->align(TD::ALIGN_RIGHT))

The only thing that confuses me is the use of all sorts of groupings and dynamic things.