Say you wrap your index view in a form to apply different filters like pagination or a search query. On submit, your index view changes when the filters are applied (through up-submit
and up-target
).
Now you want to enable more data-specific filters using a separate "Filters" button that opens a popup to not overload your UI.
Filter bar:
Filter popup:
The problem is that the Unpoly popup will be inserted at the bottom of the DOM, so its inputs will not live inside your form. This will result in losing params because they will not be submitted along.
To fix this, we need to create a dummy form in the popup layer and copy the inputs between the layers.
We will use the following HTML:
%form{ action: my_filter_path, class: 'filter-form', 'up-history': true, 'up-target': '.filter-form, .result-list' }
%input(type='text' name='query' placeholder='Search...')
%div(filter-popup)
# Trigger button for the popup
%a{ class: 'btn btn-secondary', 'filter-popup-button': '', role: 'button' }
# The content of the popup form
.filter-content(filter-popup-content)
# Your form inputs, nested and styled in filter groups as you wish
%input(type='checkbox' name='locked')
# ...
%a{ class: 'btn btn-secondary', role: 'button', 'up-dismiss': true }
Abbrechen
%button{ class: 'btn btn-primary', type: 'submit' }
Anwenden
.result-list
# ...
app/assets/components/filter_popup.js
import { copyValues } from '../util/form'
up.compiler('[filter-popup]', function(element) {
const button = element.querySelector('[filter-popup-button]')
const rootForm = button.closest('form')
const popupContent = element.querySelector('[filter-popup-content]')
up.on(button, 'click', async function(evt) {
evt.stopPropagation() // Without this, the click will bubble up and immediately close the popup
const { fragment: overlayForm } = await up.render(
{
align: 'right',
class: 'filter',
dismissable: 'key outside',
fragment: `<form>${popupContent.outerHTML}</form>`,
layer: 'swap',
mode: 'popup',
navigate: false,
origin: button,
position: 'bottom',
},
)
copyValues(rootForm, overlayForm) // Copy all values from the root layer form into the popup form
up.layer.on('submit', function(evt, overlayForm) {
evt.preventDefault() // Prevent the form submit because we need to submit the whole root form
// Copy them back to the root form as soon as the form in the layer is submitted
copyValues(overlayForm, rootForm)
up.layer.dismiss() // Close the layer
up.submit(rootForm) // Submit the root form to achieve index changes with the filters
})
})
})
app/assets/util/form.js
// Copies input values from sourceForm to targetForm.
export function copyValues(sourceForm, targetForm) {
for (const sourceField of sourceForm.elements) {
const targetField = targetForm.elements.namedItem(sourceField.name)
if (!targetField) continue
if (targetField instanceof RadioNodeList) {
for (const radio of targetField) {
if (radio.value === sourceField.value) {
radio.checked = sourceField.checked
}
}
} else if (targetField.type === 'checkbox') {
targetField.checked = sourceField.checked
} else {
targetField.value = sourceField.value
}
}
}
Posted by Dominic Beger to makandra dev (2025-04-28 11:19)