Use Turbo.fetch to prevent page refreshes bounces when using rails/requests
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ pin "application"
|
|||||||
pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
pin "@hotwired/turbo-rails", to: "turbo.min.js"
|
||||||
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
||||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
||||||
pin "@rails/request.js", to: "@rails--request.js" # @0.0.11
|
pin "@rails/request.js", to: "rails-request.js" # @0.0.11
|
||||||
|
|
||||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||||
pin_all_from "app/javascript/helpers", under: "helpers"
|
pin_all_from "app/javascript/helpers", under: "helpers"
|
||||||
|
|||||||
Vendored
-4
File diff suppressed because one or more lines are too long
Vendored
+308
@@ -0,0 +1,308 @@
|
|||||||
|
// Patched to use Turbo.fetch. @TODO Upstream this.
|
||||||
|
class FetchResponse {
|
||||||
|
constructor(response) {
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
get statusCode() {
|
||||||
|
return this.response.status;
|
||||||
|
}
|
||||||
|
get redirected() {
|
||||||
|
return this.response.redirected;
|
||||||
|
}
|
||||||
|
get ok() {
|
||||||
|
return this.response.ok;
|
||||||
|
}
|
||||||
|
get unauthenticated() {
|
||||||
|
return this.statusCode === 401;
|
||||||
|
}
|
||||||
|
get unprocessableEntity() {
|
||||||
|
return this.statusCode === 422;
|
||||||
|
}
|
||||||
|
get authenticationURL() {
|
||||||
|
return this.response.headers.get("WWW-Authenticate");
|
||||||
|
}
|
||||||
|
get contentType() {
|
||||||
|
const contentType = this.response.headers.get("Content-Type") || "";
|
||||||
|
return contentType.replace(/;.*$/, "");
|
||||||
|
}
|
||||||
|
get headers() {
|
||||||
|
return this.response.headers;
|
||||||
|
}
|
||||||
|
get html() {
|
||||||
|
if (this.contentType.match(/^(application|text)\/(html|xhtml\+xml)$/)) {
|
||||||
|
return this.text;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`Expected an HTML response but got "${this.contentType}" instead`));
|
||||||
|
}
|
||||||
|
get json() {
|
||||||
|
if (this.contentType.match(/^application\/.*json$/)) {
|
||||||
|
return this.responseJson || (this.responseJson = this.response.json());
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(`Expected a JSON response but got "${this.contentType}" instead`));
|
||||||
|
}
|
||||||
|
get text() {
|
||||||
|
return this.responseText || (this.responseText = this.response.text());
|
||||||
|
}
|
||||||
|
get isTurboStream() {
|
||||||
|
return this.contentType.match(/^text\/vnd\.turbo-stream\.html/);
|
||||||
|
}
|
||||||
|
get isScript() {
|
||||||
|
return this.contentType.match(/\b(?:java|ecma)script\b/);
|
||||||
|
}
|
||||||
|
async renderTurboStream() {
|
||||||
|
if (this.isTurboStream) {
|
||||||
|
if (window.Turbo) {
|
||||||
|
await window.Turbo.renderStreamMessage(await this.text);
|
||||||
|
} else {
|
||||||
|
console.warn("You must set `window.Turbo = Turbo` to automatically process Turbo Stream events with request.js");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.reject(new Error(`Expected a Turbo Stream response but got "${this.contentType}" instead`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async activeScript() {
|
||||||
|
if (this.isScript) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
const metaTag = document.querySelector("meta[name=csp-nonce]");
|
||||||
|
if (metaTag) {
|
||||||
|
const nonce = metaTag.nonce === "" ? metaTag.content : metaTag.nonce;
|
||||||
|
if (nonce) {
|
||||||
|
script.setAttribute("nonce", nonce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
script.innerHTML = await this.text;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(new Error(`Expected a Script response but got "${this.contentType}" instead`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestInterceptor {
|
||||||
|
static register(interceptor) {
|
||||||
|
this.interceptor = interceptor;
|
||||||
|
}
|
||||||
|
static get() {
|
||||||
|
return this.interceptor;
|
||||||
|
}
|
||||||
|
static reset() {
|
||||||
|
this.interceptor = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const cookies = document.cookie ? document.cookie.split("; ") : [];
|
||||||
|
const prefix = `${encodeURIComponent(name)}=`;
|
||||||
|
const cookie = cookies.find((cookie => cookie.startsWith(prefix)));
|
||||||
|
if (cookie) {
|
||||||
|
const value = cookie.split("=").slice(1).join("=");
|
||||||
|
if (value) {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compact(object) {
|
||||||
|
const result = {};
|
||||||
|
for (const key in object) {
|
||||||
|
const value = object[key];
|
||||||
|
if (value !== undefined) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaContent(name) {
|
||||||
|
const element = document.head.querySelector(`meta[name="${name}"]`);
|
||||||
|
return element && element.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringEntriesFromFormData(formData) {
|
||||||
|
return [ ...formData ].reduce(((entries, [name, value]) => entries.concat(typeof value === "string" ? [ [ name, value ] ] : [])), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeEntries(searchParams, entries) {
|
||||||
|
for (const [name, value] of entries) {
|
||||||
|
if (value instanceof window.File) continue;
|
||||||
|
if (searchParams.has(name) && !name.includes("[]")) {
|
||||||
|
searchParams.delete(name);
|
||||||
|
searchParams.set(name, value);
|
||||||
|
} else {
|
||||||
|
searchParams.append(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FetchRequest {
|
||||||
|
constructor(method, url, options = {}) {
|
||||||
|
this.method = method;
|
||||||
|
this.options = options;
|
||||||
|
this.originalUrl = url.toString();
|
||||||
|
}
|
||||||
|
async perform() {
|
||||||
|
try {
|
||||||
|
const requestInterceptor = RequestInterceptor.get();
|
||||||
|
if (requestInterceptor) {
|
||||||
|
await requestInterceptor(this);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
const fetch = this.responseKind === "turbo-stream" && window.Turbo ? window.Turbo.fetch : window.fetch;
|
||||||
|
const response = new FetchResponse(await Turbo.fetch(this.url, this.fetchOptions));
|
||||||
|
if (response.unauthenticated && response.authenticationURL) {
|
||||||
|
return Promise.reject(window.location.href = response.authenticationURL);
|
||||||
|
}
|
||||||
|
if (response.isScript) {
|
||||||
|
await response.activeScript();
|
||||||
|
}
|
||||||
|
const responseStatusIsTurboStreamable = response.ok || response.unprocessableEntity;
|
||||||
|
if (responseStatusIsTurboStreamable && response.isTurboStream) {
|
||||||
|
await response.renderTurboStream();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
addHeader(key, value) {
|
||||||
|
const headers = this.additionalHeaders;
|
||||||
|
headers[key] = value;
|
||||||
|
this.options.headers = headers;
|
||||||
|
}
|
||||||
|
sameHostname() {
|
||||||
|
if (!this.originalUrl.startsWith("http:") && !this.originalUrl.startsWith("https:")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new URL(this.originalUrl).hostname === window.location.hostname;
|
||||||
|
} catch (_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get fetchOptions() {
|
||||||
|
return {
|
||||||
|
method: this.method.toUpperCase(),
|
||||||
|
headers: this.headers,
|
||||||
|
body: this.formattedBody,
|
||||||
|
signal: this.signal,
|
||||||
|
credentials: this.credentials,
|
||||||
|
redirect: this.redirect,
|
||||||
|
keepalive: this.keepalive
|
||||||
|
};
|
||||||
|
}
|
||||||
|
get headers() {
|
||||||
|
const baseHeaders = {
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Content-Type": this.contentType,
|
||||||
|
Accept: this.accept
|
||||||
|
};
|
||||||
|
if (this.sameHostname()) {
|
||||||
|
baseHeaders["X-CSRF-Token"] = this.csrfToken;
|
||||||
|
}
|
||||||
|
return compact(Object.assign(baseHeaders, this.additionalHeaders));
|
||||||
|
}
|
||||||
|
get csrfToken() {
|
||||||
|
return getCookie(metaContent("csrf-param")) || metaContent("csrf-token");
|
||||||
|
}
|
||||||
|
get contentType() {
|
||||||
|
if (this.options.contentType) {
|
||||||
|
return this.options.contentType;
|
||||||
|
} else if (this.body == null || this.body instanceof window.FormData) {
|
||||||
|
return undefined;
|
||||||
|
} else if (this.body instanceof window.File) {
|
||||||
|
return this.body.type;
|
||||||
|
}
|
||||||
|
return "application/json";
|
||||||
|
}
|
||||||
|
get accept() {
|
||||||
|
switch (this.responseKind) {
|
||||||
|
case "html":
|
||||||
|
return "text/html, application/xhtml+xml";
|
||||||
|
|
||||||
|
case "turbo-stream":
|
||||||
|
return "text/vnd.turbo-stream.html, text/html, application/xhtml+xml";
|
||||||
|
|
||||||
|
case "json":
|
||||||
|
return "application/json, application/vnd.api+json";
|
||||||
|
|
||||||
|
case "script":
|
||||||
|
return "text/javascript, application/javascript";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "*/*";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get body() {
|
||||||
|
return this.options.body;
|
||||||
|
}
|
||||||
|
get query() {
|
||||||
|
const originalQuery = (this.originalUrl.split("?")[1] || "").split("#")[0];
|
||||||
|
const params = new URLSearchParams(originalQuery);
|
||||||
|
let requestQuery = this.options.query;
|
||||||
|
if (requestQuery instanceof window.FormData) {
|
||||||
|
requestQuery = stringEntriesFromFormData(requestQuery);
|
||||||
|
} else if (requestQuery instanceof window.URLSearchParams) {
|
||||||
|
requestQuery = requestQuery.entries();
|
||||||
|
} else {
|
||||||
|
requestQuery = Object.entries(requestQuery || {});
|
||||||
|
}
|
||||||
|
mergeEntries(params, requestQuery);
|
||||||
|
const query = params.toString();
|
||||||
|
return query.length > 0 ? `?${query}` : "";
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return this.originalUrl.split("?")[0].split("#")[0] + this.query;
|
||||||
|
}
|
||||||
|
get responseKind() {
|
||||||
|
return this.options.responseKind || "html";
|
||||||
|
}
|
||||||
|
get signal() {
|
||||||
|
return this.options.signal;
|
||||||
|
}
|
||||||
|
get redirect() {
|
||||||
|
return this.options.redirect || "follow";
|
||||||
|
}
|
||||||
|
get credentials() {
|
||||||
|
return this.options.credentials || "same-origin";
|
||||||
|
}
|
||||||
|
get keepalive() {
|
||||||
|
return this.options.keepalive || false;
|
||||||
|
}
|
||||||
|
get additionalHeaders() {
|
||||||
|
return this.options.headers || {};
|
||||||
|
}
|
||||||
|
get formattedBody() {
|
||||||
|
const bodyIsAString = Object.prototype.toString.call(this.body) === "[object String]";
|
||||||
|
const contentTypeIsJson = this.headers["Content-Type"] === "application/json";
|
||||||
|
if (contentTypeIsJson && !bodyIsAString) {
|
||||||
|
return JSON.stringify(this.body);
|
||||||
|
}
|
||||||
|
return this.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(url, options) {
|
||||||
|
const request = new FetchRequest("get", url, options);
|
||||||
|
return request.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(url, options) {
|
||||||
|
const request = new FetchRequest("post", url, options);
|
||||||
|
return request.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function put(url, options) {
|
||||||
|
const request = new FetchRequest("put", url, options);
|
||||||
|
return request.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function patch(url, options) {
|
||||||
|
const request = new FetchRequest("patch", url, options);
|
||||||
|
return request.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function destroy(url, options) {
|
||||||
|
const request = new FetchRequest("delete", url, options);
|
||||||
|
return request.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FetchRequest, FetchResponse, RequestInterceptor, destroy, get, patch, post, put };
|
||||||
Reference in New Issue
Block a user