Skip to content

Commit

Permalink
Banner options and native lazy loading
Browse files Browse the repository at this point in the history
- 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=<offset>` (for multiple positions only)
- the default templates have been modified and moved to the `./src/template` directory.
- Updated docs.
- Updated CHANGELOG
  • Loading branch information
tg666 committed Dec 1, 2023
1 parent d334775 commit 47f3dde
Show file tree
Hide file tree
Showing 16 changed files with 269 additions and 71 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<optionName>="<optionValue>"` 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=<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
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,13 @@ <h2>Second (added manually, random):</h2>
</div>
<div>
<h2>Third (added via data-* attributes, multiple):</h2>
<div data-amp-banner="homepage.top" data-amp-resource-roles="employee, vip" data-amp-resource-apple_products="shop/buy-iphone/iphone-xs" class="slider--splide"></div>
<div data-amp-banner="homepage.top"
data-amp-resource-roles="employee, vip"
data-amp-resource-apple_products="shop/buy-iphone/iphone-xs"
data-amp-option-loading="lazy"
data-amp-option-loading-offset="1"
class="slider--splide"
></div>
</div>
</div>

Expand Down
80 changes: 73 additions & 7 deletions docs/integration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
<div id="my-banner"></div>
<div id="banner1"></div>
<div id="banner2"></div>

<script>
AMPClient.createBanner(document.getElementById('my-banner'), 'homepage.top', {
a_resource: 'A',
b_resource: ['B', 'C']
});
// minimal setup
AMPClient.createBanner(
document.getElementById('banner1'),
'homepage.top',
);
// full setup
AMPClient.createBanner(
document.getElementById('banner2'),
'homepage.promo',
{
a_resource: 'A',
b_resource: ['B', 'C'],
},
{
loading: 'lazy',
customOption: 'customValue',
},
);
</script>
```

### 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
<div data-amp-banner="homepage.top" data-amp-resource-a_resource="A" data-amp-resource-b_resource="B,C"></div>
<div data-amp-banner="homepage.top"
data-amp-resource-a_resource="A"
data-amp-resource-b_resource="B,C"
data-amp-option-loading="lazy"
data-amp-option-customOption="customValue">
</div>
```

To instantiate banners created in this way, the method `attachBanners()` must be called.
Expand Down Expand Up @@ -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
<div data-amp-banner="homepage.top"
data-amp-option-loading="lazy">
</div>
```

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
<div data-amp-banner="homepage.top"
data-amp-option-loading="lazy"
data-amp-option-loading-offset="1">
</div>
```

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.
Expand Down Expand Up @@ -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
});
```
Expand Down
35 changes: 35 additions & 0 deletions src/banner/attributes-parser.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 2 additions & 2 deletions src/banner/banner-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class BannerManager {
return banner;
}

addManagedBanner(element, position, resources = {}) {
addManagedBanner(element, position, resources = {}, options = {}) {
const resourceArr = [];
let key;
element = getElement(element);
Expand All @@ -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);

Expand Down
11 changes: 10 additions & 1 deletion src/banner/banner.js
Original file line number Diff line number Diff line change
@@ -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.');
}
Expand All @@ -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.');
Expand Down Expand Up @@ -47,6 +49,13 @@ class Banner {
return internal(this).positionData;
}

/**
* @returns {Options}
*/
get options() {
return internal(this).options;
}

/**
* @returns {Array<Fingerprint>}
*/
Expand Down
4 changes: 3 additions & 1 deletion src/banner/external/external-banner.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 = {};
Expand Down
4 changes: 2 additions & 2 deletions src/banner/managed/managed-banner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions src/banner/options.js
Original file line number Diff line number Diff line change
@@ -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;
27 changes: 11 additions & 16 deletions src/client/client.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/config/index.js → src/client/config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const merge = require('lodash/merge');
const templates = require('../template');

const roundRatio = (ratio, optionPath) => {
const rounded = Math.round(ratio * 10) / 10;
Expand All @@ -19,7 +20,7 @@ module.exports = options => {
locale: null,
resources: {},
origin: null,
template: require('./template'),
template: templates,
interaction: {
defaultIntersectionRatio: 0.5,
intersectionRatioMap: {},
Expand Down
Loading

0 comments on commit 47f3dde

Please sign in to comment.