Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Commit

Permalink
Create Resize Observer Meta (#908)
Browse files Browse the repository at this point in the history
* add initial resize observer

* add resize observer tests

* add readme entry

* remove commented code

* remove commented code

* updated readme, extra example
  • Loading branch information
tomdye authored Apr 10, 2018
1 parent b7ac632 commit d5d2ced
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 0 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,43 @@ class TestWidget extends WidgetBase<WidgetProperties> {
}
```

#### Resize

The resize observer meta uses the latest [`ResizeObserver`](https://wicg.github.io/ResizeObserver/) within Dojo 2 based widgets. [Native browser support](https://caniuse.com/#feat=resizeobserver) is currently provided by `Chrome 64+`, other Dojo supported browsers work via [polyfill](https://github.com/WICG/ResizeObserver/issues/3).

This allows you to observe resize events at the component level. The `meta` accepts an object of `predicate` functions which receive `ContentRect` dimensions and will be executed when a resize event has occured. The results are made available in a widget's `render` function. This is an incredibly powerful tool for creating responsive components and layouts.

```ts
function isMediumWidthPredicate(contentRect: ContentRect) {
return contentRect.width < 500;
}
function isSmallHeightPredicate(contentRect: ContentRect) {
return contentRect.height < 300;
}
class TestWidget extends WidgetBase<WidgetProperties> {
render() {
const { isMediumWidth, isSmallHeight } = this.meta(Resize).get('root', {
isMediumWidth: isMediumWidthPredicate,
isSmallHeight: isSmallHeightPredicate
});
return v('div', {
key: 'root'
classes: [
isMediumWidth ? css.medium : css.large,
isSmallHeight ? css.scroll : null
]
}, [
v('div', {
innerHTML: 'Hello World'
})
]);
}
}
```

##### Implementing Custom Meta

You can create your own meta if you need access to DOM nodes.
Expand Down
78 changes: 78 additions & 0 deletions src/meta/Resize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Base } from './Base';
import Map from '@dojo/shim/Map';

interface Observer {
observe(node: HTMLElement): void;
}

declare const ResizeObserver: {
prototype: Observer;
new (callback: (entries: ResizeObserverEntry[]) => any): any;
};

interface ResizeObserverEntry {
contentRect: ContentRect;
}

export interface ContentRect {
readonly bottom: number;
readonly height: number;
readonly left: number;
readonly right: number;
readonly top: number;
readonly width: number;
readonly x: number;
readonly y: number;
}

export interface PredicateFunction {
(contentRect: ContentRect): boolean;
}

export interface PredicateFunctions {
[id: string]: PredicateFunction;
}

export type PredicateResponses<T = PredicateFunctions> = { [id in keyof T]: boolean };

export class Resize extends Base {
private _details = new Map<string | number, PredicateResponses>();

public get<T extends PredicateFunctions>(key: string | number, predicates: T): PredicateResponses<T> {
const node = this.getNode(key);

if (!node) {
const defaultResponse: PredicateResponses = {};
for (let predicateId in predicates) {
defaultResponse[predicateId] = false;
}
return defaultResponse as PredicateResponses<T>;
}

if (!this._details.has(key)) {
this._details.set(key, {});
const resizeObserver = new ResizeObserver(([entry]) => {
const { contentRect } = entry;
const previousDetails = this._details.get(key);
let predicateChanged = false;
let predicateResponses: PredicateResponses = {};

for (let predicateId in predicates) {
const response = predicates[predicateId](contentRect);
predicateResponses[predicateId] = response;
if (!predicateChanged && response !== previousDetails![predicateId]) {
predicateChanged = true;
}
}

this._details.set(key, predicateResponses);
predicateChanged && this.invalidate();
});
resizeObserver.observe(node);
}

return this._details.get(key) as PredicateResponses<T>;
}
}

export default Resize;
135 changes: 135 additions & 0 deletions tests/unit/meta/Resize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const { registerSuite } = intern.getInterface('object');
const { assert } = intern.getPlugin('chai');
import global from '@dojo/shim/global';
import { stub, SinonStub } from 'sinon';
import Resize, { ContentRect } from '../../../src/meta/Resize';
import NodeHandler from '../../../src/NodeHandler';
import WidgetBase from '../../../src/WidgetBase';

let resizeObserver: any;
let resizeCallback: ([]: any[]) => void;
const bindInstance = new WidgetBase();
let isFoo: SinonStub;
let isBar: SinonStub;

registerSuite('meta - Resize', {
beforeEach() {
isFoo = stub();
isBar = stub();
resizeObserver = stub().callsFake(function(callback: any) {
const observer = {
observe: stub()
};
resizeCallback = callback;
return observer;
});

global.ResizeObserver = resizeObserver;
},

afterEach() {
isFoo.reset();
isBar.reset();
resizeObserver.reset();
global.ResizeObserver = undefined;
},

tests: {
'Will return predicates defaulted to false if node not loaded'() {
const nodeHandler = new NodeHandler();

const resize = new Resize({
invalidate: () => {},
nodeHandler,
bind: bindInstance
});

assert.deepEqual(resize.get('foo', { isFoo, isBar }), { isFoo: false, isBar: false });
assert.isFalse(isFoo.called);
assert.isFalse(isBar.called);
},
'Will create a new ResizeObserver when node exists'() {
const nodeHandler = new NodeHandler();
const element = document.createElement('div');
document.body.appendChild(element);
nodeHandler.add(element, 'foo');

const resize = new Resize({
invalidate: () => {},
nodeHandler,
bind: bindInstance
});

resize.get('foo', { isFoo, isBar });
assert.isTrue(resizeObserver.calledOnce);
},
'Will call predicates when resize event is observed'() {
const nodeHandler = new NodeHandler();
const element = document.createElement('div');
document.body.appendChild(element);
nodeHandler.add(element, 'foo');

const resize = new Resize({
invalidate: () => {},
nodeHandler,
bind: bindInstance
});

const contentRect: Partial<ContentRect> = {
width: 10
};

resize.get('foo', { isFoo, isBar });
resizeCallback([{ contentRect }]);

assert.isTrue(isFoo.firstCall.calledWith(contentRect));
assert.isTrue(isBar.firstCall.calledWith(contentRect));
},
'Will only set up one observer per widget per key'() {
const nodeHandler = new NodeHandler();
const element = document.createElement('div');
document.body.appendChild(element);
nodeHandler.add(element, 'foo');

const resize = new Resize({
invalidate: () => {},
nodeHandler,
bind: bindInstance
});

resize.get('foo', { isFoo });
resize.get('foo', { isBar });
assert.isTrue(resizeObserver.calledOnce);
},
'Will call invalidate when predicates have changed'() {
const nodeHandler = new NodeHandler();
const invalidate = stub();
const element = document.createElement('div');
document.body.appendChild(element);
nodeHandler.add(element, 'foo');

const resize = new Resize({
invalidate,
nodeHandler,
bind: bindInstance
});

const contentRect: Partial<ContentRect> = {
width: 10
};

isFoo.onFirstCall().returns(false);
isFoo.onSecondCall().returns(true);

resize.get('foo', { isFoo, isBar });

resizeCallback([{ contentRect }]);
resizeCallback([{ contentRect }]);

const predicates = resize.get('foo', { isFoo, isBar });

assert.isTrue(invalidate.calledTwice);
assert.isTrue(predicates.isFoo);
}
}
});
1 change: 1 addition & 0 deletions tests/unit/meta/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import './Drag';
import './Focus';
import './Intersection';
import './Matches';
import './Resize';
import './WebAnimation';

0 comments on commit d5d2ced

Please sign in to comment.