EasyAdminBundle icon indicating copy to clipboard operation
EasyAdminBundle copied to clipboard

Creating new entities through AssociationField

Open Schyzophrenic opened this issue 4 years ago • 16 comments

Hello, Inside my admin interface, I have a use case that goes beyond just standard Crud operations and I am puzzled to understand what is the best approach to this. If I simplify the problem, it could be the difference between replacing an Association Field which let you choose which entity you wish to link to versus creating a new target entity (and replacing the previous one).

An example would be: A is linked to B, I open the edit form of A and can see the association from A to B. I want to link it to C. If C exists, it's very straightforward as I just pick C in the dropdown.

My issue is that sometimes C needs to be created and I can't do this in the form. What would be the best way to do this?

I have tried playing with custom action, but it seems I am loosing too much of the existing logic from EA... Thank you for the direction

Schyzophrenic avatar Aug 01 '20 13:08 Schyzophrenic

You are looking for the CollectionField rather than the Association Field. This allows you to do what you are looking for e.g. creating children of A -> B and potentially A > B > C.

micotodev avatar Aug 01 '20 15:08 micotodev

So this is really about adding a new entity through an Association Field as we are talking about a OneToMany association rather than a ManyToOne for which the Collection field would be used, no? I only have 2 levels. I start with A > B and want to end with A>C where C could be newly created.

So far, I am thinking I need to play with the Form event to allow one or another but I am honestly a bit lost on how to do that through EA.

I hope it clarifies. Thanks again for the help!

Schyzophrenic avatar Aug 01 '20 15:08 Schyzophrenic

I worked on something like this recently, but it's not polished enough to contribute yet - I think I'll get back to it sometime in the next weeks, but can make no promises 🙈 Using a CollectionField is indeed not the greatest experience, because you would need to duplicate all the fields in a custom FormType for that, which I didn't want to do (my implementation just re-uses the Crud Create Form defined by configureFields on the target entity).

If you need this sooner just mention me here and I will share the code, but you'll probably need to adapt it a bit to your use-case.

lukasluecke avatar Aug 03 '20 09:08 lukasluecke

