Skip to content

Commit

Permalink
Feature/expanding core mount fny (#1772)
Browse files Browse the repository at this point in the history
* expanding core mount fny - so that second and subsequent calls to mount act as a "remount"

* Adding mock React app that can mount and unmount the Card component

* Comment out console.log
  • Loading branch information
sponglord authored Oct 3, 2022
1 parent ad8cc6d commit 39b47d7
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 13 deletions.
26 changes: 15 additions & 11 deletions packages/lib/src/components/BaseElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,23 @@ class BaseElement<P extends BaseElementProps> {
}

if (this._node) {
throw new Error('Component is already mounted.');
this.unmount(); // new, if this._node exists then we are "remounting" so we first need to unmount if it's not already been done
} else {
// Set up analytics, once
if (this.props.modules && this.props.modules.analytics && !this.props.isDropin) {
this.props.modules.analytics.send({
containerWidth: this._node && this._node.offsetWidth,
component: this.constructor['analyticsType'] ?? this.constructor['type'],
flavor: 'components'
});
}
}

this._node = node;
this._component = this.render();

render(this._component, node);

if (this.props.modules && this.props.modules.analytics && !this.props.isDropin) {
this.props.modules.analytics.send({
containerWidth: this._node && this._node.offsetWidth,
component: this.constructor['analyticsType'] ?? this.constructor['type'],
flavor: 'components'
});
}

return this;
}

Expand All @@ -106,11 +107,13 @@ class BaseElement<P extends BaseElementProps> {
this.props = this.formatProps({ ...this.props, ...props });
this.state = {};

return this.unmount().remount();
return this.unmount().mount(this._node); // for new mount fny
}

