Skip to content

Commit

Permalink
Widget class property watch decorator (#75)
Browse files Browse the repository at this point in the history
* watch decorator

* import order

* README

* README feedback
  • Loading branch information
agubler committed Aug 22, 2018
1 parent 07bdc06 commit 05e991a
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/widget-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,34 @@ class Hello extends WidgetBase<MyProperties> {

New properties are compared with the previous properties to determine if a widget requires re-rendering. By default Dojo uses the `auto` diffing strategy, that performs a shallow comparison for objects and arrays, ignores functions (except classes that extend `WidgetBase`) and a reference comparison for all other values.

#### Internal Widget State

It is common for widgets to maintain internal state, that directly affects the results of the render output or passed as properties to child widgets. The most common pattern is that an action (often user initiated via an event) occurs which updates the internal state leaving the user to manually call `this.invalidate()` to trigger a re-render.

For class properties that always need to trigger a re-render when they're updated, a property decorator, `@watch` can be used, which implicitly calls `this.invalidate` each time the property is set.

```ts
import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import watch from '@dojo/framework/widget-core/decorators/watch';

class Counter extends WidgetBase {

@watch()
private _count = 0;

private _increment() {
this._count = this._count + 1; // this automatically calls invalidate
}

protected render() {
return v('div', [
v('button', { onclick: this._increment }, [ 'Increment' ]),
`${this._count}`)
]);
}
}
```

#### Composing Widgets

As mentioned, often widgets are composed of other widgets in their `render` output. This promotes widget reuse across an application (or multiple applications) and promotes widget best practices.
Expand Down
20 changes: 20 additions & 0 deletions src/widget-core/decorators/watch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import handleDecorator, { DecoratorHandler } from './handleDecorator';

export function watch(): DecoratorHandler {
return handleDecorator((target, propertyKey) => {
target.addDecorator('afterConstructor', function(this: any) {
if (propertyKey) {
let _value: any = this[propertyKey];
Object.defineProperty(this, propertyKey, {
set(value: any) {
_value = value;
this.invalidate();
},
get() {
return _value;
}
});
}
});
});
}
1 change: 1 addition & 0 deletions tests/widget-core/unit/decorators/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ import './customElement';
import './diffProperty';
import './inject';
import './registry';
import './watch';
28 changes: 28 additions & 0 deletions tests/widget-core/unit/decorators/watch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const { describe, it } = intern.getInterface('bdd');
const { assert } = intern.getPlugin('chai');
import WidgetBase from '../../../../src/widget-core/WidgetBase';
import { watch } from '../../../../src/widget-core/decorators/watch';
import { VNode } from '../../../../src/widget-core/interfaces';

describe('Watch', () => {
it('should invalidate on set', () => {
let invalidateCount = 0;
class A extends WidgetBase {
@watch() private _a: string;

invalidate() {
invalidateCount++;
}

render() {
this._a = 'other';
return this._a;
}
}

const widget = new A();
const result = widget.__render__() as VNode;
assert.strictEqual(result.text, 'other');
assert.strictEqual(invalidateCount, 1);
});
});

0 comments on commit 05e991a

Please sign in to comment.