Skip to content

Commit

Permalink
feat: add callback function to AdvertisingSlot (#131)
Browse files Browse the repository at this point in the history
## Description

* Add a `onAdLoaded` callback function that gets triggered once an ad
has been successfully loaded
* Refactor `useIntersectionObserver` to its own hook
* Remove `@storybook/addon-docs/blocks` that was causing issues with
more recent version of node

This PR will be merge to beta for further tests on production.
  • Loading branch information
thedaviddias committed Jun 12, 2023
1 parent 5f023ff commit fbeff4e
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 70 deletions.
17 changes: 10 additions & 7 deletions src/Advertising.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,14 @@ export default class Advertising {
this.queue = [];
}

activate(id, customEventHandlers = {}) {
activate(id, customEventHandlers = {}, onAdLoaded = {}) {
const { slots, isPrebidUsed } = this;
// check if have slots from configurations

if (Object.values(slots).length === 0) {
this.queue.push({ id, customEventHandlers });
this.queue.push({ id, customEventHandlers, onAdLoaded });
return;
}

Object.keys(customEventHandlers).forEach((customEventId) => {
if (!this.customEventCallbacks[customEventId]) {
this.customEventCallbacks[customEventId] = {};
Expand All @@ -161,6 +162,7 @@ export default class Advertising {
window.pbjs.setTargetingForGPTAsync([id]);
requestManager.prebid = true;
this.refreshSlots([slots[id].gpt], requestManager);
onAdLoaded(id);
},
}),
this.onError
Expand All @@ -178,6 +180,7 @@ export default class Advertising {
window.apstag.setDisplayBids();
requestManager.aps = true; // signals that APS request has completed
this.refreshSlots([slots[id].gpt], requestManager); // checks whether both APS and Prebid have returned
onAdLoaded(id);
}, this.onError);
}
);
Expand All @@ -187,10 +190,10 @@ export default class Advertising {
}

if (!this.isPrebidUsed && !this.isAPSUsed) {
Advertising.queueForGPT(
() => window.googletag.pubads().refresh([slots[id].gpt]),
this.onError
);
Advertising.queueForGPT(() => {
window.googletag.pubads().refresh([slots[id].gpt]);
onAdLoaded(id);
}, this.onError);
}
}

Expand Down
43 changes: 17 additions & 26 deletions src/components/AdvertisingSlot.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,44 @@
import PropTypes from 'prop-types';
import React, { useRef, useContext } from 'react';
import AdvertisingContext from '../AdvertisingContext';
import calculateRootMargin from './utils/calculateRootMargin';
import isLazyLoading from './utils/isLazyLoading';
import getLazyLoadConfig from './utils/getLazyLoadConfig';
import { useIsomorphicLayoutEffect } from '../hooks/useIsomorphicLayoutEffect';
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';

