Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions assets-public/controllers/confirm_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Controller } from '@hotwired/stimulus'
import { Modal } from 'bootstrap'

export default class extends Controller {
static values = {
modalTitle: String,
confirmationMessage: { type: String, default: 'Are you sure?' },
cancelButtonText: { type: String, default: 'Cancel' },
confirmButtonText: { type: String, default: 'Ok' },
closeButtonText: { type: String, default: 'Close' },
}
static targets = ['element']

connect () {
if (!this.isForm() && !this.isLink()) {
console.error('Confirm controller: Only form or a elements are supported.')
}

let eventName = ''
if (this.isForm()) eventName = 'submit'
if (this.isLink()) eventName = 'click'

this.elementTarget.addEventListener(eventName, (e) => {
e.preventDefault()
e.stopPropagation()
this.showModal()
})
}

isForm () {
return this.elementTarget && this.elementTarget.tagName.toLowerCase() === 'form'
}

isLink () {
return this.elementTarget && this.elementTarget.tagName.toLowerCase() === 'a'
}

showModal () {
// remove existing modal
const existingModal = document.querySelector('div[data-role="confirm-modal"]')
if (existingModal) {
existingModal.parentNode.removeChild(existingModal)
}

// create modal
const modal = document.createElement('div')
modal.className = 'modal fade'
modal.tabIndex = -1
modal.setAttribute('aria-labelledby', 'confirmModalTitle')
modal.setAttribute('aria-hidden', 'true')
modal.setAttribute('data-role', 'confirm-modal')
const modalDialog = document.createElement('div')
modalDialog.className = 'modal-dialog'
modal.appendChild(modalDialog)
const modalContent = document.createElement('div')
modalContent.className = 'modal-content'
modalDialog.appendChild(modalContent)
const modalHeader = document.createElement('div')
modalHeader.className = 'modal-header'
modalContent.appendChild(modalHeader)
if (this.modalTitleValue !== '') {
const modalTitle = document.createElement('h5')
modalTitle.className = 'modal-title'
modalTitle.id = 'confirmModalTitle'
modalTitle.textContent = this.modalTitleValue
modalHeader.appendChild(modalTitle)
}
const closeButton = document.createElement('button')
closeButton.type = 'button'
closeButton.className = 'btn-close'
closeButton.setAttribute('data-bs-dismiss', 'modal')
closeButton.setAttribute('aria-label', this.closeButtonTextValue)
modalHeader.appendChild(closeButton)
const modalBody = document.createElement('div')
modalBody.className = 'modal-body'
modalBody.textContent = this.confirmationMessageValue
modalContent.appendChild(modalBody)
const modalFooter = document.createElement('div')
modalFooter.className = 'modal-footer'
modalContent.appendChild(modalFooter)
const cancelButton = document.createElement('button')
cancelButton.type = 'button'
cancelButton.className = 'btn btn-secondary'
cancelButton.setAttribute('data-bs-dismiss', 'modal')
cancelButton.textContent = this.cancelButtonTextValue
modalFooter.appendChild(cancelButton)
const confirmButton = document.createElement('button')
confirmButton.type = 'button'
confirmButton.className = 'btn btn-primary'
confirmButton.textContent = this.confirmButtonTextValue
confirmButton.setAttribute('data-role', 'confirm-button')
modalFooter.appendChild(confirmButton)
document.body.appendChild(modal)

// show modal
const bootstrapModal = new Modal(modal, { backdrop: 'static' })
bootstrapModal.show()

// remove modal from DOM after hiding
modal.addEventListener('hidden.bs.modal', () => {
modal.parentNode && modal.parentNode.removeChild(modal)
})

// bind on confirm button click
const confirmBtn = modal.querySelector('[data-role="confirm-button"]')
confirmBtn.addEventListener('click', () => {
this.handleConfirm()
bootstrapModal.hide()
})
}

handleConfirm () {
if (this.isForm()) {
this.elementTarget.submit()
return
}

if (this.isLink()) {
if (this.elementTarget.target) {
window.open(this.elementTarget.href, this.elementTarget.target)
return
}

window.location = this.elementTarget.href
}
}

disconnect () {
const existingModal = document.querySelector('div[data-role="confirm-modal"]')
if (existingModal) {
existingModal.parentNode.removeChild(existingModal)
}
}
}
125 changes: 97 additions & 28 deletions docs/frontend/stimulus.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# Stimulus

For our javascript needs we use Symfony UX with Stimulus. See the Symfony site for information https://symfony.com/bundles/StimulusBundle/current/index.html or the stimulus documentation for more information: https://stimulus.hotwired.dev/handbook/introduction
For our javascript needs we use Symfony UX with Stimulus. See the Symfony site for
information https://symfony.com/bundles/StimulusBundle/current/index.html or the stimulus documentation for more
information: https://stimulus.hotwired.dev/handbook/introduction

We have a few default components that can be used in your project:

## Clipboard

This can be implemented two different ways:
This can be implemented two different ways:

1. With separate button

Expand All @@ -16,17 +18,19 @@ To use the button to copy the source, you add `data-action="clipboard#copy"` to
With `data-clipboard-success-content-value` you can temporarily change the button text after copying.

```html

<div data-controller="clipboard" data-clipboard-success-content-value="{{ 'Text copied!'|trans }}">
<input type="text" value="Click the button to copy me!" data-clipboard-target="source" disabled />
<button type="button" data-action="clipboard#copy" data-clipboard-target="button">Copy to clipboard</button>
<input type="text" value="Click the button to copy me!" data-clipboard-target="source" disabled/>
<button type="button" data-action="clipboard#copy" data-clipboard-target="button">Copy to clipboard</button>
</div>
```

