feat(js): add httpRequest
This commit is contained in:
parent
81fb1793e6
commit
229eb1a775
1 changed files with 118 additions and 59 deletions
|
@ -1,8 +1,9 @@
|
|||
import Rails from '@rails/ujs';
|
||||
import debounce from 'debounce';
|
||||
import { session } from '@hotwired/turbo';
|
||||
|
||||
export { debounce };
|
||||
export const { fire, csrfToken } = Rails;
|
||||
export const { fire, csrfToken, cspNonce } = Rails;
|
||||
|
||||
export function show(el: HTMLElement) {
|
||||
el && el.classList.remove('hidden');
|
||||
|
@ -93,7 +94,7 @@ export function ajax(options: Rails.AjaxOptions) {
|
|||
});
|
||||
}
|
||||
|
||||
class ResponseError extends Error {
|
||||
export class ResponseError extends Error {
|
||||
response: Response;
|
||||
|
||||
constructor(response: Response) {
|
||||
|
@ -102,18 +103,126 @@ class ResponseError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export function getJSON(url: string, data: unknown, method = 'GET') {
|
||||
const { query, ...options } = fetchOptions(data, method);
|
||||
const FETCH_TIMEOUT = 30 * 1000; // 30 sec
|
||||
|
||||
return fetch(`${url}${query}`, options).then((response) => {
|
||||
if (response.ok) {
|
||||
if (response.status === 204) {
|
||||
// Perform an HTTP request using `fetch` API,
|
||||
// and handle the result depending on the MIME type.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// Execute a GET request, and return the response as parsed JSON
|
||||
// const parsedJson = await httpRequest(url).json();
|
||||
//
|
||||
// Execute a POST request with some JSON payload
|
||||
// const parsedJson = await httpRequest(url, { method: 'POST', json: '{ "foo": 1 }').json();
|
||||
//
|
||||
// Execute a GET request, and apply the Turbo stream in the Response
|
||||
// await httpRequest(url).turbo();
|
||||
//
|
||||
// Execute a GET request, and interpret the JavaScript code in the Response
|
||||
// DEPRECATED: Don't use this in new code; instead let the server respond with a turbo stream
|
||||
// await httpRequest(url).js();
|
||||
//
|
||||
export function httpRequest(
|
||||
url: string,
|
||||
{
|
||||
csrf = true,
|
||||
timeout = FETCH_TIMEOUT,
|
||||
json,
|
||||
controller,
|
||||
...init
|
||||
}: RequestInit & {
|
||||
csrf?: boolean;
|
||||
json?: unknown;
|
||||
timeout?: number | false;
|
||||
controller?: AbortController;
|
||||
} = {}
|
||||
) {
|
||||
const headers = new Headers(init.headers);
|
||||
if (csrf) {
|
||||
headers.set('x-csrf-token', csrfToken() ?? '');
|
||||
headers.set('x-requested-with', 'XMLHttpRequest');
|
||||
init.credentials = 'same-origin';
|
||||
}
|
||||
init.headers = headers;
|
||||
init.method = init.method?.toUpperCase() ?? 'GET';
|
||||
|
||||
if (json) {
|
||||
headers.set('content-type', 'application/json');
|
||||
init.body = JSON.stringify(json);
|
||||
}
|
||||
|
||||
let timer: number;
|
||||
if (!init.signal) {
|
||||
controller = createAbortController(controller);
|
||||
if (controller) {
|
||||
init.signal = controller.signal;
|
||||
if (timeout != false) {
|
||||
timer = setTimeout(() => controller?.abort(), timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const request = (init: RequestInit, accept?: string): Promise<Response> => {
|
||||
if (accept && init.headers instanceof Headers) {
|
||||
init.headers.set('accept', accept);
|
||||
}
|
||||
return fetch(url, init)
|
||||
.then((response) => {
|
||||
clearTimeout(timer);
|
||||
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else if (response.status == 401) {
|
||||
location.reload(); // reload whole page so Devise will redirect to sign-in
|
||||
}
|
||||
throw new ResponseError(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timer);
|
||||
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
async json<T>(): Promise<T | null> {
|
||||
const response = await request(init, 'application/json');
|
||||
if (response.status == 204) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
async turbo(): Promise<void> {
|
||||
const response = await request(init, 'text/vnd.turbo-stream.html');
|
||||
if (response.status != 204) {
|
||||
const stream = await response.text();
|
||||
session.renderStreamMessage(stream);
|
||||
}
|
||||
},
|
||||
async js(): Promise<void> {
|
||||
const response = await request(init, 'text/javascript');
|
||||
if (response.status != 204) {
|
||||
const script = document.createElement('script');
|
||||
const nonce = cspNonce();
|
||||
if (nonce) {
|
||||
script.setAttribute('nonce', nonce);
|
||||
}
|
||||
script.text = await response.text();
|
||||
document.head.appendChild(script);
|
||||
document.head.removeChild(script);
|
||||
}
|
||||
}
|
||||
throw new ResponseError(response);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createAbortController(controller?: AbortController) {
|
||||
if (controller) {
|
||||
return controller;
|
||||
} else if (window.AbortController) {
|
||||
return new AbortController();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export function scrollTo(container: HTMLElement, scrollTo: HTMLElement) {
|
||||
|
@ -162,53 +271,3 @@ export function timeoutable<T>(
|
|||
});
|
||||
return Promise.race([promise, timeoutPromise]);
|
||||
}
|
||||
|
||||
const FETCH_TIMEOUT = 30 * 1000; // 30 sec
|
||||
|
||||
function fetchOptions(data: unknown, method = 'GET') {
|
||||
const options: RequestInit & {
|
||||
query: string;
|
||||
headers: Record<string, string>;
|
||||
} = {
|
||||
query: '',
|
||||
method: method.toUpperCase(),
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-csrf-token': csrfToken() ?? '',
|
||||
'x-requested-with': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
if (data) {
|
||||
if (options.method === 'GET') {
|
||||
options.query = objectToQuerystring(data as Record<string, string>);
|
||||
} else {
|
||||
options.headers['content-type'] = 'application/json';
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.AbortController) {
|
||||
const controller = new AbortController();
|
||||
options.signal = controller.signal;
|
||||
|
||||
setTimeout(() => {
|
||||
controller.abort();
|
||||
}, FETCH_TIMEOUT);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function objectToQuerystring(obj: Record<string, string>): string {
|
||||
return Object.keys(obj).reduce(function (query, key, i) {
|
||||
return [
|
||||
query,
|
||||
i === 0 ? '?' : '&',
|
||||
encodeURIComponent(key),
|
||||
'=',
|
||||
encodeURIComponent(obj[key])
|
||||
].join('');
|
||||
}, '');
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue