Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use identifiers on modal triggers and modal component instead of integral trigger #3541

Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2dd9e92
Adds some extra logic to use identifiers on modal triggers and modal …
TwistMeister Sep 16, 2024
95b3e1e
Update rocky/components/modal/README.md
TwistMeister Sep 17, 2024
c0817a8
Update rocky/components/modal/README.md
TwistMeister Sep 17, 2024
3606607
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
TwistMeister Sep 17, 2024
3473455
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
TwistMeister Sep 17, 2024
08681e4
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
TwistMeister Sep 19, 2024
1765843
Update plugin_tile_modal.html
TwistMeister Sep 23, 2024
6e18edc
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
TwistMeister Sep 23, 2024
5e00e12
Implements modal open/close using URL anchors
TwistMeister Sep 25, 2024
e0d9443
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
TwistMeister Sep 25, 2024
14c384d
Update script.js
TwistMeister Sep 25, 2024
6c80fe9
Merge branch 'feat/update-modal-component-by-removing-trigger-from-co…
TwistMeister Sep 25, 2024
55f5e28
Update script.js
TwistMeister Sep 25, 2024
1f90d17
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
TwistMeister Sep 26, 2024
20601b8
Remove early return when != trigger and improve docs to highlight the…
TwistMeister Sep 30, 2024
3d5930e
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
TwistMeister Sep 30, 2024
031a2b0
Update rocky/components/modal/script.js
TwistMeister Oct 1, 2024
7c02a48
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
TwistMeister Oct 1, 2024
486a4b2
Update README.md
TwistMeister Oct 1, 2024
4036eb7
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
stephanie0x00 Oct 1, 2024
7659d43
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
underdarknl Oct 1, 2024
19270a2
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
underdarknl Oct 1, 2024
a07a0b0
Update script.js
TwistMeister Oct 1, 2024
d69b92e
Merge branch 'feat/update-modal-component-by-removing-trigger-from-co…
TwistMeister Oct 1, 2024
e3bb5fe
Update rocky/components/modal/script.js
TwistMeister Oct 1, 2024
6f2378c
Update script.js
TwistMeister Oct 1, 2024
57d131e
Merge branch 'main' into feat/update-modal-component-by-removing-trig…
underdarknl Oct 1, 2024
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
32 changes: 18 additions & 14 deletions rocky/components/modal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,25 @@ First you need to add `{% load component_tags %}` at the top of your template. N
{% endblock html_at_end_body %}
```

After that, `{% component "modal" size="xx" %}` is enough to instantiate the dialog modal component where size should contain the appropriate class name to achieve the correct sizing. This can be either `dialog-small`, `dialog-medium` or `dialog-large`.
After that, `{% component "modal" modal_id="xx" size="xx" %}` is enough to instantiate the dialog modal component where `modal_id` should contain a unique identifier that must be also used in the triggers `data-modal-id` attribute, and `size` should contain the appropriate class name to achieve the correct sizing. This can be either `dialog-small`, `dialog-medium` or `dialog-large`.

### Slots and fills
### The trigger element

Each named `fill` corresponds with a placeholder/target `slot` in the component template. The contents between the `fill` tag will be passed to the corresponding `slot`. As shown in the below example it's possible to utilise Django template tags and `HTML` tags with these `fill` tags. This enables us to entirely build the contents of the modal in the template where we implement it. Because we can use `HTML` tags here, we can also use `forms` and leave the handling of said form up to the Django template that knows about the context and applicable data, where we implement the modal. The defaults are used when no `fill` tags are implemented for this slot at all.
The trigger element needs some explaining. This will be the element that `on click` will show the modal dialog,
the `element` gets assigned the click handler by JavaScript.
It's essential to include the data attribute `data-modal-id="xx"`, where "xx" is the same as the `id` attribute on the intended modal. This is what defines the modal dialog that this trigger is meant to target. The only exception to this is when an `<a>` is used, with an `href` containing an anchor `"#xx"`, which points to `the data-modal-id` of the target modal.
While it might seem obvious to use a `button` or a `a` as a trigger, the modal is set up in a way that allows for any HTML element to be used as a trigger.
The trigger doesn't need to be part of the `{% component %}` itself and can be placed anywhere in the HTML template, though it makes sense to place them adjacent or as close as possible to each other.

There's a total of four slots you can fill:
### Slots and fills

1. `trigger`: call to action `button` by default, with the caption "Open modal".
2. `header`: empty by default
3. `content`: empty by default
4. `footer_buttons`: cancel `button` by default. To have _no buttons_ show at all, it's needed to implement empty `fill` tags for this `slot`.
Each named `fill` corresponds with a placeholder/target `slot` in the component template. The contents between the `fill` tag will be passed to the corresponding `slot`. As shown in the below example it's possible to utilise Django template tags and `HTML` tags with these `fill` tags. This enables us to entirely build the contents of the modal in the template where we implement it. Because we can use `HTML` tags here, we can also use `forms` and leave the handling of said form up to the Django template that knows about the context and applicable data, where we implement the modal. The defaults are used when no `fill` tags are implemented for this slot at all.

### The trigger element
There's a total of three slots you can fill:

The trigger `slot` is a special one. This needs to contain the HTML `element` that gets assigned the click handler by JavaScript. It's essential to include the `class="modal-trigger"` attribute, because this is what we target to assign the click handler, using JS. While it might seem obvious to use a `button` as a trigger, the modal is setup in a way that allows for any HTML element to be used as a trigger.
1. `header`: empty by default
2. `content`: empty by default
3. `footer_buttons`: cancel `button` by default. To have _no buttons_ show at all, it's needed to implement empty `fill` tags for this `slot`.

### CSS dependencies

Expand All @@ -44,10 +47,11 @@ Including `{% component_css_dependencies %}` is needed to inject the reference t
### Example implementation

```
{% component "modal" size="dialog-small" %}
{% fill "trigger" %}
<button class="modal-trigger">Click here to open the modal.</button>
{% endfill %}
<a href="#" data-modal-id="rename-modal">
```

```
{% component "modal" modal_id="rename-modal" size="dialog-small" %}
{% fill "header" %}
{% translate "This is an example header." %}
{% endfill %}
Expand Down
1 change: 1 addition & 0 deletions rocky/components/modal/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Modal(component.Component):
def get_context_data(self, **kwargs):
return {
"size": kwargs["size"],
"modal_id": kwargs["modal_id"],
}