2. Directly on text

The only differnce is that you add the event `data-action="click->clipboard#copy"` directly on the text.
The only difference is that you add the event `data-action="click->clipboard#copy"` directly on the text.

```html

<div data-controller="clipboard">
<span data-clipboard-target="source" data-action="click->clipboard#copy">This text will be copied on click!</span>
</div>
Expand All @@ -36,12 +40,12 @@ Copying something will show a toast notification. To set this text you can use `
on the element with your `data-controller="clipboard"`.

```html

<div data-controller="clipboard" data-clipboard-success-message-value="{{ 'Text copied!'|trans }}">
<span data-clipboard-target="source" data-action="click->clipboard#copy">This text will be copied on click!</span>
<span data-clipboard-target="source" data-action="click->clipboard#copy">This text will be copied on click!</span>
</div>
```


## Toast

Toast notifications can be shown via flash messages directly from controllers. But you can also show a toast
Expand All @@ -62,7 +66,7 @@ To use tooltips, you can use `data-controller="tooltip"` with the default bootst
<span data-controller="tooltip" data-bs-title="Default tooltip">
<i class="fa fa-exclamation-triangle"></i>
</span>
<i class="fa fa-info-circle" data-controller="tooltip" data-bs-title="Little secret tip"></i>
<i class="fa fa-info-circle" data-controller="tooltip" data-bs-title="Little secret tip"></i>
```

## Popover
Expand All @@ -72,7 +76,10 @@ To use popovers, you can use `data-controller="popover"` with the default bootst
https://getbootstrap.com/docs/5.3/components/popovers/#overview for more information.

```html
<button type="button" class="btn btn-outline-primary" data-controller="popover" data-bs-trigger="hover" data-bs-title="Foo" data-bs-content="Content goes here.">Hover me</button>

<button type="button" class="btn btn-outline-primary" data-controller="popover" data-bs-trigger="hover"
data-bs-title="Foo" data-bs-content="Content goes here.">Hover me
</button>
```

## Autocomplete
Expand All @@ -96,26 +103,42 @@ be a history push when a tab is changes. This will change the url with the ancho
the correct tab will be opened.

```html
<ul class="nav nav-tabs" id="myTab" role="tablist" data-controller="tabs" data-action="shown.bs.tab->tabs#addAnchorToUrl">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#home-tab-pane" type="button" role="tab" aria-controls="home-tab-pane" aria-selected="true">Home</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile-tab-pane" type="button" role="tab" aria-controls="profile-tab-pane" aria-selected="false">Profile</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-tab-pane" type="button" role="tab" aria-controls="contact-tab-pane" aria-selected="false">Contact</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="disabled-tab" data-bs-toggle="tab" data-bs-target="#disabled-tab-pane" type="button" role="tab" aria-controls="disabled-tab-pane" aria-selected="false" disabled>Disabled</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">HOME</div>
<div class="tab-pane fade" id="profile-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">PROFILE</div>
<div class="tab-pane fade" id="contact-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">CONTACT</div>
<div class="tab-pane fade" id="disabled-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">DISABLED</div>

<ul class="nav nav-tabs" id="myTab" role="tablist" data-controller="tabs"
data-action="shown.bs.tab->tabs#addAnchorToUrl">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#home-tab-pane" type="button"
role="tab" aria-controls="home-tab-pane" aria-selected="true">Home
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile-tab-pane" type="button"
role="tab" aria-controls="profile-tab-pane" aria-selected="false">Profile
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-tab-pane" type="button"
role="tab" aria-controls="contact-tab-pane" aria-selected="false">Contact
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="disabled-tab" data-bs-toggle="tab" data-bs-target="#disabled-tab-pane" type="button"
role="tab" aria-controls="disabled-tab-pane" aria-selected="false" disabled>Disabled
</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">
HOME
</div>
<div class="tab-pane fade" id="profile-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">PROFILE
</div>
<div class="tab-pane fade" id="contact-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">CONTACT
</div>
<div class="tab-pane fade" id="disabled-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">
DISABLED
</div>
</div>
```

## Password Strength Checker
Expand Down Expand Up @@ -178,3 +201,49 @@ Set the `data-controller='busy-submit'` on the form tag or in your form type:
'attr' => ['data-controller' => 'busy-submit'],
]);
```


## Confirm modal

This controller allows you to show a confirmation modal before submitting a form or clicking a link.

For example when you have a form to delete an entity:

```twig
<div
{{ stimulus_controller(
'confirm',
{
confirmationMessage: 'Are you sure you want to delete this {entity}?'|trans({entity: entity.title}),
cancelButtonText: 'Cancel'|trans,
confirmButtonText: 'Delete'|trans,
}
) }}
>
{{ form(delete_form, { attr: { 'data-confirm-target': 'element' }}) }}
</div>
```

Or an example with a link:

```twig
<div
{{ stimulus_controller(
'confirm',
{
modalTitle: 'Open sumocoders.be?',
confirmationMessage: 'Are you sure you want to open the SumoCoders website?',
cancelButtonText: 'Cancel',
confirmButtonText: 'Open SumoCoders website',
}
) }}
>
<a
href="https://www.sumocoders.be"
{{ stimulus_target('confirm', 'element') }}
class="btn btn-secondary"
>
Open sumocoders.be
</a>
</div>
```