Skip to content

Commit ac074d8

Browse files
committed
Let @event create instances of ChangeListeners
So far @event created only instance of Listeners, since there was no functional difference in ChangeListeners. Now they have an additional "values" property, so a distinction is needed. Some tests needed adjustments as a result. In two cases it was just removing dead code.
1 parent aafaed4 commit ac074d8

File tree

6 files changed

+81
-24
lines changed

6 files changed

+81
-24
lines changed

src/decorators/event.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,60 @@ interface TargetInstance {[key: string]: ListenersStore; }
1414
* When used on a widget the `Listeners` instance will be integrated in
1515
* the existing event system. Events triggered via one API will also be issued via the other.
1616
*/
17-
export function event(targetProto: object, propertyName: string): void {
18-
const propertyType = Reflect.getMetadata('design:type', targetProto, propertyName);
19-
if (
20-
propertyType
21-
&& (propertyType !== Object)
22-
&& (propertyType !== Listeners)
23-
&& (propertyType !== ChangeListeners)
24-
) {
25-
throw new Error(`@event: Invalid type for property ${propertyName}`);
26-
}
27-
if (!/^on[A-Z]/.test(propertyName)) {
28-
throw new Error(`@event: Invalid name for property ${propertyName}`);
29-
}
30-
Object.defineProperty(targetProto, propertyName, {
31-
get() {
17+
export function event(targetProto: object, evPropertyName: string): void {
18+
const evPropertyType = Reflect.getMetadata('design:type', targetProto, evPropertyName);
19+
checkPropertyType(evPropertyType, evPropertyName);
20+
checkPropertyName(evPropertyName, evPropertyType);
21+
if (evPropertyName.endsWith('Changed')) {
22+
defineGetter(targetProto, evPropertyName, function() {
3223
const targetInstance = this as TargetInstance;
33-
const eventType = propertyName.charAt(2).toLowerCase() + propertyName.slice(3);
24+
const propertyName = eventType(evPropertyName).slice(0, -7);
3425
const store = getPropertyStore(targetInstance);
35-
if (!store[propertyName]) {
36-
store[propertyName] = new Listeners<any>(targetInstance, eventType);
26+
if (!store[evPropertyName]) {
27+
store[evPropertyName] = new ChangeListeners(targetInstance, propertyName);
3728
}
38-
return store[propertyName];
39-
},
29+
return store[evPropertyName];
30+
});
31+
} else {
32+
defineGetter(targetProto, evPropertyName, function() {
33+
const targetInstance = this as TargetInstance;
34+
const store = getPropertyStore(targetInstance);
35+
if (!store[evPropertyName]) {
36+
store[evPropertyName] = new Listeners<any>(targetInstance, eventType(evPropertyName));
37+
}
38+
return store[evPropertyName];
39+
});
40+
}
41+
}
42+
43+
function defineGetter(target: object, property: string, getter: () => any) {
44+
Object.defineProperty(target, property, {
45+
get: getter,
4046
enumerable: true,
4147
configurable: false
4248
});
49+
}
50+
51+
function eventType(evPropertyName: string): string {
52+
return evPropertyName.charAt(2).toLowerCase() + evPropertyName.slice(3);
53+
}
4354

55+
function checkPropertyType(propertyType: any, propertyName: string) {
56+
if (propertyType
57+
&& (propertyType !== Object)
58+
&& (propertyType !== Listeners)
59+
&& (propertyType !== ChangeListeners)) {
60+
throw new Error(`@event: Invalid type for property ${propertyName}`);
61+
}
62+
}
63+
64+
function checkPropertyName(propertyName: string, propertyType: any) {
65+
if (!/^on[A-Z]/.test(propertyName)) {
66+
throw new Error(`@event: Invalid name for property ${propertyName}`);
67+
}
68+
if (propertyType === ChangeListeners) {
69+
if (!/^on[A-Z].*Changed$/.test(propertyName)) {
70+
throw new Error(`@event: Invalid name for property ${propertyName}`);
71+
}
72+
}
4473
}

test/component-bind-one-way-jsx.spec.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,6 @@ describe('component', () => {
270270

271271
widget.myItem = Object.assign(new ItemB(), {text: 'bar', int: 23});
272272
widget.myItem.text = 'baz';
273-
widget.myItem.onOtherItemChanged.trigger();
274273

275274
expect(textInput.text).to.equal('baz');
276275
});

test/component-bind-one-way.spec.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,6 @@ describe('component', () => {
250250

251251
widget.myItem = Object.assign(new ItemB(), {text: 'bar', int: 23});
252252
widget.myItem.text = 'baz';
253-
(widget.myItem as ItemB).onOtherItemChanged.trigger();
254253

255254
expect(textInput.text).to.equal('baz');
256255
});

test/event.spec.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {expect} from 'chai';
2-
import {Listeners, NativeObject, tabris} from 'tabris';
2+
import {ChangeListeners, Listeners, NativeObject, Observable, tabris} from 'tabris';
33
import ClientMock from 'tabris/ClientMock';
44
import {restoreSandbox, stub} from './test';
5-
import {event} from '../src';
5+
import {event, property} from '../src';
66

77
describe('event', () => {
88

@@ -38,6 +38,20 @@ describe('event', () => {
3838
expect(listener).to.have.been.calledOnce;
3939
});
4040

41+
it('injects working ChangeListeners', () => {
42+
class PlainClass {
43+
@event readonly onFooChanged: ChangeListeners<PlainClass, 'foo'>;
44+
@property foo: string;
45+
}
46+
47+
const object = new PlainClass();
48+
object.onFooChanged(listener);
49+
object.foo = 'bar';
50+
51+
expect(object.onFooChanged.values).to.be.instanceOf(Observable);
52+
expect(listener).to.have.been.calledOnce;
53+
});
54+
4155
it('fails for incompatible types', () => {
4256
expect(() => {
4357
class PlainClass {
@@ -122,6 +136,19 @@ describe('event', () => {
122136
@event readonly myEvent: Listeners<{target: PlainClass}>;
123137
}
124138
}).to.throw(/myEvent/);
139+
expect(() => {
140+
class PlainClass {
141+
@event readonly onFoo: ChangeListeners<PlainClass, 'foo'>;
142+
@property foo: string;
143+
}
144+
}).to.throw(/onFoo/);
145+
expect(() => {
146+
class PlainClass {
147+
@event readonly onBarChanged: ChangeListeners<PlainClass, 'foo'>;
148+
@property foo: string;
149+
}
150+
new PlainClass().onBarChanged(() => {});
151+
}).to.throw(/has no property "bar"/);
125152
});
126153

127154
});

test/property-js.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class CustomComponent extends Composite {
5252
class MyModel {
5353
/** @type {boolean} */
5454
@property myBool = false;
55+
/** @type {boolean} */
56+
@property otherProp = false;
5557
/** @type {tabris.ChangeListeners<MyModel, 'myBool'>} */
5658
@event onOtherPropChanged;
5759
}

test/property.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ describe('property', () => {
8080
const listener = stub();
8181
class MyModel {
8282
@property myBool: boolean = false;
83+
@property otherProp: boolean = false;
8384
@event onOtherPropChanged: ChangeListeners<MyModel, 'myBool'>;
8485
}
8586
const myModel = new MyModel();

0 commit comments

Comments
 (0)