Thanks @lukasluecke . I think anything at this point would be helpful as I am trying to get something 'not to hacky'. I think I'd be able to do this in a standard form, but I am not yet used enough to EA to know how to properly integrate this. Ideally, I was thinking about a Add button (or 2 submit buttons allowing to identify what we want to do) that would show the form from the New Crud (but I guess I don't mind duplicating the form) and use the property_path with some events to either set the association or create a new linked entity before setting the association (I hope it's clear).

The other option I was thinking about was to hack a [+] button showing an overlay for creating a new entity but I am not a big fan of this solution as it forces 2 step when updating which could lead to orphaned entities.

Because I need this, I'd be happy to contribute (but not sure I am skilled enough haha)

Actually, the popup idea is described well in #2164 and #2654.

Schyzophrenic avatar Aug 03 '20 09:08 Schyzophrenic

Hello, @Schyzophrenic, I have been working on the plan B you described above for a week now, as I desperately need to be able to create records on the fly. So far, I have managed to achieve this with JavaScript only. Just tagging my fields as AssociationField and adding the appropriate CrudController (which is done by the configurator anyway) and everything works fine. That is including: multiple selections in the autocomplete, saving records from both the main form and any potential CollectionField rows.

Demo: demo-01

The general idea behind it is:

  1. It finds all Select2 widgets. It also listens for events when a new Collection row is added
  2. If those have an "autocomplete" URL that is derived from the AssociationField::OPTION_CRUD_CONTROLLER setting
  3. Use the "new" action from the same controller to fetch the record creation form. Show that in a popup
  4. Upon submission the script waits for a redirect to edit that record (saveAndContinue action)
  5. It extracts the Entity Id from the above URL and uses it to search the record through the CRUD Controller
    • This is done to get the __toString() representation of the Entity, which may be composed of a few fields
  6. Adds the ID and Text representation to the Autocomplete and selects it

Indeed you can encounter orphaned records, but in my case this is an acceptable compromise. I am by far not an expert in JavaScript, but this has already been tested a few times and a few bugs were fixed.

Known issues:

  • It's not fully compliant with error handling, etc. i.e. the form might get stuck in case of submit errors
  • The forms shown in the popup should be simple - CollectionFields and Autocompletes do not work properly (JS?)
  • There is no backend validation Out of the box - only the html5 one

Code: You can put this in your CRUD or Dashboard controller.

public function configureAssets(): Assets
    {
        return parent::configureAssets()
            ->addJsFile('admin/override.js')
        ;
    }

override.js is attached as a txt -> override.txt

I know there are a few things that can be improved in the code, but I need to receive some more feedback from the people using it. Also, doing several AJAX requests for something so simple is by far not the optimal solution, but for now it works for me. In case @javiereguiluz likes the idea for this enhancement and if there is more demand for such a feature I could definitely submit a PR.

imadzharov88 avatar Aug 04 '20 21:08 imadzharov88

I think solving this problem is essential. I have an optional one-to-one relationship and I want to be able to create just one entity and populate it as a sub form.

ChangePlaces avatar Sep 10 '20 21:09 ChangePlaces

Btw, the above snippet is being actively used. The error handling and backend validation are working now, so the only thing you can't do is to use complex JS into that form, which is kind of fine for my case, or probably most cases. After all it's not that user friendly to add a dynamic form into another dynamic form and forget which entity were you editing at the end.

I believe the main reason not to consider a PR with this is because it's a complex implementation, something that the owner has discouraged. But in case anyone is interested in the latest implementation, do reach out to me and I can share what I have.

imadzharov88 avatar Sep 16 '20 04:09 imadzharov88

@lukasluecke or @javiereguiluz what do you think about adding this? I think having limitations is ok, but this is a quite common need and this implementation would help cover this functional gap.

Schyzophrenic avatar Sep 16 '20 05:09 Schyzophrenic

@lukasluecke Hey Lukas, did you got any results with your idea of form reusing, can you share a kind of example?

TracKer avatar Oct 03 '20 10:10 TracKer

I also have an optional one-to-one relationship that needs the child to be created. Currently, it will need to be a two step process where the child needs to be created first. It would be nice to be able to do it in one step.

apropos avatar Mar 13 '21 08:03 apropos

Let me share share with you my hacky solution (which is probably very stupid). The idea is simple: Use A CollectionFiled but restrict it to a single Item...

So I have a optional OneToOne relation that I want to be able to add via inline form. So the Association Field won't work for that (right now?!) Lets just use a CollectionField and hide the add button after one inline form (item) is present. And also make it visible again if the item gets removed...

Lets assume the OneToOne Field of my Entity is called "content" an I want to render it "collection style", so I use the CollectionField, then I would need to provide some fake getters/setters to pretend the field would be an array as well as dynamic add/remove buttons that 'validate my constraint' (max:1 Item). The entity would look like that:

<?php

use Doctrine\ORM\Mapping as ORM;

/**
 * Class Entity.
 *
 * @ORM\Entity()
 */
class Entity
{
    /**
     * @ORM\OneToOne(
     *     targetEntity="Content", cascade="persist"
     * )
     * @ORM\JoinColumn(
     *     name="content_id",
     *     referencedColumnName="id",
     *     nullable=true
     * )
     */
    private $content;

    /**
     * the normal getter.
     */
    public function getContent(): ?Content
    {
        return $this->content;
    }

    /**
     * the normal setter.
     *
     * @return Entity
     */
    public function setContent(?Content $content): self
    {
        $this->content = $content;

        return $this;
    }

    /**
     * hacky getter for OneToOne Collection Field support.
     *
     * @return array|Content[]|null[]|null
     */
    public function getContents(): ?array
    {
        return $this->content ? [$this->content] : [];
    }

    /**
     * hacky setter for OneToOne Collection Field support.
     *
     * @return Entity
     */
    public function setContents(?array $contents): self
    {
        $this->content = array_shift($contents);

        return $this;
    }
}

after that I add my Collection Field and add a custom class "one-to-one-collection" to work with within my custom js script (I use "contents" here as propertyName, not "content", so my hacky methods will be used internally when mapping the data):

EntityCrudController.php

CollectionField::new('contents', 'Inhalt')
                ->setEntryType(ContentType::class)
                ->setFormTypeOption('allow_delete', true)
                ->setFormTypeOption('allow_add', true)
                ->addCssClass('one-to-one-collection')

And here is my script that will dynamically hide/show the add button:

oneToOneCollectionFieldHack.js

import $ from 'jquery'

$(document).ready(
    function () {
        // The Collection that should act as one to one collection should have the following class
        let oneToOneCollectionFields = $(".one-to-one-collection").each(
            function () {
                let oneToOneCollectionField = $(this);
                // reference to the add button (last to prevent removing the add button from nested collections)
                 let addButton = oneToOneCollectionField.find(".field-collection-add-button").last();
                // reference to the remove button ( if already existing ) (first to prevent accidentally using the remove button from nested collections)
                let removeButton = oneToOneCollectionField.find(".field-collection-delete-button").first();

                // if the collection initially has an item we hide the add button
                if (+oneToOneCollectionField.data('numItems') >= 1) {
                    addButton.hide();
                }

                // if the add button gets clicked, we deactivate it and trigger a change on the collection element
                // so we can add the listener for the remove button
                addButton.click(function () {
                    addButton.hide();
                    oneToOneCollectionField.trigger("change");
                });

                // add a listener to the remove button, so we can activate the add button again
                // this only works if the collection has this button initially
                removeButton.click(function () {
                    addButton.show();
                });

                // after adding a new item to the collection we trigger the change event and do the remove listener again
                oneToOneCollectionField.change(function () {
                    removeButton = oneToOneCollectionField.find(".field-collection-delete-button").first();
                    removeButton.click(function () {
                        addButton.show();
                    });
                })
            }
        );
    }
);

Then include the script somehow... eg:

    public function configureAssets(Assets $assets): Assets
    {
        return parent::configureAssets($assets)
            ->addJsFile('build/oneToOneCollectionFieldHack.js');
    }

this should work for multiple OneToOne Collection Fields in the same form, and also with nested CollectionTypes within the OneToOne Collection EntityType fingerscrossed

PopePAF avatar May 25 '21 13:05 PopePAF

@imadzharov88 Hey Ilian, I'm very interested in trying your snippet, could you please share the latest version ?

gregoire-jianda avatar Sep 27 '21 10:09 gregoire-jianda

Faced the very similar problem. I have general relation between Order and Product entities (One-To-Many). In my case I need to create Product(s) when creating Order no matter if product(s) already exist in db. Also while editing Order I don't need to reassociate Product entities but edit existing products via form. So I need add/edit form for Order and Products to be rendered on the same page for creating and editing Order. Does anybody know how this can be solved?

furang avatar Oct 14 '21 10:10 furang

Having the same issue, to be able to create a one-to-one relationship on the fly (without it existing before), I'm having to use collections with a one-to-many relationship instead and allow only one count just to have the form be automatically created correctly

akalineskou avatar Sep 13 '22 11:09 akalineskou

it is not work for edit mode

vaigtech avatar Mar 08 '24 07:03 vaigtech

Anybody found a decent solution for this? I think this is probably one of the most essential features to focus on. Unless somebody has figured out a not-too-hacky workaround in the meantime?

Meuss avatar Mar 20 '24 12:03 Meuss