class Media:
Expand Down
91 changes: 44 additions & 47 deletions rocky/components/modal/script.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { onDomReady } from "../js/imports/utils.js";

onDomReady(initDialogs);
onDomReady(function () {
initDialogs();
openDialogFromUrl();
});

export function openDialogFromUrl() {
// If ID is present in the URL on DomReady, open the dialog immediately.
let id = window.location.hash.slice(1);
if (id) {
showModalBasedOnAnchor(id);
}
}

export function initDialogs(element) {
let root = element || document;
Expand All @@ -10,67 +21,53 @@ export function initDialogs(element) {
}

export function initDialog(modal) {
let input_elements = [];
let dialog_element = modal.querySelector("dialog");
TwistMeister marked this conversation as resolved.
Show resolved Hide resolved
if (!dialog_element) return;

modal.querySelector(".modal-trigger").addEventListener("click", (event) => {
// Get and clone input elements to be able to "reset" them on "cancel".
input_elements = modal.querySelectorAll("input, textarea");
let trigger = document.querySelector(
underdarknl marked this conversation as resolved.
Show resolved Hide resolved
"[data-modal-id='" + dialog_element.id + "']",
);
TwistMeister marked this conversation as resolved.
Show resolved Hide resolved

// Store the initial value in a data attribute
input_elements.forEach((element) => {
element.defaultvalue = element.value;
// Check if trigger element is <a>, if not, on click,
// alter the URL to open the dialog using the onhaschange event.
if (trigger && trigger.nodeName !== "A") {
trigger.addEventListener("click", (event) => {
window.location.hash = "#" + dialog_element.id;
});

// Used ".closest" instead of ".parentNode" to make sure we stay flexible in terms of
// HTML-structure when implementing the trigger.
event.target.closest(".modal-wrapper").querySelector("dialog").showModal();
});
}

modal.querySelector("dialog").addEventListener("click", (event) => {
// The actual handling (like posting) of the input values should be done when implementing the component.
if (event.target.classList.contains("confirm-modal-button")) {
if (input_elements) {
// Closing is only allowed when the inputs are 'valid'.
if (checkValidity(input_elements)) {
event.target
.closest(".modal-wrapper")
.querySelector("dialog")
.close();
}
return;
}
}
// event.target.nodeName === 'DIALOG' is needed to check if the ::backdrop is clicked.
if (
event.target.classList.contains("confirm-modal-button") ||
event.target.classList.contains("close-modal-button") ||
event.target.nodeName === "DIALOG"
) {
// When canceling or closing using the 'x' remove the "error" styles.
input_elements.forEach((element) => {
element.classList.remove("error");
});

// When canceling or closing the modal, the inputs get reset to their initial value.
input_elements.forEach((element) => {
element.value = element.defaultvalue;
});

event.target.closest(".modal-wrapper").querySelector("dialog").close();
}
});
}

export function checkValidity(elements) {
let valid = true;

elements.forEach((element) => {
if (!element.checkValidity()) {
valid = false;
element.classList.add("error");
} else {
element.classList.remove("error");
}
modal.querySelector("dialog").addEventListener("close", (event) => {
removeDialogAnchor();
});
}

return valid;
export function removeDialogAnchor() {
// Remove the anchor from the URL when closing the modal
let baseUrl = window.location.toString().split("#")[0];
console.log(baseUrl);
TwistMeister marked this conversation as resolved.
Show resolved Hide resolved
window.history.pushState("", "Base URL", baseUrl);
}

export function showModalBasedOnAnchor(id) {
if (id && document.querySelector("#" + id).nodeName === "DIALOG") {
TwistMeister marked this conversation as resolved.
Show resolved Hide resolved
// Show modal, selected by ID
document.querySelector("#" + id).showModal();
}
}

addEventListener("hashchange", function () {
let id = window.location.toString().split("#")[1];
TwistMeister marked this conversation as resolved.
Show resolved Hide resolved
showModalBasedOnAnchor(id);
});
9 changes: 3 additions & 6 deletions rocky/components/modal/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
{% load compress %}

<div class="modal-wrapper">
{% slot "trigger" %}
<button class="modal-trigger">Open modal</button>
{% endslot %}

<dialog class="modal {{ size }}"
aria-modal="true"
role="dialog">
aria-modal="true"
role="dialog"
id="{{ modal_id }}">
<div class="content-wrapper">
<section class="header">
<h2>{% slot "header" %}{% endslot %}</h2>
Expand Down
14 changes: 8 additions & 6 deletions rocky/katalogus/templates/partials/plugin_tile_modal.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{% load i18n %}

{% component "modal" size="dialog-medium" %}
{% fill "trigger" %}
<a class="tile-detail-link modal-trigger">{% translate "See details" %}</a>
{% endfill %}
{% fill "header" %}
{% translate "Boefje details" %}
<a href="#{{ plugin.id }}-detail-modal"
class="tile-detail-link"
data-modal-id="{{ plugin.id }}-detail-modal">{% translate "See details" %}</a>
{% with modal_id=plugin.id|add:"-detail-modal" %}
{% component "modal" modal_id=modal_id size="dialog-medium" %}
{% fill "header" %}
{% translate "Boefje details" %}
{% endfill %}
{% fill "content" %}
<section>
Expand Down Expand Up @@ -86,3 +87,4 @@ <h3 class="heading-small">{% translate "Produces" %}</h3>
{% endfill %}
{% endcomponent %}
{% component_css_dependencies %}
{% endwith %}