Closed TastyPi closed 2 years ago
I've read that and tried several combinations with no success. The solution I provided keeps the same front-chat-container
element, and having the scripts in the head
means they should only get run once. Or does loading a cached page rerun the JavaScript in the head
? I tried adding data-turbo-eval="false"
to the two script
tags but it didn't help.
It's possible FrontChat is listening for some other event, but I don't know what event would get triggered during a cached page load but not a new page load.
Edit: I've also now tried with no-preview
instead of no-cache
and get the same error, so it's definitely something specific to caching.
What if you wrap it with a turbo:load
listener?
document.addEventListener("turbo:load", function() {
window.FrontChat('init', {chatId: '...', useDefaultLauncher: false})
})
I've figured out the problem, FrontChat
creates a <script>
element inside the <body>
when it is loaded, which it expects to only be evaluated once. According to https://turbo.hotwired.dev/handbook/building#working-with-script-elements, any script elements inside the <body>
will be evaluated on every page load unless it has data-turbo-eval="false"
. I added some code to add data-turbo-eval="false"
to the <script>
after it is added and now it works with Turbo caching.
I'll contact them to suggest having the <script>
be added to the <head>
instead so people don't run into the same issue, but I don't think this is an issue that Turbo needs to be responsible for (unless you think a general solution to "libraries that add a script to body" is needed).
My full solution:
let script = document.createElement("script")
script.onload = () => {
// Find the #front-chat-container added by Front
let container = document.querySelector("#front-chat-container:not([data-turbo-permanent])")
// Move the script element added by Front to <head>
// See https://github.com/hotwired/turbo/issues/391#issuecomment-998895413
document.head.appendChild(container.nextSibling)
// Remove the container added by Front. We have our own #front-chat-container[data-turbo-permanent] which ensures
// Turbo keeps the chat open during navigation.
container.remove()
window.FrontChat('init', {...});
}
script.defer = true
script.src = "https://chat-assets.frontapp.com/v1/chat.bundle.js"
document.head.appendChild(script)
It's definitely a hacky workaround, but it seems to work.
In case anyone finds the same issue, Front has recently updated to use an iframe
instead, and the previous workaround fails. This is my new workaround:
const FRONT_ELEMENT_ID = "front-chat-iframe"
function frontRenderWorkaround(event: TurboBeforeRenderEvent) {
// Front's chat widget adds an iframe to the body. This iframe must never be reloaded otherwise Front irreparably
// breaks - no combination of FrontChat("shutdown") before removal or FrontChat("init") after removal or even after
// being added back fixes this.
//
// The reason is the iframe is added on the initial load of Front's SDK, *not* when FrontChat("init") is called, and
// if the iframe is reloaded then the connection between it and the library is broken. Unfortunately iframes are
// reloaded whenever they are removed from the DOM, even if they are just moved to a different part of the DOM.
//
// Turbo's default rendering logic creates a new body element, so it is impossible keep the iframe element in the DOM
// without using custom rendering logic.
//
// The workaround is to use morphdom as the renderer, which will modify the existing DOM in-place to match the new
// DOM. It also provides a way to prevent the iframe being removed when it is not included in the new DOM.
// Remove the cached Front Chat iframe
// Ideally this would be done using data-turbo-cache=false, but that removes it from the DOM before rendering starts
event.detail.newBody.querySelector(`#${FRONT_ELEMENT_ID}`)?.remove()
// Replace the renderer with morphdom, and prevent the Front Chat iframe from being removed
event.detail.render = (newElement, currentElement) =>
morphdom(newElement, currentElement, { onBeforeNodeDiscarded: (node) => node.id !== FRONT_ELEMENT_ID })
}
document.addEventListener("turbo:before-render", frontRenderWorkaround)
I'm also running into this issue now. @TastyPi thanks for posting your new workaround. I tried it and it doesn't seem to fully work. The issue is here:
event.detail.newBody.querySelector(`#${FRONT_ELEMENT_ID}`)?.remove()
That query returns nothing.
Does your payload from the server include the <iframe>
? Or is this a timing thing, where some other code is running client-side and causing the <iframe>
to be added to newBody
before this logic runs?
In case this is still causing issues for people, the Front support team recently shared the following code with us:
import { FC, useEffect, useRef } from "react";
"use client";
declare global {
interface Window {
FrontChat: FrontChat | undefined;
}
}
/* Types */
interface FrontChatOptions {
nonce?: string;
}
type UnbindHandler = () => void;
type FrontChatParams = Record<string, string | boolean | object>;
type FrontChat = (cmdType: string, params?: FrontChatParams) => UnbindHandler | undefined;
const scriptSrc = "https://chat-assets.frontapp.com/v1/chat.bundle.js";
export const FrontChatLoader: FC = () => {
const isChatLoadedRef = useRef(false);
useEffect(() => {
if (isChatLoadedRef.current) {
return;
}
isChatLoadedRef.current = true;
boot()
.then((FrontChat) => {
FrontChat("init", {
chatId: "your_chatId_here",
});
})
.catch(console.error);
}, []);
return null;
};
/* Helpers */
export async function boot(element?: HTMLElement, options?: FrontChatOptions): Promise<FrontChat> {
const scriptTag = document.createElement("script");
scriptTag.setAttribute("type", "text/javascript");
scriptTag.setAttribute("src", scriptSrc);
if (options?.nonce) {
scriptTag.setAttribute("nonce", options.nonce);
}
const scriptContainer = element ?? document.body;
const loadScriptPromise = new Promise<FrontChat>((resolve) => {
scriptTag.onload = () => {
if (!window.FrontChat) {
throw new Error("[front-chat-sdk] Could not set up window.FrontChat");
}
resolve(window.FrontChat);
};
});
scriptContainer.appendChild(scriptTag);
return loadScriptPromise;
}
We don't use React, but I borrowed the key concepts from there and did the following, which seems to work for us, both on initial page load and after turbo navigation:
async function loadFrontChatJS() {
const scriptTag = document.createElement("script");
scriptTag.setAttribute("type", "text/javascript");
scriptTag.setAttribute("src", "https://chat-assets.frontapp.com/v1/chat.bundle.js");
const scriptContainer = document.body;
const loadScriptPromise = new Promise((resolve) => {
scriptTag.onload = () => {
if (!window.FrontChat) {
throw new Error("[front-chat-sdk] Could not set up window.FrontChat");
}
resolve(window.FrontChat);
};
});
scriptContainer.appendChild(scriptTag);
return loadScriptPromise;
}
function initFrontChat(FrontChat) {
FrontChat("init", {
chatId: <fill_this_in>,
useDefaultLauncher: <fill_this_in>,
email: <fill_this_in>,
userHash: <fill_this_in>,
name: <fill_this_in>,
});
}
if (!window.FrontChat) {
loadFrontChatJS().then(initFrontChat).catch(console.error);
} else {
initFrontChat(window.FrontChat);
}
We're on turbo-rails (1.0.1)
I'm glad that the code snippet worked for you @shahafabileah 😄
For anyone else who might run into issues, today our team published this public repository that has examples of running Front Chat both in a React application, as well as in any general web application: https://github.com/frontapp/front-chat-sdk
In our app (Rails 6) we use FrontChat for customer support. However, I'm having difficulties getting it to work with Turbo caching.
So far, I have the following:
This mostly works, the chat window stays open between page changes as I want, except when returning to a cached page I get an error
Please pass a `chatId` or a `FCSP` variable.
. This error does come from FrontChat, and I don't know the exact cause, however since the error only happens when loading a cached page I figured it might be of interest to the Turbo team.Currently I'm working around this by using
<meta name="turbo-cache-control" content="no-cache">
to avoid caching the page, and that works, but it would be nice to not have to disable caching.For a bit of understanding of how FrontChat works, it grabs a reference to the
#front-chat-container
element and uses that to show/hide the chat window. It has to be a permanent element otherwise FrontChat breaks since it stores a reference to the element. I'm guessing there's some issue with cached pages and permanent elements.