Fix to work without errors in Nuxt Universal (#723)

by @patarapolw 

* allow manual init and destroy for use with Nuxt

* fix onDestroy-related methods

* avoid global scope, and use function scope instead

* add createInstance function to window.REMARK42

* 1. allow DOMNode to be put in remark_config 2. check remark_config before try to attach node

* move createInstance function outside

* update embed.ts

* avoid ?.

* 📚 Docs: doc on how to make it work with SPAs

* 📚 Docs: fix spa.md to be more flexible

*  Feat: add REMARK42::ready event

* remove nuxt-specific terminologies

* tell MutationObserver to disconnect on destroy

* update docs/spa.md
This commit is contained in:
Pacharapol Withayasakpunt
2020-07-31 12:21:00 -05:00
committed by GitHub
parent d1268bbe8c
commit 5d6729ddd5
7 changed files with 197 additions and 77 deletions
+2
View File
@@ -476,6 +476,8 @@ And then add this node in the place where you want to see Remark42 widget:
After that widget will be rendered inside this node.
If you want to set this up on a Single Page App, see [/docs/spa.md](/docs/spa.md).
##### Themes
Right now Remark has two themes: light and dark.
+1
View File
@@ -2,6 +2,7 @@
- [How to configure remark42 with nginx reverse proxy](nginx-proxy.md)
- [How to configure remark42 without a subdomain](subdomain.md) with Nginx or Caddy
- [How to configure remark42 for Single Page Apps (SPA)](spa.md)
- [Telegram notifications](telegram.md)
- [Setup email authentication and\or email notifications](email.md)
- [How to add new translation to remark42](translation.md)
+68
View File
@@ -0,0 +1,68 @@
## How to configure remark42 for Single Page Apps (SPA)
Originally tested on [Nuxt.js](https://nuxtjs.org/), but it should be applicable to all SPAs.
- Add the following JavaScript to your `index.html`, which in this case, it is identical to `<script defer src="$HOST/web/embed.js"></script>`
```js
;(function () {
var host = // Your remark42 host
var components = ['embed'] // Your choice of remark42 components
;(function(c) {
for (let i = 0; i < c.length; i++) {
const d = document
const s = d.createElement('script')
s.src = remark_config.host + '/web/' + c[i] + '.js'
s.defer = true
;(d.head || d.body).appendChild(s)
}
})(components)
})
```
- Created `remark42Instance` when the `div` containing remark42 has appear, usually at `mounted` or `componentDidMount` of the SPA lifecycle. Destroy the previous instance first, if neccessary.
```ts
initRemark42() {
if (window.REMARK42) {
if (this.remark42Instance) {
this.remark42Instance.destroy()
}
this.remark42Instance = window.REMARK42.createInstance({
node: this.$refs.remark42 as HTMLElement,
...remark42_config // See <https://github.com/patarapolw/remark42#setup-on-your-website>
})
}
}
mounted() {
if (window.REMARK42) {
this.initRemark42()
} else {
window.addEventListener('REMARK42::ready', () => {
this.initRemark42()
})
}
}
```
- Ensure that this is called every time route changes
```ts
@Watch('$route.path')
onRouteChange() {
this.initRemark42()
}
```
- And, destroyed before routeLeave
```ts
beforeRouteLeave() {
if (this.remark42Instance) {
this.remark42Instance.destroy()
}
}
```
+1 -1
View File
@@ -1,7 +1,7 @@
module.exports = [
{
path: 'public/embed.js',
limit: '2.55 KB',
limit: '2.7 KB',
},
{
limit: '86 KB',
+10 -1
View File
@@ -5,7 +5,16 @@ declare global {
interface Window {
remark_config: CommentsConfig;
REMARK42: {
changeTheme(theme: Theme): void;
changeTheme?: (theme: Theme) => void;
destroy?: () => void;
createInstance: (
remark_config: CommentsConfig
) =>
| {
changeTheme(theme: Theme): void;
destroy(): void;
}
| undefined;
};
}
+1 -1
View File
@@ -15,7 +15,7 @@ export interface CommentsConfig {
max_shown_comments?: number;
theme?: Theme;
page_title?: string;
node?: string;
node?: string | HTMLElement;
locale?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
__colors__?: any;
+114 -74
View File
@@ -1,6 +1,7 @@
/* eslint-disable no-console */
import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from '@app/common/constants.config';
import { UserInfo, Theme } from '@app/common/types';
import { CommentsConfig } from './common/config-types';
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
@@ -47,41 +48,50 @@ function createFrame({
}
function init() {
const node = document.getElementById(window.remark_config.node || NODE_ID);
window.REMARK42 = window.REMARK42 || {};
window.REMARK42.createInstance = createInstance;
if (window.remark_config) {
createInstance(window.remark_config);
}
window.dispatchEvent(new Event('REMARK42::ready'));
}
function createInstance(config: CommentsConfig) {
let initDataAnimationTimeout: number | null = null;
let titleObserver: MutationObserver | null = null;
try {
config = config || {};
} catch (e) {
console.error('Remark42: Config object is undefined.');
return;
}
if (!config.site_id) {
console.error('Remark42: Site ID is undefined.');
return;
}
config.url = (config.url || window.location.origin + window.location.pathname).split('#')[0];
const node = config.node instanceof HTMLElement ? config.node : document.getElementById(config.node || NODE_ID);
if (!node) {
console.error("Remark42: Can't find root node.");
return;
}
try {
window.remark_config = window.remark_config || {};
} catch (e) {
console.error('Remark42: Config object is undefined.');
return;
}
if (!window.remark_config.site_id) {
console.error('Remark42: Site ID is undefined.');
return;
}
window.remark_config.url = (window.remark_config.url || window.location.origin + window.location.pathname).split(
'#'
)[0];
window.REMARK42 = window.REMARK42 || {};
window.REMARK42.changeTheme = changeTheme;
const query = Object.keys(window.remark_config)
const query = Object.keys(config)
.filter(key => key !== '__colors__')
.map(key => {
return `${encodeURIComponent(key)}=${encodeURIComponent(
window.remark_config[key as keyof typeof window.remark_config]
)}`;
return `${encodeURIComponent(key)}=${encodeURIComponent(config[key as keyof typeof config])}`;
})
.join('&');
const iframe = createFrame({ host: BASE_URL, query, __colors__: window.remark_config.__colors__ });
const iframe =
(document.querySelector('iframe[title=Remark42]') as HTMLIFrameElement) ||
createFrame({ host: BASE_URL, query, __colors__: config.__colors__ });
node.appendChild(iframe);
@@ -91,7 +101,8 @@ function init() {
const titleElement = document.querySelector('title');
if (titleElement) {
new MutationObserver(mutations => postTitleToIframe(mutations[0].target.textContent!)).observe(titleElement, {
titleObserver = new MutationObserver(mutations => postTitleToIframe(mutations[0].target.textContent!));
titleObserver.observe(titleElement, {
subtree: true,
characterData: true,
childList: true,
@@ -126,54 +137,54 @@ function init() {
this.style.setAttribute('rel', 'stylesheet');
this.style.setAttribute('type', 'text/css');
this.style.innerHTML = `
#${remarkRootId}-node {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 400px;
transition: transform 0.4s ease-out;
max-width: 100%;
transform: translate(400px, 0);
}
#${remarkRootId}-node[data-animation] {
transform: translate(0, 0);
}
#${remarkRootId}-back {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
opacity: 0;
transition: opacity 0.4s ease-out;
}
#${remarkRootId}-back[data-animation] {
opacity: 1;
}
#${remarkRootId}-node {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 400px;
transition: transform 0.4s ease-out;
max-width: 100%;
transform: translate(400px, 0);
}
#${remarkRootId}-node[data-animation] {
transform: translate(0, 0);
}
#${remarkRootId}-back {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
opacity: 0;
transition: opacity 0.4s ease-out;
}
#${remarkRootId}-back[data-animation] {
opacity: 1;
}
#${remarkRootId}-close {
top: 0px;
right: 400px;
position: absolute;
text-align: center;
font-size: 25px;
cursor: pointer;
color: white;
border-color: transparent;
border-width: 0;
padding: 0;
margin-right: 4px;
background-color: transparent;
}
@media all and (max-width: 430px) {
#${remarkRootId}-close {
top: 0px;
right: 400px;
position: absolute;
text-align: center;
font-size: 25px;
cursor: pointer;
color: white;
border-color: transparent;
border-width: 0;
padding: 0;
margin-right: 4px;
background-color: transparent;
right: 0px;
font-size: 20px;
color: black;
}
@media all and (max-width: 430px) {
#${remarkRootId}-close {
right: 0px;
font-size: 20px;
color: black;
}
}
`;
}
`;
}
if (!this.node) {
this.node = document.createElement('div');
@@ -202,10 +213,10 @@ function init() {
document.body.appendChild(this.back);
document.body.appendChild(this.node);
document.addEventListener('keydown', this.onKeyDown);
setTimeout(() => {
initDataAnimationTimeout = window.setTimeout(() => {
this.back!.setAttribute('data-animation', '');
this.node!.setAttribute('data-animation', '');
this.iframe!.focus();
iframe.focus();
}, 400);
},
close() {
@@ -306,4 +317,33 @@ function init() {
function changeTheme(theme: Theme) {
iframe.contentWindow!.postMessage(JSON.stringify({ theme }), '*');
}
function destroy() {
if (initDataAnimationTimeout) {
clearTimeout(initDataAnimationTimeout);
}
window.removeEventListener('message', receiveMessages);
window.removeEventListener('hashchange', postHashToIframe);
document.removeEventListener('click', postClickOutsideToIframe);
if (titleObserver) {
titleObserver.disconnect();
}
iframe.remove();
}
// TODO: These do not appear in Chrome DevTools
window.REMARK42.changeTheme = changeTheme;
window.REMARK42.destroy = () => {
destroy();
delete window.REMARK42.changeTheme;
delete window.REMARK42.destroy;
};
return {
changeTheme,
destroy,
};
}