Skip to content

Instantly share code, notes, and snippets.

@bjo3rnf
Last active October 16, 2024 14:29
Show Gist options
  • Save bjo3rnf/85e8490b98955cb6016f44445d9843fb to your computer and use it in GitHub Desktop.
Save bjo3rnf/85e8490b98955cb6016f44445d9843fb to your computer and use it in GitHub Desktop.
Stimulus.js controller for Symfony collection form type with configurable item limit
{% macro collection_item(form) %}
<div data-form-collection-target="field">
{{ form_widget(form) }}
<button type="button"
data-action="form-collection#removeItem">
remove
</button>
</div>
{% endmacro %}
{% import _self as formMacros %}
{{ form_start(form) }}
<div data-controller="form-collection"
data-form-collection-max-items-value="10"
data-form-collection-prototype-value="{{ formMacros.collection_item(form.collectionField.vars.prototype)|json_encode }}">
<div data-form-collection-target="fields">
{% do form.collectionField.setRendered %}
{% for field in form.collectionField %}
{{ formMacros.collection_item(field) }}
{% endfor %}
</div>
<button type="button"
data-action="form-collection#addItem"
data-form-collection-target="addButton">
add
</button>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
import { Controller } from 'stimulus'
export default class extends Controller {
static targets = [ 'fields', 'field', 'addButton' ]
static values = {
prototype: String,
maxItems: Number,
itemsCount: Number,
}
connect() {
this.index = this.itemsCountValue = this.fieldTargets.length
}
addItem() {
let prototype = JSON.parse(this.prototypeValue)
const newField = prototype.replace(/__name__/g, this.index)
this.fieldsTarget.insertAdjacentHTML('beforeend', newField)
this.index++
this.itemsCountValue++
}
removeItem(event) {
this.fieldTargets.forEach(element => {
if (element.contains(event.target)) {
element.remove()
this.itemsCountValue--
}
})
}
itemsCountValueChanged() {
if (false === this.hasAddButtonTarget || 0 === this.maxItemsValue) {
return
}
const maxItemsReached = this.itemsCountValue >= this.maxItemsValue
this.addButtonTarget.classList.toggle('hidden', maxItemsReached)
}
}
@MichaelBrauner
Copy link

Thank you much for sharing this.

@MichaelBrauner
Copy link

The addButton target is not used inside the html.

Instead of

    <button data-action="form-collection#addItem">
        add
    </button>

It should be:

     <button data-form-collection-target="addButton">
         add
     </button>

Than you need to add the eventListener manually.

 connect() {
     this.index = this.itemsCountValue = this.fieldTargets.length
     this.hasAddButtonTarget && this.addButtonTarget.addEventListener('click', event => this.addItem(event))
 }

@bjo3rnf
Copy link
Author

bjo3rnf commented Mar 14, 2022

Hi @MichaelBrauner,

the target is used to control if the add-button should be rendered or not depending on a possible upper limit of elements (see itemsCountValueChanged(). Moreover adding of event-handlers should be left to the framework to ensure they will be removed automatically as well.

Cheers
Björn

@MichaelBrauner
Copy link

Ok. The eventListener should be set with a data-action attribute. I did it this way because so I have not to set the target and the action. But u are right. It might be better to use the framework for that.

But the data-form-collection-target on the button is missing anyway in your example html.

The itemsCountValueChanged() function can not set a hidden class on a non existing target.

@bjo3rnf
Copy link
Author

bjo3rnf commented Mar 14, 2022

Yes, you are right. I added the missing target, thanks.

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