Skip to content

Conversation

@marcw
Copy link

@marcw marcw commented Jul 22, 2011

Based on PR #1724

book/forms.rst Outdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not always true (i.e. when some items have been deleted)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I change "number of items" by "number of rows" ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so.

Let's say you have a collection (1, 2) and you delete the item #2. The next item you add should be #3 -> (1, 3).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the phrase to "you might have have to change this to be coherent with your row ordering."

@weaverryan
Copy link
Member

Hey Marc!

I'm really glad that someone has taken this on - with a coherent example, this is a really fantastic feature (and the collection type is totally undocumented). I still need to take a look at this, but I do know that I think we should move this into the collection form type reference (http://symfony.com/doc/current/reference/forms/types/collection.html). And actually, that page needs everything, so if there are any details you can fill in there (I haven't really used the collection type yet), that would be a huge help!

Thanks!

@j
Copy link

j commented Aug 2, 2011

What about documentation on collections and prototype themeing. If I understood it more, I'd make a cookbook article. Or if I figure it out today, I'll go about it. I just think prototypes should mimic the collection widget entirely (which I'm sure it does, but I'm not sure.. I haven't looked into it)

ie. adding remove buttons and add buttons to the template instead of being forced to use javascript after dom load.

@j
Copy link

j commented Aug 2, 2011

Okay, I go this far:

{% block collection_widget %} {% spaceless %} {% if prototype is defined %} {% set attr = attr|merge({'data-prototype': block('collection_item') }) %} {% endif %} <div {{ block('widget_container_attributes') }}> {{ form_errors(form) }} {% for child in form %} <li> {% for row in child %} <div> {{ form_label(row) }} {{ form_widget(row) }} </div> {% endfor %} <div class="remove"> <a onclick="removeRow()" title="Remove" href="javascript:void()">Remove</a> </div> </li> {% endfor %} {{ form_rest(form) }} </div> <div class="add"> <a onclick="addRow()" title="Add" href="javascript:void()">Add</a> </div> {% endspaceless %} {% endblock collection_widget %} {% block collection_item_widget %} {% spaceless %} <li> {% for row in prototype %} <div> {{ form_label(row) }} {{ form_widget(row) }} </div> {% endfor %} <div class="remove"> <a onclick="removeRow()" title="Remove" href="javascript:void()">Remove</a> </div> </li> {% endspaceless %} {% endblock collection_item_widget %} 

And I just realized that the form_row(prototype) doesn't mimic the collection at all. Is there a better way to do this?

@stof
Copy link
Member

stof commented Aug 2, 2011

well, you are rendering the children by hand. You need to do the same for the prototype if you want to apply the same rendering

@weaverryan
Copy link
Member

@jstout24 Yes, we need to document the collection type in general, which of course means showing a really good example of the prototype in action, with adds and deletes. I haven't had time to try all of this yet, so I'm hoping someone else will. Obviously, @marcw has a really good start here.

Goals:

  • explain the type in its simplest form: e.g. holding a collection of strings (via "text" type)
  • explain "allow_delete": simple Javascript to remove item, on save it's gone from the collection
  • explain "allow_add" and prototype: stay simple where each field is just a text field, but Javascript can use the prototype to add new items.

Maybe we're not so far off from this - @marcw may already be really close, but it wasn't clear enough for me to understand (for example, I didn't understand the problem with "row ordering").

Thanks!

@marcw
Copy link
Author

marcw commented Aug 5, 2011

I don't have much time to work on it these days but will try to provide a cookbook entry for this one.

@brikou
Copy link
Contributor

brikou commented Aug 5, 2011

It could really be great to implement some kind of contact editor with nested collection too ass seen here http://knockoutjs.com/examples/contactsEditor.html

@tristanbes
Copy link
Contributor

+1 on this. We definitly must have a cookbook to know how to handle this correctly.

@leevigraham
Copy link
Contributor

I've been playing with collections and the prototype attribute (so far so good). I'd be willing to write some documentation once I wrap my head around it some more.

I've successfully added to the collection now I'm tackling deleting rows. As far as I can see there are two options:

  1. Remove the row from the DOM and then unset each saved row in the controller:
    • Remove the row from the DOM and submit the form
    • Before processing the collection get the parents children and cache the ID's in a variable
    • Loop over the submitted collection rows and unset their ID from the previous cache
    • Loop over the remaining cached elements and either destroy them
  2. Set a deleted flag on the row
    • Update a hidden 'deleted' input and hide the element in the DOM
    • Submit the form
    • Loop over all the rows and destroy the relationship and delete the flagged rows from the DB
    • Persist the remaining rows

What would be the preferred approach?

Update:

After thinking about this a bit more I think a deleted flag would be a better idea. From a front end point of view you could reduce the opacity of the row on delete and then commit the change in the controller. This would also add the possibility of an undo state on the front end.

@weaverryan
Copy link
Member

Hey @leevigraham!

I could be wrong since I haven't really dove into this stuff, but I believe that the server-side of the "remove" problem is handled automatically, once you have the allow_delete option on inside the collection type. So, I think by removing the row from the DOM, everything else will just fall into place.

And if I'm wrong, someone will certainly correct me :). Btw, I would very much appreciate it if you would take a shot at this - it's not so hard, just a matter of getting it going and improving it until everyone is happy.

Cheers!

@marcw
Copy link
Author

marcw commented Aug 11, 2011

Ryan, as the collection type is broken with some Doctrine relations, you might need to do some more work to handle the deletion.

@jessegreathouse
Copy link

I have been working on this. The javascript i implemented is working perfectly. I had to write a controller helper method to successfully delete collection members and ignore empty collection members.

/** * Removes dynamically submitted objects that shouldn't be persisted. * @param Symfony\Component\Form\Form $form * @param obj $entity * @param Symfony\Component\HttpFoundation\Request $request * @param Array $collections */ public function sanitizeCollections(Form &$form, &$entity, Request &$request, Array $collections) { $formName = $form->getName(); $submission = $request->request->get($formName); foreach ($collections as $collection => $fields) { $total = count($submission[$collection]); for ($i = 0; $i <= $total; $i++) { foreach ($fields as $field) { if (empty($submission[$collection][$i][$field])) { unset($submission[$collection][$i]); break; } } if (empty($submission[$collection])) { $entity->set{ucwords($collection)} = new ArrayCollection(); } } } $request->request->set($formName, $submission); $form->setData($entity); } public function createAction() { $entity = new Job(); $request = $this->getRequest(); $form = $this->createForm(new JobType(), $entity)); if ('POST' === $request->getMethod()) { $this->sanitizeCollections($form, $entity, $request, array( 'params' => array('key','value'), 'args' => array('value'), 'tags' => array('value'), )); $form->bindRequest($request); if ($form->isValid()) { $entity->setCreateDate(new \DateTime); $em = $this->getDoctrine()->getEntityManager(); $em->persist($entity); $em->flush(); return $this->redirect($this->generateUrl('jobqueue_job_show', array('id' => $entity->getId()))); } } return $this->render('NineThousandJobqueueBundle:Job:new.html.twig', array( 'entity' => $entity, 'form' => $form->createView(), 'action' => $this->generateUrl('jobqueue_job_create'), )); } 

This works great when there's only one member of each collection submitted. The problem is when I submit multiples to any collection I get this in validation:

This form should not contain extra fields This form should not contain extra fields This form should not contain extra fields 
@jessegreathouse
Copy link

re: my above post: adding allow_add was the missing piece that I didnt have set in my formtype. once I set the allow_add value it added the extra fields perfectly. This makes total sense its just not well documented.

$builder->add('params', 'collection', array( 'type' => new ParamType(), 'allow_add' => true, 'allow_delete' => true, )) ->add('args', 'collection', array( 'type' => new ArgType(), 'allow_add' => true, 'allow_delete' => true, )) ->add('tags', 'collection', array( 'type' => new TagType(), 'allow_add' => true, 'allow_delete' => true, )) 
@leevigraham
Copy link
Contributor

@jessegreathouse adding orphanRemoval=true to your entity annotations will automatically delete orphaned entities.

@jessegreathouse
Copy link

tnx leevigraham !

@leevigraham
Copy link
Contributor

I'm still planning on contributing to this doc when I ge time but in the mean time here's the JS I'm using to add / delete collection forms:

$("div[data-prototype]").each(function(index) { var $self = $(this), count = $self.children().length, prototype = $self.data('prototype'); $delete_trigger = $("<a />") .attr({ href: '#' }) .text('Delete element') .addClass('delete') .click(function(event) { $(this).parent().remove(); return false; }); $self.find("> div").append($delete_trigger.clone(true)); $add_trigger = $("<a />") .attr({ href: '#' }) .addClass('add') .text('Add element') .appendTo(this) .click(function(event) { $d = $delete_trigger.clone(true); $p = $(prototype.replace(/\$\$name\$\$/g, count++)).append($d); $(this).before($p); return false; }); }); 
@chrisben
Copy link

chrisben commented Sep 7, 2011

Having a collection documentation would be a huge relief..

The above js from leevigraham works for normal cases, though in the case collections of collections the second 'prototype' data is not expanded and the add/remove buttons are not added.

This function should be called recursively after $(this).before($p); .

That's mostly what I do in my code, though I'm now stuck with a new problem as the replace() function will replace the second '$$name$$' as well. For instance:

div="timetable_trips_$$name$$_stoptimes_$$name$$"

(Timetable contains a collection of 'Trips' that contain a collection of 'stopTimes')

I guess I will need to have some kind of parsing of the prototype data instead of a simple replace.
Or perhaps having something different than $$name$$ for all collections, for instance $$name_1$$, $$name_2$$ with the number being the depth, it would be easier to do the javascript bit. Though I have no idea how to customise this string in Symfony... any idea?

Another question: why isn't the data-prototype attribute value html encoded? is that safe to have unencoded stuff there? It contains quotes, I guess some browsers might not parse this correctly.. To be on the safe side I always encode it using the '|e' twig function like this:

{% block collection_widget %} {% spaceless %} <div class="collection"> {% if prototype is defined %} {% set attr = attr|merge({'data-prototype': form_widget(prototype)|e }) %} {% endif %} </div> {% endspaceless %} {% endblock collection_widget %}
@plesiecki
Copy link

it is possible to affect the prototype 'shape' ?

@weaverryan
Copy link
Member

Hey guys!

I've taken this as a start and added some decent documentation for all of this at sha: 6ec3854

If anyone sees any issues, let's start adding to the reference doc and also #797 where there is a new cookbook entry (with two sections that are still missing).

Thanks!

@weaverryan weaverryan closed this Nov 7, 2011
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet