kopia lustrzana https://github.com/wagtail/wagtail
Allow SwapController to use the form element's method
rodzic
3c1ba47566
commit
e7dac3e18d
client
src
components/Draftail/decorators/__snapshots__
controllers
tests
|
@ -25,7 +25,7 @@ exports[`TooltipEntity #openTooltip 1`] = `
|
|||
id="wagtail-config"
|
||||
type="application/json"
|
||||
>
|
||||
{"CSRF_TOKEN":"potato"}
|
||||
{"CSRF_HEADER_NAME":"x-xsrf-token","CSRF_TOKEN":"potato"}
|
||||
</script>
|
||||
<div
|
||||
data-draftail-trigger="true"
|
||||
|
@ -45,7 +45,7 @@ exports[`TooltipEntity #openTooltip 1`] = `
|
|||
id="wagtail-config"
|
||||
type="application/json"
|
||||
>
|
||||
{"CSRF_TOKEN":"potato"}
|
||||
{"CSRF_HEADER_NAME":"x-xsrf-token","CSRF_TOKEN":"potato"}
|
||||
</script>
|
||||
<div
|
||||
data-draftail-trigger="true"
|
||||
|
|
|
@ -696,6 +696,66 @@ describe('SwapController', () => {
|
|||
expect(window.location.search).toEqual('');
|
||||
});
|
||||
|
||||
it('should support using the form method as the fetch request method', async () => {
|
||||
const expectedRequestUrl = '/path/to-src-value/?with=param';
|
||||
|
||||
expect(window.location.search).toEqual('');
|
||||
expect(handleError).not.toHaveBeenCalled();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
|
||||
formElement.setAttribute('data-w-swap-src-value', expectedRequestUrl);
|
||||
formElement.setAttribute('method', 'post');
|
||||
|
||||
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.objectContaining({
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
'x-xsrf-token': 'potato',
|
||||
},
|
||||
method: 'post',
|
||||
}),
|
||||
);
|
||||
// We are using #replace, not #submit, so we should not have a body
|
||||
expect(global.fetch.mock.lastCall[1].body).toBeUndefined();
|
||||
|
||||
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 NOT update the current URL
|
||||
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';
|
||||
|
||||
|
@ -1023,6 +1083,89 @@ describe('SwapController', () => {
|
|||
expect(window.location.search).toEqual('');
|
||||
});
|
||||
|
||||
it('should support using the form method as the fetch request method', async () => {
|
||||
const input = document.getElementById('search');
|
||||
const formElement = document.querySelector('form');
|
||||
const expectedRequestUrl = '/custom/to-src-value/?with=param';
|
||||
|
||||
formElement.setAttribute('data-w-swap-src-value', expectedRequestUrl);
|
||||
formElement.setAttribute('method', 'post');
|
||||
|
||||
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.mockResponseSuccessText(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';
|
||||
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: expectedRequestUrl,
|
||||
});
|
||||
|
||||
// visual loading state should be active
|
||||
await Promise.resolve(); // trigger next rendering
|
||||
|
||||
expect(handleError).not.toHaveBeenCalled();
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
// The form data should be serialized and sent as the body,
|
||||
// not as query params
|
||||
expectedRequestUrl,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
'x-xsrf-token': 'potato',
|
||||
},
|
||||
method: 'post',
|
||||
body: expect.any(FormData),
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
Object.fromEntries(global.fetch.mock.lastCall[1].body.entries()),
|
||||
).toEqual({
|
||||
// eslint-disable-next-line id-length
|
||||
q: 'alpha',
|
||||
type: 'some-type',
|
||||
other: 'something on other',
|
||||
});
|
||||
|
||||
const successEvent = await onSuccess;
|
||||
|
||||
// should dispatch success event
|
||||
expect(successEvent.detail).toEqual({
|
||||
requestUrl: expectedRequestUrl,
|
||||
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 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');
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
import { debounce } from '../utils/debounce';
|
||||
import { WAGTAIL_CONFIG } from '../config/wagtailConfig';
|
||||
|
||||
/**
|
||||
* Allow for an element to trigger an async query that will
|
||||
|
@ -78,11 +79,8 @@ export class SwapController extends Controller<
|
|||
submitLazy?: { (...args: any[]): void; cancel(): void };
|
||||
|
||||
connect() {
|
||||
const formContainer = this.hasInputTarget
|
||||
? this.inputTarget.form
|
||||
: this.element;
|
||||
this.srcValue =
|
||||
this.srcValue || formContainer?.getAttribute('action') || '';
|
||||
this.srcValue || this.formElement.getAttribute('action') || '';
|
||||
const target = this.target;
|
||||
|
||||
// set up icons
|
||||
|
@ -200,24 +198,29 @@ export class SwapController extends Controller<
|
|||
});
|
||||
}
|
||||
|
||||
get formElement() {
|
||||
return (
|
||||
this.hasInputTarget ? this.inputTarget.form || this.element : this.element
|
||||
) as HTMLFormElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the target element's content with the response from a request based on the input's form
|
||||
* values serialised. Do not account for anything in the main location/URL, simply replace the content within
|
||||
* the target element.
|
||||
*/
|
||||
submit() {
|
||||
const form = (
|
||||
this.hasInputTarget ? this.inputTarget.form : this.element
|
||||
) as HTMLFormElement;
|
||||
|
||||
// serialise the form to a query string
|
||||
// https://github.com/microsoft/TypeScript/issues/43797
|
||||
const searchParams = new URLSearchParams(new FormData(form) as any);
|
||||
const form = this.formElement;
|
||||
const data = new FormData(form);
|
||||
|
||||
// serialise the form to a query string if it's a GET request
|
||||
// cast as any to avoid https://github.com/microsoft/TypeScript/issues/43797
|
||||
const searchParams = new URLSearchParams(data as any);
|
||||
const queryString = '?' + searchParams.toString();
|
||||
const url = this.srcValue;
|
||||
const url =
|
||||
form.method === 'get' ? this.srcValue + queryString : this.srcValue;
|
||||
|
||||
this.replace(url + queryString);
|
||||
this.replace(url, data);
|
||||
}
|
||||
|
||||
reflectParams(url: string) {
|
||||
|
@ -241,16 +244,18 @@ export class SwapController extends Controller<
|
|||
* a faster response does not replace an in flight request.
|
||||
*/
|
||||
async replace(
|
||||
data?:
|
||||
urlSource?:
|
||||
| string
|
||||
| (CustomEvent<{ url: string }> & { params?: { url?: string } }),
|
||||
data?: FormData,
|
||||
) {
|
||||
const target = this.target;
|
||||
/** Parse a request URL from the supplied param, as a string or inside a custom event */
|
||||
const requestUrl =
|
||||
(typeof data === 'string'
|
||||
? data
|
||||
: data?.detail?.url || data?.params?.url || '') || this.srcValue;
|
||||
(typeof urlSource === 'string'
|
||||
? urlSource
|
||||
: urlSource?.detail?.url || urlSource?.params?.url || '') ||
|
||||
this.srcValue;
|
||||
|
||||
if (this.abortController) this.abortController.abort();
|
||||
this.abortController = new AbortController();
|
||||
|
@ -265,10 +270,15 @@ export class SwapController extends Controller<
|
|||
}) as CustomEvent<{ requestUrl: string }>;
|
||||
|
||||
if (beginEvent.defaultPrevented) return Promise.resolve();
|
||||
|
||||
const formMethod = this.formElement.getAttribute('method') || undefined;
|
||||
return fetch(requestUrl, {
|
||||
headers: { 'x-requested-with': 'XMLHttpRequest' },
|
||||
headers: {
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
[WAGTAIL_CONFIG.CSRF_HEADER_NAME]: WAGTAIL_CONFIG.CSRF_TOKEN,
|
||||
},
|
||||
signal,
|
||||
method: formMethod,
|
||||
body: formMethod !== 'get' ? data : undefined,
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
|
|
|
@ -39,7 +39,10 @@ global.wagtailConfig = {
|
|||
const script = document.createElement('script');
|
||||
script.type = 'application/json';
|
||||
script.id = 'wagtail-config';
|
||||
script.textContent = JSON.stringify({ CSRF_TOKEN: 'potato' });
|
||||
script.textContent = JSON.stringify({
|
||||
CSRF_HEADER_NAME: 'x-xsrf-token',
|
||||
CSRF_TOKEN: 'potato',
|
||||
});
|
||||
document.body.appendChild(script);
|
||||
|
||||
global.wagtailVersion = '1.6a1';
|
||||
|
|
Ładowanie…
Reference in New Issue