Skip to content

Commit

Permalink
fix(askem): fix Askem component infinitely reloading on article pages
Browse files Browse the repository at this point in the history
parts:
 - don't render AskemProvider and AskemFeedbackContainer on server-side
 - don't update used AskemInstance except when it really changes, use
   deep equality to compare

also:
 - add more fields and documentation to Askem's data using
   https://docs.reactandshare.com/
 - pass all the Askem fields to window.rnsData
 - fix AskemContext and AskemProvider default import renaming typos

refs HCRC-106
  • Loading branch information
karisal-anders committed Nov 1, 2023
1 parent f62b967 commit c4be8af
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 30 deletions.
36 changes: 25 additions & 11 deletions packages/components/src/app/BaseApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import 'nprogress/nprogress.css';

import { useCookies } from 'hds-react';
import isEqual from 'lodash/isEqual';
import dynamic from 'next/dynamic';
import type { SSRConfig } from 'next-i18next';
import React, { useCallback, useEffect, useState } from 'react';
Expand All @@ -14,7 +15,7 @@ import '../styles/globals.scss';
import '../styles/askem.scss';
import { CmsHelperProvider } from '../cmsHelperProvider';
import { createAskemInstance } from '../components/askem';
import AskemProvider from '../components/askem/AskemProvider';
import type { AskemConfigs, AskemInstance } from '../components/askem/types';
import ErrorFallback from '../components/errorPages/ErrorFallback';
import EventsCookieConsent from '../components/eventsCookieConsent/EventsCookieConsent';
import ResetFocus from '../components/resetFocus/ResetFocus';
Expand Down Expand Up @@ -49,6 +50,13 @@ export type Props = {
AppThemeProviderProps &
SSRConfig;

const AskemProvider = dynamic(
() => import('../components/askem').then((mod) => mod.AskemProvider),
{
ssr: false,
}
);

const DynamicToastContainer = dynamic(
() =>
import('react-toastify').then((mod) => {
Expand Down Expand Up @@ -76,7 +84,7 @@ function BaseApp({
cookieDomain,
routerHelper,
matomoConfiguration,
askemFeedbackConfiguration,
askemFeedbackConfiguration: askemConfigurationInput,
asPath,
withConsent,
defaultButtonTheme,
Expand Down Expand Up @@ -109,6 +117,11 @@ function BaseApp({
}, []);

const [askemConsentGiven, setAskemConsentGiven] = useState<boolean>(false);
const [askemInstance, setAskemInstance] = useState<AskemInstance | null>(
null
);
const [askemConfiguration, setAskemConfiguration] =
useState<AskemConfigs | null>(null);

// todo: matomo is not updated.
const handleConsentGiven = useCallback(() => {
Expand All @@ -126,14 +139,15 @@ function BaseApp({
}
}, [handleConsentGiven, asPath]);

const askemFeedbackInstance = React.useMemo(
() =>
createAskemInstance({
...askemFeedbackConfiguration,
consentGiven: askemConsentGiven,
}),
[askemFeedbackConfiguration, askemConsentGiven]
);
const newAskemConfiguration: AskemConfigs = {
...askemConfigurationInput,
consentGiven: askemConsentGiven,
};

if (!askemInstance || !isEqual(askemConfiguration, newAskemConfiguration)) {
setAskemConfiguration(newAskemConfiguration);
setAskemInstance(createAskemInstance(newAskemConfiguration));
}

const matomoInstance = React.useMemo(
() => createMatomoInstance(matomoConfiguration),
Expand All @@ -157,7 +171,7 @@ function BaseApp({
getKeywordOnClickHandler={getKeywordOnClickHandler}
>
<MatomoProvider value={matomoInstance}>
<AskemProvider value={askemFeedbackInstance}>
<AskemProvider value={askemInstance}>
<GeolocationProvider>
<NavigationProvider
headerMenu={headerMenu}
Expand Down
50 changes: 38 additions & 12 deletions packages/components/src/components/askem/Askem.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { RnsData } from '../../types';
import type { AskemConfigs } from './types';

class Askem {
Expand All @@ -9,27 +10,52 @@ class Askem {
}

private initialize({
apiKey,
scriptUrl = 'https://cdn.reactandshare.com/plugin/rns.js',
// AskemBaseConfig properties
disabled = false,
consentGiven,
scriptUrl = 'https://cdn.reactandshare.com/plugin/rns.js',
consentGiven = false,
// RnsData properties
apiKey,
title,
canonicalUrl,
author,
date,
categories,
commentNumber,
postId,
ctaUrl,
disableFa,
disableFonts,
initCallback,
reactionCallback,
shareCallback,
}: AskemConfigs) {
this.disabled = disabled;
this.consentGiven = Boolean(consentGiven);

if (disabled || !apiKey || typeof window === 'undefined') {
return;
}
const rnsData: RnsData = {
apiKey,
title,
canonicalUrl,
author,
date,
categories,
commentNumber,
postId,
ctaUrl,
disableFa,
disableFonts,
initCallback,
reactionCallback,
shareCallback,
};
window.rnsData = rnsData;

window.rnsData = window.rnsData || {};
window.rnsData.apiKey;
if (!window.rnsData.apiKey) {
window.rnsData.apiKey = apiKey;
}

const doc = document;
const scriptElement = doc.createElement('script');
const scripts = doc.getElementsByTagName('script')[0];
const scriptElement = document.createElement('script');
const scripts = document.getElementsByTagName('script')[0];

Object.assign(scriptElement, {
type: 'text/javascript',
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/components/askem/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default as MatomoContext } from './AskemContext';
export { default as MatomoProvider } from './AskemProvider';
export { default as AskemContext } from './AskemContext';
export { default as AskemProvider } from './AskemProvider';
export { default as createAskemInstance } from './instance';
export { default as AskemFeedbackContainer } from './AskemFeedbackContainer';
6 changes: 4 additions & 2 deletions packages/components/src/components/askem/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { RnsData } from '../../types';
import type Askem from './Askem';

export type AskemConfigs = {
export type AskemBaseConfig = {
disabled?: boolean;
scriptUrl?: string;
consentGiven?: boolean;
} & RnsData;
};

export type AskemConfigs = AskemBaseConfig & RnsData;

export interface AskemInstance {
disabled: boolean;
Expand Down
10 changes: 9 additions & 1 deletion packages/components/src/components/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { Footer, Link } from 'hds-react';
import dynamic from 'next/dynamic';
import type { FunctionComponent } from 'react';
import type { Menu } from 'react-helsinki-headless-cms';
import { useMenuQuery } from 'react-helsinki-headless-cms/apollo';
import { AskemFeedbackContainer } from '../../components/askem';
import { DEFAULT_FOOTER_MENU_NAME } from '../../constants';
import useFooterTranslation from '../../hooks/useFooterTranslation';
import useLocale from '../../hooks/useLocale';

import { resetFocusId } from '../resetFocus/ResetFocus';
import styles from './footer.module.scss';

const AskemFeedbackContainer = dynamic(
() =>
import('../../components/askem').then((mod) => mod.AskemFeedbackContainer),
{
ssr: false,
}
);

type FooterSectionProps = {
appName: string;
menu?: Menu;
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/styles/askem.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// RNS i.e. React and Share i.e. Askem Classic custom stylings
// @see https://docs.reactandshare.com/#custom-styling
.rns {
--hel-icon--face-smile: url(/shared-assets/images/feedback/face-smile.svg);
--hel-icon--face-sad: url(/shared-assets/images/feedback/face-sad.svg);
Expand Down
98 changes: 96 additions & 2 deletions packages/components/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type React from 'react';
import type { APP_LANGUAGES } from '../constants';
import type { EventFields } from './event-types';
import type { Venue } from './generated/graphql';

export type AppLanguage = (typeof APP_LANGUAGES)[number];

export type AutosuggestType = 'keyword' | 'text';
Expand Down Expand Up @@ -123,15 +122,110 @@ export enum TARGET_GROUPS {
export const isTargetGroup = (value: unknown): value is TARGET_GROUPS =>
Object.values(TARGET_GROUPS).includes(value as TARGET_GROUPS);

/**
* RNS i.e. React and Share i.e. Askem Classic data
* @see https://docs.reactandshare.com/
*/
export type RnsData = {
/**
* API Key that was created upon the registration.
*/
apiKey?: string;

/**
* Title of the page or post.
*
* Defaults to the value of supported metadata, if it exists. If neither metadata or
* title property exist, the title of the page is used instead.
*/
title?: string;

/**
* Canonical url of the page or post.
*
* By default the plugin uses the value of <link ref="canonical"> element. The default
* value can be overdriven by this property. If no link element or property is not
* provided, the url of the page is used instead.
*/
canonicalUrl?: string;

/**
* Name of the author of the page or post.
*/
author?: string;
date?: string;

/**
* ISO 8601 datetime string or Unix timestamp type in milliseconds.
*/
date?: string | number;

/**
* Categories of the page or post
*/
categories?: string[];

/**
* Number of comments, if there is a possibility to leave comments on the page.
*/
commentNumber?: number;

/**
* Custom ID to identify the post/page instead of URL. The use of this is recommended,
* if the URL is likely to be changed over time.
*/
postId?: string;

/**
* Custom call-to-action URL after a reaction button click.
*/
ctaUrl?: string;

/**
* Flag for disabling Font Awesome
*/
disableFa?: boolean;

/**
* Flag for disabling Google Fonts
*/
disableFonts?: boolean;

/**
* Function to be called after plugin initiation.
* This function can be used to access the existing plugin element, e.g. add elements
* to the plugin.
* @param {HTMLElement} element - DOM element capsulating the plugin
* @param {string} url - Canonical url of the parent of the plugin or url of the page
*/
initCallback?: (element?: HTMLElement, url?: string) => void;

/**
* Function to be called after a reaction button is clicked.
* This function can be used e.g. to trigger 3rd party analytics or marketing
* automation services.
* @param {string} eventType - reaction or unreaction
* @param {string} reactionLabel - Label of the reaction button clicked
* @param {string} url - Canonical url of the parent of the plugin or url of the page
*/
reactionCallback?: (
eventType?: string,
reactionLabel?: string,
url?: string
) => void;

/**
* Function to be called after a share button is clicked.
* This function can be used e.g. to trigger 3rd party analytics or marketing
* automation services.
* @param {string} eventType - reaction or unreaction
* @param {string} reactionLabel - Label of the reaction button clicked
* @param {string} url - Canonical url of the parent of the plugin or url of the page
*/
shareCallback?: (
eventType?: string,
reactionLabel?: string,
url?: string
) => void;
};

export type KeywordOnClickHandlerType = (
Expand Down

0 comments on commit c4be8af

Please sign in to comment.