Skip to content

Commit

Permalink
Adopt w-bulk Stimulus controller for form submissions listing
Browse files Browse the repository at this point in the history
  • Loading branch information
lb- committed Aug 24, 2023
1 parent 714beab commit 585a08f
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 67 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Changelog
* Maintenance: Allow `ViewSet` subclasses to customise `url_prefix` and `url_namespace` logic (Matt Westcott)
* Maintenance: Simplify `SnippetViewSet` registration code (Sage Abdullah)
* Maintenance: Rename groups `IndexView.results_template_name` to `results.html` (Sage Abdullah)
* Maintenance: Migrate form submission listing checkbox toggling to the shared `w-bulk` Stimulus implementation (LB (Ben) Johnston)


5.1.1 (14.08.2023)
Expand Down
39 changes: 38 additions & 1 deletion client/src/controllers/BulkController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BulkController } from './BulkController';
describe('BulkController', () => {
beforeEach(() => {
document.body.innerHTML = `
<div data-controller="w-bulk">
<div id="bulk-container" data-controller="w-bulk">
<input id="select-all" type="checkbox" data-w-bulk-target="all" data-action="w-bulk#toggleAll">
<div id="checkboxes">
<input type="checkbox" data-w-bulk-target="item" disabled data-action="w-bulk#toggle">
Expand Down Expand Up @@ -115,4 +115,41 @@ describe('BulkController', () => {
expect(itemCheckbox.checked).toBe(true);
});
});

it('should allow for action targets to have classes toggled when any checkboxes are clicked', async () => {
const container = document.getElementById('bulk-container');

// create innerActions container that will be conditionally hidden with test classes
container.setAttribute(
'data-w-bulk-action-inactive-class',
'hidden w-invisible',
);
const innerActions = document.createElement('div');
innerActions.id = 'inner-actions';
innerActions.className = 'keep-me hidden w-invisible';
innerActions.setAttribute('data-w-bulk-target', 'action');
container.prepend(innerActions);

const innerActionsElement = document.getElementById('inner-actions');

expect(
document
.getElementById('checkboxes')
.querySelectorAll(':checked:not(:disabled)').length,
).toEqual(0);

expect(innerActionsElement.className).toEqual('keep-me hidden w-invisible');

const firstCheckbox = document
.getElementById('checkboxes')
.querySelector("[type='checkbox']:not([disabled])");

firstCheckbox.click();

expect(innerActionsElement.className).toEqual('keep-me');

firstCheckbox.click();

expect(innerActionsElement.className).toEqual('keep-me hidden w-invisible');
});
});
48 changes: 42 additions & 6 deletions client/src/controllers/BulkController.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,50 @@
import { Controller } from '@hotwired/stimulus';

/**
* Adds the ability to collectively toggle a set of (non-disabled) checkboxes.
*
* @example
* @example - Basic usage
* <div data-controller="w-bulk">
* <input type="checkbox" data-action="w-bulk#toggleAll" data-w-bulk-target="all">
* <div>
* <input type="checkbox" data-action="w-bulk#change" data-w-bulk-target="item" disabled>
* <input type="checkbox" data-action="w-bulk#change" data-w-bulk-target="item">
* <input type="checkbox" data-action="w-bulk#change" data-w-bulk-target="item">
* <input type="checkbox" data-action="w-bulk#toggle" data-w-bulk-target="item" disabled>
* <input type="checkbox" data-action="w-bulk#toggle" data-w-bulk-target="item">
* <input type="checkbox" data-action="w-bulk#toggle" data-w-bulk-target="item">
* </div>
* <button data-action="w-bulk#toggleAll" data-w-bulk-force-param="false">Clear all</button>
* <button data-action="w-bulk#toggleAll" data-w-bulk-force-param="true">Select all</button>
* </div>
*
* @example - Showing and hiding an actions container
* <div data-controller="w-bulk" data-w-bulk-action-inactive-class="w-invisible">
* <div class="w-invisible" data-w-bulk-target="action" id="inner-actions">
* <button type="button">Some action</button>
* </div>
* <input data-action="w-bulk#toggleAll" data-w-bulk-target="all" type="checkbox"/>
* <div id="checkboxes">
* <input data-action="w-bulk#toggle" data-w-bulk-target="item" disabled="" type="checkbox" />
* <input data-action="w-bulk#toggle" data-w-bulk-target="item" type="checkbox"/>
* <input data-action="w-bulk#toggle" data-w-bulk-target="item" type="checkbox" />
* </div>
* </div>
*/
export class BulkController extends Controller<HTMLElement> {
static targets = ['all', 'item'];
static classes = ['actionInactive'];
static targets = ['action', 'all', 'item'];

/** Target(s) that will have the `actionInactive` classes removed if any actions are checked */
declare readonly actionTargets: HTMLElement[];

/** All select-all checkbox targets */
declare readonly allTargets: HTMLInputElement[];

/** All item checkbox targets */
declare readonly itemTargets: HTMLInputElement[];

/** Classes to remove on the actions target if any actions are checked */
declare readonly actionInactiveClasses: string[];

get activeItems() {
return this.itemTargets.filter(({ disabled }) => !disabled);
}
Expand All @@ -36,13 +58,27 @@ export class BulkController extends Controller<HTMLElement> {

/**
* When something is toggled, ensure the select all targets are kept in sync.
* Update the classes on the action targets to reflect the current state.
*/
toggle() {
const isAllChecked = !this.activeItems.some((item) => !item.checked);
const activeItems = this.activeItems;
const totalCheckedItems = activeItems.filter((item) => item.checked).length;
const isAnyChecked = totalCheckedItems > 0;
const isAllChecked = totalCheckedItems === activeItems.length;

this.allTargets.forEach((target) => {
// eslint-disable-next-line no-param-reassign
target.checked = isAllChecked;
});

const actionInactiveClasses = this.actionInactiveClasses;
if (!actionInactiveClasses.length) return;

this.actionTargets.forEach((element) => {
actionInactiveClasses.forEach((actionInactiveClass) => {
element.classList.toggle(actionInactiveClass, !isAnyChecked);
});
});
}

/**
Expand Down
1 change: 1 addition & 0 deletions docs/releases/5.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ depth: 1
* Allow `ViewSet` subclasses to customise `url_prefix` and `url_namespace` logic (Matt Westcott)
* Simplify `SnippetViewSet` registration code (Sage Abdullah)
* Rename groups `IndexView.results_template_name` to `results.html` (Sage Abdullah)
* Migrate form submission listing checkbox toggling to the shared `w-bulk` Stimulus implementation (LB (Ben) Johnston)


## Upgrade considerations - changes affecting all projects
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{% load i18n %}
<div class="overflow">
<table class="listing">
<table class="listing" data-controller="w-bulk" data-w-bulk-action-inactive-class="w-invisible">
<col />
<col />
<col />
<thead>
<tr>
<th colspan="{{ data_headings|length|add:1 }}">
<button class="button no" id="delete-submissions" style="visibility: hidden">{% trans "Delete selected submissions" %}</button>
<button class="button no w-invisible" data-w-bulk-target="action">{% trans "Delete selected submissions" %}</button>
</th>
</tr>
<tr>
<th><input type="checkbox" id="select-all" /></th>
<th><input type="checkbox" data-action="w-bulk#toggleAll" data-w-bulk-target="all" /></th>
{% for heading in data_headings %}
<th id="{{ heading.name }}" class="{% if heading.order %}ordered icon {% if heading.order == 'ascending' %}icon-arrow-up-after{% else %}icon-arrow-down-after{% endif %}{% endif %}">
{% if heading.order %}<a href="?order_by={% if heading.order == 'ascending' %}-{% endif %}{{ heading.name }}">{{ heading.label }}</a>{% else %}{{ heading.label }}{% endif %}
Expand All @@ -23,7 +23,7 @@
{% for row in data_rows %}
<tr>
<td>
<input type="checkbox" name="selected-submissions" class="select-submission" value="{{ row.model_id }}" />
<input type="checkbox" name="selected-submissions" class="select-submission" value="{{ row.model_id }}" data-action="w-bulk#toggle" data-w-bulk-target="item" />
</td>
{% for cell in row.fields %}
<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,62 +21,6 @@
timepicker: false,
format: 'Y-m-d',
});

var selectAllCheckbox = document.getElementById('select-all');
var deleteButton = document.getElementById('delete-submissions');

function updateActions() {
var submissionCheckboxes = $('input[type=checkbox].select-submission');
var someSubmissionsSelected = submissionCheckboxes.is(':checked');
var everySubmissionSelected = !submissionCheckboxes.is(':not(:checked)');

// Select all box state
if (everySubmissionSelected) {
// Every submission has been selected
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else if (someSubmissionsSelected) {
// At least one, but not all submissions have been selected
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
} else {
// No submissions have been selected
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}

// Delete button state
if (someSubmissionsSelected) {
deleteButton.classList.remove('disabled')
deleteButton.style.visibility = "visible";
} else {
deleteButton.classList.add('disabled')
deleteButton.style.visibility = "hidden";
}
}


// Event handlers

$(selectAllCheckbox).on('change', function() {
let checked = this.checked;

// Update checkbox states
$('input[type=checkbox].select-submission').each(function() {
this.checked = checked;
});

updateActions();
});

$('input[type=checkbox].select-submission').on('change', function() {
updateActions();
});

// initial call to updateActions to bring delete button state in sync with checkboxes
// in the case that some checkboxes are pre-checked (which will be the case in some
// browsers when using the back button)
updateActions();
});
</script>
{% endblock %}
Expand Down

0 comments on commit 585a08f

Please sign in to comment.