From 47f3dde45fff8c9fa1d3425cef32e27e2382e29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Glawaty?= Date: Fri, 1 Dec 2023 04:17:42 +0100 Subject: [PATCH] Banner options and native lazy loading - added ability to provide custom options for each banner. - added support for native lazy loading. Feature can be enabled through banner options `loading=lazy` and `loading-offset=` (for multiple positions only) - the default templates have been modified and moved to the `./src/template` directory. - Updated docs. - Updated CHANGELOG --- CHANGELOG.md | 9 ++- demo/index.html | 8 ++- docs/integration-guide.md | 80 +++++++++++++++++++++-- src/banner/attributes-parser.js | 35 ++++++++++ src/banner/banner-manager.js | 4 +- src/banner/banner.js | 11 +++- src/banner/external/external-banner.js | 4 +- src/banner/managed/managed-banner.js | 4 +- src/banner/options.js | 15 +++++ src/client/client.js | 27 ++++---- src/{config/index.js => client/config.js} | 3 +- src/config/template.js | 39 ----------- src/template/index.js | 5 ++ src/template/multiple.js | 36 ++++++++++ src/template/random.js | 30 +++++++++ src/template/single.js | 30 +++++++++ 16 files changed, 269 insertions(+), 71 deletions(-) create mode 100644 src/banner/attributes-parser.js create mode 100644 src/banner/options.js rename src/{config/index.js => client/config.js} (98%) delete mode 100644 src/config/template.js create mode 100644 src/template/index.js create mode 100644 src/template/multiple.js create mode 100644 src/template/random.js create mode 100644 src/template/single.js diff --git a/CHANGELOG.md b/CHANGELOG.md index abc64f5..60d3608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added ability to provide custom options for each banner. Options can be passed via data attributes `data-amp-option-=""` and can be retrieved in event handlers. +- Added support for native lazy loading. Feature can be enabled through banner options `loading=lazy` and `loading-offset=` (for multiple positions only). + +### Changed +- The default templates have been modified and moved to the `./src/template` directory. +- Updated docs. ## [1.4.0-beta.1] - 2023-11-30 ### Added @@ -14,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Property `banner.data` is now deprecated. To access information about a position use property `banner.positionData`. For example, replace `banner.data.displayType` with `banner.positionData.displayType`. -- Updated docs +- Updated docs. ## [1.3.1] - 2023-10-25 ### Fixed diff --git a/demo/index.html b/demo/index.html index dfdcfde..2f699e2 100644 --- a/demo/index.html +++ b/demo/index.html @@ -115,7 +115,13 @@

Second (added manually, random):

Third (added via data-* attributes, multiple):

