Skip to content

Conversation

@dunglas
Copy link
Member

@dunglas dunglas commented Jul 18, 2022

Q A
Bug fix? no
New feature? yes
Tickets Fix #3, alternative to #90 and #397
License MIT

This alternative to #90 is adding support for dynamic collection in forms (adding and removing elements).

symfony-collection.mov

The two main benefits of this PR are:

  1. This pure JS implementation doesn't require custom form types. It is plug & play: you just have to install the bundle, it natively works with the default CollectionType. It works with the default theme, the Tailwind theme, and the Bootstrap theme. I haven't tested with the Foundation theme but it should be compatible too.
  2. The code natively handles any level of nested collections, which, as you can see in the code, is quite tricky.

The Stimulus controller provides several data attributes that can be used to customize the buttons when pointing to a <template> tag. If this patch is accepted, I'll also provide some patches to the Form component to automatically leverage this feature to not generate the add and remove buttons when not needed according to the form options. We could also provide better default buttons directly in form themes for Tailwind, Bootstrap, and Foundation.

Usage:

<div data-controller="symfony--ux-collection--collection"> {{ form(form) }} </div>

TODO:

  • Write more tests
  • Write docs
@alexander-schranz
Copy link
Contributor

I would not go with a plug & play integration. That will just make exist projects which already using an own data-prototype on other cases crash. I would go with an explicit ux collection type which also can be extended with other feature in the future. E.g. Copy & Paste, Sortable, ...

If you want to have a look at it I currently already implemented a Form Type here #397.

@dunglas
Copy link
Member Author

dunglas commented Jul 19, 2022

Why would it crash? The code I wrote should be robust enough to not crash and is already configurable: you can use data attributes to customize the CSS selectors to use and the buttons to add. My implementation should work easily with any form theme, including custom ones. Also, AFAIK data-prototype is an attribute very specific to Symfony. The core framework should have used a prefix, for sure, but I'm not aware of any other popular project using this convention. We could make the attribute to look for configurable, but I'm not sure it is worth it.

It's also totally possible to extend it to add extra features such as sorting.

Dynamic collection is a feature that is already supported by the core framework. IMO Symfony should be as consistent and as easy to use as possible.
Currently, this core feature is very hard to use because the JS included in the documentation as well as the existing NPM packages we recommend have bugs.

Creating this kind of form is currently very hard, even for senior developers, and it should not. We also must not require to change the PHP code to use Symfony UX. Has much as possible, Symfony UX should provide JS code compatible with the features and especially the HTML generated by the core framework.

@dunglas dunglas mentioned this pull request Jul 19, 2022
8 tasks
@alexander-schranz
Copy link
Contributor

I understand your target of the pull request. But handling all [data-prototype] as a collection I would really avoid. I have in my current projects other form types working with prototypes which are not a collection.

If you want togo with a Plug & Play the easiet way would be go with a FormExtension adding the data-controller or if you want to keep your implementation go with data-collection-target="collection" to all CollectionType. Current implementation with parent.querySelector('[data-prototype]') hurts the usage of a component library like stimulus where selectors should always be limited scope like you did for the item selector.

But then we are in the second case not all CollectionType should be handled by this component. There are cases where I want to create or even have already created a custom stimulus component for a CollectionType. The current implementation would hurt this component as it tries to start on this collection a component which is already handled by itself. And would not allow me using this collection side by side with my other collection components.

I did go with #397 to make the things extendable, even have the possibility iwth FormExtension to add new buttons for adding "Copy & Paste" functionality. Have the collection itself as controller and as own Type allows us to add also seperate controller for "sortable" to make our entries sortable in the future.

We also must not require to change the PHP code to use Symfony UX. Has much as possible, Symfony UX should provide JS code compatible with the features and especially the HTML generated by the core framework.

I disagree here that this is a good idea. As in some cases I want to use a Dropzone and in some oter cases I want to have FileType same is for the Collection. Forcing a specific JS component for a specific existing form type I would avoid.

@dunglas
Copy link
Member Author

dunglas commented Jul 19, 2022

But handling all [data-prototype] as a collection

This isn't what the code does, it only targets the children of the HTML element using the controller (and it's not mandatory to use it if you want a custom behavior for a given form): https://github.com/symfony/ux/pull/398/files#diff-0c84303846c77e8d3377b23960aed4a1dfc3db0bb6206c7435ef6fe890a7fa1bR11

That being said, I totally agree that this controller must be compatible other UX components such as Dropzone etc, I'll update the code to support this!

I disagree here that this is a good idea. As in some cases I want to use a Dropzone and in some oter cases I want to have FileType same is for the Collection.

I think we haven't the same use cases in mind. I crafted this patch because a junior developper from our company had to create a "simple" multi-level nested form... He read the official documentation entry, didn't managed to get his code working... and asked for help. I took a look and I figured out that it was almost impossible to use this feature. My main goal here is to have something that just works for simple cases (prototypes, MVP etc), that can be easily documented, and that doesn't require any code change.

If we include a feature in core such as dynamic collection, it must not be half-finished, we most provide everything to get it working and it's currently not the case. This patch fixes this bad situation (it's a common use case, and probably hurts many newcomers).

Also, in term of DX, a user should be able to have a working dynamic form without having to write custom PHP or Twig code.

Another option would be to deprecate all the data-prototype things currently generated by core and recommend using a new UX form type instead, but IMHO (as this patch proves) it's not needed. With this approach, we'll be able to progressively improve the HTML generated by core (switching to <template> instead of data-prototype using a config flag, adding useful new ids, classes and attributes that will help writing custom JS etc).

Forcing a specific JS component for a specific existing form type I would avoid.

You're not forced to that. You can use this controller on a per-form basis, use the existing (unfortunately buggy) jQuery and pure-JS packages or write your own JS. I really don't understand what's the issue here.

I did go with #397 to make the things extendable

I'm pretty sure that we can merge both approaches and have something both plug & play and fully customizable. Would you be OK to try to merge both patches (cc @stakovicz)?

@javiereguiluz
Copy link
Member

If you can, could you please some screenshots and/or short GIF showing this feature in action for complex forms? Thanks!

Copy link
Member

@stof stof left a comment

Choose a reason for hiding this comment

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

Shouldn't we also ship the dist/controller.d.ts file alongside the compiled JS code ?

// If no template is provided, create a raw HTML button
const button = document.createElement('button') as HTMLButtonElement;
button.type = 'button';
button.textContent = buttonType === ButtonType.Add ? 'Add' : 'Delete';
Copy link
Member

Choose a reason for hiding this comment

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

this should support a way to customize the labels. The common use case is to translate them.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is supported using custom templates. Isn't it enough?

const collectionNamePattern = collectionEl.id.replace(/_/g, '(?:_|\\[|]\\[)');

const prototype = (collectionEl.dataset.prototype as string) // We're sure that dataset.prototype exists, because of the CSS selector used in connect()
.replace('__name__label__', currentIndex)
Copy link
Member

Choose a reason for hiding this comment

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

The __name__ part is configuration in CollectionType (which is necessary to support nested collection types, as you should then use different prototype names for them))

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually my code supports nested collections even when not using another placeholder, but I'll add support for this option!

Copy link
Member

Choose a reason for hiding this comment

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

No it does not. The prototype of the outer collection actually contains the attribute storing the prototype of the inner collection (as it contains the whole inner collection). If you use the same placeholder for both, the replacement done here will replace both placeholders inside the inner prototype, breaking the inner collection.

Copy link
Member

Choose a reason for hiding this comment

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

Try doing a nested collection where you add multiple items in the inner collection of an item added in the outer collection, and then look at the submitted data in the Request (you need to inspect the request itself, not the data decoded in $_POST as duplicate keys would be lost during the conversion to a PHP array)

Copy link
Member Author

Choose a reason for hiding this comment

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

the replacement done here will replace both placeholders inside the inner prototype

I didn't handle the label yet, but for __name__, the regex only replaces the first occurrence, which should be enough to fix this issue. I'll double-check.

Copy link
Member

Choose a reason for hiding this comment

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

