Arkounay / ux-collection

Collection for Symfony forms using Symfony UX
MIT License
37 stars 5 forks source link

custom controller not taken into account #11

Closed fecou closed 4 months ago

fecou commented 4 months ago

I have an entity project with a OneToMany relation to entity Media. I use easy admin, version 4.8 . I installed everything (stimulus, webpack encore desactivating assets mapper) like indicated in you documentation. My composer.json is like this (require part) :

"require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.2",
        "arkounay/ux-collection": "^4.0",
        "doctrine/doctrine-bundle": "^2.11",
        "doctrine/doctrine-migrations-bundle": "^3.3",
        "doctrine/orm": "^2.17",
        "easycorp/easyadmin-bundle": "^4.8",
        "friendsofsymfony/ckeditor-bundle": "^2.4",
        "gedmo/doctrine-extensions": "^3.14",
        "liip/imagine-bundle": "^2.12",
        "nelmio/api-doc-bundle": "^4.19",
        "nelmio/cors-bundle": "^2.4",
        "phpdocumentor/reflection-docblock": "^5.3",
        "phpstan/phpdoc-parser": "^1.25",
        "stof/doctrine-extensions-bundle": "^1.11",
        "symfony/asset": "6.4.*",
        "symfony/console": "6.4.*",
        "symfony/doctrine-messenger": "6.4.*",
        "symfony/dotenv": "6.4.*",
        "symfony/expression-language": "6.4.*",
        "symfony/flex": "^2",
        "symfony/form": "6.4.*",
        "symfony/framework-bundle": "6.4.*",
        "symfony/http-client": "6.4.*",
        "symfony/intl": "6.4.*",
        "symfony/mailer": "6.4.*",
        "symfony/mime": "6.4.*",
        "symfony/monolog-bundle": "^3.0",
        "symfony/notifier": "6.4.*",
        "symfony/process": "6.4.*",
        "symfony/property-access": "6.4.*",
        "symfony/property-info": "6.4.*",
        "symfony/runtime": "6.4.*",
        "symfony/security-bundle": "6.4.*",
        "symfony/serializer": "6.4.*",
        "symfony/stimulus-bundle": "^2.17",
        "symfony/string": "6.4.*",
        "symfony/translation": "6.4.*",
        "symfony/twig-bundle": "6.4.*",
        "symfony/ux-turbo": "^2.17",
        "symfony/validator": "6.4.*",
        "symfony/web-link": "6.4.*",
        "symfony/webpack-encore-bundle": "^2.1",
        "symfony/yaml": "6.4.*",
        "symfonycasts/verify-email-bundle": "^1.16",
        "twig/extra-bundle": "^2.12|^3.0",
        "twig/twig": "^2.12|^3.0",
        "vich/uploader-bundle": "^2.3",
        "webonyx/graphql-php": "^15.9"
    },

My assets/controllers/media_collection_controller.js is the same as indicated in your documentation. My src/Controller/Admin/ProjectCrudController lokks like this (configureFields part) :

return [
            TextField::new('title'),
            TextEditorField::new('description'),
            CollectionField::new('medias')
                ->useEntryCrudForm(null, "option B")
                ->setFormTypeOption('by_reference', false)
                ->hideOnIndex()
                ->setFormType(UxCollectionType::class, [
                    'entry_type' => VichFileType::class,
                    'allow_add' => true,
                    'allow_delete' => false,
                    'allow_drag_and_drop' => false,
                    'drag_and_drop_filter' => 'input,textarea,a,button,label',
                    'display_sort_buttons' => false,
                    'add_label' => 'Add an item',
                    'min' => 3,
                    'max' => 10,
                    'attr' => ['data-controller' => 'media-collection']
                ])
                ,
        ];

The UX interface works. It mean I can move medias up and down. My problem is that it seems that the media_collection_controller,js file isn't loaded. In fact, when I move a media up for example, I see in the console : "arkounay--ux-collection--collection #moveUp" but I should see "The custom collection changed". So I can't make any ajax call so that I could save the position of my media entities. It would be great if someone could help me to understand why my custom controller is not taken into account. Thank you.

Arkounay commented 4 months ago

Hello, I've tried it on symfony 6.4 and easyadmin 4.8 with the code you provided.

The thing that links the collection to custom hooks is the data-controller, which seems to not be taken into account with your example because the setFormType function doesn't take a second argument, instead you should just use

CollectionField::new('medias')
   // ...
    ->setFormType(UxCollectionType::class)
    ->setFormTypeOptions([
        'by_reference' => false,
        'attr' => ['data-controller' => 'media-collection']
        // other options you need like allow_add etc will go here
    ])

Does that fix it?

fecou commented 4 months ago

Thank you so much for your quick answer. Yes, it works ! When I have the code, I will post the javascript with the ajax request, maybe it helps...