-
+
diff --git a/docs/integration-guide.md b/docs/integration-guide.md index bbe755d..11cf346 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -10,6 +10,7 @@ * [Creating banners manually](#creating-banners-manually) * [Creating banners using data attributes](#creating-banners-using-data-attributes) * [Banners fetching and rendering](#banners-fetching-and-rendering) + * [Lazy loading of image banners](#lazy-loading-of-image-banners) * [Integration with banners that are rendered server-side](#integration-with-banners-that-are-rendered-server-side) * [Banner states](#banner-states) * [Client events](#client-events) @@ -86,24 +87,49 @@ Banners can be created manually through the client, or they can be created direc Banners are created using method `createBanner()`. The first argument of the method is the HTML element into which a banner will be rendered. The second argument is a position code from the AMP and the third optional argument can be an object that contains resources of the banner. +The last optional argument is an object that can contain arbitrary custom values. These options can then be retrieved in event handlers and templates. + ```html -
+
+
``` ### Creating banners using data attributes -The creation of banners is controlled by two types of data attributes. The first is `data-amp-banner`, which contains the position code from the AMP. +The creation of banners is controlled by three types of data attributes. The first one is `data-amp-banner`, which contains the position code from the AMP. The second type are attributes with the prefix `data-amp-resource-`, which contain the resources of a given banner separated by a comma. +The third type are attributes with the prefix `data-amp-option-`, which can contain arbitrary custom values. These options can then be retrieved in event handlers and templates. + ```html -
+
+
``` To instantiate banners created in this way, the method `attachBanners()` must be called. @@ -139,6 +165,42 @@ for (let snippet of snippets) { AMPClient.fetch(); ``` +### Lazy loading of image banners + +The default client templates support [native lazy loading](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes) of images. +To activate lazy loading the option `loading: lazy` must be passed to the banner. + +```javascript +AMPClient.createBanner(element, 'homepage.top', {}, { + loading: 'lazy', +}); +``` + +```html +
+
+``` + +A special case is a position of type `multiple`, where it may be desirable to lazily load all banners except the first. +This can be achieved by adding the option `loading-offset`, whose value specifies from which banner the attribute `loading` should be rendered. + +```javascript +AMPClient.createBanner(element, 'homepage.top', {}, { + loading: 'lazy', + 'loading-offset': 1, +}); +``` + +```html +
+
+``` + +If you prefer a different implementation of lazy loading, it is possible to pass custom templates to the client in the configuration object instead of [the default ones](../src/template) and integrate a different solution in these templates. + ### Integration with banners that are rendered server-side Banners that are rendered server-side using [68publishers/amp-client-php](https://github.com/68publishers/amp-client-php) don't need any special integration. @@ -190,6 +252,10 @@ AMPClient.on('amp:banner:state-changed', function (banner) { const positionDisplayType = banner.positionData.displayType; // type of the position [single, random, multiple] const rotationSeconds = banner.positionData.rotationSeconds; // how often the slider should scroll in seconds + // It is also possible to retrieve custom options that were passed to the banner during initialization: + const loadingOption = banner.options.get('loading', 'eager'); // the second argument is the default value + const customOption = banner.options.get('customOption', undefined); + // do anything, e.g. initialize your favourite slider }); ``` diff --git a/src/banner/attributes-parser.js b/src/banner/attributes-parser.js new file mode 100644 index 0000000..227f2e2 --- /dev/null +++ b/src/banner/attributes-parser.js @@ -0,0 +1,35 @@ +class AttributesParser { + static parseResources(element) { + const resources = {}; + + const attributes = [].filter.call(element.attributes, attr => { + return /^data-amp-resource-\S+/.test(attr.name); + }); + + for (let attr of attributes) { + if (attr.value) { + resources[attr.name.slice(18)] = attr.value.split(',').map(v => v.trim()); + } + } + + return resources; + } + + static parseOptions(element) { + const options = {}; + + const attributes = [].filter.call(element.attributes, attr => { + return /^data-amp-option-\S+/.test(attr.name); + }); + + for (let opt of attributes) { + if (opt.value) { + options[opt.name.slice(16)] = opt.value.trim(); + } + } + + return options; + } +} + +module.exports = AttributesParser; diff --git a/src/banner/banner-manager.js b/src/banner/banner-manager.js index caa3768..23b4504 100644 --- a/src/banner/banner-manager.js +++ b/src/banner/banner-manager.js @@ -51,7 +51,7 @@ class BannerManager { return banner; } - addManagedBanner(element, position, resources = {}) { + addManagedBanner(element, position, resources = {}, options = {}) { const resourceArr = []; let key; element = getElement(element); @@ -62,7 +62,7 @@ class BannerManager { resourceArr.push(new Resource(key, resources[key])); } - const banner = new ManagedBanner(internal(this).eventBus, element, position, resourceArr); + const banner = new ManagedBanner(internal(this).eventBus, element, position, resourceArr, options); internal(this).banners.push(banner); diff --git a/src/banner/banner.js b/src/banner/banner.js index 8049675..4292369 100644 --- a/src/banner/banner.js +++ b/src/banner/banner.js @@ -1,10 +1,11 @@ const State = require('./state'); const Events = require('../event/events'); const PositionData = require('./position-data'); +const Options = require('./options'); const internal = require('../utils/internal-state'); class Banner { - constructor(eventBus, element, position) { + constructor(eventBus, element, position, options) { if (this.constructor === Banner) { throw new TypeError('Can not construct abstract class Banner.'); } @@ -15,6 +16,7 @@ class Banner { internal(this).eventBus = eventBus; internal(this).element = element; internal(this).positionData = PositionData.createInitial(position); + internal(this).options = new Options(options); internal(this).stateCounters = {}; this.setState(this.STATE.NEW, 'Banner created.'); @@ -47,6 +49,13 @@ class Banner { return internal(this).positionData; } + /** + * @returns {Options} + */ + get options() { + return internal(this).options; + } + /** * @returns {Array} */ diff --git a/src/banner/external/external-banner.js b/src/banner/external/external-banner.js index 4ff4b6e..b0fd9ce 100644 --- a/src/banner/external/external-banner.js +++ b/src/banner/external/external-banner.js @@ -1,6 +1,7 @@ const Banner = require('../banner'); const Fingerprint = require('../fingerprint'); const PositionData = require('../position-data'); +const AttributesParser = require('../attributes-parser'); const internal = require('../../utils/internal-state'); class ExternalBanner extends Banner { @@ -17,8 +18,9 @@ class ExternalBanner extends Banner { const state = externalData.state; const positionData = new PositionData(externalData.positionData); + const options = AttributesParser.parseOptions(element); - super(eventBus, element, positionData.code); + super(eventBus, element, positionData.code, options); const fingerprints = []; const breakpointsByBannerId = {}; diff --git a/src/banner/managed/managed-banner.js b/src/banner/managed/managed-banner.js index 9155205..9129016 100644 --- a/src/banner/managed/managed-banner.js +++ b/src/banner/managed/managed-banner.js @@ -7,8 +7,8 @@ const Fingerprint = require('../fingerprint'); const internal = require('../../utils/internal-state'); class ManagedBanner extends Banner { - constructor(eventBus, element, position, resources = []) { - super(eventBus, element, position); + constructor(eventBus, element, position, resources = [], options = {}) { + super(eventBus, element, position, options); internal(this).resources = resources; internal(this).responseDataReceived = false; diff --git a/src/banner/options.js b/src/banner/options.js new file mode 100644 index 0000000..dc7670c --- /dev/null +++ b/src/banner/options.js @@ -0,0 +1,15 @@ +class Options { + constructor(options) { + this.options = options; + } + + has(optionName) { + return undefined !== this.options[optionName]; + } + + get(optionName, defaultValue = undefined) { + return this.options[optionName] || defaultValue; + } +} + +module.exports = Options; diff --git a/src/client/client.js b/src/client/client.js index 9685cc7..16ded4e 100644 --- a/src/client/client.js +++ b/src/client/client.js @@ -1,10 +1,11 @@ const version = require('../../package.json').version; const internal = require('../utils/internal-state'); -const _config = require('../config/index'); +const _config = require('./config'); const _gateway = require('../gateway/index'); const RequestFactory = require('../request/request-factory'); const BannerManager = require('../banner/banner-manager'); const ManagedBanner = require('../banner/managed/managed-banner'); +const AttributesParser = require('../banner/attributes-parser'); const EventBus = require('../event/event-bus'); const Events = require('../event/events'); const BannerRenderer = require('../renderer/banner-renderer'); @@ -98,8 +99,8 @@ class Client { return internal(this).gateway; } - createBanner(element, position, resources = {}) { - return internal(this).bannerManager.addManagedBanner(element, position, resources); + createBanner(element, position, resources = {}, options = {}) { + return internal(this).bannerManager.addManagedBanner(element, position, resources, options); } attachBanners(snippet = document) { @@ -112,24 +113,18 @@ class Client { if ('ampBannerExternal' in element.dataset) { banner = internal(this).bannerManager.addExternalBanner(element); } else { - const position = element.getAttribute('data-amp-banner'); - const resources = {}; + const position = element.dataset.ampBanner; if (!position) { - continue; // the empty position, throw an error? - } - - const attributes = [].filter.call(element.attributes, attr => { - return /^data-amp-resource-[\S]+/.test(attr.name); - }); + console.warn('Unable to attach a banner to the element ', element, ' because the attribute "data-amp-banner" has an empty value.'); - for (let attr of attributes) { - if (attr.value) { - resources[attr.name.slice(18)] = attr.value.split(',').map(v => v.trim()); - } + continue; } - banner = this.createBanner(element, position, resources); + const resources = AttributesParser.parseResources(element); + const options = AttributesParser.parseOptions(element); + + banner = this.createBanner(element, position, resources, options); } privateProperties.eventBus.dispatch(this.EVENTS.ON_BANNER_ATTACHED, banner); diff --git a/src/config/index.js b/src/client/config.js similarity index 98% rename from src/config/index.js rename to src/client/config.js index 88ffb3c..5d01c27 100644 --- a/src/config/index.js +++ b/src/client/config.js @@ -1,4 +1,5 @@ const merge = require('lodash/merge'); +const templates = require('../template'); const roundRatio = (ratio, optionPath) => { const rounded = Math.round(ratio * 10) / 10; @@ -19,7 +20,7 @@ module.exports = options => { locale: null, resources: {}, origin: null, - template: require('./template'), + template: templates, interaction: { defaultIntersectionRatio: 0.5, intersectionRatioMap: {}, diff --git a/src/config/template.js b/src/config/template.js deleted file mode 100644 index daf9d7d..0000000 --- a/src/config/template.js +++ /dev/null @@ -1,39 +0,0 @@ -const singleBannerContent = d => { - return ` - <% if('img' === ${d}.content.type) { %> - > - - <% (${d}.content.sources || []).forEach(function(source) { %> - - <% }); %> - <%- ${d}.content.alt %>> - - - <% } else if ('html' === ${d}.content.type) { %> -
- <%= ${d}.content.html %> -
- <% } %> - `; -}; - -/** - * Arguments: ({ManagedBanner} banner, {Array[Object]|Object} data) - * - * - data contains single banner(s) data - */ -module.exports = { - single: `
${singleBannerContent('data')}
`, - random: `
${singleBannerContent('data')}
`, - multiple: ` -
-
- <% data.forEach(function(b) { %> -
- ${singleBannerContent('b')} -
- <% }); %> -
-
- `, -} diff --git a/src/template/index.js b/src/template/index.js new file mode 100644 index 0000000..2838e26 --- /dev/null +++ b/src/template/index.js @@ -0,0 +1,5 @@ +module.exports = { + single: require('./single'), + random: require('./random'), + multiple: require('./multiple'), +}; diff --git a/src/template/multiple.js b/src/template/multiple.js new file mode 100644 index 0000000..77e5d2a --- /dev/null +++ b/src/template/multiple.js @@ -0,0 +1,36 @@ +module.exports = ` + +`; diff --git a/src/template/random.js b/src/template/random.js new file mode 100644 index 0000000..58b0b37 --- /dev/null +++ b/src/template/random.js @@ -0,0 +1,30 @@ +module.exports = ` + +`; diff --git a/src/template/single.js b/src/template/single.js new file mode 100644 index 0000000..2757f70 --- /dev/null +++ b/src/template/single.js @@ -0,0 +1,30 @@ +module.exports = ` + +`;