-
-
Notifications
You must be signed in to change notification settings - Fork 399
Add Collection component #398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 2.x
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| /.gitattributes export-ignore | ||
| /.gitignore export-ignore | ||
| /.symfony.bundle.yaml export-ignore | ||
| /phpunit.xml.dist export-ignore | ||
| /phpstan.neon.dist export-ignore | ||
| /Resources/assets/test export-ignore | ||
| /Resources/assets/jest.config.js export-ignore | ||
| /Tests export-ignore |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| /.php_cs.cache | ||
| /.php_cs | ||
| /.phpunit.result.cache | ||
| /composer.phar | ||
| /composer.lock | ||
| /phpunit.xml | ||
| /vendor/ | ||
| /Tests/app/var | ||
| /Tests/app/public/build/ | ||
| node_modules/ | ||
| package-lock.json | ||
| yarn.lock |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| branches: ["2.x"] | ||
| maintained_branches: ["2.x"] | ||
| doc_dir: "Resources/doc" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Contributing | ||
|
|
||
| Install the test app: | ||
|
|
||
| $ composer install | ||
| $ cd Tests/app | ||
| $ yarn install | ||
| $ yarn build | ||
|
|
||
| Start the test app: | ||
|
|
||
| $ symfony serve | ||
|
|
||
| ## Run tests | ||
|
|
||
| $ php vendor/bin/simple-phpunit |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <?php | ||
|
|
||
| /* | ||
| * This file is part of the Symfony package. | ||
| * | ||
| * (c) Fabien Potencier <[email protected]> | ||
| * | ||
| * For the full copyright and license information, please view the LICENSE | ||
| * file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace Symfony\UX\Collection; | ||
|
|
||
| use Symfony\Component\HttpKernel\Bundle\Bundle; | ||
|
|
||
| /** | ||
| * @author Kévin Dunglas <[email protected]> | ||
| */ | ||
| final class CollectionBundle extends Bundle | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| Copyright (c) 2021 Kévin Dunglas | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is furnished | ||
| to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
| THE SOFTWARE. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Symfony UX Collection | ||
|
|
||
| **This repository is a READ-ONLY sub-tree split**. See | ||
| https://github.com/symfony/ux to create issues or submit pull requests. | ||
|
|
||
| ## Resources | ||
|
|
||
| - [Documentation](https://symfony.com/bundles/ux-collection/current/index.html) | ||
| - [Report issues](https://github.com/symfony/ux/issues) and | ||
| [send Pull Requests](https://github.com/symfony/ux/pulls) | ||
| in the [main Symfony UX repository](https://github.com/symfony/ux) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { Controller } from '@hotwired/stimulus'; | ||
|
|
||
| const DEFAULT_ITEMS_SELECTOR = ':scope > :is(div, fieldset)'; | ||
| var ButtonType; | ||
| (function (ButtonType) { | ||
| ButtonType["Add"] = "add"; | ||
| ButtonType["Delete"] = "delete"; | ||
| })(ButtonType || (ButtonType = {})); | ||
| class controller extends Controller { | ||
| connect() { | ||
| this.connectCollection(this.element); | ||
| } | ||
| connectCollection(parent) { | ||
| parent.querySelectorAll('[data-prototype]').forEach((el) => { | ||
| const collectionEl = el; | ||
| const items = this.getItems(collectionEl); | ||
| collectionEl.dataset.currentIndex = items.length.toString(); | ||
| this.addAddButton(collectionEl); | ||
| this.getItems(collectionEl).forEach(itemEl => this.addDeleteButton(collectionEl, itemEl)); | ||
| }); | ||
| } | ||
| getItems(collectionElement) { | ||
| return collectionElement.querySelectorAll(collectionElement.dataset.itemsSelector || DEFAULT_ITEMS_SELECTOR); | ||
| } | ||
| createButton(collectionEl, buttonType) { | ||
| const buttonTemplateID = collectionEl.dataset[`${buttonType}ButtonTemplateId`]; | ||
| if (buttonTemplateID && 'content' in document.createElement('template')) { | ||
| const buttonTemplate = document.getElementById(buttonTemplateID); | ||
| if (!buttonTemplate) | ||
| throw new Error(`element with ID "${buttonTemplateID}" not found`); | ||
| return buttonTemplate.content.cloneNode(true); | ||
| } | ||
| const button = document.createElement('button'); | ||
| button.type = 'button'; | ||
| button.textContent = buttonType === ButtonType.Add ? 'Add' : 'Delete'; | ||
| return button; | ||
| } | ||
| addItem(collectionEl) { | ||
| const currentIndex = collectionEl.dataset.currentIndex; | ||
| collectionEl.dataset.currentIndex++; | ||
| const collectionNamePattern = collectionEl.id.replace(/_/g, '(?:_|\\[|]\\[)'); | ||
| const prototype = collectionEl.dataset.prototype | ||
| .replace('__name__label__', currentIndex) | ||
| .replace(new RegExp(`(${collectionNamePattern}(?:_|]\\[))__name__`, 'g'), `$1${currentIndex}`); | ||
| const fakeEl = document.createElement('div'); | ||
| fakeEl.innerHTML = prototype; | ||
| const itemEl = fakeEl.firstElementChild; | ||
| this.connectCollection(itemEl); | ||
| this.addDeleteButton(collectionEl, itemEl); | ||
| const items = this.getItems(collectionEl); | ||
| items.length ? items[items.length - 1].insertAdjacentElement('afterend', itemEl) : collectionEl.prepend(itemEl); | ||
| } | ||
| addAddButton(collectionEl) { | ||
| const addButton = this.createButton(collectionEl, ButtonType.Add); | ||
| addButton.onclick = (e) => { | ||
| e.preventDefault(); | ||
| this.addItem(collectionEl); | ||
| }; | ||
| collectionEl.appendChild(addButton); | ||
| } | ||
| addDeleteButton(collectionEl, itemEl) { | ||
| const deleteButton = this.createButton(collectionEl, ButtonType.Delete); | ||
| deleteButton.onclick = (e) => { | ||
| e.preventDefault(); | ||
| itemEl.remove(); | ||
| }; | ||
| itemEl.appendChild(deleteButton); | ||
| } | ||
| } | ||
|
|
||
| export { controller as default }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| module.exports = require('../../../../jest.config.js'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| { | ||
| "name": "@symfony/ux-collection", | ||
| "description": "Support for collection embedding with Symfony Form", | ||
| "license": "MIT", | ||
| "main": "dist/controller.js", | ||
| "module": "dist/controller.js", | ||
| "version": "1.0.0", | ||
| "symfony": { | ||
| "controllers": { | ||
| "collection": { | ||
| "main": "dist/controller.js", | ||
| "webpackMode": "eager", | ||
| "fetch": "eager", | ||
| "enabled": true | ||
| } | ||
| } | ||
| }, | ||
| "peerDependencies": { | ||
| "@hotwired/stimulus": "^3.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@hotwired/stimulus": "^3.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import { Controller } from '@hotwired/stimulus'; | ||
|
|
||
| const DEFAULT_ITEMS_SELECTOR = ':scope > :is(div, fieldset)'; | ||
|
|
||
| interface CollectionDataset extends DOMStringMap { | ||
| prototype: string; | ||
| currentIndex: string; | ||
| itemsSelector?: string; | ||
| addButtonTemplateId?: string; | ||
| disableAddButton?: string; | ||
| deleteButtonTemplateId?: string; | ||
| disableDeleteButton?: string; | ||
| } | ||
|
|
||
| enum ButtonType { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed the code and using
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another alternative is to use a tagged union |
||
| Add = 'add', | ||
| Delete = 'delete', | ||
| } | ||
|
|
||
| export default class extends Controller { | ||
| connect() { | ||
| this.connectCollection(this.element as HTMLElement); | ||
| } | ||
|
|
||
| connectCollection(parent: HTMLElement) { | ||
| parent.querySelectorAll('[data-prototype]').forEach((el) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 So we even don't need to wrap the whole form with a Why I would avoid the Via a form extension like in #400 we can add a better way by adding
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 As I did use component libraries over the last years which work very simular to What problem do you see that the |
||
| const collectionEl = el as HTMLElement; | ||
| const items = this.getItems(collectionEl); | ||
| collectionEl.dataset.currentIndex = items.length.toString(); | ||
|
|
||
| this.addAddButton(collectionEl); | ||
|
|
||
| this.getItems(collectionEl).forEach((itemEl) => this.addDeleteButton(collectionEl, itemEl as HTMLElement)); | ||
| }); | ||
| } | ||
|
|
||
| getItems(collectionElement: HTMLElement) { | ||
| return collectionElement.querySelectorAll(collectionElement.dataset.itemsSelector || DEFAULT_ITEMS_SELECTOR); | ||
| } | ||
|
|
||
| createButton(collectionEl: HTMLElement, buttonType: ButtonType): HTMLElement { | ||
| const buttonTemplateID = collectionEl.dataset[`${buttonType}ButtonTemplateId`]; | ||
| if (buttonTemplateID && 'content' in document.createElement('template')) { | ||
| // Get from template | ||
| const buttonTemplate = document.getElementById(buttonTemplateID) as HTMLTemplateElement | null; | ||
| if (!buttonTemplate) throw new Error(`element with ID "${buttonTemplateID}" not found`); | ||
|
|
||
| return buttonTemplate.content.cloneNode(true) as HTMLElement; | ||
dunglas marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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'; | ||
|
||
|
|
||
| return button; | ||
| } | ||
|
|
||
| addItem(collectionEl: HTMLElement) { | ||
| const currentIndex = (collectionEl.dataset as CollectionDataset).currentIndex; | ||
| (collectionEl.dataset.currentIndex as unknown as number)++; | ||
|
|
||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I didn't handle the label yet, but for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. replacing only the first occurrence is also broken.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dunglas AFAICT, this would replace both placecholders in 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 |
||
| .replace(new RegExp(`(${collectionNamePattern}(?:_|]\\[))__name__`, 'g'), `$1${currentIndex}`); | ||
|
|
||
| const fakeEl = document.createElement('div'); | ||
| fakeEl.innerHTML = prototype; | ||
| const itemEl = fakeEl.firstElementChild as HTMLElement; | ||
|
|
||
| this.connectCollection(itemEl); | ||
|
|
||
| this.addDeleteButton(collectionEl, itemEl); | ||
|
|
||
| const items = this.getItems(collectionEl); | ||
| items.length ? items[items.length - 1].insertAdjacentElement('afterend', itemEl) : collectionEl.prepend(itemEl); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My long-term goal is to totally remove I'll open the PR on core in the next few days.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree still that
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!) |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /* | ||
| * This file is part of the Symfony package. | ||
| * | ||
| * (c) Fabien Potencier <[email protected]> | ||
| * | ||
| * 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 { getByTestId } 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); | ||
| }; | ||
|
|
||
| /* eslint-disable no-undef */ | ||
| describe('CollectionController', () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I planned to add a Panther test.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 <[email protected]>
*
* 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="<div class="team-entry"><label class="required">__name__label__</label><div id="game_teams___name__"><div><label for="game_teams___name___name" class="required">Name</label><input type="text" id="game_teams___name___name" name="game[teams][__name__][name]" required="required" data-controller="test" /></div></div></div>">' +
' </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="<div><label class="required">__name__label__</label><div id="game_teams___name__"><div><label for="game_teams___name___name" class="required">Name</label><input type="text" id="game_teams___name___name" name="game[teams][__name__][name]" required="required" data-controller="test" /></div><div><label class="required">Players</label><div id="game_teams___name___players" data-prototype="&lt;div&gt;&lt;label class=&quot;required&quot;&gt;__name__label__&lt;/label&gt;&lt;div id=&quot;game_teams___name___players___name__&quot;&gt;&lt;div&gt;&lt;label for=&quot;game_teams___name___players___name___firstName&quot; class=&quot;required&quot;&gt;First name&lt;/label&gt;&lt;input type=&quot;text&quot; id=&quot;game_teams___name___players___name___firstName&quot; name=&quot;game[teams][__name__][players][__name__][firstName]&quot; required=&quot;required&quot; /&gt;&lt;/div&gt;&lt;div&gt;&lt;label for=&quot;game_teams___name___players___name___lastName&quot; class=&quot;required&quot;&gt;Last name&lt;/label&gt;&lt;input type=&quot;text&quot; id=&quot;game_teams___name___players___name___lastName&quot; name=&quot;game[teams][__name__][players][__name__][lastName]&quot; required=&quot;required&quot; /&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;"></div></div></div></div>">' +
' </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);
});
});
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| let container: any; | ||
|
|
||
| beforeEach(() => { | ||
| container = mountDOM('<div data-testid="collection" data-controller="symfony--ux-collection--collection"></div>'); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| clearDOM(); | ||
| }); | ||
|
|
||
| it('connects', async () => { | ||
| startStimulus(); | ||
|
|
||
| // smoke test | ||
| expect(getByTestId(container, 'collection')).toHaveAttribute('data-controller', 'symfony--ux-collection--collection'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Entity; | ||
|
|
||
| /** | ||
| * @author Kévin Dunglas <[email protected]> | ||
| */ | ||
| class Game | ||
| { | ||
| public \DateTimeImmutable $date; | ||
| /** | ||
| * @var Team[] | ||
| */ | ||
| private array $teams = []; | ||
|
|
||
| public function addTeam(Team $team): void | ||
| { | ||
| $this->teams[] = $team; | ||
| $this->teams = array_values($this->teams); | ||
|
|
||
| dump($this->teams); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should probably be removed |
||
| } | ||
|
|
||
| public function removeTeam(Team $team): void | ||
| { | ||
| if (false === $i = array_search($team, $this->teams, true)) { | ||
| return; | ||
| } | ||
|
|
||
| unset($this->teams[$i]); | ||
| $this->teams = array_values($this->teams); | ||
| } | ||
|
|
||
| public function getTeams(): array | ||
| { | ||
| return $this->teams; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Entity; | ||
|
|
||
| /** | ||
| * @author Kévin Dunglas <[email protected]> | ||
| */ | ||
| class Player | ||
| { | ||
| public string $firstName; | ||
| public string $lastName; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2022? 😌