fecou commented 4 months ago

@Arkounay Is there a way to retrieve the ids (in the new order) of the items in the collection so that I can pass them as arguments in the ajax request in my javascript file ? I don't see this possibility... Thanks again !

Arkounay commented 4 months ago

@Arkounay Is there a way to retrieve the ids (in the new order) of the items in the collection so that I can pass them as arguments in the ajax request in my javascript file ? I don't see this possibility... Thanks again !

Np, yes, if you really want to do it like that one way would be first to add their ids in your form theme. If you don't have a custom form theme add one in your DashboardController for example:

public function configureCrud(): Crud
{
    return Crud::new()
        ->addFormTheme('@ArkounayUxCollection/ux_collection_form_theme.html.twig')
        ->addFormTheme('your_custom_form_theme.html.twig')
    ;
}

Then in your formTypeOptions from your configureFields function set a block_prefix for the entries for example

CollectionField::new('medias')
    ->setFormTypeOption('by_reference', false)
    ->hideOnIndex()
    ->setFormTypeOptions([
        'by_reference' => false,
        'attr' => ['data-controller' => 'media-collection'],
        'entry_options' => [
            'block_prefix' => 'media_entry',
        ]
    ])
    ->setFormType(UxCollectionType::class)

and add the entries' id as as a data attribute in your form row, in your form_theme file, using the block_prefix you specified appended with _row:

{% block media_entry_row %}
    {{ form_row(form, {row_attr: {'data-id': form.vars.value.id|default}}) }}
{% endblock %}

And so in the js you would just retrieve the ordered ids like that:

onChange() {
    const ids = [...this.querySelectorAll('[data-arkounay--ux-collection--collection-target="collectionElement"]')].map(e => e.dataset.id);
}

Now I don't know your project and why you want to do it in ajax etc but there are caveats - if you needs the ids it won't work with newly created items since they don't have ids etc. One simpler way would be to just submit the whole form in ajax. This bundle is more oriented for default form behavior (ie with a submit/save button) and to add / remove items dynamically. Here the sorting would work by default. If you just need ajax sorting maybe simply doing it manually with SortableJS would be easier?

fecou commented 4 months ago

Thank you so much. It seems perfect. Maybe I misunderstood how to use your bundle. I use it the same way as I would use SortableJS. I have to save the order of my medias. Of course if I just save, the order is not maintained.
The entity media (belonging to project) has a field named position. I just want to assign a position to the media according to the order we choose by moving the items. As I use easy admin, it's not easy to manage what and how to treat the submitted form . That's because I am thinking of saving the order with an ajax method on change.

Other option would be, with the template, to have a hidden position input that I could update with javascript when order changes (when we move the items up or down). That could avoid the problem with newly created media (you're right !). Is there an easy way to update the value of the position in the onchange method of the javascript controller ?

Arkounay commented 4 months ago

Other option would be, with the template, to have a hidden position input that I could update with javascript when order changes (when we move the items up or down). That could avoid the problem with newly created media (you're right !). Is there an easy way to update the value of the position in the onchange method of the javascript controller ?

Yea, you can use the position_selector form type option and it does exactly that, no need to do anything else. e.g if you set an entry_type that has an HiddenType position, if your position input has a class "position" you can set the UxCollection's option position_selector to .position and it'll do that automatically. And in your entity sort your collection by position with the orderBy doctrine attribute

fecou commented 4 months ago

Hello. It works very well. Thanks a lot again. It was not clear for me that your bundle aims to work the way you described here. I will publish some parts of my code, it may help others. So, in src/Controller/Admin/MediaCrudController, I have :

public function configureFields 
  ...
  HiddenField::new('position')->addCssClass('position')

in src/Controller/Admin/ProjectCrudController, I have :

public function configureCrud(Crud $crud): Crud {
  return $crud->addFormTheme('@ArkounayUxCollection/ux_collection_form_theme.html.twig');
}
public function configureFields(string $pageName): iterable {
  return [
      CollectionField::new('medias')
                ->setFormType(UxCollectionType::class)
                ->setFormTypeOptions([
                    'by_reference' => false,
                    // 'attr' => ['data-controller' => 'media-collection'], // not needed as I don't use ajax call
                    'entry_type' => VichFileType::class,
                    'allow_add' => true,
                    'allow_delete' => true,
                    'allow_drag_and_drop' => false,
                    'display_sort_buttons' => true,
                    'add_label' => 'Add an item',
                    'min' => 0,
                    'max' => 10,
                    'position_selector' => '.position input'
                ]) 

And in src/Entity/Media, because position is not nullable but I must set a value if it's null (appears when I add an item without reordering after) :

public function setPosition(?int $position): void
    {
        if($position === null) $position = 10;
        $this->position = $position;
    }

Thats all. I hope it helps.