If I have a multifield with an image field and a term reference, and have two MFs, save the node, then edit again, then remove the 2nd MF, then 'add another item' I get the item I just removed added back.

See attached GIF.

Comments

Elijah Lynn created an issue. See original summary.

elijah lynn’s picture

Issue summary: View changes
elijah lynn’s picture

So, I've been working on looking at all the linked issues I just added and starting to refactor those updates into Multifield. Not trivial and there are many patches stacked on top of each other in the current state of Field Collections but I do think that those issues hold the answer to this.

elijah lynn’s picture

Just throwing notes in here:

The 'Added a remove button to field submit widget' was added with http://cgit.drupalcode.org/multifield/commit/?id=15683eb1b68f75a37df4a2e... on 2013-07-10 22:57:55 which consists of a copied field_collection_remove_submit() and field_collection_remove_js() but it was not precisely a copy and appears to have been modified a little bit when comparing against the field_collection code base around that time.

elijah lynn’s picture

Issue summary: View changes
StatusFileSize
new50.13 KB

I did a git history for selection in PhpStorm on just the functions field_collection_remove_js() & field_collection_remove_submit() and these are the changes between the time these functions were copied from FC, 2013-07-10 and now:

  1. https://www.drupal.org/node/2416237 - 2016.07.28 - Deleting field collection items from node, then adding more creates multiple revisions of same field collection entity on save
  2. https://www.drupal.org/node/2456307 - 2016.05.02 - Item Remove Button confuses #parents and #array_parents
  3. https://www.drupal.org/node/1675522 - 2015.10.19 - Remove button broken if form is rebuilt (via ajax or failed validation)
  4. https://www.drupal.org/node/2242751 - 2014.07.03 - Ajax form page callback require updates for Drupal 7.27 (SECURITY) list() = drupal_get_form()/Implemented #2242663 for SA-CORE-2014-002 (Ajax form page callback security fix for anonymous users).
  5. https://www.drupal.org/node/2123559 - 2014.03.18 - - Removing field collection item also removes sibling's nested child content/ Fixed form_state on javascript remove button.

elijah lynn’s picture

So, a while ago I did reapply all of these patches but that did not resolve this issue.

Field Collection does still work fine, these seems like an off by 1 issue.

elijah lynn’s picture

Just so I don't lose the work and maybe it will be useful, here are the patches in #7 backported to Multifield. https://github.com/ElijahLynn/drupal-multifield/commits/7.x-1.x

I don't remember why #2416237: Deleting field collection items from node, then adding more creates multiple revisions of same field collection entity on save isn't there.

update: Oh yeah, because it is trying to get $field_state['entity'] and that appears to be something Field Collection puts there. I don't think that patch is valid for multifield but maybe the concept is in a different way.

