├── README.md
├── doc
├── cover.png
├── howTo.md
└── tryIt.md
├── example
├── public
│ └── js
│ │ ├── textCollection.js
│ │ └── textListCollection.js
└── views
│ ├── complex.html
│ └── simple.html
└── src
└── collection.js
/README.md:
--------------------------------------------------------------------------------
1 | # Javascript to handle Symfony form collections
2 |
3 | 
4 |
5 | ## Goal
6 |
7 | The aim of [collection.js](src/collection.js) is to provide the
8 | minimum javascript code to handle the
9 | [form collection](http://symfony.com/doc/current/reference/forms/types/collection.html)
10 | of symfony. I strongly advise you to do it yourself at least once by
11 | following the
12 | [cookbook](http://symfony.com/doc/current/cookbook/form/form_collections.html).
13 |
14 | This work relies on [jQuery](https://jquery.com/).
15 |
16 | ## Presentation
17 |
18 | This repository only aims at presenting
19 | [collection.js](src/collection.js). There are two examples.
20 |
21 | [simple.html](example/view/simple.html) is an example of were you
22 | should use [collection.js](src/collection.js). With no extra code you
23 | get a working form collection based on your symfony template.
24 |
25 | [complex.html](example/view/complex.html) is not necessarily a case
26 | were you should use [collection.js](src/collection.js). As there are
27 | lot of specific behaviors, there is no real need for an external
28 | part. However it exists to present how you can add your specific
29 | behaviors on top of [collection.js](src/collection.js).
30 |
31 | ## Try it
32 |
33 | To try it on your computer please read [this](doc/tryIt.md).
34 | For more information please read the [doc](doc/howTo.md).
35 |
--------------------------------------------------------------------------------
/doc/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanlucc/sf-form-collection-js/6fdc76f5625f2e656c65beccb5a97d36d625e02f/doc/cover.png
--------------------------------------------------------------------------------
/doc/howTo.md:
--------------------------------------------------------------------------------
1 | # How it works
2 |
3 | I'll assume you are familiar with form collection.
4 |
5 | You will need:
6 | - a container: top html block with all elements which interact with the collection;
7 | - a collection holder: direct parent of the items of your collection;
8 | - a prototype: necessary html to create a new item;
9 | - a placeholder: pattern in the prototype to replace with a unique id at item creation;
10 | - a button to add an item;
11 | - a button to delete an item.
12 |
13 | ## Simple case
14 |
15 | I will explain the details of how
16 | [simple.html](../example/view/simple.html) works.
17 | - The container has the class `collection_container`.
18 | `collection.js` automatically initiate a `CollectionManager` on element with this class.
19 | - The collection holder has the class `collection_holder`.
20 | - The prototype is stored in `data-prototype` of the collection holder
21 | (default in symfony).
22 | - The placeholder is `__name__` (default in symfony).
23 | - The delete buttons have the class `delete_item` and the add button
24 | has the class `add_item`.
25 | - The items have the class `item`.
26 |
27 | The index used to replace the placeholder is intern to the
28 | `CollectionManager` and initialized with the children count of the
29 | holder. By default new elements are appended to the collection
30 | holder.
31 |
32 | I guess that's enough for default cases.
33 |
34 | ## Basic customization
35 |
36 | For some reason you might want to change some parameters. In order to
37 | do so, you have two choices. Either create the `CollectionManager`
38 | manually and provide an option object as argument. or use data on
39 | your container.
40 |
41 | ### Use data on the container
42 |
43 | ```
44 |
45 | ...
46 |
47 | ```
48 |
49 | ### Create CollectionManager manually
50 |
51 | Assuming `$collectionContainer` is the jQuery object associated to
52 | your container:
53 |
54 | ```
55 | $collectionContainer.manageCollection({'option-name': 'option-value'});
56 | ```
57 |
58 | If you use both customizations, the data customization will be
59 | overridden by the one of the creation.
60 |
61 | ## Basic options
62 |
63 | - `insert-method`: can be `append` (default), `prepend`, `before` or `after`.
64 | [Append](http://api.jquery.com/append/) and
65 | [preprend](http://api.jquery.com/prepend/) uses jQuery to add the new
66 | item in the collection holder. [Before](http://api.jquery.com/before/)
67 | and [after](http://api.jquery.com/after/) uses jQuery to add the new
68 | item next to the chose item. In order to use one of those two method,
69 | each item must contain its own add button.
70 | - `prototype-name`: default `prototype`.
71 | The name of the data attached to the collection holder which contains the prototype.
72 | - `index-placeholder`: default `__name__`.
73 | The placeholder of the item index in the prototype. It must not
74 | contain regexp special characters.
75 | - `item-closest-selector`: default `.item`.
76 | The selector used to find the item from the click event target of
77 | delete. The same selector is used to add an item with the `before` or
78 | `after` insert method.
79 | - `collection-holder-selector`: default `.collection_holder`.
80 | The selector used to find the collection holder in the container.
81 | - `add-button-selector`: default `.add_item`.
82 | The selector used to find the add button(s) in the container.
83 | - `delete-button-selector`: default `.delete_item`.
84 | The selector used to find the delete buttons in the container.
85 | - `name`: aims to give an easy way to customize almost all other options.
86 |
87 | `name` effect:
88 |
89 | | option | `name` undefined | `name` = 'tag' |
90 | |------------------------------|----------------------|--------------------------|
91 | | `prototype-name` | `prototype` | `tag-prototype` |
92 | | `index-placeholder` | `__name__` | `__tag_name__` |
93 | | `item-closest-selector` | `.item` | `.tag_item` |
94 | | `collection-holder-selector` | `.collection_holder` | `.tag_collection_holder` |
95 | | `add-button-selector` | `.add_item` | `.add_tag_item` |
96 | | `delete-button-selector` | `.delete_item` | `.delete_tag_item` |
97 |
98 | If you provide a `name` and an option changed by `name`, the provided
99 | option override the option modified by `name`.
100 |
101 | All `selector` options can be any
102 | [selector](https://api.jquery.com/category/selectors/) valid in
103 | jQuery.
104 |
105 | ## Access the CollectionManager
106 |
107 | When you manage a collection manually, the created `CollectionManager`
108 | is returned (in an array) and you can access it.
109 |
110 | ```
111 | var collectionManager = $collectionContainer.manageCollection()[0];
112 | ```
113 |
114 | Get the manager allows you to use its methods.
115 | - `getHolder`: returns the collection holder.
116 |
117 | var $collectionHolder = $(collectionManager.getHolder());
118 | - `append`: append a new item to the holder. You can optionnaly provide the jQuery object you want to append.
119 |
120 | collectionManager.append();
121 | or
122 |
123 | var $item = $('create your item');
124 | collectionManager.append($item);
125 | - `prepend`: same as append but use prepend.
126 | - `before`: add an item before the provided element. You can optionnaly provide the jQuery object you want to append.
127 |
128 | var $elementBeforeWhichItemWillBeAdded = $('create here');
129 | collectionManager.before($elementBeforeWhichItemWillBeAdded)
130 | or
131 |
132 | var $elementBeforeWhichItemWillBeAdded = $('create here');
133 | var $item = $('create your item');
134 | collectionManager.before($elementBeforeWhichItemWillBeAdded, $item);
135 | - `after`: same as before but use after.
136 | - `deleteItem`: delete the provided item.
137 |
138 | var $itemToDelete = $collectionHolder.find('item to delete');
139 | collectionManager.deleteItem($itemToDelete);
140 | - `addItem`: add an item. This method takes three optional arguments.
141 | For each argument, if you provide `null`, the default option will be used.
142 | - The method can be any value of the `insert-method` option, it is used to override the option chose at `CollectionManager` creation.
143 | - The item to add.
144 | - The reference item used only if method is `before` or `after`.
145 |
146 | Examples:
147 | - Add an item with the default method (`append` or `prepend`)
148 |
149 | collectionManager.addItem();
150 | - Add the given item using the default method (`append` or `preprend`)
151 |
152 | var $item = $('create your item');
153 | collectionManager.addItem(null, $item);
154 | - Add an item relatively to another using default method (`before` or `after`)
155 |
156 | var $referenceItem = $('create here');
157 | collectionManager.addItem(null, null, $referenceItem);
158 | - Add the given item relatively to another using default method (`before` or `after`)
159 |
160 | var $item = $('create your item');
161 | var $referenceItem = $('create here');
162 | collectionManager.addItem(null, $item, $referenceItem);
163 | - Append an item
164 |
165 | collectionManager.addItem('append');
166 | - Put an item after another
167 |
168 | var $referenceItem = $('create here');
169 | collectionManager.addItem('after', null, $referenceItem);
170 |
171 | The difference between addItem and the four specialized method
172 | (`append`, `preprend`, `before`, `after`) is that `addItem` like
173 | `deleteItem` fire events and the other four do not. If you do not wish
174 | to fire events on delete, you just need to use
175 | [remove](https://api.jquery.com/remove/) yourself.
176 |
177 | ## Use events
178 |
179 | You must wonder how you can add your custom javascript behaviors such as:
180 | - keep at least one (empty) item in the collection;
181 | - add javascript behavior to the created elements
182 | (e.g. a datepicker on an input, or create a nested collection);
183 | - disable add if there are "invalid" items;
184 | - duplicate an item instead of creating a new one;
185 | - store deleted elements somewhere else in the DOM.
186 |
187 | You will have to use the events.
188 |
189 | ### Customize event names
190 |
191 | - `before-add-event-name`: default `before-add`, the name of the event triggered on
192 | container before an item is added;
193 | - `after-add-event-name`: default `after-add`, the name of the event triggered on
194 | container after an item is added;
195 | - `before-delete-event-name`: default `before-delete`, the name of the event triggered on
196 | container before an item is deleted;
197 | - `after-delete-event-name`: default `after-delete`, the name of the event triggered on
198 | container after an item is deleted.
199 |
200 |
201 | The events name are also affected by the `name` option. Alternatively
202 | you can use `event-name` which overrides `name` and affect the event
203 | names in the same way.
204 |
205 | `name` effect:
206 |
207 | | option | `name` and `event-name` undefined | `name` or `event-name` = 'tag' |
208 | |----------------------------|-----------------------------------|--------------------------------|
209 | | `before-add-event-name` | `before-add` | `tag.before-add` |
210 | | `after-add-event-name` | `after-add` | `tag.after-add` |
211 | | `before-delete-event-name` | `before-delete` | `tag.before-delete` |
212 | | `after-delete-event-name` | `after-delete` | `tag.after-delete` |
213 |
214 | ### Register to an event
215 |
216 | ```
217 | $collectionContainer.on('event-name', function(event) {
218 | ...
219 | });
220 | ```
221 |
222 |
223 | ### Before add
224 |
225 | If you do not want to add an item, use `preventDefault` on the event:
226 |
227 | ```
228 | beforeAddEvent.preventDefault();
229 | ```
230 |
231 | The event has some arguments that you can use to customize the behavior.
232 | - `itemToAdd`: the item that will be added;
233 | - `method`: the method that will be used to add the item;
234 | - `referenceItem`: the reference item or undefined if the clicked
235 | button is not inside an item.
236 |
237 | ```
238 | var $icon = $('my beautifull icon');
239 | var $item = $(beforeAddEvent.itemToAdd);
240 | $item.append($icon);
241 | beforeAddEvent.itemToAdd = $item;
242 |
243 | beforeAddEvent.method = 'after';
244 |
245 | beforeAddEvent.referenceItem = $(beforeAddEvent.referenceItem).siblings().first();
246 | ```
247 |
248 | ### After add
249 |
250 | You have access to the added item in `addedItem`.
251 |
252 | ```
253 | var $icon = $('my beautifull icon');
254 | $(afterAddEvent.addedItem).prepend($icon);
255 | ```
256 |
257 | ### Before delete
258 |
259 | If you do not want to remove an item, use `preventDefault` on the event:
260 |
261 | ```
262 | beforeDeleteEvent.preventDefault();
263 | ```
264 |
265 | The event has the item that will be deleted in `itemToDelete` and you
266 | can modify it.
267 |
268 | ```
269 | beforeDeleteEvent.itemToDelete = $(beforeDeleteEvent.itemToDelete).next();
270 | ```
271 |
272 | ### After delete
273 |
274 | You have access to the deleted item in `deletedItem`.
275 |
276 | ```
277 | $('body').append(afterDeleteEvent.deletedItem);
278 | ```
279 |
280 | ### Common argument
281 |
282 | If you do not call `addItem` or `deleteItem` yourself, you also have access to the
283 | original click event in `clickEvent` in the four events.
284 |
--------------------------------------------------------------------------------
/doc/tryIt.md:
--------------------------------------------------------------------------------
1 | # Launch the examples
2 |
3 | Get the repository `git clone git@github.com:jeanlucc/sf-form-collection-js.git`.
4 |
5 | `cd collection/example/views` and open the `simple.html` and
6 | `complex.html` files in your browser.
7 |
8 | Alternatively you can enter the url directly in your browser:
9 |
10 | ```
11 | file:///path/to/sf-form-collection-js/example/views/simple.html
12 | file:///path/to/sf-form-collection-js/example/views/complex.html
13 | ```
14 |
--------------------------------------------------------------------------------
/example/public/js/textCollection.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | var TEXT = window.TEXT || {};
3 | window.TEXT = TEXT;
4 |
5 | TEXT.TextCollection = function (collectionContainer) {
6 | this.$collectionContainer = $(collectionContainer);
7 |
8 | this.init();
9 | }
10 |
11 | TEXT.TextCollection.prototype = {
12 | init: function (collectionContainer, options) {
13 | this.collectionManager = this.$collectionContainer.manageCollection({
14 | 'name': 'text',
15 | 'index-placeholder': '__name__',
16 | 'insert-method': 'before',
17 | })[0];
18 |
19 | this.$collectionContainer.on('text.before-delete', function(event) {
20 | this.beforeDelete(event);
21 | }.bind(this));
22 |
23 | this.$collectionContainer.on('text.before-add', function(event) {
24 | this.beforeAdd(event);
25 | }.bind(this));
26 |
27 | if (this.collectionManager.getHolder().children().length === 0) {
28 | this.collectionManager.addItem('append');
29 | }
30 | },
31 |
32 | beforeAdd: function(event) {
33 | var validItemCount = 0;
34 | this.collectionManager.getHolder().children().each(function(index, item) {
35 | if (this.isItemValid(item)) {
36 | ++validItemCount;
37 | }
38 | }.bind(this));
39 |
40 | if (validItemCount !== this.collectionManager.getHolder().children().length) {
41 | event.preventDefault();
42 | }
43 |
44 | var $itemToAdd = $(event.itemToAdd);
45 | $itemToAdd.find('input').val('before');
46 |
47 | event.itemToAdd = $itemToAdd;
48 | },
49 |
50 | beforeDelete: function(event) {
51 | if (this.collectionManager.getHolder().children().length === 1) {
52 | event.preventDefault();
53 | $input = $(event.itemToDelete.find('input'));
54 | if ($input.val() !== '') {
55 | $input.val('');
56 | }
57 | }
58 | },
59 |
60 | isItemValid: function(item) {
61 | return $(item).find('input').val() !== '';
62 | },
63 | };
64 | });
65 |
--------------------------------------------------------------------------------
/example/public/js/textListCollection.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | var TEXT = window.TEXT || {};
3 | window.TEXT = TEXT;
4 |
5 | TEXT.TextListCollection = function (collectionContainer) {
6 | this.$collectionContainer = $(collectionContainer);
7 | this.$navTabContainer = this.$collectionContainer.find('.nav-tabs');
8 | this.$tabContentContainer = this.$collectionContainer.find('.tab-content');
9 |
10 | this.init();
11 | }
12 |
13 | TEXT.TextListCollection.prototype = {
14 | init: function (collectionContainer, options) {
15 | this.collectionManager = this.$collectionContainer.manageCollection({
16 | 'name': 'textlist',
17 | 'index-placeholder': '__parent_name__',
18 | 'item-closest-selector': 'li',
19 | })[0];
20 |
21 | this.$collectionContainer.on('textlist.after-add', function(event) {
22 | this.afterAdd(event);
23 | }.bind(this));
24 |
25 | this.$collectionContainer.on('textlist.before-delete', function(event) {
26 | this.beforeDelete(event);
27 | }.bind(this));
28 |
29 | this.$collectionContainer.on('textlist.after-delete', function(event) {
30 | this.afterDelete(event);
31 | }.bind(this));
32 |
33 | this.$navTabContainer.on('click', 'a', function(event) {
34 | event.preventDefault();
35 | $(event.target).tab('show');
36 | });
37 |
38 | if (this.collectionManager.getHolder().children().length === 0) {
39 | this.collectionManager.addItem();
40 | }
41 | },
42 |
43 | afterAdd: function(event) {
44 | var $addedItem = $(event.addedItem);
45 | var $navTab = $addedItem.first();
46 | var $textCollectionContainer = $addedItem.last();
47 |
48 | $navTab.remove();
49 | this.$navTabContainer.append($navTab);
50 |
51 | $textCollectionContainer.data('text-collection-manager', new TEXT.TextCollection($textCollectionContainer));
52 |
53 | $navTab.find('a').trigger('click');
54 | },
55 |
56 | beforeDelete: function(event) {
57 | if (this.$navTabContainer.children().length === 1) {
58 | event.preventDefault();
59 | return;
60 | }
61 |
62 | if (!confirm('Do you really want to delete this list?')) {
63 | event.preventDefault();
64 | return;
65 | }
66 |
67 | var $navTab = $(event.itemToDelete);
68 | var $textCollectionContainer = this.$tabContentContainer.find($navTab.find('a').attr('href'));
69 |
70 | textCollectionManager = $textCollectionContainer.data('text-collection-manager').collectionManager;
71 | textCollectionManager.getHolder().children().each(function(index, item) {
72 | textCollectionManager.deleteItem($(item));
73 | });
74 |
75 | $textCollectionContainer.remove();
76 | },
77 |
78 | afterDelete: function(event) {
79 | this.$navTabContainer.children().first().find('a').trigger('click');
80 | },
81 | };
82 |
83 | var $collectionContainer = $('.textlist_collection_container');
84 | if ($collectionContainer.length > 0) {
85 | new TEXT.TextListCollection($collectionContainer);
86 | }
87 | });
88 |
--------------------------------------------------------------------------------
/example/views/complex.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test collection
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |