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:
parent
c4f953e90f
commit
4fedf3e2d4
@ -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');
|
||||
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user