+++ b/field_collection.module
@@ -1087,9 +1087,14 @@ function field_collection_remove_submit($form, &$form_state) {
+  // Use the actual array of field collection items as the upper limit of this
+  // for loop rather than 'item_count'. This is because it will be creating extra
+  // dummy items here and the two measures go out of sync after the fist delete.
+  $field_collection_item_count = count($field_state['entity']) - 1;
+
   // Go ahead and renumber everything from our delta to the last
   // item down one. This will overwrite the item being removed.
-  for ($i = $delta; $i <= $field_state['items_count']; $i++) {
+  for ($i = $delta; $i <= $field_collection_item_count; $i++) {
     $old_element_address = array_merge($address, array($i + 1));
     $new_element_address = array_merge($address, array($i));
elijah lynn’s picture

Status: Active » Needs review
Parent issue: » #1234574: Add a delete button on the field-collection item edit form
StatusFileSize
new4.42 KB

Okay, so I deep dove into this and I have a working patch.

I tried not to but ended up doing mostly what Field Collection is doing. FC is managing the field state with $form_state['field']['entity'] where they update it on the remove_submit() handler and rebuild the form based off those values. I couldn't find any other way.

This patch introduces $form_state['field']['state'] (I didn't want to call it entity). It is initialized in multifield_field_widget_form() and then updates the state in multifield_field_widget_item_remove_submit().

The remove (delete) button functionality was originally introduced in this patch in this issue #1234574: Add a delete button on the field-collection item edit form which is also the source of field_collection_remove_submit() that we copied into multifield_field_widget_item_remove_submit(). The issue came across the same issue this issue is about (https://www.drupal.org/node/1234574#comment-5205538). I think we didn't copy the parts that said 'entity' because it made sense we didn't have to deal with that. But although it was for entities, the field state saving part is what really mattered. In my patch I don't have entities but enough information to be useful within multifield_field_widget_form().

bleen’s picture

  1. +++ b/multifield.field.inc
    @@ -763,8 +778,23 @@ function multifield_field_widget_remove_item_submit($form, &$form_state) {
    +    // We use $field_state['state'] so that we can manage removed fields with
    +    // the 'remove' button.
    +    // @see multifield_widget_form()
    +    if (isset($field_state['state'][$i + 1])) {
    +      $field_state['state'][$i] = $field_state['state'][$i + 1];
    +    }
    

    It looks like this logic will only work when you remove the (n-1)th item. Does this still work if you have 10 items and you remove number 3?

  2. +++ b/multifield.field.inc
    @@ -763,8 +778,23 @@ function multifield_field_widget_remove_item_submit($form, &$form_state) {
    +  // This is so "add another item" doesn't ressurects deleted fields.
    

    2 nits: There is still room in teh 80 char limit for "This is so" to be on the previous line AND ressurects should be ressurect

  3. +++ b/multifield.field.inc
    @@ -780,10 +810,10 @@ function multifield_field_widget_remove_item_submit($form, &$form_state) {
    +  // Sort by weight because ...
    ...
    +  // Reweight everything in the correct order so that ...
    

    Did you mean to finish these comments?

elijah lynn’s picture

Thanks for the review.

1. Yeah, it would delete the 3rd item. The logic took me a bit to figure out. $i is actually set from a $delta earlier on. When that submit handler is called it is passed the delta of the item being removed. So the for loop starts with $i being the item being removed. Here is the full context:

 // Go ahead and renumber everything from our delta to the last
  // item down one. This will overwrite the item being removed.
  for ($i = $delta; $i <= $field_state['items_count']; $i++) {
    $old_element_address = array_merge($address, array($i + 1));
    $new_element_address = array_merge($address, array($i));

    $moving_element = drupal_array_get_nested_value($form, $old_element_address);
    $moving_element_value = drupal_array_get_nested_value($form_state['values'], $old_element_address);
    $moving_element_input = drupal_array_get_nested_value($form_state['input'], $old_element_address);

    // Tell the element where it's being moved to.
    $moving_element['#parents'] = $new_element_address;

    // Move the element around.
    form_set_value($moving_element, $moving_element_value, $form_state);
    drupal_array_set_nested_value($form_state['input'], $moving_element['#parents'], $moving_element_input);

    // We use $field_state['state'] so that we can manage removed fields with
    // the 'remove' button.
    // @see multifield_widget_form()
    if (isset($field_state['state'][$i + 1])) {
      $field_state['state'][$i] = $field_state['state'][$i + 1];
    }
    else {
      unset($field_state['state'][$i]);
    }
  }

2. Yeah, sometimes I leave comments some breathing room. The standards say cannot be more than 80 characters but don't say that I must use all 80 characters when possible ;).

3. Yeah, those comments should actually be todos. But it was super late and I definitely just left them in there. If we re-roll this patch it would be nice to turn them into real "why" comments. There is a lot of "why" missing from this code.

zekvyrin’s picture

Great job Elijah!
Yes, patch #10 works as described.

I tried to fix that about a week ago (based on your notes), but didn't have much success (or time to follow your analysis exactly to figure this out), so this suits me perfectly just in time. Thanks :)

Can't wait to get this committed :)

greendemiurge’s picture

Looks like I'm a bit late to the party on this, but I'll put in my two cents since my solution resolves this with only a few lines of code and may prove helpful:

My diagnosis of the root cause here is that the current code does not manage to overwrite the #default_value keys in the form array when there are no values in the $items array. My approach was to use multifield_field_widget_remove_item_submit() to leave a note in formstate that a multifield needed to take its values from $form_state['input'] rather than $items when the form was rebuilt. This allows the existing code to properly blank and/or remove elements from the form array when needed. I'm attaching my personal patch for reference. Hope it is useful to the discussion!

Thanks everyone for looking into this!

laryn’s picture

Status: Needs review » Reviewed & tested by the community

We came across this on a Backdrop site using multifield and the last patch here was successful. It's been merged into the Backdrop version and will be included in the next release there. Marking RTBC here as well.

https://github.com/backdrop-contrib/multifield/commit/dfbef95c3d23d92dbc...