From dbba3b839525ad5ba520f229c1a982b1da261bd9 Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Mon, 3 May 2021 11:59:15 +0200
Subject: [PATCH 01/34] UX FormCollection
---
src/FormCollection/.gitattributes | 2 +
.../FormCollectionExtension.php | 37 ++++
src/FormCollection/Form/CollectionType.php | 57 +++++
src/FormCollection/FormCollectionBundle.php | 22 ++
src/FormCollection/LICENSE | 19 ++
src/FormCollection/README.md | 141 ++++++++++++
src/FormCollection/Resources/assets/.babelrc | 4 +
.../Resources/assets/.gitignore | 1 +
.../Resources/assets/dist/controller.js | 203 ++++++++++++++++++
.../Resources/assets/package.json | 32 +++
.../Resources/assets/src/controller.js | 156 ++++++++++++++
.../Resources/views/form_theme_div.html.twig | 47 ++++
.../views/form_theme_table.html.twig | 51 +++++
src/FormCollection/composer.json | 52 +++++
14 files changed, 824 insertions(+)
create mode 100644 src/FormCollection/.gitattributes
create mode 100644 src/FormCollection/DependencyInjection/FormCollectionExtension.php
create mode 100644 src/FormCollection/Form/CollectionType.php
create mode 100644 src/FormCollection/FormCollectionBundle.php
create mode 100644 src/FormCollection/LICENSE
create mode 100644 src/FormCollection/README.md
create mode 100644 src/FormCollection/Resources/assets/.babelrc
create mode 100644 src/FormCollection/Resources/assets/.gitignore
create mode 100644 src/FormCollection/Resources/assets/dist/controller.js
create mode 100644 src/FormCollection/Resources/assets/package.json
create mode 100644 src/FormCollection/Resources/assets/src/controller.js
create mode 100644 src/FormCollection/Resources/views/form_theme_div.html.twig
create mode 100644 src/FormCollection/Resources/views/form_theme_table.html.twig
create mode 100644 src/FormCollection/composer.json
diff --git a/src/FormCollection/.gitattributes b/src/FormCollection/.gitattributes
new file mode 100644
index 00000000000..dfe0770424b
--- /dev/null
+++ b/src/FormCollection/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/src/FormCollection/DependencyInjection/FormCollectionExtension.php b/src/FormCollection/DependencyInjection/FormCollectionExtension.php
new file mode 100644
index 00000000000..208ecacf550
--- /dev/null
+++ b/src/FormCollection/DependencyInjection/FormCollectionExtension.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\FormCollection\DependencyInjection;
+
+use Symfony\UX\FormCollection\Form\CollectionType;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
+use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+
+/**
+ * @internal
+ */
+class FormCollectionExtension extends Extension implements PrependExtensionInterface
+{
+ public function prepend(ContainerBuilder $container)
+ {
+ }
+
+ public function load(array $configs, ContainerBuilder $container)
+ {
+ $container
+ ->setDefinition('form.ux_collection', new Definition(CollectionType::class))
+ ->addTag('form.type')
+ ->setPublic(false)
+ ;
+ }
+}
diff --git a/src/FormCollection/Form/CollectionType.php b/src/FormCollection/Form/CollectionType.php
new file mode 100644
index 00000000000..120f9607cac
--- /dev/null
+++ b/src/FormCollection/Form/CollectionType.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\FormCollection\Form;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\CollectionType as BaseCollectionType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+/**
+ * @final
+ * @experimental
+ */
+class CollectionType extends AbstractType
+{
+ public function getParent()
+ {
+ return BaseCollectionType::class;
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'button_add' => [
+ 'text' => 'Add',
+ 'attr' => ['class' => 'btn btn-outline-primary'],
+ ],
+ 'button_delete' => [
+ 'text' => 'Remove',
+ 'attr' => ['class' => 'btn btn-outline-secondary'],
+ ],
+ ]);
+ }
+
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ parent::finishView($view, $form, $options);
+
+ $view->vars['button_add'] = $options['button_add'];
+ $view->vars['button_delete'] = $options['button_delete'];
+ }
+
+ public function getBlockPrefix()
+ {
+ return 'form_collection';
+ }
+}
diff --git a/src/FormCollection/FormCollectionBundle.php b/src/FormCollection/FormCollectionBundle.php
new file mode 100644
index 00000000000..7eae8679a47
--- /dev/null
+++ b/src/FormCollection/FormCollectionBundle.php
@@ -0,0 +1,22 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\FormCollection;
+
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * @final
+ * @experimental
+ */
+class FormCollectionBundle extends Bundle
+{
+}
diff --git a/src/FormCollection/LICENSE b/src/FormCollection/LICENSE
new file mode 100644
index 00000000000..ad85e173748
--- /dev/null
+++ b/src/FormCollection/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2020-2021 Fabien Potencier
+
+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.
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
new file mode 100644
index 00000000000..c6f4e6125e6
--- /dev/null
+++ b/src/FormCollection/README.md
@@ -0,0 +1,141 @@
+# UX Form Collection
+
+Symfony UX Form collection is a Symfony bundle providing light UX for collection
+in Symfony Forms.
+
+## Installation
+
+UX Form Collection requires PHP 7.2+ and Symfony 4.4+.
+
+Install this bundle using Composer and Symfony Flex:
+
+```sh
+composer require symfony/ux-form-collection
+
+# Don't forget to install the JavaScript dependencies as well and compile
+yarn install --force
+yarn encore dev
+```
+
+Also make sure you have at least version 2.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge)
+in your `package.json` file.
+
+You need to select the right theme from the one you are using :
+```yaml
+# config/packages/twig.yaml
+twig:
+ # For bootstrap for example
+ form_themes: ['@FormCollection/form_theme_div.html.twig']
+```
+You have 2 different themes :
+- `@FormCollection/form_theme_div.html.twig`
+- `@FormCollection/form_theme_table.html.twig`
+
+[Check the Symfony doc](https://symfony.com/doc/4.4/form/form_themes.html) for the different ways to set themes in Symfony.
+
+## Usage
+
+The most common usage of Form Collection is to use it as a replacement of
+the native CollectionType class:
+
+```php
+// ...
+use Symfony\UX\FormCollection\Form\CollectionType;
+
+class BlogFormType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ // ...
+ ->add('comments', CollectionType::class, [
+ // ...
+ 'button_add' => [
+ // Default text for the add button
+ 'text' => 'Add',
+ // Default attr class for the add button
+ 'attr' => ['class' => 'btn btn-outline-primary']
+ ],
+ 'button_delete' => [
+ // Default text for the delete button
+ 'text' => 'Remove',
+ // Default class for the delete button
+ 'attr' => ['class' => 'btn btn-outline-secondary']
+ ],
+ ])
+ // ...
+ ;
+ }
+
+ // ...
+}
+```
+
+### Extend the default behavior
+
+Symfony UX Form Collection allows you to extend its default behavior using a custom Stimulus controller:
+
+```js
+// mycollection_controller.js
+
+import { Controller } from 'stimulus';
+
+export default class extends Controller {
+ connect() {
+ this.element.addEventListener('collection:pre-connect', this._onPreConnect);
+ this.element.addEventListener('collection:connect', this._onConnect);
+ this.element.addEventListener('collection:pre-add', this._onPreAdd);
+ this.element.addEventListener('collection:add', this._onAdd);
+ this.element.addEventListener('collection:pre-delete', this._onPreDelete);
+ this.element.addEventListener('collection:delete', this._onDelete);
+ }
+
+ disconnect() {
+ // You should always remove listeners when the controller is disconnected to avoid side effects
+ this.element.removeEventListener('collection:pre-connect', this._onPreConnect);
+ this.element.removeEventListener('collection:connect', this._onConnect);
+ }
+
+ _onPreConnect(event) {
+ // The collection is not yet connected
+ console.log(event.detail.allowAdd); // Access to the allow_add option of the form
+ console.log(event.detail.allowDelete); // Access to the allow_delete option of the form
+ }
+
+ _onConnect(event) {
+ // Same as collection:pre-connect event
+ }
+
+ _onPreAdd(event) {
+ console.log(event.detail.index); // Access to the curent index will be added
+ console.log(event.detail.element); // Access to the element will be added
+ }
+
+ _onAdd(event) {
+ // Same as collection:pre-add event
+ }
+
+ _onPreDelete(event) {
+ console.log(event.detail.index); // Access to the index will be removed
+ console.log(event.detail.element); // Access to the elemnt will be removed
+ }
+
+ _onDelete(event) {
+ // Same as collection:pre-delete event
+ }
+}
+```
+
+Then in your render call, add your controller as an HTML attribute:
+
+```php
+ $builder
+ // ...
+ ->add('comments', UXCollectionType::class, [
+ // ...
+ 'attr' => [
+ // Change the controller name
+ 'data-controller' => 'mycollection'
+ ]
+ ]);
+```
\ No newline at end of file
diff --git a/src/FormCollection/Resources/assets/.babelrc b/src/FormCollection/Resources/assets/.babelrc
new file mode 100644
index 00000000000..77f182d2ca8
--- /dev/null
+++ b/src/FormCollection/Resources/assets/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["@babel/env"],
+ "plugins": ["@babel/plugin-proposal-class-properties"]
+}
diff --git a/src/FormCollection/Resources/assets/.gitignore b/src/FormCollection/Resources/assets/.gitignore
new file mode 100644
index 00000000000..096746c1480
--- /dev/null
+++ b/src/FormCollection/Resources/assets/.gitignore
@@ -0,0 +1 @@
+/node_modules/
\ No newline at end of file
diff --git a/src/FormCollection/Resources/assets/dist/controller.js b/src/FormCollection/Resources/assets/dist/controller.js
new file mode 100644
index 00000000000..45685d3234f
--- /dev/null
+++ b/src/FormCollection/Resources/assets/dist/controller.js
@@ -0,0 +1,203 @@
+'use strict';
+
+function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports["default"] = void 0;
+
+var _stimulus = require("stimulus");
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
+
+function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
+
+function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
+
+function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
+
+function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
+
+function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
+
+function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+var _default = /*#__PURE__*/function (_Controller) {
+ _inherits(_default, _Controller);
+
+ var _super = _createSuper(_default);
+
+ function _default() {
+ var _this;
+
+ _classCallCheck(this, _default);
+
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
+
+ _this = _super.call.apply(_super, [this].concat(args));
+
+ _defineProperty(_assertThisInitialized(_this), "index", 0);
+
+ _defineProperty(_assertThisInitialized(_this), "controllerName", null);
+
+ return _this;
+ }
+
+ _createClass(_default, [{
+ key: "connect",
+ value: function connect() {
+ this.controllerName = this.context.scope.identifier;
+
+ this._dispatchEvent('form-collection:pre-connect', {
+ allowAdd: this.allowAddValue,
+ allowDelete: this.allowDeleteValue
+ });
+
+ if (true === this.allowAddValue) {
+ // Add button Add
+ var buttonAdd = this._textToNode(this.buttonAddValue);
+
+ this.containerTarget.prepend(buttonAdd);
+ } // Add buttons Delete
+
+
+ if (true === this.allowDeleteValue) {
+ for (var i = 0; i < this.entryTargets.length; i++) {
+ this.index = i;
+ var entry = this.entryTargets[i];
+
+ this._addDeleteButton(entry, this.index);
+ }
+ }
+
+ this._dispatchEvent('form-collection:connect', {
+ allowAdd: this.allowAddValue,
+ allowDelete: this.allowDeleteValue
+ });
+ }
+ }, {
+ key: "add",
+ value: function add(event) {
+ this.index++; // Compute the new entry
+
+ var newEntry = this.containerTarget.dataset.prototype;
+ newEntry = newEntry.replace(/__name__label__/g, this.index);
+ newEntry = newEntry.replace(/__name__/g, this.index);
+ newEntry = this._textToNode(newEntry);
+
+ this._dispatchEvent('form-collection:pre-add', {
+ index: this.index,
+ element: newEntry
+ });
+
+ this.containerTarget.append(newEntry); // Retrieve the entry from targets to make sure that this is the one
+
+ var entry = this.entryTargets[this.entryTargets.length - 1];
+ entry = this._addDeleteButton(entry, this.index);
+
+ this._dispatchEvent('form-collection:add', {
+ index: this.index,
+ element: entry
+ });
+ }
+ }, {
+ key: "delete",
+ value: function _delete(event) {
+ var theIndexEntryToDelete = event.target.dataset.indexEntry; // Search the entry to delete from the data-index-entry attribute
+
+ for (var i = 0; i < this.entryTargets.length; i++) {
+ var entry = this.entryTargets[i];
+
+ if (theIndexEntryToDelete === entry.dataset.indexEntry) {
+ this._dispatchEvent('form-collection:pre-delete', {
+ index: entry.dataset.indexEntry,
+ element: entry
+ });
+
+ entry.remove();
+
+ this._dispatchEvent('form-collection:delete', {
+ index: entry.dataset.indexEntry,
+ element: entry
+ });
+ }
+ }
+ }
+ /**
+ * Add the delete button to the entry
+ * @param String entry
+ * @param Number index
+ * @returns {ChildNode}
+ * @private
+ */
+
+ }, {
+ key: "_addDeleteButton",
+ value: function _addDeleteButton(entry, index) {
+ // link the button and the entry by the data-index-entry attribute
+ entry.dataset.indexEntry = index;
+
+ var buttonDelete = this._textToNode(this.buttonDeleteValue);
+
+ buttonDelete.dataset.indexEntry = index;
+
+ if ('TR' === entry.nodeName) {
+ entry.lastElementChild.append(buttonDelete);
+ } else {
+ entry.append(buttonDelete);
+ }
+
+ return entry;
+ }
+ /**
+ * Convert text to Element to insert in the DOM
+ * @param String text
+ * @returns {ChildNode}
+ * @private
+ */
+
+ }, {
+ key: "_textToNode",
+ value: function _textToNode(text) {
+ var template = document.createElement('template');
+ text = text.trim(); // Never return a text node of whitespace as the result
+
+ template.innerHTML = text;
+ return template.content.firstChild;
+ }
+ }, {
+ key: "_dispatchEvent",
+ value: function _dispatchEvent(name) {
+ var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+ var canBubble = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ var cancelable = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+ var userEvent = document.createEvent('CustomEvent');
+ userEvent.initCustomEvent(name, canBubble, cancelable, payload);
+ this.element.dispatchEvent(userEvent);
+ }
+ }]);
+
+ return _default;
+}(_stimulus.Controller);
+
+exports["default"] = _default;
+
+_defineProperty(_default, "targets", ['container', 'entry']);
+
+_defineProperty(_default, "values", {
+ allowAdd: Boolean,
+ allowDelete: Boolean,
+ buttonAdd: String,
+ buttonDelete: String
+});
\ No newline at end of file
diff --git a/src/FormCollection/Resources/assets/package.json b/src/FormCollection/Resources/assets/package.json
new file mode 100644
index 00000000000..b821fddc88a
--- /dev/null
+++ b/src/FormCollection/Resources/assets/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@symfony/ux-form-collection",
+ "description": "UX Form Collection for Symfony Forms",
+ "license": "MIT",
+ "version": "1.0.0",
+ "symfony": {
+ "controllers": {
+ "collection": {
+ "main": "dist/controller.js",
+ "webpackMode": "eager",
+ "fetch": "eager",
+ "enabled": true
+ }
+ }
+ },
+ "scripts": {
+ "build": "babel src -d dist",
+ "test": "babel src -d dist && jest",
+ "lint": "eslint src test"
+ },
+ "peerDependencies": {
+ "stimulus": "^2.0.0"
+ },
+ "devDependencies": {
+ "@babel/cli": "^7.12.1",
+ "@babel/core": "^7.12.3",
+ "@babel/plugin-proposal-class-properties": "^7.12.1",
+ "@babel/preset-env": "^7.12.7",
+ "@symfony/stimulus-testing": "^1.1.0",
+ "stimulus": "^2.0.0"
+ }
+}
diff --git a/src/FormCollection/Resources/assets/src/controller.js b/src/FormCollection/Resources/assets/src/controller.js
new file mode 100644
index 00000000000..d65f29c784a
--- /dev/null
+++ b/src/FormCollection/Resources/assets/src/controller.js
@@ -0,0 +1,156 @@
+'use strict';
+
+import { Controller } from 'stimulus';
+
+export default class extends Controller {
+ static targets = [
+ 'container',
+ 'entry'
+ ];
+
+ static values = {
+ allowAdd: Boolean,
+ allowDelete: Boolean,
+ buttonAdd: String,
+ buttonDelete: String
+ };
+
+ /**
+ * Number of elements for the index of the collection
+ * @type Number
+ */
+ index = 0;
+
+ /**
+ * Controller name of this
+ * @type String|null
+ */
+ controllerName = null;
+
+ connect() {
+ this.controllerName = this.context.scope.identifier;
+
+
+ this._dispatchEvent('form-collection:pre-connect', {
+ allowAdd: this.allowAddValue,
+ allowDelete: this.allowDeleteValue,
+ });
+
+ if (true === this.allowAddValue) {
+ // Add button Add
+ let buttonAdd = this._textToNode(this.buttonAddValue);
+ this.containerTarget.prepend(buttonAdd);
+ }
+
+ // Add buttons Delete
+ if (true === this.allowDeleteValue) {
+ for (let i = 0; i < this.entryTargets.length; i++) {
+ this.index = i;
+ let entry = this.entryTargets[i];
+ this._addDeleteButton(entry, this.index);
+ }
+ }
+
+ this._dispatchEvent('form-collection:connect', {
+ allowAdd: this.allowAddValue,
+ allowDelete: this.allowDeleteValue,
+ });
+ }
+
+ add(event) {
+
+ this.index++;
+
+ // Compute the new entry
+ let newEntry = this.containerTarget.dataset.prototype;
+ newEntry = newEntry.replace(/__name__label__/g, this.index);
+ newEntry = newEntry.replace(/__name__/g, this.index);
+ newEntry = this._textToNode(newEntry);
+
+ this._dispatchEvent('form-collection:pre-add', {
+ index: this.index,
+ element: newEntry
+ });
+
+ this.containerTarget.append(newEntry);
+
+ // Retrieve the entry from targets to make sure that this is the one
+ let entry = this.entryTargets[this.entryTargets.length - 1];
+ entry = this._addDeleteButton(entry, this.index);
+
+ this._dispatchEvent('form-collection:add', {
+ index: this.index,
+ element: entry
+ });
+ }
+
+ delete(event) {
+
+ let theIndexEntryToDelete = event.target.dataset.indexEntry;
+
+ // Search the entry to delete from the data-index-entry attribute
+ for (let i = 0; i < this.entryTargets.length; i++) {
+ let entry = this.entryTargets[i];
+ if (theIndexEntryToDelete === entry.dataset.indexEntry) {
+
+ this._dispatchEvent('form-collection:pre-delete', {
+ index: entry.dataset.indexEntry,
+ element: entry
+ });
+
+ entry.remove();
+
+ this._dispatchEvent('form-collection:delete', {
+ index: entry.dataset.indexEntry,
+ element: entry
+ });
+ }
+ }
+ }
+
+ /**
+ * Add the delete button to the entry
+ * @param String entry
+ * @param Number index
+ * @returns {ChildNode}
+ * @private
+ */
+ _addDeleteButton(entry, index) {
+
+ // link the button and the entry by the data-index-entry attribute
+ entry.dataset.indexEntry = index;
+
+ let buttonDelete = this._textToNode(this.buttonDeleteValue);
+ buttonDelete.dataset.indexEntry = index;
+
+ if('TR' === entry.nodeName) {
+ entry.lastElementChild.append(buttonDelete);
+ } else {
+ entry.append(buttonDelete);
+ }
+
+ return entry;
+ }
+
+ /**
+ * Convert text to Element to insert in the DOM
+ * @param String text
+ * @returns {ChildNode}
+ * @private
+ */
+ _textToNode(text) {
+
+ let template = document.createElement('template');
+ text = text.trim(); // Never return a text node of whitespace as the result
+ template.innerHTML = text;
+
+ return template.content.firstChild;
+ }
+
+ _dispatchEvent(name, payload = null, canBubble = false, cancelable = false) {
+ const userEvent = document.createEvent('CustomEvent');
+ userEvent.initCustomEvent(name, canBubble, cancelable, payload);
+
+ this.element.dispatchEvent(userEvent);
+ }
+}
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
new file mode 100644
index 00000000000..6ddc3282152
--- /dev/null
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -0,0 +1,47 @@
+{%- block button_add -%}
+ {%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#add')|trim -%}
+ {{ button_add.text|trans }}
+{%- endblock button_add -%}
+
+{%- block button_delete -%}
+ {%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
+
+ {{ button_delete.text|trans }}
+{%- endblock button_delete -%}
+
+{% block form_collection_widget -%}
+ {%- set controllerName = 'symfony--ux-form-collection--collection' -%}
+ {%- set dataController = (attr['data-controller']|default('') ~ ' ' ~ controllerName)|trim -%}
+
+ {# attr for the data target on the entry of the collection #}
+ {%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
+
+
+ {% if prototype is defined and not prototype.rendered %}
+ {%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
+ {%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
+ {% endif %}
+ {%- set attr = attr|merge({('data-' ~ controllerName ~ '-target'): 'container' }) -%}
+
+
+ {%- if form is rootform -%}
+ {{ form_errors(form) }}
+ {%- endif -%}
+
+ {% for child in form|filter(child => not child.rendered) %}
+
+ {%- set child_attr = child.vars.attr|merge(attrDataTarget) -%}
+ {{- form_row(child, {'row_attr': child_attr}) -}}
+
+ {% endfor %}
+
+ {{- form_rest(form) -}}
+
+{%- endblock %}
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
new file mode 100644
index 00000000000..75c5745b1ab
--- /dev/null
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -0,0 +1,51 @@
+{%- block button_add -%}
+ {%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#add')|trim -%}
+
+
+ {{ button_add.text|trans }}
+
+
+{%- endblock button_add -%}
+
+{%- block button_delete -%}
+ {%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
+
+ {{ button_delete.text|trans }}
+{%- endblock button_delete -%}
+
+{% block form_collection_widget -%}
+ {%- set controllerName = 'symfony--ux-form-collection--collection' -%}
+ {%- set dataController = (attr['data-controller']|default('') ~ ' ' ~ controllerName)|trim -%}
+
+ {# attr for the data target on the entry of the collection #}
+ {%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
+
+
+ {% if prototype is defined and not prototype.rendered %}
+ {%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
+ {%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
+ {% endif %}
+ {%- set attr = attr|merge({('data-' ~ controllerName ~ '-target'): 'container' }) -%}
+
+
+ {%- if form is rootform -%}
+ {{ form_errors(form) }}
+ {%- endif -%}
+
+ {% for child in form|filter(child => not child.rendered) %}
+
+ {%- set child_attr = child.vars.attr|merge(attrDataTarget) -%}
+ {{- form_row(child, {'row_attr': child_attr}) -}}
+
+ {% endfor %}
+
+ {{- form_rest(form) -}}
+
+{%- endblock %}
diff --git a/src/FormCollection/composer.json b/src/FormCollection/composer.json
new file mode 100644
index 00000000000..181b5cbf466
--- /dev/null
+++ b/src/FormCollection/composer.json
@@ -0,0 +1,52 @@
+{
+ "name": "symfony/ux-form-collection",
+ "type": "symfony-bundle",
+ "description": "UX Form Collection for Symfony Forms",
+ "keywords": [
+ "symfony-ux",
+ "ux-form-collection"
+ ],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Stakovicz",
+ "email": "stakovicz@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "Symfony\\UX\\FormCollection\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/config": "^4.4.17|^5.0",
+ "symfony/dependency-injection": "^4.4.17|^5.0",
+ "symfony/form": "^4.4.17|^5.0",
+ "symfony/http-kernel": "^4.4.17|^5.0"
+ },
+ "require-dev": {
+ "symfony/framework-bundle": "^4.4.17|^5.0",
+ "symfony/phpunit-bridge": "^5.2",
+ "symfony/twig-bundle": "^4.4.17|^5.0",
+ "symfony/var-dumper": "^4.4.17|^5.0"
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.0-dev"
+ },
+ "thanks": {
+ "name": "symfony/ux",
+ "url": "https://github.com/symfony/ux"
+ }
+ },
+ "minimum-stability": "dev"
+}
From 83ec2ead9a9251b739ecdc426ef57f98bffa95aa Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Tue, 4 May 2021 09:13:00 +0200
Subject: [PATCH 02/34] Remove hard coded property_name __name__
---
src/FormCollection/Form/CollectionType.php | 1 +
.../Resources/assets/dist/controller.js | 9 ++++++---
.../Resources/assets/src/controller.js | 12 +++++++++---
.../Resources/views/form_theme_div.html.twig | 1 +
4 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/src/FormCollection/Form/CollectionType.php b/src/FormCollection/Form/CollectionType.php
index 120f9607cac..1e31499a88b 100644
--- a/src/FormCollection/Form/CollectionType.php
+++ b/src/FormCollection/Form/CollectionType.php
@@ -48,6 +48,7 @@ public function finishView(FormView $view, FormInterface $form, array $options)
$view->vars['button_add'] = $options['button_add'];
$view->vars['button_delete'] = $options['button_delete'];
+ $view->vars['prototype_name'] = $options['prototype_name'];
}
public function getBlockPrefix()
diff --git a/src/FormCollection/Resources/assets/dist/controller.js b/src/FormCollection/Resources/assets/dist/controller.js
index 45685d3234f..271c7ca00e2 100644
--- a/src/FormCollection/Resources/assets/dist/controller.js
+++ b/src/FormCollection/Resources/assets/dist/controller.js
@@ -92,8 +92,10 @@ var _default = /*#__PURE__*/function (_Controller) {
this.index++; // Compute the new entry
var newEntry = this.containerTarget.dataset.prototype;
- newEntry = newEntry.replace(/__name__label__/g, this.index);
- newEntry = newEntry.replace(/__name__/g, this.index);
+ var regExp = new RegExp(this.prototypeNameValue + 'label__', 'g');
+ newEntry = newEntry.replace(regExp, this.index);
+ regExp = new RegExp(this.prototypeNameValue, 'g');
+ newEntry = newEntry.replace(regExp, this.index);
newEntry = this._textToNode(newEntry);
this._dispatchEvent('form-collection:pre-add', {
@@ -199,5 +201,6 @@ _defineProperty(_default, "values", {
allowAdd: Boolean,
allowDelete: Boolean,
buttonAdd: String,
- buttonDelete: String
+ buttonDelete: String,
+ prototypeName: String
});
\ No newline at end of file
diff --git a/src/FormCollection/Resources/assets/src/controller.js b/src/FormCollection/Resources/assets/src/controller.js
index d65f29c784a..72311e4ef4e 100644
--- a/src/FormCollection/Resources/assets/src/controller.js
+++ b/src/FormCollection/Resources/assets/src/controller.js
@@ -12,7 +12,8 @@ export default class extends Controller {
allowAdd: Boolean,
allowDelete: Boolean,
buttonAdd: String,
- buttonDelete: String
+ buttonDelete: String,
+ prototypeName: String
};
/**
@@ -63,8 +64,13 @@ export default class extends Controller {
// Compute the new entry
let newEntry = this.containerTarget.dataset.prototype;
- newEntry = newEntry.replace(/__name__label__/g, this.index);
- newEntry = newEntry.replace(/__name__/g, this.index);
+
+ let regExp = new RegExp(this.prototypeNameValue+'label__', 'g');
+ newEntry = newEntry.replace(regExp, this.index);
+
+ regExp = new RegExp(this.prototypeNameValue, 'g');
+ newEntry = newEntry.replace(regExp, this.index);
+
newEntry = this._textToNode(newEntry);
this._dispatchEvent('form-collection:pre-add', {
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index 6ddc3282152..47f2a3c5b24 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -23,6 +23,7 @@
data-{{ controllerName }}-allow-delete-value="{{ allow_delete|json_encode }}"
data-{{ controllerName }}-button-add-value="{{ block('button_add')|e }}"
data-{{ controllerName }}-button-delete-value="{{ block('button_delete')|e }}"
+ data-{{ controllerName }}-prototype-name-value="{{ prototype_name }}"
>
{% if prototype is defined and not prototype.rendered %}
{%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
From d983cf0a7c1a359455cf365065a0af6a046000db Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Tue, 4 May 2021 18:37:35 +0200
Subject: [PATCH 03/34] PHP CS Fixer
---
.../DependencyInjection/FormCollectionExtension.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/FormCollection/DependencyInjection/FormCollectionExtension.php b/src/FormCollection/DependencyInjection/FormCollectionExtension.php
index 208ecacf550..6959667f1da 100644
--- a/src/FormCollection/DependencyInjection/FormCollectionExtension.php
+++ b/src/FormCollection/DependencyInjection/FormCollectionExtension.php
@@ -11,11 +11,11 @@
namespace Symfony\UX\FormCollection\DependencyInjection;
-use Symfony\UX\FormCollection\Form\CollectionType;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\UX\FormCollection\Form\CollectionType;
/**
* @internal
From a35ff32b660803ef629c1ee49790833cbe4117a7 Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Tue, 4 May 2021 20:59:20 +0200
Subject: [PATCH 04/34] First jests
---
.../Resources/assets/package.json | 6 ++
.../Resources/assets/test/controller.test.js | 60 +++++++++++++++++++
.../Resources/assets/test/setup.js | 12 ++++
3 files changed, 78 insertions(+)
create mode 100644 src/FormCollection/Resources/assets/test/controller.test.js
create mode 100644 src/FormCollection/Resources/assets/test/setup.js
diff --git a/src/FormCollection/Resources/assets/package.json b/src/FormCollection/Resources/assets/package.json
index b821fddc88a..f30194403c6 100644
--- a/src/FormCollection/Resources/assets/package.json
+++ b/src/FormCollection/Resources/assets/package.json
@@ -28,5 +28,11 @@
"@babel/preset-env": "^7.12.7",
"@symfony/stimulus-testing": "^1.1.0",
"stimulus": "^2.0.0"
+ },
+ "jest": {
+ "testRegex": "test/.*\\.test.js",
+ "setupFilesAfterEnv": [
+ "./test/setup.js"
+ ]
}
}
diff --git a/src/FormCollection/Resources/assets/test/controller.test.js b/src/FormCollection/Resources/assets/test/controller.test.js
new file mode 100644
index 00000000000..f764a48d6e8
--- /dev/null
+++ b/src/FormCollection/Resources/assets/test/controller.test.js
@@ -0,0 +1,60 @@
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+'use strict';
+
+import { Application, Controller } from 'stimulus';
+import { getByTestId, waitFor } from '@testing-library/dom';
+import user from '@testing-library/user-event';
+import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
+import FormCollectionController from '../dist/controller';
+
+// Controller used to check the actual controller was properly booted
+class CheckController extends Controller {
+ connect() {
+ this.element.addEventListener('form-collection:pre-connect', () => {
+ this.element.classList.add('pre-connected');
+ });
+ this.element.addEventListener('form-collection:connect', () => {
+ this.element.classList.add('connected');
+ });
+ }
+}
+
+const startStimulus = () => {
+ const application = Application.start();
+ application.register('check', CheckController);
+ application.register('formCollection', FormCollectionController);
+};
+
+describe('FormCollectionController', () => {
+ let container;
+
+ beforeEach(() => {
+ container = mountDOM(`
+
+
+
+ `);
+ });
+
+ afterEach(() => {
+ clearDOM();
+ });
+
+ it('events', async () => {
+ expect(getByTestId(container, 'container')).not.toHaveClass('connected');
+ expect(getByTestId(container, 'container')).not.toHaveClass('pre-connected');
+
+ startStimulus();
+ await waitFor(() => expect(getByTestId(container, 'container')).toHaveClass('connected'));
+ await waitFor(() => expect(getByTestId(container, 'container')).toHaveClass('pre-connected'));
+ });
+
+});
diff --git a/src/FormCollection/Resources/assets/test/setup.js b/src/FormCollection/Resources/assets/test/setup.js
new file mode 100644
index 00000000000..ddd4655c30e
--- /dev/null
+++ b/src/FormCollection/Resources/assets/test/setup.js
@@ -0,0 +1,12 @@
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+'use strict';
+
+import '@symfony/stimulus-testing/setup';
From e1d7574d444f706fc67aee74b1624d2324563745 Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Thu, 6 May 2021 20:50:31 +0200
Subject: [PATCH 05/34] Rename CollectionType > UXCollectionType
---
.../DependencyInjection/FormCollectionExtension.php | 4 ++--
.../{CollectionType.php => UXCollectionType.php} | 12 +++++-------
src/FormCollection/README.md | 4 ++--
3 files changed, 9 insertions(+), 11 deletions(-)
rename src/FormCollection/Form/{CollectionType.php => UXCollectionType.php} (76%)
diff --git a/src/FormCollection/DependencyInjection/FormCollectionExtension.php b/src/FormCollection/DependencyInjection/FormCollectionExtension.php
index 6959667f1da..7eb856a7b5c 100644
--- a/src/FormCollection/DependencyInjection/FormCollectionExtension.php
+++ b/src/FormCollection/DependencyInjection/FormCollectionExtension.php
@@ -15,7 +15,7 @@
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
-use Symfony\UX\FormCollection\Form\CollectionType;
+use Symfony\UX\FormCollection\Form\UXCollectionType;
/**
* @internal
@@ -29,7 +29,7 @@ public function prepend(ContainerBuilder $container)
public function load(array $configs, ContainerBuilder $container)
{
$container
- ->setDefinition('form.ux_collection', new Definition(CollectionType::class))
+ ->setDefinition('form.ux_collection', new Definition(UXCollectionType::class))
->addTag('form.type')
->setPublic(false)
;
diff --git a/src/FormCollection/Form/CollectionType.php b/src/FormCollection/Form/UXCollectionType.php
similarity index 76%
rename from src/FormCollection/Form/CollectionType.php
rename to src/FormCollection/Form/UXCollectionType.php
index 1e31499a88b..6ef3390484d 100644
--- a/src/FormCollection/Form/CollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -12,7 +12,7 @@
namespace Symfony\UX\FormCollection\Form;
use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\Extension\Core\Type\CollectionType as BaseCollectionType;
+use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -21,23 +21,21 @@
* @final
* @experimental
*/
-class CollectionType extends AbstractType
+class UXCollectionType extends AbstractType
{
public function getParent()
{
- return BaseCollectionType::class;
+ return CollectionType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'button_add' => [
- 'text' => 'Add',
- 'attr' => ['class' => 'btn btn-outline-primary'],
+ 'text' => 'Add'
],
'button_delete' => [
- 'text' => 'Remove',
- 'attr' => ['class' => 'btn btn-outline-secondary'],
+ 'text' => 'Remove'
],
]);
}
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index c6f4e6125e6..f7c1b58a12a 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -40,7 +40,7 @@ the native CollectionType class:
```php
// ...
-use Symfony\UX\FormCollection\Form\CollectionType;
+use Symfony\UX\FormCollection\Form\UXCollectionType;
class BlogFormType extends AbstractType
{
@@ -48,7 +48,7 @@ class BlogFormType extends AbstractType
{
$builder
// ...
- ->add('comments', CollectionType::class, [
+ ->add('comments', UXCollectionType::class, [
// ...
'button_add' => [
// Default text for the add button
From 409c8e7e4b89df897da2acf1d0f89fdb22a77900 Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Thu, 6 May 2021 21:03:20 +0200
Subject: [PATCH 06/34] Rename CollectionType > UXCollectionType
---
src/FormCollection/Form/UXCollectionType.php | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index 6ef3390484d..02ff3c0c514 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -32,10 +32,12 @@ public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'button_add' => [
- 'text' => 'Add'
+ 'text' => 'Add',
+ 'attr' => ['class' => 'btn btn-outline-primary'],
],
'button_delete' => [
- 'text' => 'Remove'
+ 'text' => 'Remove',
+ 'attr' => ['class' => 'btn btn-outline-secondary'],
],
]);
}
From a624f257e470980f7da00c07ff7b19f87943c045 Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Thu, 6 May 2021 21:03:43 +0200
Subject: [PATCH 07/34] DependencyInjection Clean
---
.../DependencyInjection/FormCollectionExtension.php | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/src/FormCollection/DependencyInjection/FormCollectionExtension.php b/src/FormCollection/DependencyInjection/FormCollectionExtension.php
index 7eb856a7b5c..3266037fd37 100644
--- a/src/FormCollection/DependencyInjection/FormCollectionExtension.php
+++ b/src/FormCollection/DependencyInjection/FormCollectionExtension.php
@@ -13,19 +13,14 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
-use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\UX\FormCollection\Form\UXCollectionType;
/**
* @internal
*/
-class FormCollectionExtension extends Extension implements PrependExtensionInterface
+class FormCollectionExtension extends Extension
{
- public function prepend(ContainerBuilder $container)
- {
- }
-
public function load(array $configs, ContainerBuilder $container)
{
$container
From f4ba0118db973eb0526febf05d5590ea7e286131 Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Thu, 6 May 2021 21:08:32 +0200
Subject: [PATCH 08/34] Fix .gitattributes
---
src/FormCollection/.gitattributes | 5 +++--
src/FormCollection/Resources/assets/.gitignore | 1 -
2 files changed, 3 insertions(+), 3 deletions(-)
delete mode 100644 src/FormCollection/Resources/assets/.gitignore
diff --git a/src/FormCollection/.gitattributes b/src/FormCollection/.gitattributes
index dfe0770424b..17bf7a840e9 100644
--- a/src/FormCollection/.gitattributes
+++ b/src/FormCollection/.gitattributes
@@ -1,2 +1,3 @@
-# Auto detect text files and perform LF normalization
-* text=auto
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/Resources/assets/test export-ignore
diff --git a/src/FormCollection/Resources/assets/.gitignore b/src/FormCollection/Resources/assets/.gitignore
deleted file mode 100644
index 096746c1480..00000000000
--- a/src/FormCollection/Resources/assets/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/node_modules/
\ No newline at end of file
From 7741907030c362a60020cc4f91df1b3bd482b022 Mon Sep 17 00:00:00 2001
From: Albin <83301974+stakovicz@users.noreply.github.com>
Date: Fri, 7 May 2021 21:03:09 +0200
Subject: [PATCH 09/34] Move default values
From UXCollectionType to the view
---
src/FormCollection/Form/UXCollectionType.php | 10 +++++-----
src/FormCollection/README.md | 20 +++++++++++++++----
.../Resources/views/form_theme_div.html.twig | 18 ++++++++---------
.../views/form_theme_table.html.twig | 13 ++++++------
4 files changed, 37 insertions(+), 24 deletions(-)
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index 02ff3c0c514..d3c2d2e87e0 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -32,12 +32,12 @@ public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'button_add' => [
- 'text' => 'Add',
- 'attr' => ['class' => 'btn btn-outline-primary'],
+ 'text' => '',
+ 'class' => '',
],
'button_delete' => [
- 'text' => 'Remove',
- 'attr' => ['class' => 'btn btn-outline-secondary'],
+ 'text' => '',
+ 'class' => '',
],
]);
}
@@ -53,6 +53,6 @@ public function finishView(FormView $view, FormInterface $form, array $options)
public function getBlockPrefix()
{
- return 'form_collection';
+ return 'ux_collection';
}
}
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index f7c1b58a12a..77d48699750 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -53,14 +53,14 @@ class BlogFormType extends AbstractType
'button_add' => [
// Default text for the add button
'text' => 'Add',
- // Default attr class for the add button
- 'attr' => ['class' => 'btn btn-outline-primary']
+ // Add HTML classes to the add button
+ 'class' => 'btn btn-outline-primary'
],
'button_delete' => [
// Default text for the delete button
'text' => 'Remove',
- // Default class for the delete button
- 'attr' => ['class' => 'btn btn-outline-secondary']
+ // Add HTML classes to the add button
+ 'class' => 'btn btn-outline-secondary'
],
])
// ...
@@ -71,6 +71,18 @@ class BlogFormType extends AbstractType
}
```
+You can display it using Twig as you would normally with any form:
+
+```twig
+{# edit.html.twig #}
+
+{{ form_start(form) }}
+ {# ... #}
+ {{ form_row(comments) }}
+ {# ... #}
+{{ form_end(form) }}
+```
+
### Extend the default behavior
Symfony UX Form Collection allows you to extend its default behavior using a custom Stimulus controller:
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index 47f2a3c5b24..eae309cc496 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -1,17 +1,17 @@
{%- block button_add -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#add')|trim -%}
{{ button_add.text|trans }}
+ class="{{ button_add.class|default('') }}" type="button">{{ button_add.text|default('Add')|trans }}
{%- endblock button_add -%}
{%- block button_delete -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
- {{ button_delete.text|trans }}
+ class="{{ button_delete.class|default('') }}" type="button">
+ {{ button_delete.text|default('Remove')|trans }}
{%- endblock button_delete -%}
-{% block form_collection_widget -%}
+{% block ux_collection_widget -%}
{%- set controllerName = 'symfony--ux-form-collection--collection' -%}
{%- set dataController = (attr['data-controller']|default('') ~ ' ' ~ controllerName)|trim -%}
@@ -19,11 +19,11 @@
{%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
{% if prototype is defined and not prototype.rendered %}
{%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
index 75c5745b1ab..2ca17aadeda 100644
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -3,7 +3,7 @@
{{ button_add.text|trans }}
+ class="{{ button_add.class|default('') }}" type="button">{{ button_add.text|default('Add')|trans }}
{%- endblock button_add -%}
@@ -11,11 +11,11 @@
{%- block button_delete -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
- {{ button_delete.text|trans }}
+ class="{{ button_delete.class|default('') }}" type="button">
+ {{ button_delete.text|default('Remove')|trans }}
{%- endblock button_delete -%}
-{% block form_collection_widget -%}
+{% block ux_collection_widget -%}
{%- set controllerName = 'symfony--ux-form-collection--collection' -%}
{%- set dataController = (attr['data-controller']|default('') ~ ' ' ~ controllerName)|trim -%}
@@ -23,10 +23,11 @@
{%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
{% if prototype is defined and not prototype.rendered %}
{%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
From 66defabee2b376faadecfed81520fc701b4c210e Mon Sep 17 00:00:00 2001
From: Albin
Date: Sun, 23 May 2021 15:13:12 +0200
Subject: [PATCH 10/34] Predefined theme or not
---
src/FormCollection/README.md | 46 +++++++++++++--
.../Resources/assets/dist/controller.js | 54 +++++++++--------
.../Resources/assets/src/controller.js | 59 ++++++++++---------
.../Resources/views/form_theme_div.html.twig | 28 ++++-----
.../views/form_theme_table.html.twig | 14 ++---
5 files changed, 124 insertions(+), 77 deletions(-)
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index 77d48699750..224bc8cc7f7 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -20,6 +20,8 @@ yarn encore dev
Also make sure you have at least version 2.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge)
in your `package.json` file.
+## Use predefined theme
+
You need to select the right theme from the one you are using :
```yaml
# config/packages/twig.yaml
@@ -33,6 +35,42 @@ You have 2 different themes :
[Check the Symfony doc](https://symfony.com/doc/4.4/form/form_themes.html) for the different ways to set themes in Symfony.
+## Use manual theming
+
+> Consider your `BlogFormType` form set up and with a comments field that is a `CollectionType`, you can
+render it in your template:
+
+```twig
+{% macro commentFormRow(commentForm) %}
+
+ {{ form_errors(commentForm) }}
+ {{ form_row(commentForm.content) }}
+ {{ form_row(commentForm.otherField) }}
+
+
+ Remove
+
+
+{% endmacro %}
+
+
+ {% for commentForm in form.comments %}
+ {{ _self.commentFormRow(commentForm) }}
+ {% endfor %}
+
+
+ Add Another
+
+
+```
+
## Usage
The most common usage of Form Collection is to use it as a replacement of
@@ -51,15 +89,15 @@ class BlogFormType extends AbstractType
->add('comments', UXCollectionType::class, [
// ...
'button_add' => [
- // Default text for the add button
+ // Default text for the add button (used by predefined theme)
'text' => 'Add',
- // Add HTML classes to the add button
+ // Add HTML classes to the add button (used by predefined theme)
'class' => 'btn btn-outline-primary'
],
'button_delete' => [
- // Default text for the delete button
+ // Default text for the delete button (used by predefined theme)
'text' => 'Remove',
- // Add HTML classes to the add button
+ // Add HTML classes to the add button (used by predefined theme)
'class' => 'btn btn-outline-secondary'
],
])
diff --git a/src/FormCollection/Resources/assets/dist/controller.js b/src/FormCollection/Resources/assets/dist/controller.js
index 271c7ca00e2..15236f786d0 100644
--- a/src/FormCollection/Resources/assets/dist/controller.js
+++ b/src/FormCollection/Resources/assets/dist/controller.js
@@ -58,6 +58,11 @@ var _default = /*#__PURE__*/function (_Controller) {
key: "connect",
value: function connect() {
this.controllerName = this.context.scope.identifier;
+ this.index = this.entryTargets.length - 1;
+
+ if (!this.prototypeNameValue) {
+ this.prototypeNameValue = '__name__';
+ }
this._dispatchEvent('form-collection:pre-connect', {
allowAdd: this.allowAddValue,
@@ -68,16 +73,15 @@ var _default = /*#__PURE__*/function (_Controller) {
// Add button Add
var buttonAdd = this._textToNode(this.buttonAddValue);
- this.containerTarget.prepend(buttonAdd);
+ this.element.prepend(buttonAdd);
} // Add buttons Delete
if (true === this.allowDeleteValue) {
for (var i = 0; i < this.entryTargets.length; i++) {
- this.index = i;
var entry = this.entryTargets[i];
- this._addDeleteButton(entry, this.index);
+ this._addDeleteButton(entry, i);
}
}
@@ -91,7 +95,12 @@ var _default = /*#__PURE__*/function (_Controller) {
value: function add(event) {
this.index++; // Compute the new entry
- var newEntry = this.containerTarget.dataset.prototype;
+ var newEntry = this.element.dataset.prototype;
+
+ if (!newEntry) {
+ newEntry = this.prototypeValue;
+ }
+
var regExp = new RegExp(this.prototypeNameValue + 'label__', 'g');
newEntry = newEntry.replace(regExp, this.index);
regExp = new RegExp(this.prototypeNameValue, 'g');
@@ -103,7 +112,7 @@ var _default = /*#__PURE__*/function (_Controller) {
element: newEntry
});
- this.containerTarget.append(newEntry); // Retrieve the entry from targets to make sure that this is the one
+ this.element.append(newEntry); // Retrieve the entry from targets to make sure that this is the one
var entry = this.entryTargets[this.entryTargets.length - 1];
entry = this._addDeleteButton(entry, this.index);
@@ -116,25 +125,19 @@ var _default = /*#__PURE__*/function (_Controller) {
}, {
key: "delete",
value: function _delete(event) {
- var theIndexEntryToDelete = event.target.dataset.indexEntry; // Search the entry to delete from the data-index-entry attribute
-
- for (var i = 0; i < this.entryTargets.length; i++) {
- var entry = this.entryTargets[i];
+ var entry = event.target.closest('[data-' + this.controllerName + '-target="entry"]');
- if (theIndexEntryToDelete === entry.dataset.indexEntry) {
- this._dispatchEvent('form-collection:pre-delete', {
- index: entry.dataset.indexEntry,
- element: entry
- });
+ this._dispatchEvent('form-collection:pre-delete', {
+ index: entry.dataset.indexEntry,
+ element: entry
+ });
- entry.remove();
+ entry.remove();
- this._dispatchEvent('form-collection:delete', {
- index: entry.dataset.indexEntry,
- element: entry
- });
- }
- }
+ this._dispatchEvent('form-collection:delete', {
+ index: entry.dataset.indexEntry,
+ element: entry
+ });
}
/**
* Add the delete button to the entry
@@ -152,6 +155,10 @@ var _default = /*#__PURE__*/function (_Controller) {
var buttonDelete = this._textToNode(this.buttonDeleteValue);
+ if (!buttonDelete) {
+ return entry;
+ }
+
buttonDelete.dataset.indexEntry = index;
if ('TR' === entry.nodeName) {
@@ -195,12 +202,13 @@ var _default = /*#__PURE__*/function (_Controller) {
exports["default"] = _default;
-_defineProperty(_default, "targets", ['container', 'entry']);
+_defineProperty(_default, "targets", ['entry']);
_defineProperty(_default, "values", {
allowAdd: Boolean,
allowDelete: Boolean,
buttonAdd: String,
buttonDelete: String,
- prototypeName: String
+ prototypeName: String,
+ prototype: String
});
\ No newline at end of file
diff --git a/src/FormCollection/Resources/assets/src/controller.js b/src/FormCollection/Resources/assets/src/controller.js
index 72311e4ef4e..98a18addada 100644
--- a/src/FormCollection/Resources/assets/src/controller.js
+++ b/src/FormCollection/Resources/assets/src/controller.js
@@ -1,10 +1,9 @@
'use strict';
-import { Controller } from 'stimulus';
+import {Controller} from 'stimulus';
export default class extends Controller {
static targets = [
- 'container',
'entry'
];
@@ -13,7 +12,8 @@ export default class extends Controller {
allowDelete: Boolean,
buttonAdd: String,
buttonDelete: String,
- prototypeName: String
+ prototypeName: String,
+ prototype: String
};
/**
@@ -30,7 +30,11 @@ export default class extends Controller {
connect() {
this.controllerName = this.context.scope.identifier;
+ this.index = this.entryTargets.length - 1;
+ if (!this.prototypeNameValue) {
+ this.prototypeNameValue = '__name__';
+ }
this._dispatchEvent('form-collection:pre-connect', {
allowAdd: this.allowAddValue,
@@ -40,15 +44,14 @@ export default class extends Controller {
if (true === this.allowAddValue) {
// Add button Add
let buttonAdd = this._textToNode(this.buttonAddValue);
- this.containerTarget.prepend(buttonAdd);
+ this.element.prepend(buttonAdd);
}
// Add buttons Delete
if (true === this.allowDeleteValue) {
for (let i = 0; i < this.entryTargets.length; i++) {
- this.index = i;
let entry = this.entryTargets[i];
- this._addDeleteButton(entry, this.index);
+ this._addDeleteButton(entry, i);
}
}
@@ -63,9 +66,12 @@ export default class extends Controller {
this.index++;
// Compute the new entry
- let newEntry = this.containerTarget.dataset.prototype;
-
- let regExp = new RegExp(this.prototypeNameValue+'label__', 'g');
+ let newEntry = this.element.dataset.prototype;
+ if (!newEntry) {
+ newEntry = this.prototypeValue;
+ }
+
+ let regExp = new RegExp(this.prototypeNameValue + 'label__', 'g');
newEntry = newEntry.replace(regExp, this.index);
regExp = new RegExp(this.prototypeNameValue, 'g');
@@ -78,7 +84,7 @@ export default class extends Controller {
element: newEntry
});
- this.containerTarget.append(newEntry);
+ this.element.append(newEntry);
// Retrieve the entry from targets to make sure that this is the one
let entry = this.entryTargets[this.entryTargets.length - 1];
@@ -91,27 +97,19 @@ export default class extends Controller {
}
delete(event) {
+ let entry = event.target.closest('[data-' + this.controllerName + '-target="entry"]');
- let theIndexEntryToDelete = event.target.dataset.indexEntry;
-
- // Search the entry to delete from the data-index-entry attribute
- for (let i = 0; i < this.entryTargets.length; i++) {
- let entry = this.entryTargets[i];
- if (theIndexEntryToDelete === entry.dataset.indexEntry) {
-
- this._dispatchEvent('form-collection:pre-delete', {
- index: entry.dataset.indexEntry,
- element: entry
- });
+ this._dispatchEvent('form-collection:pre-delete', {
+ index: entry.dataset.indexEntry,
+ element: entry
+ });
- entry.remove();
+ entry.remove();
- this._dispatchEvent('form-collection:delete', {
- index: entry.dataset.indexEntry,
- element: entry
- });
- }
- }
+ this._dispatchEvent('form-collection:delete', {
+ index: entry.dataset.indexEntry,
+ element: entry
+ });
}
/**
@@ -127,9 +125,12 @@ export default class extends Controller {
entry.dataset.indexEntry = index;
let buttonDelete = this._textToNode(this.buttonDeleteValue);
+ if (!buttonDelete) {
+ return entry;
+ }
buttonDelete.dataset.indexEntry = index;
- if('TR' === entry.nodeName) {
+ if ('TR' === entry.nodeName) {
entry.lastElementChild.append(buttonDelete);
} else {
entry.append(buttonDelete);
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index eae309cc496..dff35ae93f3 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -18,31 +18,31 @@
{# attr for the data target on the entry of the collection #}
{%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
+ {% if prototype is defined and not prototype.rendered %}
+ {%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
+ {%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
+ {% endif %}
+ {%- set attr = attr|merge({('data-' ~ controllerName ~ '-target'): 'container' }) -%}
+
- {% if prototype is defined and not prototype.rendered %}
- {%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
- {%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
- {% endif %}
- {%- set attr = attr|merge({('data-' ~ controllerName ~ '-target'): 'container' }) -%}
+ {%- if form is rootform -%}
+ {{ form_errors(form) }}
+ {%- endif -%}
-
- {%- if form is rootform -%}
- {{ form_errors(form) }}
- {%- endif -%}
+ {% for child in form|filter(child => not child.rendered) %}
- {% for child in form|filter(child => not child.rendered) %}
+ {%- set child_attr = child.vars.attr|merge(attrDataTarget) -%}
+ {{- form_row(child, {'row_attr': child_attr}) -}}
- {%- set child_attr = child.vars.attr|merge(attrDataTarget) -%}
- {{- form_row(child, {'row_attr': child_attr}) -}}
+ {% endfor %}
- {% endfor %}
-
{{- form_rest(form) -}}
{%- endblock %}
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
index 2ca17aadeda..0bf6f012507 100644
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -22,20 +22,20 @@
{# attr for the data target on the entry of the collection #}
{%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
+ {% if prototype is defined and not prototype.rendered %}
+ {%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
+ {%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
+ {% endif %}
+
- {% if prototype is defined and not prototype.rendered %}
- {%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
- {%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
- {% endif %}
- {%- set attr = attr|merge({('data-' ~ controllerName ~ '-target'): 'container' }) -%}
-
-
+
{%- if form is rootform -%}
{{ form_errors(form) }}
{%- endif -%}
From 145b1cb2089b87bfc8e1ccbcaea6aabca88e3fe1 Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Mon, 24 May 2021 14:12:02 +0200
Subject: [PATCH 11/34] Update src/FormCollection/README.md
Co-authored-by: jmsche
---
src/FormCollection/README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index 224bc8cc7f7..24b23caa0cf 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -22,7 +22,8 @@ in your `package.json` file.
## Use predefined theme
-You need to select the right theme from the one you are using :
+You need to select the right theme from the one you are using:
+
```yaml
# config/packages/twig.yaml
twig:
@@ -188,4 +189,4 @@ Then in your render call, add your controller as an HTML attribute:
'data-controller' => 'mycollection'
]
]);
-```
\ No newline at end of file
+```
From 2b900cc45f10160b8d6c810c8855b481008910a1 Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Mon, 24 May 2021 14:12:10 +0200
Subject: [PATCH 12/34] Update src/FormCollection/README.md
Co-authored-by: jmsche
---
src/FormCollection/README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index 24b23caa0cf..82ebc7d9ae1 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -30,7 +30,9 @@ twig:
# For bootstrap for example
form_themes: ['@FormCollection/form_theme_div.html.twig']
```
-You have 2 different themes :
+
+There are 2 predefined themes available:
+
- `@FormCollection/form_theme_div.html.twig`
- `@FormCollection/form_theme_table.html.twig`
From 25c1454a725e8b5fa0e236581a9c329c02aa9b4a Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Mon, 24 May 2021 14:12:19 +0200
Subject: [PATCH 13/34] Update src/FormCollection/README.md
Co-authored-by: jmsche
---
src/FormCollection/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index 82ebc7d9ae1..ad71edb50d6 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -40,7 +40,7 @@ There are 2 predefined themes available:
## Use manual theming
-> Consider your `BlogFormType` form set up and with a comments field that is a `CollectionType`, you can
+Consider your `BlogFormType` form set up and with a comments field that is a `CollectionType`, you can
render it in your template:
```twig
From d66250d00d0da845893929e802c25ae7fff0630d Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Mon, 24 May 2021 14:12:50 +0200
Subject: [PATCH 14/34] Update
src/FormCollection/Resources/views/form_theme_div.html.twig
Co-authored-by: jmsche
---
src/FormCollection/Resources/views/form_theme_div.html.twig | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index dff35ae93f3..417604b2c87 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -27,8 +27,8 @@
From 2db95e071b8b4f4fbdcd79004bb23633126d00c9 Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Mon, 24 May 2021 14:14:53 +0200
Subject: [PATCH 15/34] Update src/FormCollection/README.md
Co-authored-by: jmsche
---
src/FormCollection/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index ad71edb50d6..a8e46bb827b 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -38,7 +38,7 @@ There are 2 predefined themes available:
[Check the Symfony doc](https://symfony.com/doc/4.4/form/form_themes.html) for the different ways to set themes in Symfony.
-## Use manual theming
+## Use a custom form theme
Consider your `BlogFormType` form set up and with a comments field that is a `CollectionType`, you can
render it in your template:
From 8f8b5c29a1cc3b1f1fb2730111bbb97f2aa13ff4 Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Mon, 24 May 2021 14:15:14 +0200
Subject: [PATCH 16/34] Update
src/FormCollection/Resources/views/form_theme_table.html.twig
Co-authored-by: jmsche
---
src/FormCollection/Resources/views/form_theme_table.html.twig | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
index 0bf6f012507..64e4bb3cec8 100644
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -30,8 +30,8 @@
From 4a68faae5b32a67d5a928ec4cd4ba63eb173dfe6 Mon Sep 17 00:00:00 2001
From: Albin
Date: Mon, 24 May 2021 14:45:01 +0200
Subject: [PATCH 17/34] Split in 4 options
---
src/FormCollection/Form/UXCollectionType.php | 23 +++++++++++--------
src/FormCollection/README.md | 20 +++++++---------
.../Resources/views/form_theme_div.html.twig | 6 ++---
.../views/form_theme_table.html.twig | 6 ++---
4 files changed, 27 insertions(+), 28 deletions(-)
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index d3c2d2e87e0..095807cb0e5 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -31,23 +31,26 @@ public function getParent()
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
- 'button_add' => [
- 'text' => '',
- 'class' => '',
- ],
- 'button_delete' => [
- 'text' => '',
- 'class' => '',
- ],
+ 'button_add_text' => 'Add',
+ 'button_add_class' => '',
+ 'button_delete_text' => 'Remove',
+ 'button_delete_class' => '',
]);
+
+ $resolver->setAllowedTypes('button_add_text', 'string');
+ $resolver->setAllowedTypes('button_add_class', 'string');
+ $resolver->setAllowedTypes('button_delete_text', 'string');
+ $resolver->setAllowedTypes('button_delete_class', 'string');
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
parent::finishView($view, $form, $options);
- $view->vars['button_add'] = $options['button_add'];
- $view->vars['button_delete'] = $options['button_delete'];
+ $view->vars['button_add_text'] = $options['button_add_text'];
+ $view->vars['button_add_class'] = $options['button_add_class'];
+ $view->vars['button_delete_text'] = $options['button_delete_text'];
+ $view->vars['button_delete_class'] = $options['button_delete_class'];
$view->vars['prototype_name'] = $options['prototype_name'];
}
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index a8e46bb827b..de3a6a405aa 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -91,18 +91,14 @@ class BlogFormType extends AbstractType
// ...
->add('comments', UXCollectionType::class, [
// ...
- 'button_add' => [
- // Default text for the add button (used by predefined theme)
- 'text' => 'Add',
- // Add HTML classes to the add button (used by predefined theme)
- 'class' => 'btn btn-outline-primary'
- ],
- 'button_delete' => [
- // Default text for the delete button (used by predefined theme)
- 'text' => 'Remove',
- // Add HTML classes to the add button (used by predefined theme)
- 'class' => 'btn btn-outline-secondary'
- ],
+ // Default text for the add button (used by predefined theme)
+ 'button_add_text' => 'Add',
+ // Add HTML classes to the add button (used by predefined theme)
+ 'button_add_class' => 'btn btn-outline-primary',
+ // Default text for the delete button (used by predefined theme)
+ 'button_delete_text' => 'Remove',
+ // Add HTML classes to the add button (used by predefined theme)
+ 'button_delete_class' => 'btn btn-outline-secondary',
])
// ...
;
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index 417604b2c87..fb2aabcb48e 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -1,14 +1,14 @@
{%- block button_add -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#add')|trim -%}
{{ button_add.text|default('Add')|trans }}
+ class="{{ button_add_class }}" type="button">{{ button_add_text|trans }}
{%- endblock button_add -%}
{%- block button_delete -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
- {{ button_delete.text|default('Remove')|trans }}
+ class="{{ button_delete_class }}" type="button">
+ {{ button_delete_text|trans }}
{%- endblock button_delete -%}
{% block ux_collection_widget -%}
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
index 64e4bb3cec8..4522d831a4d 100644
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -3,7 +3,7 @@
{{ button_add.text|default('Add')|trans }}
+ class="{{ button_add_class }}" type="button">{{ button_add_text|trans }}
{%- endblock button_add -%}
@@ -11,8 +11,8 @@
{%- block button_delete -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
- {{ button_delete.text|default('Remove')|trans }}
+ class="{{ button_delete_class }}" type="button">
+ {{ button_delete_text|trans }}
{%- endblock button_delete -%}
{% block ux_collection_widget -%}
From a170618aec95f5b5e7007182d8d05fdfd2871627 Mon Sep 17 00:00:00 2001
From: Albin
Date: Sun, 6 Jun 2021 13:54:44 +0200
Subject: [PATCH 18/34] Default startIndex value
---
src/FormCollection/Resources/assets/dist/controller.js | 5 +++--
src/FormCollection/Resources/assets/src/controller.js | 5 +++--
src/FormCollection/Resources/views/form_theme_div.html.twig | 4 ++++
.../Resources/views/form_theme_table.html.twig | 4 ++++
4 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/src/FormCollection/Resources/assets/dist/controller.js b/src/FormCollection/Resources/assets/dist/controller.js
index 15236f786d0..e3aced57ace 100644
--- a/src/FormCollection/Resources/assets/dist/controller.js
+++ b/src/FormCollection/Resources/assets/dist/controller.js
@@ -58,7 +58,7 @@ var _default = /*#__PURE__*/function (_Controller) {
key: "connect",
value: function connect() {
this.controllerName = this.context.scope.identifier;
- this.index = this.entryTargets.length - 1;
+ this.index = this.startIndexValue ? this.startIndexValue : this.entryTargets.length - 1;
if (!this.prototypeNameValue) {
this.prototypeNameValue = '__name__';
@@ -210,5 +210,6 @@ _defineProperty(_default, "values", {
buttonAdd: String,
buttonDelete: String,
prototypeName: String,
- prototype: String
+ prototype: String,
+ startIndex: Number
});
\ No newline at end of file
diff --git a/src/FormCollection/Resources/assets/src/controller.js b/src/FormCollection/Resources/assets/src/controller.js
index 98a18addada..b8547947220 100644
--- a/src/FormCollection/Resources/assets/src/controller.js
+++ b/src/FormCollection/Resources/assets/src/controller.js
@@ -13,7 +13,8 @@ export default class extends Controller {
buttonAdd: String,
buttonDelete: String,
prototypeName: String,
- prototype: String
+ prototype: String,
+ startIndex: Number
};
/**
@@ -30,7 +31,7 @@ export default class extends Controller {
connect() {
this.controllerName = this.context.scope.identifier;
- this.index = this.entryTargets.length - 1;
+ this.index = this.startIndexValue ? this.startIndexValue : this.entryTargets.length - 1;
if (!this.prototypeNameValue) {
this.prototypeNameValue = '__name__';
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index fb2aabcb48e..cf1e9f8326b 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -24,12 +24,16 @@
{% endif %}
{%- set attr = attr|merge({('data-' ~ controllerName ~ '-target'): 'container' }) -%}
+ {% set indexKeys = data|keys %}
+ {% set startIndex = indexKeys|length == 0 ? 0 : max(indexKeys) %}
+
{%- if form is rootform -%}
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
index 4522d831a4d..5692477a281 100644
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -27,12 +27,16 @@
{%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
{% endif %}
+ {% set indexKeys = data|keys %}
+ {% set startIndex = indexKeys|length == 0 ? 0 : max(indexKeys) %}
+
From 13561e7841e66a07d01e1267addade60268fbe2b Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Wed, 21 Jul 2021 08:30:07 +0200
Subject: [PATCH 19/34] Update
src/FormCollection/Resources/views/form_theme_div.html.twig
Co-authored-by: Romain Monteil
---
src/FormCollection/Resources/views/form_theme_div.html.twig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index cf1e9f8326b..21ed12e5d73 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -19,7 +19,7 @@
{%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
{% if prototype is defined and not prototype.rendered %}
- {%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
+ {%- set prototype_attr = prototype.vars.row_attr|merge(attrDataTarget) -%}
{%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
{% endif %}
{%- set attr = attr|merge({('data-' ~ controllerName ~ '-target'): 'container' }) -%}
From fb847c6fc77f49c7f7e8c0f1a7eb3a396fda501e Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Wed, 21 Jul 2021 08:30:15 +0200
Subject: [PATCH 20/34] Update
src/FormCollection/Resources/views/form_theme_div.html.twig
Co-authored-by: Romain Monteil
---
src/FormCollection/Resources/views/form_theme_div.html.twig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index 21ed12e5d73..3380930e574 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -42,7 +42,7 @@
{% for child in form|filter(child => not child.rendered) %}
- {%- set child_attr = child.vars.attr|merge(attrDataTarget) -%}
+ {%- set child_attr = child.vars.row_attr|merge(attrDataTarget) -%}
{{- form_row(child, {'row_attr': child_attr}) -}}
{% endfor %}
From 740d19bcb9c73ca3a665cd995d14ee858c67a740 Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Wed, 21 Jul 2021 08:30:22 +0200
Subject: [PATCH 21/34] Update
src/FormCollection/Resources/views/form_theme_table.html.twig
Co-authored-by: Romain Monteil
---
src/FormCollection/Resources/views/form_theme_table.html.twig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
index 5692477a281..ff2a9a46b4c 100644
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -23,7 +23,7 @@
{%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
{% if prototype is defined and not prototype.rendered %}
- {%- set prototype_attr = prototype.vars.attr|merge(attrDataTarget) -%}
+ {%- set prototype_attr = prototype.vars.row_attr|merge(attrDataTarget) -%}
{%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
{% endif %}
From c57d86ede02497de20543caa255c1dcda427cd6b Mon Sep 17 00:00:00 2001
From: Stakovicz <83301974+stakovicz@users.noreply.github.com>
Date: Wed, 21 Jul 2021 08:30:38 +0200
Subject: [PATCH 22/34] Update
src/FormCollection/Resources/views/form_theme_table.html.twig
Co-authored-by: Romain Monteil
---
src/FormCollection/Resources/views/form_theme_table.html.twig | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
index ff2a9a46b4c..77af4be8510 100644
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -46,7 +46,7 @@
{% for child in form|filter(child => not child.rendered) %}
- {%- set child_attr = child.vars.attr|merge(attrDataTarget) -%}
+ {%- set child_attr = child.vars.row_attr|merge(attrDataTarget) -%}
{{- form_row(child, {'row_attr': child_attr}) -}}
{% endfor %}
From 0bbac2f4ff92ac46535bb669578dbf9f33ddbfa3 Mon Sep 17 00:00:00 2001
From: akester
Date: Sat, 6 Nov 2021 21:04:18 +0100
Subject: [PATCH 23/34] Fix coding-style-js
---
src/FormCollection/Resources/assets/src/controller.js | 2 +-
src/FormCollection/Resources/assets/test/controller.test.js | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/FormCollection/Resources/assets/src/controller.js b/src/FormCollection/Resources/assets/src/controller.js
index b8547947220..d1341691675 100644
--- a/src/FormCollection/Resources/assets/src/controller.js
+++ b/src/FormCollection/Resources/assets/src/controller.js
@@ -62,7 +62,7 @@ export default class extends Controller {
});
}
- add(event) {
+ add() {
this.index++;
diff --git a/src/FormCollection/Resources/assets/test/controller.test.js b/src/FormCollection/Resources/assets/test/controller.test.js
index f764a48d6e8..e22620cfc25 100644
--- a/src/FormCollection/Resources/assets/test/controller.test.js
+++ b/src/FormCollection/Resources/assets/test/controller.test.js
@@ -11,7 +11,6 @@
import { Application, Controller } from 'stimulus';
import { getByTestId, waitFor } from '@testing-library/dom';
-import user from '@testing-library/user-event';
import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
import FormCollectionController from '../dist/controller';
From edfe323c6b0a97a957405d5a1e1a88a13f43774d Mon Sep 17 00:00:00 2001
From: akester
Date: Sat, 6 Nov 2021 21:09:41 +0100
Subject: [PATCH 24/34] Prettier
---
src/FormCollection/README.md | 22 +++++++++----------
.../Resources/assets/src/controller.js | 19 ++++++----------
.../Resources/assets/test/controller.test.js | 1 -
3 files changed, 18 insertions(+), 24 deletions(-)
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index de3a6a405aa..8de796c67cd 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -27,14 +27,14 @@ You need to select the right theme from the one you are using:
```yaml
# config/packages/twig.yaml
twig:
- # For bootstrap for example
- form_themes: ['@FormCollection/form_theme_div.html.twig']
+ # For bootstrap for example
+ form_themes: ['@FormCollection/form_theme_div.html.twig']
```
There are 2 predefined themes available:
-- `@FormCollection/form_theme_div.html.twig`
-- `@FormCollection/form_theme_table.html.twig`
+- `@FormCollection/form_theme_div.html.twig`
+- `@FormCollection/form_theme_table.html.twig`
[Check the Symfony doc](https://symfony.com/doc/4.4/form/form_themes.html) for the different ways to set themes in Symfony.
@@ -92,11 +92,11 @@ class BlogFormType extends AbstractType
->add('comments', UXCollectionType::class, [
// ...
// Default text for the add button (used by predefined theme)
- 'button_add_text' => 'Add',
+ 'button_add_text' => 'Add',
// Add HTML classes to the add button (used by predefined theme)
'button_add_class' => 'btn btn-outline-primary',
// Default text for the delete button (used by predefined theme)
- 'button_delete_text' => 'Remove',
+ 'button_delete_text' => 'Remove',
// Add HTML classes to the add button (used by predefined theme)
'button_delete_class' => 'btn btn-outline-secondary',
])
@@ -147,7 +147,7 @@ export default class extends Controller {
_onPreConnect(event) {
// The collection is not yet connected
- console.log(event.detail.allowAdd); // Access to the allow_add option of the form
+ console.log(event.detail.allowAdd); // Access to the allow_add option of the form
console.log(event.detail.allowDelete); // Access to the allow_delete option of the form
}
@@ -156,7 +156,7 @@ export default class extends Controller {
}
_onPreAdd(event) {
- console.log(event.detail.index); // Access to the curent index will be added
+ console.log(event.detail.index); // Access to the curent index will be added
console.log(event.detail.element); // Access to the element will be added
}
@@ -165,7 +165,7 @@ export default class extends Controller {
}
_onPreDelete(event) {
- console.log(event.detail.index); // Access to the index will be removed
+ console.log(event.detail.index); // Access to the index will be removed
console.log(event.detail.element); // Access to the elemnt will be removed
}
@@ -183,8 +183,8 @@ Then in your render call, add your controller as an HTML attribute:
->add('comments', UXCollectionType::class, [
// ...
'attr' => [
- // Change the controller name
- 'data-controller' => 'mycollection'
+ // Change the controller name
+ 'data-controller' => 'mycollection'
]
]);
```
diff --git a/src/FormCollection/Resources/assets/src/controller.js b/src/FormCollection/Resources/assets/src/controller.js
index d1341691675..6aae58379e5 100644
--- a/src/FormCollection/Resources/assets/src/controller.js
+++ b/src/FormCollection/Resources/assets/src/controller.js
@@ -1,11 +1,9 @@
'use strict';
-import {Controller} from 'stimulus';
+import { Controller } from 'stimulus';
export default class extends Controller {
- static targets = [
- 'entry'
- ];
+ static targets = ['entry'];
static values = {
allowAdd: Boolean,
@@ -14,7 +12,7 @@ export default class extends Controller {
buttonDelete: String,
prototypeName: String,
prototype: String,
- startIndex: Number
+ startIndex: Number,
};
/**
@@ -63,7 +61,6 @@ export default class extends Controller {
}
add() {
-
this.index++;
// Compute the new entry
@@ -82,7 +79,7 @@ export default class extends Controller {
this._dispatchEvent('form-collection:pre-add', {
index: this.index,
- element: newEntry
+ element: newEntry,
});
this.element.append(newEntry);
@@ -93,7 +90,7 @@ export default class extends Controller {
this._dispatchEvent('form-collection:add', {
index: this.index,
- element: entry
+ element: entry,
});
}
@@ -102,14 +99,14 @@ export default class extends Controller {
this._dispatchEvent('form-collection:pre-delete', {
index: entry.dataset.indexEntry,
- element: entry
+ element: entry,
});
entry.remove();
this._dispatchEvent('form-collection:delete', {
index: entry.dataset.indexEntry,
- element: entry
+ element: entry,
});
}
@@ -121,7 +118,6 @@ export default class extends Controller {
* @private
*/
_addDeleteButton(entry, index) {
-
// link the button and the entry by the data-index-entry attribute
entry.dataset.indexEntry = index;
@@ -147,7 +143,6 @@ export default class extends Controller {
* @private
*/
_textToNode(text) {
-
let template = document.createElement('template');
text = text.trim(); // Never return a text node of whitespace as the result
template.innerHTML = text;
diff --git a/src/FormCollection/Resources/assets/test/controller.test.js b/src/FormCollection/Resources/assets/test/controller.test.js
index e22620cfc25..ea9b89903df 100644
--- a/src/FormCollection/Resources/assets/test/controller.test.js
+++ b/src/FormCollection/Resources/assets/test/controller.test.js
@@ -55,5 +55,4 @@ describe('FormCollectionController', () => {
await waitFor(() => expect(getByTestId(container, 'container')).toHaveClass('connected'));
await waitFor(() => expect(getByTestId(container, 'container')).toHaveClass('pre-connected'));
});
-
});
From 086c3a303c77698e068d1acb1c656e6886f15186 Mon Sep 17 00:00:00 2001
From: akester
Date: Sat, 21 May 2022 21:43:57 +0200
Subject: [PATCH 25/34] Rebase and refresh the code
---
.../Resources/assets/dist/controller.js | 300 ++++++------------
.../Resources/assets/jest.config.js | 5 +
.../Resources/assets/package.json | 20 +-
.../src/{controller.js => controller.ts} | 51 ++-
...{controller.test.js => controller.test.ts} | 4 +-
5 files changed, 128 insertions(+), 252 deletions(-)
create mode 100644 src/FormCollection/Resources/assets/jest.config.js
rename src/FormCollection/Resources/assets/src/{controller.js => controller.ts} (75%)
rename src/FormCollection/Resources/assets/test/{controller.test.js => controller.test.ts} (95%)
diff --git a/src/FormCollection/Resources/assets/dist/controller.js b/src/FormCollection/Resources/assets/dist/controller.js
index e3aced57ace..8e5ae980139 100644
--- a/src/FormCollection/Resources/assets/dist/controller.js
+++ b/src/FormCollection/Resources/assets/dist/controller.js
@@ -1,215 +1,105 @@
-'use strict';
+import { Controller } from '@hotwired/stimulus';
-function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
-
-Object.defineProperty(exports, "__esModule", {
- value: true
-});
-exports["default"] = void 0;
-
-var _stimulus = require("stimulus");
-
-function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
-
-function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
-
-function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
-
-function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
-
-function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
-
-function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
-
-function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
-
-function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
-
-function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
-
-function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
-
-function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
-
-var _default = /*#__PURE__*/function (_Controller) {
- _inherits(_default, _Controller);
-
- var _super = _createSuper(_default);
-
- function _default() {
- var _this;
-
- _classCallCheck(this, _default);
-
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
- args[_key] = arguments[_key];
+class default_1 extends Controller {
+ constructor() {
+ super(...arguments);
+ this.index = 0;
}
-
- _this = _super.call.apply(_super, [this].concat(args));
-
- _defineProperty(_assertThisInitialized(_this), "index", 0);
-
- _defineProperty(_assertThisInitialized(_this), "controllerName", null);
-
- return _this;
- }
-
- _createClass(_default, [{
- key: "connect",
- value: function connect() {
- this.controllerName = this.context.scope.identifier;
- this.index = this.startIndexValue ? this.startIndexValue : this.entryTargets.length - 1;
-
- if (!this.prototypeNameValue) {
- this.prototypeNameValue = '__name__';
- }
-
- this._dispatchEvent('form-collection:pre-connect', {
- allowAdd: this.allowAddValue,
- allowDelete: this.allowDeleteValue
- });
-
- if (true === this.allowAddValue) {
- // Add button Add
- var buttonAdd = this._textToNode(this.buttonAddValue);
-
- this.element.prepend(buttonAdd);
- } // Add buttons Delete
-
-
- if (true === this.allowDeleteValue) {
- for (var i = 0; i < this.entryTargets.length; i++) {
- var entry = this.entryTargets[i];
-
- this._addDeleteButton(entry, i);
+ connect() {
+ this.controllerName = this.context.scope.identifier;
+ this.index = this.startIndexValue ? this.startIndexValue : this.entryTargets.length - 1;
+ if (!this.prototypeNameValue) {
+ this.prototypeNameValue = '__name__';
}
- }
-
- this._dispatchEvent('form-collection:connect', {
- allowAdd: this.allowAddValue,
- allowDelete: this.allowDeleteValue
- });
+ this._dispatchEvent('form-collection:pre-connect', {
+ allowAdd: this.allowAddValue,
+ allowDelete: this.allowDeleteValue,
+ });
+ if (this.allowAddValue) {
+ const buttonAdd = this._textToNode(this.buttonAddValue);
+ this.element.prepend(buttonAdd);
+ }
+ if (this.allowDeleteValue) {
+ for (let i = 0; i < this.entryTargets.length; i++) {
+ const entry = this.entryTargets[i];
+ this._addDeleteButton(entry, i);
+ }
+ }
+ this._dispatchEvent('form-collection:connect', {
+ allowAdd: this.allowAddValue,
+ allowDelete: this.allowDeleteValue,
+ });
}
- }, {
- key: "add",
- value: function add(event) {
- this.index++; // Compute the new entry
-
- var newEntry = this.element.dataset.prototype;
-
- if (!newEntry) {
- newEntry = this.prototypeValue;
- }
-
- var regExp = new RegExp(this.prototypeNameValue + 'label__', 'g');
- newEntry = newEntry.replace(regExp, this.index);
- regExp = new RegExp(this.prototypeNameValue, 'g');
- newEntry = newEntry.replace(regExp, this.index);
- newEntry = this._textToNode(newEntry);
-
- this._dispatchEvent('form-collection:pre-add', {
- index: this.index,
- element: newEntry
- });
-
- this.element.append(newEntry); // Retrieve the entry from targets to make sure that this is the one
-
- var entry = this.entryTargets[this.entryTargets.length - 1];
- entry = this._addDeleteButton(entry, this.index);
-
- this._dispatchEvent('form-collection:add', {
- index: this.index,
- element: entry
- });
+ add() {
+ this.index++;
+ let newEntry = this.element.dataset.prototype;
+ if (!newEntry) {
+ newEntry = this.prototypeValue;
+ }
+ let regExp = new RegExp(this.prototypeNameValue + 'label__', 'g');
+ newEntry = newEntry.replace(regExp, this.index);
+ regExp = new RegExp(this.prototypeNameValue, 'g');
+ newEntry = newEntry.replace(regExp, this.index);
+ newEntry = this._textToNode(newEntry);
+ this._dispatchEvent('form-collection:pre-add', {
+ index: this.index,
+ element: newEntry,
+ });
+ this.element.append(newEntry);
+ let entry = this.entryTargets[this.entryTargets.length - 1];
+ entry = this._addDeleteButton(entry, this.index);
+ this._dispatchEvent('form-collection:add', {
+ index: this.index,
+ element: entry,
+ });
}
- }, {
- key: "delete",
- value: function _delete(event) {
- var entry = event.target.closest('[data-' + this.controllerName + '-target="entry"]');
-
- this._dispatchEvent('form-collection:pre-delete', {
- index: entry.dataset.indexEntry,
- element: entry
- });
-
- entry.remove();
-
- this._dispatchEvent('form-collection:delete', {
- index: entry.dataset.indexEntry,
- element: entry
- });
+ delete(event) {
+ const entry = event.target.closest('[data-' + this.controllerName + '-target="entry"]');
+ this._dispatchEvent('form-collection:pre-delete', {
+ index: entry.dataset.indexEntry,
+ element: entry,
+ });
+ entry.remove();
+ this._dispatchEvent('form-collection:delete', {
+ index: entry.dataset.indexEntry,
+ element: entry,
+ });
}
- /**
- * Add the delete button to the entry
- * @param String entry
- * @param Number index
- * @returns {ChildNode}
- * @private
- */
-
- }, {
- key: "_addDeleteButton",
- value: function _addDeleteButton(entry, index) {
- // link the button and the entry by the data-index-entry attribute
- entry.dataset.indexEntry = index;
-
- var buttonDelete = this._textToNode(this.buttonDeleteValue);
-
- if (!buttonDelete) {
+ _addDeleteButton(entry, index) {
+ entry.dataset.indexEntry = index.toString();
+ const buttonDelete = this._textToNode(this.buttonDeleteValue);
+ if (!buttonDelete) {
+ return entry;
+ }
+ buttonDelete.dataset.indexEntry = index;
+ if ('TR' === entry.nodeName) {
+ entry.lastElementChild.append(buttonDelete);
+ }
+ else {
+ entry.append(buttonDelete);
+ }
return entry;
- }
-
- buttonDelete.dataset.indexEntry = index;
-
- if ('TR' === entry.nodeName) {
- entry.lastElementChild.append(buttonDelete);
- } else {
- entry.append(buttonDelete);
- }
-
- return entry;
}
- /**
- * Convert text to Element to insert in the DOM
- * @param String text
- * @returns {ChildNode}
- * @private
- */
-
- }, {
- key: "_textToNode",
- value: function _textToNode(text) {
- var template = document.createElement('template');
- text = text.trim(); // Never return a text node of whitespace as the result
-
- template.innerHTML = text;
- return template.content.firstChild;
+ _textToNode(text) {
+ const template = document.createElement('template');
+ text = text.trim();
+ template.innerHTML = text;
+ return template.content.firstChild;
}
- }, {
- key: "_dispatchEvent",
- value: function _dispatchEvent(name) {
- var payload = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
- var canBubble = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
- var cancelable = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
- var userEvent = document.createEvent('CustomEvent');
- userEvent.initCustomEvent(name, canBubble, cancelable, payload);
- this.element.dispatchEvent(userEvent);
+ _dispatchEvent(name, payload) {
+ console.log('TTTT');
+ this.element.dispatchEvent(new CustomEvent(name, { detail: payload }));
}
- }]);
-
- return _default;
-}(_stimulus.Controller);
-
-exports["default"] = _default;
-
-_defineProperty(_default, "targets", ['entry']);
-
-_defineProperty(_default, "values", {
- allowAdd: Boolean,
- allowDelete: Boolean,
- buttonAdd: String,
- buttonDelete: String,
- prototypeName: String,
- prototype: String,
- startIndex: Number
-});
\ No newline at end of file
+}
+default_1.targets = ['entry'];
+default_1.values = {
+ allowAdd: Boolean,
+ allowDelete: Boolean,
+ buttonAdd: String,
+ buttonDelete: String,
+ prototypeName: String,
+ prototype: String,
+ startIndex: Number,
+};
+
+export { default_1 as default };
diff --git a/src/FormCollection/Resources/assets/jest.config.js b/src/FormCollection/Resources/assets/jest.config.js
new file mode 100644
index 00000000000..5cc9ba23dab
--- /dev/null
+++ b/src/FormCollection/Resources/assets/jest.config.js
@@ -0,0 +1,5 @@
+const config = require('../../../../jest.config.js');
+
+config.setupFilesAfterEnv.push('./test/setup.js');
+
+module.exports = config;
diff --git a/src/FormCollection/Resources/assets/package.json b/src/FormCollection/Resources/assets/package.json
index f30194403c6..bccdc162439 100644
--- a/src/FormCollection/Resources/assets/package.json
+++ b/src/FormCollection/Resources/assets/package.json
@@ -13,26 +13,10 @@
}
}
},
- "scripts": {
- "build": "babel src -d dist",
- "test": "babel src -d dist && jest",
- "lint": "eslint src test"
- },
"peerDependencies": {
- "stimulus": "^2.0.0"
+ "@hotwired/stimulus": "^3.0.0"
},
"devDependencies": {
- "@babel/cli": "^7.12.1",
- "@babel/core": "^7.12.3",
- "@babel/plugin-proposal-class-properties": "^7.12.1",
- "@babel/preset-env": "^7.12.7",
- "@symfony/stimulus-testing": "^1.1.0",
- "stimulus": "^2.0.0"
- },
- "jest": {
- "testRegex": "test/.*\\.test.js",
- "setupFilesAfterEnv": [
- "./test/setup.js"
- ]
+ "@hotwired/stimulus": "^3.0.0"
}
}
diff --git a/src/FormCollection/Resources/assets/src/controller.js b/src/FormCollection/Resources/assets/src/controller.ts
similarity index 75%
rename from src/FormCollection/Resources/assets/src/controller.js
rename to src/FormCollection/Resources/assets/src/controller.ts
index 6aae58379e5..fcc288fe3ab 100644
--- a/src/FormCollection/Resources/assets/src/controller.js
+++ b/src/FormCollection/Resources/assets/src/controller.ts
@@ -1,6 +1,6 @@
'use strict';
-import { Controller } from 'stimulus';
+import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['entry'];
@@ -15,17 +15,22 @@ export default class extends Controller {
startIndex: Number,
};
+ allowAddValue: boolean;
+ allowDeleteValue: boolean;
+ buttonAddValue: string;
+ buttonDeleteValue: string;
+ prototypeNameValue: string;
+ prototypeValue: string;
+ startIndexValue: number;
+
/**
* Number of elements for the index of the collection
- * @type Number
*/
index = 0;
- /**
- * Controller name of this
- * @type String|null
- */
- controllerName = null;
+ controllerName: string;
+
+ entryTargets: Array;
connect() {
this.controllerName = this.context.scope.identifier;
@@ -40,16 +45,16 @@ export default class extends Controller {
allowDelete: this.allowDeleteValue,
});
- if (true === this.allowAddValue) {
+ if (this.allowAddValue) {
// Add button Add
- let buttonAdd = this._textToNode(this.buttonAddValue);
+ const buttonAdd = this._textToNode(this.buttonAddValue);
this.element.prepend(buttonAdd);
}
// Add buttons Delete
- if (true === this.allowDeleteValue) {
+ if (this.allowDeleteValue) {
for (let i = 0; i < this.entryTargets.length; i++) {
- let entry = this.entryTargets[i];
+ const entry = this.entryTargets[i];
this._addDeleteButton(entry, i);
}
}
@@ -95,7 +100,7 @@ export default class extends Controller {
}
delete(event) {
- let entry = event.target.closest('[data-' + this.controllerName + '-target="entry"]');
+ const entry = event.target.closest('[data-' + this.controllerName + '-target="entry"]');
this._dispatchEvent('form-collection:pre-delete', {
index: entry.dataset.indexEntry,
@@ -112,16 +117,13 @@ export default class extends Controller {
/**
* Add the delete button to the entry
- * @param String entry
- * @param Number index
- * @returns {ChildNode}
* @private
*/
- _addDeleteButton(entry, index) {
+ _addDeleteButton(entry: HTMLElement, index: number) {
// link the button and the entry by the data-index-entry attribute
- entry.dataset.indexEntry = index;
+ entry.dataset.indexEntry = index.toString();
- let buttonDelete = this._textToNode(this.buttonDeleteValue);
+ const buttonDelete = this._textToNode(this.buttonDeleteValue);
if (!buttonDelete) {
return entry;
}
@@ -138,22 +140,17 @@ export default class extends Controller {
/**
* Convert text to Element to insert in the DOM
- * @param String text
- * @returns {ChildNode}
* @private
*/
- _textToNode(text) {
- let template = document.createElement('template');
+ _textToNode(text: string) {
+ const template = document.createElement('template');
text = text.trim(); // Never return a text node of whitespace as the result
template.innerHTML = text;
return template.content.firstChild;
}
- _dispatchEvent(name, payload = null, canBubble = false, cancelable = false) {
- const userEvent = document.createEvent('CustomEvent');
- userEvent.initCustomEvent(name, canBubble, cancelable, payload);
-
- this.element.dispatchEvent(userEvent);
+ _dispatchEvent(name: string, payload: any) {
+ this.element.dispatchEvent(new CustomEvent(name, { detail: payload }));
}
}
diff --git a/src/FormCollection/Resources/assets/test/controller.test.js b/src/FormCollection/Resources/assets/test/controller.test.ts
similarity index 95%
rename from src/FormCollection/Resources/assets/test/controller.test.js
rename to src/FormCollection/Resources/assets/test/controller.test.ts
index ea9b89903df..62087209499 100644
--- a/src/FormCollection/Resources/assets/test/controller.test.js
+++ b/src/FormCollection/Resources/assets/test/controller.test.ts
@@ -12,7 +12,7 @@
import { Application, Controller } from 'stimulus';
import { getByTestId, waitFor } from '@testing-library/dom';
import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
-import FormCollectionController from '../dist/controller';
+import FormCollectionController from '../src/controller';
// Controller used to check the actual controller was properly booted
class CheckController extends Controller {
@@ -33,7 +33,7 @@ const startStimulus = () => {
};
describe('FormCollectionController', () => {
- let container;
+ let container: HTMLElement;
beforeEach(() => {
container = mountDOM(`
From 480dbb508f37521acf52751e1c9e3aa3c0b40d6c Mon Sep 17 00:00:00 2001
From: akester
Date: Sat, 21 May 2022 22:33:34 +0200
Subject: [PATCH 26/34] fix TU
---
.../Resources/assets/test/controller.test.ts | 34 +++++++++++--------
1 file changed, 20 insertions(+), 14 deletions(-)
diff --git a/src/FormCollection/Resources/assets/test/controller.test.ts b/src/FormCollection/Resources/assets/test/controller.test.ts
index 62087209499..bad41d3fbf4 100644
--- a/src/FormCollection/Resources/assets/test/controller.test.ts
+++ b/src/FormCollection/Resources/assets/test/controller.test.ts
@@ -9,7 +9,7 @@
'use strict';
-import { Application, Controller } from 'stimulus';
+import { Application, Controller } from '@hotwired/stimulus';
import { getByTestId, waitFor } from '@testing-library/dom';
import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
import FormCollectionController from '../src/controller';
@@ -30,29 +30,35 @@ const startStimulus = () => {
const application = Application.start();
application.register('check', CheckController);
application.register('formCollection', FormCollectionController);
+
+ return application;
};
describe('FormCollectionController', () => {
- let container: HTMLElement;
-
- beforeEach(() => {
- container = mountDOM(`
-
-
-
- `);
- });
+ let application;
afterEach(() => {
clearDOM();
+ application.stop();
});
it('events', async () => {
- expect(getByTestId(container, 'container')).not.toHaveClass('connected');
+ const container = mountDOM(`
+
+
+ `);
+
expect(getByTestId(container, 'container')).not.toHaveClass('pre-connected');
+ expect(getByTestId(container, 'container')).not.toHaveClass('connected');
+
+ application = startStimulus();
- startStimulus();
- await waitFor(() => expect(getByTestId(container, 'container')).toHaveClass('connected'));
- await waitFor(() => expect(getByTestId(container, 'container')).toHaveClass('pre-connected'));
+ await waitFor(() => {
+ expect(getByTestId(container, 'container')).toHaveClass('pre-connected')
+ expect(getByTestId(container, 'container')).toHaveClass('connected')
+ });
});
});
From 3223fae4687ba83ae4432da2d80942685eeff884 Mon Sep 17 00:00:00 2001
From: akester
Date: Sat, 21 May 2022 22:33:55 +0200
Subject: [PATCH 27/34] change buttons attr
---
src/FormCollection/Form/UXCollectionType.php | 40 +++++++++++++------
src/FormCollection/README.md | 16 ++++----
.../Resources/assets/src/controller.ts | 2 +-
.../Resources/views/form_theme_div.html.twig | 5 +--
.../views/form_theme_table.html.twig | 7 ++--
5 files changed, 42 insertions(+), 28 deletions(-)
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index 095807cb0e5..42affc88800 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -15,6 +15,7 @@
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -30,27 +31,42 @@ public function getParent()
public function configureOptions(OptionsResolver $resolver)
{
+ $defaultButtonAddOptions = [
+ 'label' => 'Add',
+ 'class' => '',
+ ];
+ $defaultButtonDeleteOptions = [
+ 'label' => 'Remove',
+ 'class' => '',
+ ];
$resolver->setDefaults([
- 'button_add_text' => 'Add',
- 'button_add_class' => '',
- 'button_delete_text' => 'Remove',
- 'button_delete_class' => '',
+ 'button_add_options' => $defaultButtonAddOptions,
+ 'button_delete_options' => $defaultButtonDeleteOptions,
]);
- $resolver->setAllowedTypes('button_add_text', 'string');
- $resolver->setAllowedTypes('button_add_class', 'string');
- $resolver->setAllowedTypes('button_delete_text', 'string');
- $resolver->setAllowedTypes('button_delete_class', 'string');
+ $resolver->setAllowedTypes('button_add_options', 'array');
+ $resolver->setAllowedTypes('button_delete_options', 'array');
+
+ $resolver->setNormalizer('button_add_options', function (Options $options, $value) use ($defaultButtonAddOptions) {
+ $value['label'] = $value['label'] ?? $defaultButtonAddOptions['label'];
+ $value['class'] = $value['class'] ?? $defaultButtonAddOptions['class'];
+
+ return $value;
+ });
+ $resolver->setNormalizer('button_delete_options', function (Options $options, $value) use ($defaultButtonDeleteOptions) {
+ $value['label'] = $value['label'] ?? $defaultButtonDeleteOptions['label'];
+ $value['class'] = $value['class'] ?? $defaultButtonDeleteOptions['class'];
+
+ return $value;
+ });
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
parent::finishView($view, $form, $options);
- $view->vars['button_add_text'] = $options['button_add_text'];
- $view->vars['button_add_class'] = $options['button_add_class'];
- $view->vars['button_delete_text'] = $options['button_delete_text'];
- $view->vars['button_delete_class'] = $options['button_delete_class'];
+ $view->vars['button_add_options'] = $options['button_add_options'];
+ $view->vars['button_delete_options'] = $options['button_delete_options'];
$view->vars['prototype_name'] = $options['prototype_name'];
}
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index 8de796c67cd..d34a380e400 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -91,14 +91,14 @@ class BlogFormType extends AbstractType
// ...
->add('comments', UXCollectionType::class, [
// ...
- // Default text for the add button (used by predefined theme)
- 'button_add_text' => 'Add',
- // Add HTML classes to the add button (used by predefined theme)
- 'button_add_class' => 'btn btn-outline-primary',
- // Default text for the delete button (used by predefined theme)
- 'button_delete_text' => 'Remove',
- // Add HTML classes to the add button (used by predefined theme)
- 'button_delete_class' => 'btn btn-outline-secondary',
+ 'button_add_options' => [
+ 'label' => 'Add', // Default text for the add button (used by predefined theme)
+ 'class' => 'btn btn-outline-primary', // Add HTML classes to the add button (used by predefined theme)
+ ],
+ 'button_delete_options' => [
+ 'label' => 'Remove', // Default text for the delete button (used by predefined theme)
+ 'class' => 'btn btn-outline-secondary', // Add HTML classes to the add button (used by predefined theme)
+ ],
])
// ...
;
diff --git a/src/FormCollection/Resources/assets/src/controller.ts b/src/FormCollection/Resources/assets/src/controller.ts
index fcc288fe3ab..eebd4ab9e9d 100644
--- a/src/FormCollection/Resources/assets/src/controller.ts
+++ b/src/FormCollection/Resources/assets/src/controller.ts
@@ -30,7 +30,7 @@ export default class extends Controller {
controllerName: string;
- entryTargets: Array;
+ entryTargets: Array = [];
connect() {
this.controllerName = this.context.scope.identifier;
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
index 3380930e574..9c74d0f9140 100644
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_div.html.twig
@@ -1,14 +1,13 @@
{%- block button_add -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#add')|trim -%}
{{ button_add_text|trans }}
+ class="{{ button_add_options.class }}" type="button">{{ button_add_options.label|trans }}
{%- endblock button_add -%}
{%- block button_delete -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
- {{ button_delete_text|trans }}
+ class="{{ button_delete_options.class }}" type="button">{{ button_delete_options.label|trans }}
{%- endblock button_delete -%}
{% block ux_collection_widget -%}
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
index 77af4be8510..59aacd3f759 100644
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ b/src/FormCollection/Resources/views/form_theme_table.html.twig
@@ -3,16 +3,15 @@
{{ button_add_text|trans }}
+ class="{{ button_add_options.class }}" type="button">{{ button_add_options.label|trans }}
{%- endblock button_add -%}
{%- block button_delete -%}
{%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
-
- {{ button_delete_text|trans }}
+
+ {{ button_delete_options.label|trans }}
{%- endblock button_delete -%}
{% block ux_collection_widget -%}
From 911f18f63d295488072a27b5aa3b138c3c70b884 Mon Sep 17 00:00:00 2001
From: Alexander Schranz
Date: Mon, 18 Jul 2022 23:03:34 +0200
Subject: [PATCH 28/34] Move logic from twig files to form types
---
.../Form/UXCollectionEntryToolbarType.php | 43 ++++++
.../Form/UXCollectionEntryType.php | 66 +++++++++
.../Form/UXCollectionToolbarType.php | 43 ++++++
src/FormCollection/Form/UXCollectionType.php | 106 ++++++++------
src/FormCollection/README.md | 132 ++++++++----------
.../Resources/views/form_theme_div.html.twig | 51 -------
.../views/form_theme_table.html.twig | 55 --------
7 files changed, 273 insertions(+), 223 deletions(-)
create mode 100644 src/FormCollection/Form/UXCollectionEntryToolbarType.php
create mode 100644 src/FormCollection/Form/UXCollectionEntryType.php
create mode 100644 src/FormCollection/Form/UXCollectionToolbarType.php
delete mode 100644 src/FormCollection/Resources/views/form_theme_div.html.twig
delete mode 100644 src/FormCollection/Resources/views/form_theme_table.html.twig
diff --git a/src/FormCollection/Form/UXCollectionEntryToolbarType.php b/src/FormCollection/Form/UXCollectionEntryToolbarType.php
new file mode 100644
index 00000000000..b7c2f31cf67
--- /dev/null
+++ b/src/FormCollection/Form/UXCollectionEntryToolbarType.php
@@ -0,0 +1,43 @@
+add(
+ 'uxCollectionEntryDeleteButton',
+ ButtonType::class,
+ $options['delete_options']
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'label' => false,
+ 'allow_delete' => false,
+ 'delete_options' => [],
+ ]);
+ }
+}
diff --git a/src/FormCollection/Form/UXCollectionEntryType.php b/src/FormCollection/Form/UXCollectionEntryType.php
new file mode 100644
index 00000000000..7f72add0934
--- /dev/null
+++ b/src/FormCollection/Form/UXCollectionEntryType.php
@@ -0,0 +1,66 @@
+add(
+ 'entry',
+ $options['entry_type'],
+ $options['entry_options']
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options): void
+ {
+ $form->add('toolbar', UXCollectionEntryToolbarType::class, [
+ 'allow_delete' => $options['allow_delete'],
+ 'delete_options' => $options['delete_options'],
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'entry_type' => TextType::class,
+ 'entry_options' => [],
+ 'label' => false,
+ 'allow_delete' => false,
+ 'delete_options' => [],
+ 'row_attr' => [
+ 'data-collection-target' => 'entry',
+ ],
+ ]);
+
+ $entryOptionsNormalizer = function (OptionsResolver $options, $value) {
+ $value['inherit_data'] = true;
+
+ return $value;
+ };
+
+ $resolver->addNormalizer('entry_options', $entryOptionsNormalizer);
+ }
+}
diff --git a/src/FormCollection/Form/UXCollectionToolbarType.php b/src/FormCollection/Form/UXCollectionToolbarType.php
new file mode 100644
index 00000000000..4c79d9aab1b
--- /dev/null
+++ b/src/FormCollection/Form/UXCollectionToolbarType.php
@@ -0,0 +1,43 @@
+add(
+ 'uxCollectionAddButton',
+ ButtonType::class,
+ $options['add_options']
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults([
+ 'label' => false,
+ 'allow_add' => false,
+ 'add_options' => [],
+ ]);
+ }
+}
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index 42affc88800..f90ab960e28 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -1,18 +1,12 @@
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
namespace Symfony\UX\FormCollection\Form;
use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
+use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
@@ -29,49 +23,77 @@ public function getParent()
return CollectionType::class;
}
- public function configureOptions(OptionsResolver $resolver)
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options): void
{
- $defaultButtonAddOptions = [
- 'label' => 'Add',
- 'class' => '',
- ];
- $defaultButtonDeleteOptions = [
- 'label' => 'Remove',
- 'class' => '',
- ];
- $resolver->setDefaults([
- 'button_add_options' => $defaultButtonAddOptions,
- 'button_delete_options' => $defaultButtonDeleteOptions,
+ $form->add('toolbar', UXCollectionToolbarType::class, [
+ 'allow_add' => $options['allow_add'],
+ 'add_options' => $options['add_options'],
]);
+ }
- $resolver->setAllowedTypes('button_add_options', 'array');
- $resolver->setAllowedTypes('button_delete_options', 'array');
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $addOptionsNormalizer = function (Options $options, $value) {
+ $value['block_name'] = 'add_button';
+ $value['attr'] = array_merge([
+ 'data-collection-target' => 'addButton',
+ 'data-action' => 'collection#add',
+ ], $value['attr'] ?? []);
+
+ return $value;
+ };
- $resolver->setNormalizer('button_add_options', function (Options $options, $value) use ($defaultButtonAddOptions) {
- $value['label'] = $value['label'] ?? $defaultButtonAddOptions['label'];
- $value['class'] = $value['class'] ?? $defaultButtonAddOptions['class'];
+ $deleteOptionsNormalizer = function (Options $options, $value) {
+ $value['block_name'] = 'delete_button';
+ $value['attr'] = array_merge([
+ 'data-collection-target' => 'deleteButton',
+ 'data-action' => 'collection#delete',
+ ], $value['attr'] ?? []);
return $value;
- });
- $resolver->setNormalizer('button_delete_options', function (Options $options, $value) use ($defaultButtonDeleteOptions) {
- $value['label'] = $value['label'] ?? $defaultButtonDeleteOptions['label'];
- $value['class'] = $value['class'] ?? $defaultButtonDeleteOptions['class'];
+ };
+
+ $attrNormalizer = function (Options $options, $value) {
+ $value['data-controller'] = 'collection';
return $value;
- });
- }
+ };
- public function finishView(FormView $view, FormInterface $form, array $options)
- {
- parent::finishView($view, $form, $options);
+ $entryTypeNormalizer = function (OptionsResolver $options, $value) {
+ return UXCollectionEntryType::class;
+ };
- $view->vars['button_add_options'] = $options['button_add_options'];
- $view->vars['button_delete_options'] = $options['button_delete_options'];
- $view->vars['prototype_name'] = $options['prototype_name'];
- }
+ $entryOptionsNormalizer = function (OptionsResolver $options, $value) {
+ $value['row_attr']['data-controller-target'] = 'entry';
- public function getBlockPrefix()
- {
- return 'ux_collection';
+ return [
+ 'row_attr' => [
+ 'data-controller-target' => 'entry',
+ ],
+ 'allow_delete' => $options['allow_delete'],
+ 'delete_options' => $options['delete_options'],
+ 'entry_type' => $options['ux_entry_type'] ?? null,
+ 'entry_options' => $value,
+ ];
+ };
+
+ $resolver->setDefaults([
+ 'ux_entry_type' => TextType::class,
+ 'add_options' => [],
+ 'delete_options' => [],
+ 'original_entry_type' => [],
+ ]);
+
+ $resolver->setNormalizer('add_options', $addOptionsNormalizer);
+ $resolver->setNormalizer('delete_options', $deleteOptionsNormalizer);
+ $resolver->setNormalizer('attr', $attrNormalizer);
+ $resolver->addNormalizer('entry_type', $entryTypeNormalizer);
+ $resolver->addNormalizer('entry_options', $entryOptionsNormalizer);
}
}
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index d34a380e400..c627a4d2ebb 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -20,60 +20,6 @@ yarn encore dev
Also make sure you have at least version 2.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge)
in your `package.json` file.
-## Use predefined theme
-
-You need to select the right theme from the one you are using:
-
-```yaml
-# config/packages/twig.yaml
-twig:
- # For bootstrap for example
- form_themes: ['@FormCollection/form_theme_div.html.twig']
-```
-
-There are 2 predefined themes available:
-
-- `@FormCollection/form_theme_div.html.twig`
-- `@FormCollection/form_theme_table.html.twig`
-
-[Check the Symfony doc](https://symfony.com/doc/4.4/form/form_themes.html) for the different ways to set themes in Symfony.
-
-## Use a custom form theme
-
-Consider your `BlogFormType` form set up and with a comments field that is a `CollectionType`, you can
-render it in your template:
-
-```twig
-{% macro commentFormRow(commentForm) %}
-
- {{ form_errors(commentForm) }}
- {{ form_row(commentForm.content) }}
- {{ form_row(commentForm.otherField) }}
-
-
- Remove
-
-
-{% endmacro %}
-
-
- {% for commentForm in form.comments %}
- {{ _self.commentFormRow(commentForm) }}
- {% endfor %}
-
-
- Add Another
-
-
-```
-
## Usage
The most common usage of Form Collection is to use it as a replacement of
@@ -87,21 +33,21 @@ class BlogFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
- $builder
- // ...
- ->add('comments', UXCollectionType::class, [
- // ...
- 'button_add_options' => [
- 'label' => 'Add', // Default text for the add button (used by predefined theme)
- 'class' => 'btn btn-outline-primary', // Add HTML classes to the add button (used by predefined theme)
- ],
- 'button_delete_options' => [
- 'label' => 'Remove', // Default text for the delete button (used by predefined theme)
- 'class' => 'btn btn-outline-secondary', // Add HTML classes to the add button (used by predefined theme)
- ],
- ])
- // ...
- ;
+ $builder->add('comments', UXCollectionType::class, [
+ 'label' => 'Comments',
+ 'ux_entry_type' => CommentType::class,
+ 'entry_options' => [
+ 'label' => false,
+ ],
+ 'allow_add' => true,
+ 'allow_delete' => true,
+ 'add_options' => [
+ 'label' => 'Add comment',
+ ],
+ 'delete_options' => [
+ 'label' => 'Remove Comment',
+ ],
+ ]);
}
// ...
@@ -111,13 +57,49 @@ class BlogFormType extends AbstractType
You can display it using Twig as you would normally with any form:
```twig
-{# edit.html.twig #}
+{{ form(form) }}
+```
+
+## Theming
+
+### Change position of the entry toolbar
+
+```twig
+{%- block ux_collection_entry_widget -%}
+ {%- set toolbar -%}
+ {{- form_widget(form.toolbar) -}}
+ {%- endset -%}
+
+
+ {{- toolbar -}}
+
+ {{- block('form_rows') -}}
+
+
+ {{- toolbar -}}
+
+
+{%- endblock -%}
+```
+
+### Change entry toolbar
+
+```twig
+{%- block ux_collection_entry_toolbar_widget -%}
+
+ {{- block('form_widget') -}}
+
+{%- endblock -%}
+```
+
+### Change collection toolbar
-{{ form_start(form) }}
- {# ... #}
- {{ form_row(comments) }}
- {# ... #}
-{{ form_end(form) }}
+```twig
+{%- block ux_collection_toolbar_widget -%}
+
+ {{- block('form_widget') -}}
+
+{%- endblock -%}
```
### Extend the default behavior
diff --git a/src/FormCollection/Resources/views/form_theme_div.html.twig b/src/FormCollection/Resources/views/form_theme_div.html.twig
deleted file mode 100644
index 9c74d0f9140..00000000000
--- a/src/FormCollection/Resources/views/form_theme_div.html.twig
+++ /dev/null
@@ -1,51 +0,0 @@
-{%- block button_add -%}
- {%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#add')|trim -%}
- {{ button_add_options.label|trans }}
-{%- endblock button_add -%}
-
-{%- block button_delete -%}
- {%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
- {{ button_delete_options.label|trans }}
-{%- endblock button_delete -%}
-
-{% block ux_collection_widget -%}
- {%- set controllerName = 'symfony--ux-form-collection--collection' -%}
- {%- set dataController = (attr['data-controller']|default('') ~ ' ' ~ controllerName)|trim -%}
-
- {# attr for the data target on the entry of the collection #}
- {%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
-
- {% if prototype is defined and not prototype.rendered %}
- {%- set prototype_attr = prototype.vars.row_attr|merge(attrDataTarget) -%}
- {%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
- {% endif %}
- {%- set attr = attr|merge({('data-' ~ controllerName ~ '-target'): 'container' }) -%}
-
- {% set indexKeys = data|keys %}
- {% set startIndex = indexKeys|length == 0 ? 0 : max(indexKeys) %}
-
-
- {%- if form is rootform -%}
- {{ form_errors(form) }}
- {%- endif -%}
-
- {% for child in form|filter(child => not child.rendered) %}
-
- {%- set child_attr = child.vars.row_attr|merge(attrDataTarget) -%}
- {{- form_row(child, {'row_attr': child_attr}) -}}
-
- {% endfor %}
-
- {{- form_rest(form) -}}
-
-{%- endblock %}
diff --git a/src/FormCollection/Resources/views/form_theme_table.html.twig b/src/FormCollection/Resources/views/form_theme_table.html.twig
deleted file mode 100644
index 59aacd3f759..00000000000
--- a/src/FormCollection/Resources/views/form_theme_table.html.twig
+++ /dev/null
@@ -1,55 +0,0 @@
-{%- block button_add -%}
- {%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#add')|trim -%}
-
-
- {{ button_add_options.label|trans }}
-
-
-{%- endblock button_add -%}
-
-{%- block button_delete -%}
- {%- set attrDataAction = (attr['data-action']|default('') ~ ' ' ~ controllerName ~ '#delete')|trim -%}
-
- {{ button_delete_options.label|trans }}
-{%- endblock button_delete -%}
-
-{% block ux_collection_widget -%}
- {%- set controllerName = 'symfony--ux-form-collection--collection' -%}
- {%- set dataController = (attr['data-controller']|default('') ~ ' ' ~ controllerName)|trim -%}
-
- {# attr for the data target on the entry of the collection #}
- {%- set attrDataTarget = {('data-' ~ controllerName ~ '-target'): 'entry' } -%}
-
- {% if prototype is defined and not prototype.rendered %}
- {%- set prototype_attr = prototype.vars.row_attr|merge(attrDataTarget) -%}
- {%- set attr = attr|merge({'data-prototype': form_row(prototype, {'row_attr': prototype_attr}) }) -%}
- {% endif %}
-
- {% set indexKeys = data|keys %}
- {% set startIndex = indexKeys|length == 0 ? 0 : max(indexKeys) %}
-
-
-
- {%- if form is rootform -%}
- {{ form_errors(form) }}
- {%- endif -%}
-
- {% for child in form|filter(child => not child.rendered) %}
-
- {%- set child_attr = child.vars.row_attr|merge(attrDataTarget) -%}
- {{- form_row(child, {'row_attr': child_attr}) -}}
-
- {% endfor %}
-
- {{- form_rest(form) -}}
-
-{%- endblock %}
From 4cfa27d3d7f7643c2ed0441c2477d9ef2b695bec Mon Sep 17 00:00:00 2001
From: Alexander Schranz
Date: Mon, 18 Jul 2022 23:17:31 +0200
Subject: [PATCH 29/34] Fix types and not used imports
---
.../Form/UXCollectionEntryToolbarType.php | 14 +++-----------
.../Form/UXCollectionEntryType.php | 18 +++++-------------
.../Form/UXCollectionToolbarType.php | 14 +++-----------
src/FormCollection/Form/UXCollectionType.php | 14 +++-----------
4 files changed, 14 insertions(+), 46 deletions(-)
diff --git a/src/FormCollection/Form/UXCollectionEntryToolbarType.php b/src/FormCollection/Form/UXCollectionEntryToolbarType.php
index b7c2f31cf67..a84cfe38238 100644
--- a/src/FormCollection/Form/UXCollectionEntryToolbarType.php
+++ b/src/FormCollection/Form/UXCollectionEntryToolbarType.php
@@ -4,12 +4,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
-use Symfony\Component\Form\Extension\Core\Type\CollectionType;
-use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
-use Symfony\Component\Form\FormInterface;
-use Symfony\Component\Form\FormView;
-use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -18,21 +13,18 @@
*/
class UXCollectionEntryToolbarType extends AbstractType
{
- public function buildForm(FormBuilderInterface $builder, array $options)
+ public function buildForm(FormBuilderInterface $builder, array $options): void
{
if ($options['allow_delete']) {
$builder->add(
'uxCollectionEntryDeleteButton',
ButtonType::class,
- $options['delete_options']
+ $options['delete_options'],
);
}
}
- /**
- * {@inheritdoc}
- */
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => false,
diff --git a/src/FormCollection/Form/UXCollectionEntryType.php b/src/FormCollection/Form/UXCollectionEntryType.php
index 7f72add0934..40fdab46bfa 100644
--- a/src/FormCollection/Form/UXCollectionEntryType.php
+++ b/src/FormCollection/Form/UXCollectionEntryType.php
@@ -3,34 +3,29 @@
namespace Symfony\UX\FormCollection\Form;
use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\Extension\Core\Type\ButtonType;
-use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
-use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @final
* @experimental
- * @internal Only for internal usage to add the toolbar to every entry of a collection.
+ *
+ * @internal only for internal usage to add the toolbar to every entry of a collection
*/
class UXCollectionEntryType extends AbstractType
{
- public function buildForm(FormBuilderInterface $builder, array $options)
+ public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'entry',
$options['entry_type'],
- $options['entry_options']
+ $options['entry_options'],
);
}
- /**
- * {@inheritdoc}
- */
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$form->add('toolbar', UXCollectionEntryToolbarType::class, [
@@ -39,10 +34,7 @@ public function buildView(FormView $view, FormInterface $form, array $options):
]);
}
- /**
- * {@inheritdoc}
- */
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'entry_type' => TextType::class,
diff --git a/src/FormCollection/Form/UXCollectionToolbarType.php b/src/FormCollection/Form/UXCollectionToolbarType.php
index 4c79d9aab1b..c3b6f567dee 100644
--- a/src/FormCollection/Form/UXCollectionToolbarType.php
+++ b/src/FormCollection/Form/UXCollectionToolbarType.php
@@ -4,12 +4,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
-use Symfony\Component\Form\Extension\Core\Type\CollectionType;
-use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
-use Symfony\Component\Form\FormInterface;
-use Symfony\Component\Form\FormView;
-use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -18,21 +13,18 @@
*/
class UXCollectionToolbarType extends AbstractType
{
- public function buildForm(FormBuilderInterface $builder, array $options)
+ public function buildForm(FormBuilderInterface $builder, array $options): void
{
if ($options['allow_add']) {
$builder->add(
'uxCollectionAddButton',
ButtonType::class,
- $options['add_options']
+ $options['add_options'],
);
}
}
- /**
- * {@inheritdoc}
- */
- public function configureOptions(OptionsResolver $resolver)
+ public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => false,
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index f90ab960e28..c746f0a2d16 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -3,10 +3,8 @@
namespace Symfony\UX\FormCollection\Form;
use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
-use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
@@ -18,14 +16,11 @@
*/
class UXCollectionType extends AbstractType
{
- public function getParent()
+ public function getParent(): string
{
return CollectionType::class;
}
- /**
- * {@inheritdoc}
- */
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$form->add('toolbar', UXCollectionToolbarType::class, [
@@ -34,14 +29,11 @@ public function buildView(FormView $view, FormInterface $form, array $options):
]);
}
- /**
- * {@inheritdoc}
- */
public function configureOptions(OptionsResolver $resolver): void
{
$addOptionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'add_button';
- $value['attr'] = array_merge([
+ $value['attr'] = \array_merge([
'data-collection-target' => 'addButton',
'data-action' => 'collection#add',
], $value['attr'] ?? []);
@@ -51,7 +43,7 @@ public function configureOptions(OptionsResolver $resolver): void
$deleteOptionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'delete_button';
- $value['attr'] = array_merge([
+ $value['attr'] = \array_merge([
'data-collection-target' => 'deleteButton',
'data-action' => 'collection#delete',
], $value['attr'] ?? []);
From 0976a9e78275b37737daaff8d43d615a77d3768f Mon Sep 17 00:00:00 2001
From: Alexander Schranz
Date: Mon, 18 Jul 2022 23:19:06 +0200
Subject: [PATCH 30/34] Allow overriding the data-controller via attr
---
src/FormCollection/Form/UXCollectionType.php | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index c746f0a2d16..7a69d5a5b5d 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -52,7 +52,9 @@ public function configureOptions(OptionsResolver $resolver): void
};
$attrNormalizer = function (Options $options, $value) {
- $value['data-controller'] = 'collection';
+ if (!isset($value['data-controller'])) {
+ $value['data-controller'] = 'collection';
+ }
return $value;
};
From dc18974cce25866c2efca0fd9b30eb2e8ae2e9b9 Mon Sep 17 00:00:00 2001
From: Alexander Schranz
Date: Tue, 19 Jul 2022 00:36:14 +0200
Subject: [PATCH 31/34] Simplify controller integration
---
src/FormCollection/Form/UXCollectionType.php | 4 +-
.../Resources/assets/dist/controller.js | 95 ++++--------
.../Resources/assets/src/controller.ts | 145 +++++-------------
3 files changed, 63 insertions(+), 181 deletions(-)
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index 7a69d5a5b5d..fdedcec0294 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -64,11 +64,9 @@ public function configureOptions(OptionsResolver $resolver): void
};
$entryOptionsNormalizer = function (OptionsResolver $options, $value) {
- $value['row_attr']['data-controller-target'] = 'entry';
-
return [
'row_attr' => [
- 'data-controller-target' => 'entry',
+ 'data-collection-target' => 'entry',
],
'allow_delete' => $options['allow_delete'],
'delete_options' => $options['delete_options'],
diff --git a/src/FormCollection/Resources/assets/dist/controller.js b/src/FormCollection/Resources/assets/dist/controller.js
index 8e5ae980139..2f11ddec5bb 100644
--- a/src/FormCollection/Resources/assets/dist/controller.js
+++ b/src/FormCollection/Resources/assets/dist/controller.js
@@ -1,105 +1,62 @@
import { Controller } from '@hotwired/stimulus';
-class default_1 extends Controller {
+class controller extends Controller {
constructor() {
super(...arguments);
this.index = 0;
+ this.controllerName = 'collection';
+ }
+ static get targets() {
+ return ['entry', 'addButton', 'removeButton'];
}
connect() {
this.controllerName = this.context.scope.identifier;
- this.index = this.startIndexValue ? this.startIndexValue : this.entryTargets.length - 1;
- if (!this.prototypeNameValue) {
- this.prototypeNameValue = '__name__';
- }
- this._dispatchEvent('form-collection:pre-connect', {
- allowAdd: this.allowAddValue,
- allowDelete: this.allowDeleteValue,
- });
- if (this.allowAddValue) {
- const buttonAdd = this._textToNode(this.buttonAddValue);
- this.element.prepend(buttonAdd);
- }
- if (this.allowDeleteValue) {
- for (let i = 0; i < this.entryTargets.length; i++) {
- const entry = this.entryTargets[i];
- this._addDeleteButton(entry, i);
- }
- }
- this._dispatchEvent('form-collection:connect', {
- allowAdd: this.allowAddValue,
- allowDelete: this.allowDeleteValue,
- });
+ this._dispatchEvent('form-collection:pre-connect');
+ this.index = this.entryTargets.length;
+ this._dispatchEvent('form-collection:connect');
}
add() {
- this.index++;
- let newEntry = this.element.dataset.prototype;
- if (!newEntry) {
- newEntry = this.prototypeValue;
+ const prototype = this.element.dataset.prototype;
+ if (!prototype) {
+ throw new Error('A "data-prototype" attribute was expected on data-controller="' + this.controllerName + '" element.');
}
- let regExp = new RegExp(this.prototypeNameValue + 'label__', 'g');
- newEntry = newEntry.replace(regExp, this.index);
- regExp = new RegExp(this.prototypeNameValue, 'g');
- newEntry = newEntry.replace(regExp, this.index);
- newEntry = this._textToNode(newEntry);
this._dispatchEvent('form-collection:pre-add', {
+ prototype: prototype,
index: this.index,
- element: newEntry,
});
- this.element.append(newEntry);
- let entry = this.entryTargets[this.entryTargets.length - 1];
- entry = this._addDeleteButton(entry, this.index);
+ const newEntry = this._textToNode(prototype.replace(/__name__/g, this.index.toString()));
+ if (this.entryTargets.length > 1) {
+ this.entryTargets[this.entryTargets.length - 1].after(newEntry);
+ }
+ else {
+ this.element.prepend(newEntry);
+ }
this._dispatchEvent('form-collection:add', {
+ prototype: prototype,
index: this.index,
- element: entry,
});
+ this.index++;
}
delete(event) {
- const entry = event.target.closest('[data-' + this.controllerName + '-target="entry"]');
+ const clickTarget = event.target;
+ const entry = clickTarget.closest('[data-' + this.controllerName + '-target="entry"]');
this._dispatchEvent('form-collection:pre-delete', {
- index: entry.dataset.indexEntry,
element: entry,
});
entry.remove();
this._dispatchEvent('form-collection:delete', {
- index: entry.dataset.indexEntry,
element: entry,
});
}
- _addDeleteButton(entry, index) {
- entry.dataset.indexEntry = index.toString();
- const buttonDelete = this._textToNode(this.buttonDeleteValue);
- if (!buttonDelete) {
- return entry;
- }
- buttonDelete.dataset.indexEntry = index;
- if ('TR' === entry.nodeName) {
- entry.lastElementChild.append(buttonDelete);
- }
- else {
- entry.append(buttonDelete);
- }
- return entry;
- }
_textToNode(text) {
const template = document.createElement('template');
text = text.trim();
template.innerHTML = text;
return template.content.firstChild;
}
- _dispatchEvent(name, payload) {
- console.log('TTTT');
- this.element.dispatchEvent(new CustomEvent(name, { detail: payload }));
+ _dispatchEvent(name, payload = {}) {
+ this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
}
}
-default_1.targets = ['entry'];
-default_1.values = {
- allowAdd: Boolean,
- allowDelete: Boolean,
- buttonAdd: String,
- buttonDelete: String,
- prototypeName: String,
- prototype: String,
- startIndex: Number,
-};
-export { default_1 as default };
+export { controller as default };
diff --git a/src/FormCollection/Resources/assets/src/controller.ts b/src/FormCollection/Resources/assets/src/controller.ts
index eebd4ab9e9d..485cf2a9c39 100644
--- a/src/FormCollection/Resources/assets/src/controller.ts
+++ b/src/FormCollection/Resources/assets/src/controller.ts
@@ -3,154 +3,81 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
- static targets = ['entry'];
-
- static values = {
- allowAdd: Boolean,
- allowDelete: Boolean,
- buttonAdd: String,
- buttonDelete: String,
- prototypeName: String,
- prototype: String,
- startIndex: Number,
- };
-
- allowAddValue: boolean;
- allowDeleteValue: boolean;
- buttonAddValue: string;
- buttonDeleteValue: string;
- prototypeNameValue: string;
- prototypeValue: string;
- startIndexValue: number;
-
- /**
- * Number of elements for the index of the collection
- */
- index = 0;
-
- controllerName: string;
-
- entryTargets: Array = [];
+ static get targets() {
+ return ['entry', 'addButton', 'removeButton'];
+ }
- connect() {
- this.controllerName = this.context.scope.identifier;
- this.index = this.startIndexValue ? this.startIndexValue : this.entryTargets.length - 1;
+ declare readonly entryTargets: HTMLElement[];
- if (!this.prototypeNameValue) {
- this.prototypeNameValue = '__name__';
- }
+ index: Number = 0;
+ controllerName: string = 'collection';
- this._dispatchEvent('form-collection:pre-connect', {
- allowAdd: this.allowAddValue,
- allowDelete: this.allowDeleteValue,
- });
+ connect() {
+ this.controllerName = this.context.scope.identifier;
- if (this.allowAddValue) {
- // Add button Add
- const buttonAdd = this._textToNode(this.buttonAddValue);
- this.element.prepend(buttonAdd);
- }
+ this._dispatchEvent('form-collection:pre-connect');
- // Add buttons Delete
- if (this.allowDeleteValue) {
- for (let i = 0; i < this.entryTargets.length; i++) {
- const entry = this.entryTargets[i];
- this._addDeleteButton(entry, i);
- }
- }
+ this.index = this.entryTargets.length;
- this._dispatchEvent('form-collection:connect', {
- allowAdd: this.allowAddValue,
- allowDelete: this.allowDeleteValue,
- });
+ this._dispatchEvent('form-collection:connect');
}
add() {
- this.index++;
+ const prototype = this.element.dataset.prototype;
- // Compute the new entry
- let newEntry = this.element.dataset.prototype;
- if (!newEntry) {
- newEntry = this.prototypeValue;
+ if (!prototype) {
+ throw new Error(
+ 'A "data-prototype" attribute was expected on data-controller="' + this.controllerName + '" element.'
+ );
}
- let regExp = new RegExp(this.prototypeNameValue + 'label__', 'g');
- newEntry = newEntry.replace(regExp, this.index);
-
- regExp = new RegExp(this.prototypeNameValue, 'g');
- newEntry = newEntry.replace(regExp, this.index);
-
- newEntry = this._textToNode(newEntry);
-
this._dispatchEvent('form-collection:pre-add', {
+ prototype: prototype,
index: this.index,
- element: newEntry,
});
- this.element.append(newEntry);
+ const newEntry = this._textToNode(prototype.replace(/__name__/g, this.index.toString()));
- // Retrieve the entry from targets to make sure that this is the one
- let entry = this.entryTargets[this.entryTargets.length - 1];
- entry = this._addDeleteButton(entry, this.index);
+ if (this.entryTargets.length > 1) {
+ this.entryTargets[this.entryTargets.length - 1].after(newEntry);
+ } else {
+ this.element.prepend(newEntry);
+ }
this._dispatchEvent('form-collection:add', {
+ prototype: prototype,
index: this.index,
- element: entry,
});
+
+ this.index++;
}
- delete(event) {
- const entry = event.target.closest('[data-' + this.controllerName + '-target="entry"]');
+ delete(event: MouseEvent) {
+ const clickTarget = event.target as HTMLButtonElement;
+
+ const entry = clickTarget.closest('[data-' + this.controllerName + '-target="entry"]') as HTMLElement;
this._dispatchEvent('form-collection:pre-delete', {
- index: entry.dataset.indexEntry,
element: entry,
});
entry.remove();
this._dispatchEvent('form-collection:delete', {
- index: entry.dataset.indexEntry,
element: entry,
});
}
- /**
- * Add the delete button to the entry
- * @private
- */
- _addDeleteButton(entry: HTMLElement, index: number) {
- // link the button and the entry by the data-index-entry attribute
- entry.dataset.indexEntry = index.toString();
-
- const buttonDelete = this._textToNode(this.buttonDeleteValue);
- if (!buttonDelete) {
- return entry;
- }
- buttonDelete.dataset.indexEntry = index;
-
- if ('TR' === entry.nodeName) {
- entry.lastElementChild.append(buttonDelete);
- } else {
- entry.append(buttonDelete);
- }
-
- return entry;
- }
-
- /**
- * Convert text to Element to insert in the DOM
- * @private
- */
- _textToNode(text: string) {
+ _textToNode(text: string): HTMLElement {
const template = document.createElement('template');
- text = text.trim(); // Never return a text node of whitespace as the result
+ text = text.trim();
+
template.innerHTML = text;
- return template.content.firstChild;
+ return template.content.firstChild as HTMLElement;
}
- _dispatchEvent(name: string, payload: any) {
- this.element.dispatchEvent(new CustomEvent(name, { detail: payload }));
+ _dispatchEvent(name: string, payload: {} = {}) {
+ this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
}
}
From 9ee00dcb0bb5fe724da46c6168754ba078ede4c8 Mon Sep 17 00:00:00 2001
From: Alexander Schranz
Date: Tue, 19 Jul 2022 01:34:58 +0200
Subject: [PATCH 32/34] Fix handling of nested blocks
---
src/FormCollection/Form/UXCollectionType.php | 12 +++---
.../Resources/assets/dist/controller.js | 36 +++++++++-------
.../Resources/assets/src/controller.ts | 41 +++++++++++--------
3 files changed, 53 insertions(+), 36 deletions(-)
diff --git a/src/FormCollection/Form/UXCollectionType.php b/src/FormCollection/Form/UXCollectionType.php
index fdedcec0294..ca41dfbeef5 100644
--- a/src/FormCollection/Form/UXCollectionType.php
+++ b/src/FormCollection/Form/UXCollectionType.php
@@ -34,8 +34,8 @@ public function configureOptions(OptionsResolver $resolver): void
$addOptionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'add_button';
$value['attr'] = \array_merge([
- 'data-collection-target' => 'addButton',
- 'data-action' => 'collection#add',
+ 'data-' . $options['attr']['data-controller'] . '-target' => 'addButton',
+ 'data-action' => $options['attr']['data-controller'] . '#add',
], $value['attr'] ?? []);
return $value;
@@ -44,8 +44,8 @@ public function configureOptions(OptionsResolver $resolver): void
$deleteOptionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'delete_button';
$value['attr'] = \array_merge([
- 'data-collection-target' => 'deleteButton',
- 'data-action' => 'collection#delete',
+ 'data-' . $options['attr']['data-controller'] . '-target' => 'deleteButton',
+ 'data-action' => $options['attr']['data-controller'] . '#delete',
], $value['attr'] ?? []);
return $value;
@@ -55,6 +55,7 @@ public function configureOptions(OptionsResolver $resolver): void
if (!isset($value['data-controller'])) {
$value['data-controller'] = 'collection';
}
+ $value['data-' . $value['data-controller'] . '-prototype-name-value'] = $options['prototype_name'];
return $value;
};
@@ -66,8 +67,9 @@ public function configureOptions(OptionsResolver $resolver): void
$entryOptionsNormalizer = function (OptionsResolver $options, $value) {
return [
'row_attr' => [
- 'data-collection-target' => 'entry',
+ 'data-' . $options['attr']['data-controller'] . '-target' => 'entry',
],
+ 'label' => false,
'allow_delete' => $options['allow_delete'],
'delete_options' => $options['delete_options'],
'entry_type' => $options['ux_entry_type'] ?? null,
diff --git a/src/FormCollection/Resources/assets/dist/controller.js b/src/FormCollection/Resources/assets/dist/controller.js
index 2f11ddec5bb..a1bda540a7a 100644
--- a/src/FormCollection/Resources/assets/dist/controller.js
+++ b/src/FormCollection/Resources/assets/dist/controller.js
@@ -1,38 +1,40 @@
import { Controller } from '@hotwired/stimulus';
-class controller extends Controller {
+class default_1 extends Controller {
constructor() {
super(...arguments);
this.index = 0;
this.controllerName = 'collection';
- }
- static get targets() {
- return ['entry', 'addButton', 'removeButton'];
+ this.entries = [];
}
connect() {
this.controllerName = this.context.scope.identifier;
this._dispatchEvent('form-collection:pre-connect');
- this.index = this.entryTargets.length;
+ this.entries = [];
+ this.element.querySelectorAll(':scope > [data-' + this.controllerName + '-target="entry"]').forEach(entry => {
+ this.entries.push(entry);
+ });
this._dispatchEvent('form-collection:connect');
}
add() {
- const prototype = this.element.dataset.prototype;
- if (!prototype) {
+ const prototypeHTML = this.element.dataset.prototype;
+ if (!prototypeHTML) {
throw new Error('A "data-prototype" attribute was expected on data-controller="' + this.controllerName + '" element.');
}
+ const newEntry = this._textToNode(prototypeHTML.replace(new RegExp('/' + this.prototypeNameValue + '/', 'g'), this.index.toString()));
this._dispatchEvent('form-collection:pre-add', {
- prototype: prototype,
+ entry: newEntry,
index: this.index,
});
- const newEntry = this._textToNode(prototype.replace(/__name__/g, this.index.toString()));
- if (this.entryTargets.length > 1) {
- this.entryTargets[this.entryTargets.length - 1].after(newEntry);
+ if (this.entries.length > 1) {
+ this.entries[this.entries.length - 1].after(newEntry);
}
else {
this.element.prepend(newEntry);
}
+ this.entries.push(newEntry);
this._dispatchEvent('form-collection:add', {
- prototype: prototype,
+ entry: newEntry,
index: this.index,
});
this.index++;
@@ -41,11 +43,12 @@ class controller extends Controller {
const clickTarget = event.target;
const entry = clickTarget.closest('[data-' + this.controllerName + '-target="entry"]');
this._dispatchEvent('form-collection:pre-delete', {
- element: entry,
+ entry: entry,
});
entry.remove();
+ this.entries = this.entries.filter(currentEntry => currentEntry !== entry);
this._dispatchEvent('form-collection:delete', {
- element: entry,
+ entry: entry,
});
}
_textToNode(text) {
@@ -58,5 +61,8 @@ class controller extends Controller {
this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
}
}
+default_1.values = {
+ prototypeName: String,
+};
-export { controller as default };
+export { default_1 as default };
diff --git a/src/FormCollection/Resources/assets/src/controller.ts b/src/FormCollection/Resources/assets/src/controller.ts
index 485cf2a9c39..4fc84927266 100644
--- a/src/FormCollection/Resources/assets/src/controller.ts
+++ b/src/FormCollection/Resources/assets/src/controller.ts
@@ -3,49 +3,57 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
- static get targets() {
- return ['entry', 'addButton', 'removeButton'];
+ static values = {
+ prototypeName: String,
}
- declare readonly entryTargets: HTMLElement[];
+ declare readonly prototypeNameValue: string;
- index: Number = 0;
- controllerName: string = 'collection';
+ index = 0;
+ controllerName = 'collection';
+ entries: Element[] = [];
connect() {
this.controllerName = this.context.scope.identifier;
this._dispatchEvent('form-collection:pre-connect');
- this.index = this.entryTargets.length;
+ this.entries = [];
+ this.element.querySelectorAll(':scope > [data-' + this.controllerName + '-target="entry"]').forEach(entry => {
+ this.entries.push(entry);
+ });
this._dispatchEvent('form-collection:connect');
}
add() {
- const prototype = this.element.dataset.prototype;
+ const prototypeHTML = this.element.dataset.prototype;
- if (!prototype) {
+ if (!prototypeHTML) {
throw new Error(
'A "data-prototype" attribute was expected on data-controller="' + this.controllerName + '" element.'
);
}
+ const newEntry = this._textToNode(
+ prototypeHTML.replace(new RegExp('/' + this.prototypeNameValue + '/', 'g'), this.index.toString())
+ );
+
this._dispatchEvent('form-collection:pre-add', {
- prototype: prototype,
+ entry: newEntry,
index: this.index,
});
- const newEntry = this._textToNode(prototype.replace(/__name__/g, this.index.toString()));
-
- if (this.entryTargets.length > 1) {
- this.entryTargets[this.entryTargets.length - 1].after(newEntry);
+ if (this.entries.length > 1) {
+ this.entries[this.entries.length - 1].after(newEntry);
} else {
this.element.prepend(newEntry);
}
+ this.entries.push(newEntry);
+
this._dispatchEvent('form-collection:add', {
- prototype: prototype,
+ entry: newEntry,
index: this.index,
});
@@ -58,13 +66,14 @@ export default class extends Controller {
const entry = clickTarget.closest('[data-' + this.controllerName + '-target="entry"]') as HTMLElement;
this._dispatchEvent('form-collection:pre-delete', {
- element: entry,
+ entry: entry,
});
entry.remove();
+ this.entries = this.entries.filter(currentEntry => currentEntry !== entry);
this._dispatchEvent('form-collection:delete', {
- element: entry,
+ entry: entry,
});
}
From d29459272bdf08543e40f2f25ed337764ecba368 Mon Sep 17 00:00:00 2001
From: Alexander Schranz
Date: Tue, 19 Jul 2022 01:53:02 +0200
Subject: [PATCH 33/34] Move docs to index.rst
---
src/FormCollection/README.md | 178 ++------------------
src/FormCollection/Resources/doc/index.rst | 180 +++++++++++++++++++++
2 files changed, 190 insertions(+), 168 deletions(-)
create mode 100644 src/FormCollection/Resources/doc/index.rst
diff --git a/src/FormCollection/README.md b/src/FormCollection/README.md
index c627a4d2ebb..7fd16b20a9a 100644
--- a/src/FormCollection/README.md
+++ b/src/FormCollection/README.md
@@ -1,172 +1,14 @@
-# UX Form Collection
+# Symfony UX Form Collection
-Symfony UX Form collection is a Symfony bundle providing light UX for collection
-in Symfony Forms.
+Symfony UX Form collection is a Symfony bundle provides a Stimulus integration
+for add and remove of for [Symfony Form Collection Type](https://symfony.com/doc/current/form/collection.html).
-## Installation
+**This repository is a READ-ONLY sub-tree split**. See
+https://github.com/symfony/ux to create issues or submit pull requests.
-UX Form Collection requires PHP 7.2+ and Symfony 4.4+.
+## Resources
-Install this bundle using Composer and Symfony Flex:
-
-```sh
-composer require symfony/ux-form-collection
-
-# Don't forget to install the JavaScript dependencies as well and compile
-yarn install --force
-yarn encore dev
-```
-
-Also make sure you have at least version 2.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge)
-in your `package.json` file.
-
-## Usage
-
-The most common usage of Form Collection is to use it as a replacement of
-the native CollectionType class:
-
-```php
-// ...
-use Symfony\UX\FormCollection\Form\UXCollectionType;
-
-class BlogFormType extends AbstractType
-{
- public function buildForm(FormBuilderInterface $builder, array $options)
- {
- $builder->add('comments', UXCollectionType::class, [
- 'label' => 'Comments',
- 'ux_entry_type' => CommentType::class,
- 'entry_options' => [
- 'label' => false,
- ],
- 'allow_add' => true,
- 'allow_delete' => true,
- 'add_options' => [
- 'label' => 'Add comment',
- ],
- 'delete_options' => [
- 'label' => 'Remove Comment',
- ],
- ]);
- }
-
- // ...
-}
-```
-
-You can display it using Twig as you would normally with any form:
-
-```twig
-{{ form(form) }}
-```
-
-## Theming
-
-### Change position of the entry toolbar
-
-```twig
-{%- block ux_collection_entry_widget -%}
- {%- set toolbar -%}
- {{- form_widget(form.toolbar) -}}
- {%- endset -%}
-
-
- {{- toolbar -}}
-
- {{- block('form_rows') -}}
-
-
- {{- toolbar -}}
-
-
-{%- endblock -%}
-```
-
-### Change entry toolbar
-
-```twig
-{%- block ux_collection_entry_toolbar_widget -%}
-
- {{- block('form_widget') -}}
-
-{%- endblock -%}
-```
-
-### Change collection toolbar
-
-```twig
-{%- block ux_collection_toolbar_widget -%}
-
- {{- block('form_widget') -}}
-
-{%- endblock -%}
-```
-
-### Extend the default behavior
-
-Symfony UX Form Collection allows you to extend its default behavior using a custom Stimulus controller:
-
-```js
-// mycollection_controller.js
-
-import { Controller } from 'stimulus';
-
-export default class extends Controller {
- connect() {
- this.element.addEventListener('collection:pre-connect', this._onPreConnect);
- this.element.addEventListener('collection:connect', this._onConnect);
- this.element.addEventListener('collection:pre-add', this._onPreAdd);
- this.element.addEventListener('collection:add', this._onAdd);
- this.element.addEventListener('collection:pre-delete', this._onPreDelete);
- this.element.addEventListener('collection:delete', this._onDelete);
- }
-
- disconnect() {
- // You should always remove listeners when the controller is disconnected to avoid side effects
- this.element.removeEventListener('collection:pre-connect', this._onPreConnect);
- this.element.removeEventListener('collection:connect', this._onConnect);
- }
-
- _onPreConnect(event) {
- // The collection is not yet connected
- console.log(event.detail.allowAdd); // Access to the allow_add option of the form
- console.log(event.detail.allowDelete); // Access to the allow_delete option of the form
- }
-
- _onConnect(event) {
- // Same as collection:pre-connect event
- }
-
- _onPreAdd(event) {
- console.log(event.detail.index); // Access to the curent index will be added
- console.log(event.detail.element); // Access to the element will be added
- }
-
- _onAdd(event) {
- // Same as collection:pre-add event
- }
-
- _onPreDelete(event) {
- console.log(event.detail.index); // Access to the index will be removed
- console.log(event.detail.element); // Access to the elemnt will be removed
- }
-
- _onDelete(event) {
- // Same as collection:pre-delete event
- }
-}
-```
-
-Then in your render call, add your controller as an HTML attribute:
-
-```php
- $builder
- // ...
- ->add('comments', UXCollectionType::class, [
- // ...
- 'attr' => [
- // Change the controller name
- 'data-controller' => 'mycollection'
- ]
- ]);
-```
+- [Documentation](https://symfony.com/bundles/ux-form-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)
diff --git a/src/FormCollection/Resources/doc/index.rst b/src/FormCollection/Resources/doc/index.rst
new file mode 100644
index 00000000000..f4101dfd133
--- /dev/null
+++ b/src/FormCollection/Resources/doc/index.rst
@@ -0,0 +1,180 @@
+UX Form Collection
+===================
+
+Symfony UX Form collection is a Symfony bundle providing light UX for collection
+in Symfony Forms.
+
+Installation
+------------
+
+UX Form Collection requires PHP 7.2+ and Symfony 4.4+.
+
+Install this bundle using Composer and Symfony Flex:
+
+.. code-block:: sh
+
+ composer require symfony/ux-form-collection
+
+ # Don't forget to install the JavaScript dependencies as well and compile
+ yarn install --force
+ yarn encore dev
+
+Also make sure you have at least version 2.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge)
+in your `package.json` file.
+
+Usage
+-----
+
+The most common usage of Form Collection is to use it as a replacement of
+the native CollectionType class:
+
+.. code-block:: php
+
+ // ...
+ use Symfony\UX\FormCollection\Form\UXCollectionType;
+
+ class BlogFormType extends AbstractType
+ {
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->add('comments', UXCollectionType::class, [
+ 'label' => 'Comments',
+ 'ux_entry_type' => CommentType::class,
+ 'entry_options' => [
+ 'label' => false,
+ ],
+ 'allow_add' => true,
+ 'allow_delete' => true,
+ 'add_options' => [
+ 'label' => 'Add comment',
+ ],
+ 'delete_options' => [
+ 'label' => 'Remove Comment',
+ ],
+ ]);
+ }
+
+ // ...
+ }
+
+You can display it using Twig as you would normally with any form:
+
+.. code-block:: twig
+
+ {{ form(form) }}
+
+
+Theming
+-------
+
+Change position of the entry toolbar
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: twig
+
+ {%- block ux_collection_entry_widget -%}
+ {%- set toolbar -%}
+ {{- form_widget(form.toolbar) -}}
+ {%- endset -%}
+
+
+ {{- toolbar -}}
+
+ {{- block('form_rows') -}}
+
+
+ {{- toolbar -}}
+
+
+ {%- endblock -%}
+
+Change entry toolbar
+~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: twig
+ {%- block ux_collection_entry_toolbar_widget -%}
+
+ {{- block('form_widget') -}}
+
+ {%- endblock -%}
+
+Change collection toolbar
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: twig
+
+ {%- block ux_collection_toolbar_widget -%}
+
+ {{- block('form_widget') -}}
+
+ {%- endblock -%}
+
+Extend the default behavior
+---------------------------
+
+Symfony UX Form Collection allows you to extend its default behavior using a custom Stimulus controller:
+
+.. code-block:: js
+
+ // mycollection_controller.js
+
+ import { Controller } from 'stimulus';
+
+ export default class extends Controller {
+ connect() {
+ this.element.addEventListener('collection:pre-connect', this._onPreConnect);
+ this.element.addEventListener('collection:connect', this._onConnect);
+ this.element.addEventListener('collection:pre-add', this._onPreAdd);
+ this.element.addEventListener('collection:add', this._onAdd);
+ this.element.addEventListener('collection:pre-delete', this._onPreDelete);
+ this.element.addEventListener('collection:delete', this._onDelete);
+ }
+
+ disconnect() {
+ // You should always remove listeners when the controller is disconnected to avoid side effects
+ this.element.removeEventListener('collection:pre-connect', this._onPreConnect);
+ this.element.removeEventListener('collection:connect', this._onConnect);
+ }
+
+ _onPreConnect(event) {
+ // The collection is not yet connected
+ console.log(event.detail.allowAdd); // Access to the allow_add option of the form
+ console.log(event.detail.allowDelete); // Access to the allow_delete option of the form
+ }
+
+ _onConnect(event) {
+ // Same as collection:pre-connect event
+ }
+
+ _onPreAdd(event) {
+ console.log(event.detail.index); // Access to the curent index will be added
+ console.log(event.detail.element); // Access to the element will be added
+ }
+
+ _onAdd(event) {
+ // Same as collection:pre-add event
+ }
+
+ _onPreDelete(event) {
+ console.log(event.detail.index); // Access to the index will be removed
+ console.log(event.detail.element); // Access to the elemnt will be removed
+ }
+
+ _onDelete(event) {
+ // Same as collection:pre-delete event
+ }
+ }
+
+Then in your render call, add your controller as an HTML attribute:
+
+.. code-block:: php
+
+ $builder
+ // ...
+ ->add('comments', UXCollectionType::class, [
+ // ...
+ 'attr' => [
+ // Change the controller name
+ 'data-controller' => 'mycollection'
+ ]
+ ]);
From 4edfefbca9ae4526eab69d0868372e16657bac28 Mon Sep 17 00:00:00 2001
From: Alexander Schranz
Date: Tue, 19 Jul 2022 19:45:36 +0200
Subject: [PATCH 34/34] Fix add problem
---
src/FormCollection/Resources/assets/dist/controller.js | 2 +-
src/FormCollection/Resources/assets/src/controller.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/FormCollection/Resources/assets/dist/controller.js b/src/FormCollection/Resources/assets/dist/controller.js
index a1bda540a7a..03d21d159a8 100644
--- a/src/FormCollection/Resources/assets/dist/controller.js
+++ b/src/FormCollection/Resources/assets/dist/controller.js
@@ -26,7 +26,7 @@ class default_1 extends Controller {
entry: newEntry,
index: this.index,
});
- if (this.entries.length > 1) {
+ if (this.entries.length > 0) {
this.entries[this.entries.length - 1].after(newEntry);
}
else {
diff --git a/src/FormCollection/Resources/assets/src/controller.ts b/src/FormCollection/Resources/assets/src/controller.ts
index 4fc84927266..65a31126842 100644
--- a/src/FormCollection/Resources/assets/src/controller.ts
+++ b/src/FormCollection/Resources/assets/src/controller.ts
@@ -44,7 +44,7 @@ export default class extends Controller {
index: this.index,
});
- if (this.entries.length > 1) {
+ if (this.entries.length > 0) {
this.entries[this.entries.length - 1].after(newEntry);
} else {
this.element.prepend(newEntry);