function AdvertisingSlot({
id,
style,
className,
children,
customEventHandlers,
onAdLoaded = () => {},
...restProps
}) {
const observerRef = useRef(null);
const containerDivRef = useRef();

const { activate, config } = useContext(AdvertisingContext);

const lazyLoadConfig = getLazyLoadConfig(config, id);
const isLazyLoadEnabled = isLazyLoading(lazyLoadConfig);

useIsomorphicLayoutEffect(() => {
if (!config || !isLazyLoadEnabled) {
return () => {};
}
const rootMargin = calculateRootMargin(lazyLoadConfig);
observerRef.current = new IntersectionObserver(
([{ isIntersecting }]) => {
if (isIntersecting) {
activate(id, customEventHandlers);
if (containerDivRef.current) {
observerRef.current.unobserve(containerDivRef.current);
}
}
},
{ rootMargin }
);
observerRef.current.observe(containerDivRef.current);
return () => {
if (containerDivRef.current) {
observerRef.current.unobserve(containerDivRef.current);
}
};
}, [activate, config]);
useIntersectionObserver(
activate,
config,
id,
customEventHandlers,
onAdLoaded,
containerDivRef,
isLazyLoadEnabled
);

useIsomorphicLayoutEffect(() => {
if (!config || isLazyLoadEnabled) {
return;
}
activate(id, customEventHandlers);
activate(id, customEventHandlers, onAdLoaded);
}, [activate, config]);

return (
<div
id={id}
Expand All @@ -68,10 +57,12 @@ AdvertisingSlot.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
customEventHandlers: PropTypes.objectOf(PropTypes.func).isRequired,
onAdLoaded: PropTypes.func,
};

AdvertisingSlot.defaultProps = {
customEventHandlers: {},
onAdLoaded: () => {},
};

export default AdvertisingSlot;
18 changes: 16 additions & 2 deletions src/components/AdvertisingSlot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AdvertisingContext from '../AdvertisingContext';
import { config, DIV_ID_FOO } from '../utils/testAdvertisingConfig';

const mockActivate = jest.fn();
const mockOnAdLoaded = jest.fn();

describe('The advertising slot component', () => {
let slot, rerender;
Expand All @@ -25,6 +26,7 @@ describe('The advertising slot component', () => {
id={DIV_ID_FOO}
style={{ color: 'hotpink' }}
className="my-class"
onAdLoaded={mockOnAdLoaded}
>
<h1>hello</h1>
</AdvertisingSlot>
Expand All @@ -37,13 +39,18 @@ describe('The advertising slot component', () => {
});

it('calls the activate function with the ID', () => {
expect(mockActivate).toHaveBeenCalledWith(DIV_ID_FOO, expect.anything());
expect(mockActivate).toHaveBeenCalledWith(
DIV_ID_FOO,
expect.anything(),
mockOnAdLoaded
);
});

it('calls the activate function with a collapse callback', () => {
expect(mockActivate).toHaveBeenCalledWith(
expect.anything(),
expect.any(Object)
expect.any(Object),
mockOnAdLoaded
);
});

Expand Down Expand Up @@ -83,4 +90,11 @@ describe('The advertising slot component', () => {
done();
}, 0);
});

it('calls the onAdLoaded function when ad is loaded', () => {
mockOnAdLoaded(DIV_ID_FOO);

expect(mockOnAdLoaded).toHaveBeenCalledTimes(1);
expect(mockOnAdLoaded).toHaveBeenCalledWith(DIV_ID_FOO);
});
});
71 changes: 71 additions & 0 deletions src/hooks/useIntersectionObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useRef } from 'react';
import calculateRootMargin from '../components/utils/calculateRootMargin';
import getLazyLoadConfig from '../components/utils/getLazyLoadConfig';
import { useIsomorphicLayoutEffect } from '../hooks/useIsomorphicLayoutEffect';