replacing only the first occurrence is also broken. __name__ will appear multiple time in the prototype when the entry type is a compound type (as the name of each child form will have the placeholder in its name.

Copy link
Member Author

Choose a reason for hiding this comment

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

The first occurence in every string: https://github.com/symfony/ux/pull/398/files#diff-0c84303846c77e8d3377b23960aed4a1dfc3db0bb6206c7435ef6fe890a7fa1bR49

Unless I'm missing something, this works with the current example I provided for test purpose.

Copy link
Member

Choose a reason for hiding this comment

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

@dunglas AFAICT, this would replace both placecholders in name="root[outer_collection][__name__][inner_collection][__name][field_1]"
And trying to do clever tricks based on the quote delimiting attributes (to replace the first occurrence in each attribute) is a bad idea: in the prototype attribute of the nested collection, those would be HTML entities and not actual quotes.

Note that you need to add multiple items in each collection to notice the issue (if you only add one outer item with a single inner item, you won't notice whether the [0] come from the replacements done by the outer or inner item)

};

/* eslint-disable no-undef */
describe('CollectionController', () => {
Copy link
Member

Choose a reason for hiding this comment

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

is there any way to make the tests cover the interactions (add and delete) ? I'm fearing that they might be broken right now due to adding the listeners on the DocumentFragment.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I planned to add a Panther test.

Copy link
Contributor

@alexander-schranz alexander-schranz Jul 20, 2022

Choose a reason for hiding this comment

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

I don't think we need panther here if we use the testing library. I currently could not get your branch running. But something into this direction should work to test add and remove:

/*  * This file is part of the Symfony package.  *  * (c) Fabien Potencier <fabien@symfony.com>  *  * For the full copyright and license information, please view the LICENSE  * file that was distributed with this source code.  */ 'use strict'; import { Application } from '@hotwired/stimulus'; import {fireEvent, getByTestId, getByText, waitFor} from '@testing-library/dom'; import { clearDOM, mountDOM } from '@symfony/stimulus-testing'; import CollectionController from '../src/controller'; const startStimulus = () => { const application = Application.start(); application.register('symfony--ux-collection--collection', CollectionController); }; const emptyCollection = '<div data-testid="collection" data-controller="symfony--ux-collection--collection"></div>'; const simpleCollection = '' + ' <template id="addButton">' + ' <button type="button" class="add-button">Add button</button>' + ' </template>' + '' + ' <template id="deleteButton">' + ' <button type="button" class="delete-button">Delete button</button>' + ' </template>' + '' + '<form name="game" method="post" data-controller="symfony--ux-collection--collection" data-add-button="addButton" data-delete-button="deleteButton">' + ' <div id="game">' + ' <div>' + ' <label class="required">Teams</label>' + ' <div id="game_teams" data-prototype="&lt;div class=&quot;team-entry&quot;&gt;&lt;label class=&quot;required&quot;&gt;__name__label__&lt;/label&gt;&lt;div id=&quot;game_teams___name__&quot;&gt;&lt;div&gt;&lt;label for=&quot;game_teams___name___name&quot; class=&quot;required&quot;&gt;Name&lt;/label&gt;&lt;input type=&quot;text&quot; id=&quot;game_teams___name___name&quot; name=&quot;game[teams][__name__][name]&quot; required=&quot;required&quot; data-controller=&quot;test&quot; /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;">' + ' </div>' + ' </div>' + ' <div>' + ' <button type="submit" id="game_submit" name="game[submit]">Submit</button>' + ' </div>' + '</form>' const nestedCollection = '' + ' <template id="addButton">' + ' <button type="button" class="add-button">Add button</button>' + ' </template>' + '' + ' <template id="deleteButton">' + ' <button type="button" class="delete-button">Delete button</button>' + ' </template>' + '' + '<form name="game" method="post" data-controller="symfony--ux-collection--collection" data-add-button="addButton" data-delete-button="deleteButton">' + ' <div id="game">' + ' <div>' + ' <label class="required">Teams</label>' + ' <div id="game_teams" data-prototype="&lt;div&gt;&lt;label class=&quot;required&quot;&gt;__name__label__&lt;/label&gt;&lt;div id=&quot;game_teams___name__&quot;&gt;&lt;div&gt;&lt;label for=&quot;game_teams___name___name&quot; class=&quot;required&quot;&gt;Name&lt;/label&gt;&lt;input type=&quot;text&quot; id=&quot;game_teams___name___name&quot; name=&quot;game[teams][__name__][name]&quot; required=&quot;required&quot; data-controller=&quot;test&quot; /&gt;&lt;/div&gt;&lt;div&gt;&lt;label class=&quot;required&quot;&gt;Players&lt;/label&gt;&lt;div id=&quot;game_teams___name___players&quot; data-prototype=&quot;&amp;lt;div&amp;gt;&amp;lt;label class=&amp;quot;required&amp;quot;&amp;gt;__name__label__&amp;lt;/label&amp;gt;&amp;lt;div id=&amp;quot;game_teams___name___players___name__&amp;quot;&amp;gt;&amp;lt;div&amp;gt;&amp;lt;label for=&amp;quot;game_teams___name___players___name___firstName&amp;quot; class=&amp;quot;required&amp;quot;&amp;gt;First name&amp;lt;/label&amp;gt;&amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;game_teams___name___players___name___firstName&amp;quot; name=&amp;quot;game[teams][__name__][players][__name__][firstName]&amp;quot; required=&amp;quot;required&amp;quot; /&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;div&amp;gt;&amp;lt;label for=&amp;quot;game_teams___name___players___name___lastName&amp;quot; class=&amp;quot;required&amp;quot;&amp;gt;Last name&amp;lt;/label&amp;gt;&amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;game_teams___name___players___name___lastName&amp;quot; name=&amp;quot;game[teams][__name__][players][__name__][lastName]&amp;quot; required=&amp;quot;required&amp;quot; /&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;">' + ' </div>' + ' </div>' + ' <div>' + ' <button type="submit" id="game_submit" name="game[submit]">Submit</button>' + ' </div>' + '</form>' /* eslint-disable no-undef */ describe('CollectionController', () => { startStimulus(); afterEach(() => { clearDOM(); }); it('connects', async () => { const container = mountDOM(emptyCollection); // smoke test expect(getByTestId(container, 'collection')).toHaveAttribute('data-controller', 'symfony--ux-collection--collection'); }); it('add a new collection', async () => { const container = mountDOM(simpleCollection); await waitFor(() => getByText(container, 'Add button')); fireEvent( getByText(container, 'Add button'), new MouseEvent('click', { bubbles: true, cancelable: true, }), ) expect(container.querySelectorAll('.team-entry').length).toBe(1); }); it('remove a collection', async () => { const container = mountDOM(simpleCollection); await waitFor(() => getByText(container, 'Add button')); fireEvent( getByText(container, 'Add button'), new MouseEvent('click', { bubbles: true, cancelable: true, }), ) expect(container.querySelectorAll('.team-entry').length).toBe(1); await waitFor(() => getByText(container, 'Delete button')); fireEvent( getByText(container, 'Delete button'), new MouseEvent('click', { bubbles: true, cancelable: true, }), ) expect(container.querySelectorAll('.team-entry').length).toBe(0); }); });
Copy link
Contributor

Choose a reason for hiding this comment

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

@dunglas could get it running and updated the code snippet. Currently only test for simple add and remove and no tests yet for the nested collection but already created the required html for it.

Copy link
Member Author

Choose a reason for hiding this comment

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

IMHO it's better to create an E2E test here because we want to be sure that the HTML generated by the form component is still compatible with the JS code.

Copy link
Member

Choose a reason for hiding this comment

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

The Panther tests would still be worthy as they would ensure that the submitted data is the one expected by the backend code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Make sense specially @stof usecase to make sure that the submitted data is the one we are expecting. Still I think jest tests should also be added. I moved it to a PR:

dunglas#5

$this->teams[] = $team;
$this->teams = array_values($this->teams);

dump($this->teams);
Copy link
Member

Choose a reason for hiding this comment

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

should probably be removed

disableDeleteButton?: string;
}

enum ButtonType {
Copy link
Member

Choose a reason for hiding this comment

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

This should be a const enum instead, to inline the add and remove string in the emitted JS code instead of emitting an enum object.

Copy link
Member Author

Choose a reason for hiding this comment

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

I changed the code and using const enum isn't possible anymore (IIRC, const enum causes various issues anyway).

Copy link
Member

Choose a reason for hiding this comment

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

Another alternative is to use a tagged union 'add' | 'remove', which will then produce more efficient JS code.

@alexander-schranz
Copy link
Contributor

alexander-schranz commented Jul 19, 2022

I'm pretty sure that we can merge both approaches and have something both plug & play and fully customizable. Would you be OK to try to merge both patches.

@dunglas Let me have a look at it. I will try to provide a prototype this week.

@mano-lis
Copy link

In my use case (a two levels nested form), the code written in the docs can't help. That solution works just fine.

@dunglas
Copy link
Member Author

dunglas commented Jul 19, 2022

Shouldn't we also ship the dist/controller.d.ts file alongside the compiled JS code ?

Declarations are entirely disabled for the whole project. I don't know why (cc @tgalopin) but if changed, this probably should be done globally in another PR.

@stof
Copy link
Member

stof commented Jul 19, 2022

Declarations are entirely disabled for the whole project. I don't know why (cc @tgalopin) but if changed, this probably should be done globally in another PR.

I was not aware of that. But this is indeed a separate discussion then.

@alexander-schranz
Copy link
Contributor

I did on top of the different discussion (#90, #397, #398) create a form type extension based implementation here: #400. There are still things todo but maybe we can discuss in which direction a Collection UX component should go.

For easier testing I also added some kind of form theme to the example:

Bildschirmfoto 2022-07-19 um 23 31 51

@javiereguiluz
Copy link
Member

@alexander-schranz thanks for sharing some screenshots.

Sorry for being blunt, but the autoincremental numeric labels (0, 1, etc.) always looked ugly to me and it's one of the things I hoped this new component was going to fix 🙏

@alexander-schranz
Copy link
Contributor

alexander-schranz commented Jul 20, 2022

@javiereguiluz i normally disabled them via:

 $builder->add('slots', CollectionType::class, [ 'entry_options' => [ 'label' => false, ], ]);

but didn't want to change the default behaviour. But a form extension could do that automatically if the label is not explicit set.

@stof
Copy link
Member

stof commented Jul 20, 2022

@javiereguiluz if you want something more human, this generally involves configuring the label in the options. I don't see how the JS code could automatically generate good-looking labels.

@dunglas
Copy link
Member Author

dunglas commented Jul 20, 2022

I added an example confirming that nested dynamic controllers are well supported, e.g if you use Dropzone in a form type that is added dynamically when clicking on the "add" button, the Stimulus correctly detects the added element and connects the controller automatically.

@dunglas
Copy link
Member Author

dunglas commented Jul 20, 2022

@javiereguiluz here is a recording:

symfony-collection.mov
}

connectCollection(parent: HTMLElement) {
parent.querySelectorAll('[data-prototype]').forEach((el) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I still want to add here a comment that this the most questionable line in my point of view.

In #400 I implemented the JS the way I think it should be implemented that every collection itself has its collection_controller and not that one controller controls all collections.

So we even don't need to wrap the whole form with a data-controller.

Why I would avoid the data-prototype selector here is that for example I have inside my current forms other Form Types using [data-prototye] which are even inside collection types, this component currently would identify them also as "collection" which they are not they just use the data-prototype for different usecases.

Via a form extension like in #400 we can add a better way by adding data-controller to all collection which would solve this problem and even allows to create collection type with a custom stimulus controller like in #90 or #397 also allows extending the current implementaton via events.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll make this selector configurable and split this controller in two (the main one in case you want all collections to be automatically handled, and a nested one). WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

It still is hard to find here a correct selector as the main problems is that the component is registered on the form tag and handles multiple instances of collections.

As I did use component libraries over the last years which work very simular to stimulus I would really not recommend creating components like this which handles multiple instances. Instead I would make sure to have a collection_controller which handles the a single collection, which should make also the code simpler.

What problem do you see that the JS component handles just a single collection and adding a data-controller via form extension to the collection row like in #400.

Comment on lines 93 to 109
addAddButton(collectionEl: HTMLElement) {
const addButton = this.createButton(collectionEl, ButtonType.Add);
addButton.onclick = (e) => {
e.preventDefault();
this.addItem(collectionEl);
};
collectionEl.appendChild(addButton);
}

addDeleteButton(collectionEl: HTMLElement, itemEl: HTMLElement) {
const deleteButton = this.createButton(collectionEl, ButtonType.Delete);
deleteButton.onclick = (e) => {
e.preventDefault();
itemEl.remove();
};
itemEl.appendChild(deleteButton);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

In #90 where was discussed that it would be nice that the buttons are rendered by the symfony form theme. That make it special for design systems easier so the can directly use there already designed button_widget and bootstrap and other themes already ships with a already good looking button. currently buttons are here if not explicit a template is defined rendered via this js component. in #400 I used the twig extension to add the buttons already to the twig and to the prototype, which did make also the JS a lot smaller which I think is great from maintainance point of view.

Copy link
Member Author

@dunglas dunglas Jul 20, 2022

Choose a reason for hiding this comment

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

My long-term goal is to totally remove data-prototype from core, generate <template> elements instead (which are better suited for this use case), and provide default buttons directly in the core form themes (as <template> elements too). However, during the deprecation process, we'll need to support both this new approach and data-prototype (for the "plug & play" DX) until Symfony 7.

I'll open the PR on core in the next few days.

Copy link
Contributor

@alexander-schranz alexander-schranz Jul 20, 2022

Choose a reason for hiding this comment

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

Agree still that template are better suited for the data-prototype. Still I recommed to have the buttons rendered already in twig because adding them via JS will trigger else a Layout shift and google and lighthouse hate layout shifts.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, that's the plan (and will be the default in Symfony 7!)

@@ -0,0 +1,19 @@
Copyright (c) 2021 Kévin Dunglas
Copy link
Contributor

Choose a reason for hiding this comment

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

2022? 😌

@vvasiloi
Copy link

I'm not sure if it helps settle the debate around the data-prototype attribute but Sylius is using the same attribute to render a different prototype depending on a select element's value.
Source JS: https://github.com/Sylius/Sylius/blob/204c60bc7faf9caff2378d4204680129a4379740/src/Sylius/Bundle/UiBundle/Resources/private/js/sylius-prototype-handler.js
Example UI:
image

There's also a custom implementation of form collections that also supports dynamic prototypes based on select inputs just like the one previously mentioned but inside a collection.
Source JS: https://github.com/Sylius/Sylius/blob/204c60bc7faf9caff2378d4204680129a4379740/src/Sylius/Bundle/UiBundle/Resources/private/js/sylius-form-collection.js
Example UI:
image

if (!buttonTemplate) throw new Error(`template with ID "${buttonTemplate}" not found`);

const fragment = buttonTemplate.content.cloneNode(true) as DocumentFragment;
if (1 !== fragment.children.length)
Copy link
Member

Choose a reason for hiding this comment

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

this could use fragment.childElementCount


const fragment = buttonTemplate.content.cloneNode(true) as DocumentFragment;
if (1 !== fragment.children.length)
throw new Error('template with ID "${buttonTemplateID}" must have exactly one child');
Copy link
Member

Choose a reason for hiding this comment

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

this needs to use backticks, not single quotes. Otherwise the interpolation won't work.

@alexander-schranz
Copy link
Contributor

@vvasiloi thats also the same case for my projects, why I would avoid that selector. added comment here: #398 (comment)

@weaverryan
Copy link
Member

Hi! I'd love to have this get pushed forward - we also have a competing PR at #400, which seems to address some issues in this PR. @dunglas WDYT about #400? Which PR should continue? This is a pretty popular feature.

Thanks!

@dunglas
Copy link
Member Author

dunglas commented Sep 22, 2022

400 looks interesting.

I think we have a choice to make:

  • either we remove the HTML generation code needed for this feature from core and move it in UX
  • or we fix/improve the HTML generated by core (using <template> for instance) and we provide only the JS in UX (this is my preferred approach and we I try do do here as it's more future-proof in case new JS tools emerge etc)

What I don't like with the current state of 400 is that it overlaps with (partially broken) features that we already have in core. But I would be ok with deprecating these features from core and moving them to UX too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

9 participants