0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-25 05:02:57 +01:00

Add the ability to reflect query params in SwapController

This is useful when the controller is used in views that would produce an identical response if the page is hard-reloaded with the same params, e.g. listings
This commit is contained in:
Sage Abdullah 2024-01-25 11:46:11 +00:00 committed by Thibaud Colas
parent c4f953e90f
commit 4fedf3e2d4
2 changed files with 327 additions and 0 deletions

View File

@ -718,6 +718,138 @@ describe('SwapController', () => {
expect(window.location.search).toEqual('');
});
it('should reflect the query params of the request URL if reflect-value is true', async () => {
const expectedRequestUrl = '/path/to-src-value/?foo=bar&abc=&xyz=123';
expect(window.location.search).toEqual('');
expect(handleError).not.toHaveBeenCalled();
expect(global.fetch).not.toHaveBeenCalled();
const reflectEventHandler = new Promise((resolve) => {
document.addEventListener('w-swap:reflect', resolve);
});
formElement.setAttribute('data-w-swap-src-value', expectedRequestUrl);
formElement.setAttribute('data-w-swap-reflect-value', 'true');
formElement.dispatchEvent(
new CustomEvent('custom:event', { bubbles: false }),
);
expect(beginEventHandler).not.toHaveBeenCalled();
jest.runAllTimers(); // search is debounced
// should fire a begin event before the request is made
expect(beginEventHandler).toHaveBeenCalledTimes(1);
expect(beginEventHandler.mock.calls[0][0].detail).toEqual({
requestUrl: expectedRequestUrl,
});
// visual loading state should be active
await Promise.resolve(); // trigger next rendering
expect(handleError).not.toHaveBeenCalled();
expect(global.fetch).toHaveBeenCalledWith(
expectedRequestUrl,
expect.any(Object),
);
const reflectEvent = await reflectEventHandler;
// should dispatch reflect event
expect(reflectEvent.detail).toEqual({
requestUrl: expectedRequestUrl,
});
const successEvent = await onSuccess;
// should dispatch success event
expect(successEvent.detail).toEqual({
requestUrl: expectedRequestUrl,
results: expect.any(String),
});
// should update HTML
expect(
document.getElementById('content').querySelectorAll('li'),
).toHaveLength(2);
await flushPromises();
// should update the current URL to have the query params from requestUrl
// (except for those that are empty)
// as the reflect-value attribute is set to true
expect(window.location.search).toEqual('?foo=bar&xyz=123');
});
it('should allow for blocking the reflection of query params with event handlers', async () => {
const expectedRequestUrl = '/path/to-src-value/?foo=bar&abc=&xyz=123';
expect(window.location.search).toEqual('');
expect(handleError).not.toHaveBeenCalled();
expect(global.fetch).not.toHaveBeenCalled();
const reflectEventHandler = jest.fn((event) => {
event.preventDefault();
});
document.addEventListener('w-swap:reflect', reflectEventHandler);
formElement.setAttribute('data-w-swap-src-value', expectedRequestUrl);
formElement.setAttribute('data-w-swap-reflect-value', 'true');
formElement.dispatchEvent(
new CustomEvent('custom:event', { bubbles: false }),
);
expect(beginEventHandler).not.toHaveBeenCalled();
jest.runAllTimers(); // search is debounced
// should fire a begin event before the request is made
expect(beginEventHandler).toHaveBeenCalledTimes(1);
expect(beginEventHandler.mock.calls[0][0].detail).toEqual({
requestUrl: expectedRequestUrl,
});
// visual loading state should be active
await Promise.resolve(); // trigger next rendering
expect(handleError).not.toHaveBeenCalled();
expect(global.fetch).toHaveBeenCalledWith(
expectedRequestUrl,
expect.any(Object),
);
const successEvent = await onSuccess;
// should dispatch reflect event
expect(reflectEventHandler).toHaveBeenCalledTimes(1);
expect(reflectEventHandler.mock.calls[0][0].detail).toEqual({
requestUrl: expectedRequestUrl,
});
// should dispatch success event
expect(successEvent.detail).toEqual({
requestUrl: expectedRequestUrl,
results: expect.any(String),
});
// should update HTML
expect(
document.getElementById('content').querySelectorAll('li'),
).toHaveLength(2);
await flushPromises();
// should NOT update the current URL
// as the reflect-value attribute is set to false
expect(window.location.search).toEqual('');
document.removeEventListener('w-swap:reflect', reflectEventHandler);
});
it('should support replace with a url value provided via the Custom event detail', async () => {
const expectedRequestUrl = '/path/to/url-in-event-detail/?q=alpha';
@ -767,6 +899,7 @@ describe('SwapController', () => {
await flushPromises();
// should NOT update the current URL
// as the reflect-value attribute is not set
expect(window.location.search).toEqual('');
});
@ -914,9 +1047,174 @@ describe('SwapController', () => {
await flushPromises();
// should NOT update the current URL
// as the reflect-value attribute is not set
expect(window.location.search).toEqual('');
});
it('should reflect the query params of the request URL if reflect-value is true', async () => {
const formElement = document.querySelector('form');
formElement.setAttribute('data-w-swap-reflect-value', 'true');
const input = document.getElementById('search');
const results = getMockResults({ total: 5 });
const onSuccess = new Promise((resolve) => {
document.addEventListener('w-swap:success', resolve);
});
const beginEventHandler = jest.fn();
document.addEventListener('w-swap:begin', beginEventHandler);
fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
status: 200,
text: () => Promise.resolve(results),
}),
);
expect(window.location.search).toEqual('');
expect(handleError).not.toHaveBeenCalled();
expect(global.fetch).not.toHaveBeenCalled();
input.value = 'alpha';
document.querySelector('[name="other"]').value = 'something on other';
document.querySelector('[name="type"]').value = '';
input.dispatchEvent(new CustomEvent('change', { bubbles: true }));
expect(beginEventHandler).not.toHaveBeenCalled();
jest.runAllTimers(); // search is debounced
// should fire a begin event before the request is made
expect(beginEventHandler).toHaveBeenCalledTimes(1);
expect(beginEventHandler.mock.calls[0][0].detail).toEqual({
requestUrl:
'/path/to/form/action/?q=alpha&type=&other=something+on+other',
});
// visual loading state should be active
await Promise.resolve(); // trigger next rendering
expect(handleError).not.toHaveBeenCalled();
expect(global.fetch).toHaveBeenCalledWith(
'/path/to/form/action/?q=alpha&type=&other=something+on+other',
expect.any(Object),
);
const successEvent = await onSuccess;
// should dispatch success event
expect(successEvent.detail).toEqual({
requestUrl:
'/path/to/form/action/?q=alpha&type=&other=something+on+other',
results: expect.any(String),
});
// should update HTML
expect(
document.getElementById('task-results').querySelectorAll('li').length,
).toBeTruthy();
await flushPromises();
// should update the current URL to have the query params from requestUrl
// (except for those that are empty)
// as the reflect-value attribute is set to true
expect(window.location.search).toEqual(
'?q=alpha&other=something+on+other',
);
});
it('should allow for blocking the reflection of query params with event handlers', async () => {
const formElement = document.querySelector('form');
formElement.setAttribute('data-w-swap-reflect-value', 'true');
const reflectEventHandler = jest.fn((event) => {
event.preventDefault();
});
document.addEventListener('w-swap:reflect', reflectEventHandler);
const input = document.getElementById('search');
const results = getMockResults({ total: 5 });
const onSuccess = new Promise((resolve) => {
document.addEventListener('w-swap:success', resolve);
});
const beginEventHandler = jest.fn();
document.addEventListener('w-swap:begin', beginEventHandler);
fetch.mockImplementationOnce(() =>
Promise.resolve({
ok: true,
status: 200,
text: () => Promise.resolve(results),
}),
);
expect(window.location.search).toEqual('');
expect(handleError).not.toHaveBeenCalled();
expect(global.fetch).not.toHaveBeenCalled();
input.value = 'alpha';
document.querySelector('[name="other"]').value = 'something on other';
document.querySelector('[name="type"]').value = '';
input.dispatchEvent(new CustomEvent('change', { bubbles: true }));
expect(beginEventHandler).not.toHaveBeenCalled();
jest.runAllTimers(); // search is debounced
// should fire a begin event before the request is made
expect(beginEventHandler).toHaveBeenCalledTimes(1);
expect(beginEventHandler.mock.calls[0][0].detail).toEqual({
requestUrl:
'/path/to/form/action/?q=alpha&type=&other=something+on+other',
});
// visual loading state should be active
await Promise.resolve(); // trigger next rendering
expect(handleError).not.toHaveBeenCalled();
expect(global.fetch).toHaveBeenCalledWith(
'/path/to/form/action/?q=alpha&type=&other=something+on+other',
expect.any(Object),
);
const successEvent = await onSuccess;
// should dispatch reflect event
expect(reflectEventHandler).toHaveBeenCalledTimes(1);
expect(reflectEventHandler.mock.calls[0][0].detail).toEqual({
requestUrl:
'/path/to/form/action/?q=alpha&type=&other=something+on+other',
});
// should dispatch success event
expect(successEvent.detail).toEqual({
requestUrl:
'/path/to/form/action/?q=alpha&type=&other=something+on+other',
results: expect.any(String),
});
// should update HTML
expect(
document.getElementById('task-results').querySelectorAll('li').length,
).toBeTruthy();
await flushPromises();
// should NOT update the current URL
// as the reflect-value attribute is set to false
expect(window.location.search).toEqual('');
document.removeEventListener('w-swap:reflect', reflectEventHandler);
});
it('should allow for blocking the request with custom events', async () => {
const input = document.getElementById('search');

View File

@ -46,6 +46,7 @@ export class SwapController extends Controller<
icon: { default: '', type: String },
loading: { default: false, type: Boolean },
src: { default: '', type: String },
reflect: { default: false, type: Boolean },
target: { default: '#listing-results', type: String },
wait: { default: 200, type: Number },
};
@ -58,6 +59,7 @@ export class SwapController extends Controller<
declare iconValue: string;
declare loadingValue: boolean;
declare srcValue: string;
declare reflectValue: boolean;
declare targetValue: string;
declare waitValue: number;
@ -215,6 +217,20 @@ export class SwapController extends Controller<
this.replace(url + queryString);
}
reflectParams(url: string) {
const params = new URL(url, window.location.href).searchParams;
const filteredParams = new URLSearchParams();
params.forEach((value, key) => {
// Check if the value is not empty after trimming white space
// and if the key is not a Wagtail internal param
if (value.trim() !== '' && !key.startsWith('_w_')) {
filteredParams.append(key, value);
}
});
const queryString = `?${filteredParams.toString()}`;
window.history.replaceState(null, '', queryString);
}
/**
* Abort any existing requests & set up new abort controller, then fetch and replace
* the HTML target with the new results.
@ -259,11 +275,24 @@ export class SwapController extends Controller<
})
.then((results) => {
target.innerHTML = results;
if (this.reflectValue) {
const event = this.dispatch('reflect', {
cancelable: true,
detail: { requestUrl },
target,
});
if (!event.defaultPrevented) {
this.reflectParams(requestUrl);
}
}
this.dispatch('success', {
cancelable: false,
detail: { requestUrl, results },
target,
});
return results;
})
.catch((error) => {