/**
* Unmounts an element and mounts it again on the same node
* Unmounts an element and mounts it again on the same node i.e. allows mount w/o having to pass a node.
* Should be "private" & undocumented (although being a public function is useful for testing).
* Left in for legacy reasons
*/
public remount(component?): this {
if (!this._node) {
Expand All @@ -137,6 +140,7 @@ class BaseElement<P extends BaseElementProps> {

/**
* Unmounts an element and removes it from the parent instance
* For "destroy" type cleanup - when you don't intend to use the component again
*/
public remove() {
this.unmount();
Expand Down
43 changes: 43 additions & 0 deletions packages/playground/src/Script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
class Script {
constructor(src, node = 'body', attributes = {}, dataAttributes = {}) {
this.script = document.createElement('script');
this.src = src;
this.node = node;
this.attributes = attributes;
this.dataAttributes = dataAttributes;
}

load = () =>
new Promise((resolve, reject) => {
Object.assign(this.script, this.attributes);
Object.assign(this.script.dataset, this.dataAttributes);

this.script.src = this.src;
this.script.async = true;
this.script.onload = event => {
this.script.setAttribute('data-script-loaded', 'true');
resolve(event);
};
this.script.onerror = () => {
this.remove();
reject(new Error(`Unable to load script ${this.src}`));
};

const container = document.querySelector(this.node);
const addedScript = container.querySelector(`script[src="${this.src}"]`);

if (addedScript) {
const isLoaded = addedScript.getAttribute('data-script-loaded');
if (isLoaded) resolve(true);
else addedScript.addEventListener('load', resolve);
} else {
container.appendChild(this.script);
}
});

remove = () => {
return this.script.parentNode && this.script.parentNode.removeChild(this.script);
};
}

export default Script;
9 changes: 9 additions & 0 deletions packages/playground/src/pages/Cards/Cards.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ <h2>Card</h2>
</div>
</div>

<div class="merchant-checkout__payment-method">
<div class="merchant-checkout__payment-method__header">
<h2>Card in React</h2>
</div>
<div class="merchant-checkout__payment-method__details">
<div class="react-card-field"></div>
</div>
</div>

<div class="merchant-checkout__payment-method">
<div class="merchant-checkout__payment-method__header">
<h2>Bancontact</h2>
Expand Down
14 changes: 12 additions & 2 deletions packages/playground/src/pages/Cards/Cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { handleSubmit, handleAdditionalDetails, handleError, handleChange } from
import { amount, shopperLocale } from '../../config/commonConfig';
import '../../../config/polyfills';
import '../../style.scss';
import { MockReactApp } from './MockReactApp';

const showComps = {
clickToPay: true,
storedCard: true,
card: true,
cardInReact: true,
bcmcCard: true,
avsCard: true,
avsPartialCard: true,
kcpCard: true,
clickToPay: true
kcpCard: true
};

getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse => {
Expand Down Expand Up @@ -70,6 +72,12 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse =
.mount('.card-field');
}

// Card mounted in a React app
if (showComps.cardInReact) {
window.cardReact = checkout.create('card', {});
MockReactApp(window, 'cardReact', document.querySelector('.react-card-field'), false);
}

// Bancontact card
if (showComps.bcmcCard) {
window.bancontact = checkout.create('bcmc').mount('.bancontact-field');
Expand Down Expand Up @@ -134,6 +142,8 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse =
.create('card', {
type: 'scheme',
brands: ['mc', 'visa', 'amex', 'bcmc', 'maestro', 'korean_local_card'],
// Set koreanAuthenticationRequired AND countryCode so KCP fields show at start
// Just set koreanAuthenticationRequired if KCP fields should only show if korean_local_card entered
configuration: {
koreanAuthenticationRequired: true
},
Expand Down
85 changes: 85 additions & 0 deletions packages/playground/src/pages/Cards/MockReactApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Script from '../../Script';

function MountButton(props) {
const e = React.createElement;

const [isMounted, setIsMounted] = React.useState(props.mountAtStart);
return e(
'button',
{
type: 'button',
onClick: () => {
setIsMounted(!isMounted);
props.onClick();
},
className: 'adyen-checkout__button adyen-checkout__button--secondary'
},
isMounted ? 'Unmount' : 'Mount'
);
}

function MockReactComp(props) {
const e = React.createElement;

const ref = React.createRef();

const [isMounted, setIsMounted] = React.useState(false);

const listenerFn = React.useCallback(
e => {
// console.log('### MockReactComp:::: isMounted=', isMounted, 'so...', isMounted ? 'unmount it' : 'mount it');
if (!isMounted) {
setIsMounted(true);
// New mount/unmount/mount fny
props.docWindow[props.type].mount(ref.current);

// Old mount/unmount/remount fny
// if (!props.docWindow[props.type]._node) {
// console.log('### MockReactComp:::: first mount');
// props.docWindow[props.type].mount(ref.current);
// } else {
// console.log('### MockReactComp:::: second & sub remount');
// props.docWindow[props.type].remount();
// }
} else {
setIsMounted(false);
props.docWindow[props.type].unmount();
// props.docWindow[props.type]._node = null;
}
},
[isMounted]
);

React.useEffect(() => {
if (props.mountAtStart) {
setIsMounted(true);
// New mount/unmount/mount fny
props.docWindow[props.type].mount(ref.current); // = window.card.mount(ref.current)
}
}, []);

React.useEffect(() => {
// console.log('### MockReactComp:::: RENDERING isMounted=', isMounted);
}, [isMounted]);

return [
e(MountButton, {
key: 'mountBtn',
onClick: listenerFn,
mountAtStart: props.mountAtStart
}),
e('div', { ref, id: 'myReactDiv', key: 'holder', style: { marginTop: '20px' } })
];
}

export async function MockReactApp(docWindow, type, domContainer, mountAtStart = true) {
const script1 = new Script('https://unpkg.com/react@18/umd/react.development.js');
await script1.load();

const script2 = new Script('https://unpkg.com/react-dom@18/umd/react-dom.development.js');
await script2.load();

const e = React.createElement;
const root = ReactDOM.createRoot(domContainer);
root.render(e(MockReactComp, { docWindow, type, mountAtStart }));
}

0 comments on commit 39b47d7

Please sign in to comment.