/**
* Custom hook that creates an Intersection Observer to lazy load an element when it appears within the viewport.
*
* @param {Function} activate - Function to activate the advertisement when the intersection is observed.
* @param {Object} config - Configuration object for the advertising context.
* @param {string} id - ID of the advertising slot.
* @param {Object} customEventHandlers - An object containing custom event handlers.
* @param {Object} containerDivRef - Ref object to the element to be observed.
* @param {boolean} isLazyLoadEnabled - Boolean to indicate if lazy loading is enabled.
*
* @example
*
* const containerDivRef = useRef();
* const { activate, config } = useContext(AdvertisingContext);
* const isLazyLoadEnabled = isLazyLoading(getLazyLoadConfig(config, id));
*
* // Usage of useIntersectionObserver
* useIntersectionObserver(activate, config, id, customEventHandlers, containerDivRef, isLazyLoadEnabled);
*
* @returns {void}
*/
export const useIntersectionObserver = (
activate,
config,
id,
customEventHandlers,
onAdLoaded,
containerDivRef,
isLazyLoadEnabled
) => {
const observerRef = useRef(null);

useIsomorphicLayoutEffect(() => {
if (!config || !isLazyLoadEnabled) {
return () => {};
}

const rootMargin = calculateRootMargin(getLazyLoadConfig(config, id));
observerRef.current = new IntersectionObserver(
([{ isIntersecting }]) => {
if (isIntersecting) {
activate(id, customEventHandlers, onAdLoaded);
if (containerDivRef.current) {
observerRef.current.unobserve(containerDivRef.current);
}
}
},
{ rootMargin }
);
observerRef.current.observe(containerDivRef.current);

return () => {
if (containerDivRef.current) {
observerRef.current.unobserve(containerDivRef.current);
}
};
}, [
activate,
config,
id,
customEventHandlers,
onAdLoaded,
containerDivRef,
isLazyLoadEnabled,
]);
};
10 changes: 1 addition & 9 deletions src/stories/GptBanner.stories.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Title, Description, Primary } from '@storybook/addon-docs/blocks';
import React from 'react';
import AdvertisingConfigPropType from '../components/utils/AdvertisingConfigPropType';
import { AdvertisingProvider, AdvertisingSlot } from '../index';
Expand Down Expand Up @@ -37,17 +36,10 @@ export default {
description: {
component: `
In this most basic example of them all, we show a medium rectangle banner
(320x200 pixels) delivered by the Google Ad Manager, through Google Publisher
(320x200 pixels) delivered by the Google Ad Manager, through Google Publisher
Tag (GPT).
`,
},
page: () => (
<>
<Title />
<Description />
<Primary />
</>
),
},
},
};
24 changes: 8 additions & 16 deletions src/stories/GptInterstitial.stories.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Title, Description, Primary } from '@storybook/addon-docs/blocks';
import React from 'react';
import AdvertisingConfigPropType from '../components/utils/AdvertisingConfigPropType';
import { AdvertisingProvider, AdvertisingSlot } from '../index';
Expand All @@ -8,14 +7,14 @@ export const DefaultStory = () => {
const config = {
slots: [
{
id: "div-slot",
path: "/6355419/Travel/Europe",
sizes: [[100, 100]]
}
id: 'div-slot',
path: '/6355419/Travel/Europe',
sizes: [[100, 100]],
},
],
interstitialSlot: {
path: "/6355419/Travel/Europe/France/Paris"
}
path: '/6355419/Travel/Europe/France/Paris',
},
};
return (
<AdvertisingProvider config={config}>
Expand All @@ -42,25 +41,18 @@ export default {
source: { type: 'code' },
description: {
component: `
This example is the simplest implementation of an interstitial ad, delivered by the Google Ad
This example is the simplest implementation of an interstitial ad, delivered by the Google Ad
Manager, through Google Publisher Tag (GPT). see more information of the default implementation here:
https://developers.google.com/publisher-tag/samples/display-web-interstitial-ad
You can prevent specific links from triggering GPT-managed web interstials by adding a
You can prevent specific links from triggering GPT-managed web interstials by adding a
data-google-interstitial="false" attribute to the anchor element or any ancestor of the anchor element.
⚠️ **Please note** currently an interstitial is not working standalone, there must be a basic slot available that would be displayed.
⚠️ **Please note** an interstistial can only triggered in separate window, it doesn't work in an iframe....
`,
},
page: () => (
<>
<Title />
<Description />
<Primary />
</>
),
},
},
};
12 changes: 2 additions & 10 deletions src/stories/GptLazyLoading.stories.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Title, Description, Primary } from '@storybook/addon-docs/blocks';
import React from 'react';
import AdvertisingConfigPropType from '../components/utils/AdvertisingConfigPropType';
import { AdvertisingProvider, AdvertisingSlot } from '../index';
Expand Down Expand Up @@ -64,17 +63,10 @@ export default {
source: { type: 'code' },
description: {
component: `
This example show how you can “lazy load” ads, i.e. the ad slots on a long page get only activated and filled with
banners when they the user scrolls down the page and the ads are almost in the browser viewport.
This example show how you can “lazy load” ads, i.e. the ad slots on a long page get only activated and filled with
banners when they the user scrolls down the page and the ads are almost in the browser viewport.
`,
},
page: () => (
<>
<Title />
<Description />
<Primary />
</>
),
},
},
};

0 comments on commit fbeff4e

Please sign in to comment.