Skip to content

Commit

Permalink
feat(observer-locator): ability to create getter based observer (#1750)
Browse files Browse the repository at this point in the history
* feat(observer-locator): enable getter observer
resolves #1747

* feat(effect): add watch api
  • Loading branch information
bigopon committed Apr 24, 2023
1 parent a22826a commit ba40b2d
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 205 deletions.
177 changes: 105 additions & 72 deletions docs/user-docs/getting-to-know-aurelia/observation/effect-observation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,45 +17,76 @@ class MouseTracker {

The property `coord` of a `MouseTracker` instance will be turned into a reactive property and is also aware of effect function dependency tracking.

### Creating an Effect
{% hint style="info" %}
Properties decorated with `@observable` and any proxy based property access will be tracked as dependencies of the effect
{% endhint %}

The effect API is provided via the default implementation of an interface named `IObservation`.
The effect APIs are provided via the default implementation of the interface `IObservation`, which can be retrieved like one of the following examples:

An example to retrieve an instance of this interface is per following:
- **Getting from a container directly**:
```typescript
import { IObservation } from 'aurelia';

#### Getting from a container directly
...
const observation = someContainer.get(IObservation);
```

```typescript
import { IObservation } from 'aurelia';
- **Getting through injection**:
```typescript
import { inject, IObservation } from 'aurelia';

...
const observation = someContainer.get(IObservation);
```
@inject(IObservation)
class MyElement {
constructor(observation) {
// ...
}
}
```
Or

```typescript
class MyElement {
constructor(@IObservation readonly observation) {
// ...
}
}
```

#### Getting through injection
After getting the observation object, there are two APIs that can be used to created effects as described in the following sections:

## Watch effect

Watch effect is a way to describe a getter based observation of an object. An example to create watch effect is per the following:

```typescript
import { inject, IObservation } from 'aurelia';

@inject(IObservation)
class MyElement {
constructor(observation) {
// ...
}
}
```
import { inject, IObservation } from 'aurelia';

#### Autoinjection (if you are using TypeScript)
@inject(IObservation)
class PersonalInfo {
constructor(observation) {
const effect = observation.watch(this.primaryInfo, (primaryInfo) => primaryInfo.name, function nameChanged(newName, oldName) {
// do something with name
});

// effect.stop() later when necessary
}
}
```

Note that the effect function will be run immediately. If you do not want to run the callback immediately, pass an option `immediate: false` as the 4th parameter:
```typescript
class MyElement {
constructor(@IObservation readonly observation) {
// ...
}
}
observation.watch(obj, getter, callback, { immediate: false });
```

After getting ahold of an `IObservation` instance, an effect can be created via the method `run` of it:
By default, a watch effect is independent of any application lifecycle, which means it does not stop when the application that owns the `observation` instance has stopped. To stop/destroy an effect, call the method `stop()` on the effect object.

## Run effect

Run effects describe a function to be called repeatedly whenever any dependency tracked inside it changes.

### Creating an Effect

After getting an `IObservation` instance, a run effect can be created via the method `run` of it:

```typescript
const effect = observation.run(() => {
Expand All @@ -65,7 +96,7 @@ const effect = observation.run(() => {

Note that the effect function will be run immediately.

By default, an effect is independent of any application lifecycle, which means it does not stop when the application that owns the `observation` instance has stopped. To stop/destroy an effect, call the method `stop()` on the effect object:
By default, a effect is independent of any application lifecycle, which means it does not stop when the application that owns the `observation` instance has stopped. To stop/destroy an effect, call the method `stop()` on the effect object:

```typescript
const effect = IObservation.run(() => {
Expand All @@ -76,58 +107,60 @@ const effect = IObservation.run(() => {
effect.stop();
```

### Effect Observation & Reaction Examples
## Effect examples

#### Creating an effect that logs the user mouse movement on the document
The following section gives some examples of what it looks like when combining `@observable` and run effect.

### Creating a run effect that logs the user mouse movement on the document

```typescript
import { inject, IObservation, observable } from 'aurelia'

class MouseTracker {
@observable coord = [0, 0]; // x: 0, y: 0 is the default value
}

// Inside an application:
@inject(IObservation)
class App {
constructor(observation) {
const mouseTracker = new MouseTracker();

document.addEventListener('mousemove', (e) => {
mouseTracker.coord = [e.pageX, e.pageY]
});

observation.run(() => {
console.log(mouseTracker.coord)
});
}
}
import { inject, IObservation, observable } from 'aurelia'

class MouseTracker {
@observable coord = [0, 0]; // x: 0, y: 0 is the default value
}

// Inside an application:
@inject(IObservation)
class App {
constructor(observation) {
const mouseTracker = new MouseTracker();

document.addEventListener('mousemove', (e) => {
mouseTracker.coord = [e.pageX, e.pageY]
});

observation.run(() => {
console.log(mouseTracker.coord)
});
}
}
```

Now whenever the user moves the mouse around, a log will be added to the console with the coordinate of the mouse.

#### Creating an effect that sends a request whenever user focus/unfocus the browser tab
### Creating a run effect that sends a request whenever user focus/unfocus the browser tab

```typescript
import { inject, IObservation, observable } from 'aurelia'

class PageActivity {
@observable active = false
}

// Inside an application:
@inject(IObservation)
class App {
constructor(observation) {
const pageActivity = new PageActivity();

document.addEventListener(visibilityChange, (e) => {
pageActivity.active = !document.hidden;
});

observation.run(() => {
fetch('my-game/user-activity', { body: JSON.stringify({ active: pageActivity.active }) })
});
}
}
import { inject, IObservation, observable } from 'aurelia'

class PageActivity {
@observable active = false
}

// Inside an application:
@inject(IObservation)
class App {
constructor(observation) {
const pageActivity = new PageActivity();

document.addEventListener(visibilityChange, (e) => {
pageActivity.active = !document.hidden;
});

observation.run(() => {
fetch('my-game/user-activity', { body: JSON.stringify({ active: pageActivity.active }) })
});
}
}
```
20 changes: 11 additions & 9 deletions packages/__tests__/2-runtime/computed-observer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('2-runtime/computed-observer.spec.ts', function () {
}

// TODO: use tracer to deeply verify calls
const sut = ComputedObserver.create(instance, 'prop', propDescriptor, locator, true);
const sut = new ComputedObserver(instance, propDescriptor.get, propDescriptor.set, locator, true);
sut.subscribe(subscriber1);
sut.subscribe(subscriber2);

Expand Down Expand Up @@ -285,7 +285,7 @@ describe('2-runtime/computed-observer.spec.ts', function () {
}
};

const sut = ComputedObserver.create(parent, 'getter', pd, locator, true);
const sut = new ComputedObserver(parent, pd.get, pd.set, locator, true);
sut.subscribe(subscriber1);

let verifiedCount = 0;
Expand Down Expand Up @@ -349,18 +349,20 @@ describe('2-runtime/computed-observer.spec.ts', function () {
let getterCallCount = 0;
const { locator } = createFixture();
const obj = { prop: 1, prop1: 1 };
const observer = ComputedObserver.create(
const observer = new ComputedObserver(
obj,
'prop',
{
get() {
getterCallCount++;
return this.prop1;
}
function (obj) {
getterCallCount++;
return obj.prop1;
},
void 0,
locator,
true,
);
Object.defineProperty(obj, 'prop', {
get: () => observer.getValue(),
set: (v) => {observer.setValue(v);}
});
let _handleChangeCallCount = 0;
observer.subscribe({
handleChange() {
Expand Down
58 changes: 10 additions & 48 deletions packages/__tests__/3-runtime-html/computed-observer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { ComputedObserver, IDirtyChecker, IObserverLocator } from '@aurelia/runtime';
import {
CustomElement,
Aurelia,
} from '@aurelia/runtime-html';
import {
Constructable,
} from '@aurelia/kernel';
import {
assert,
createFixture,
eachCartesianJoin,
TestContext,
} from '@aurelia/testing';
Expand Down Expand Up @@ -243,7 +240,7 @@ describe('3-runtime-html/computed-observer.spec.ts', function () {

const observerLocator = ctx.container.get(IObserverLocator);
const namePropValueObserver = observerLocator
.getObserver(component.nameProp, 'value') as ComputedObserver;
.getObserver(component.nameProp, 'value') as ComputedObserver<Property>;

assert.instanceOf(namePropValueObserver, ComputedObserver);
assert.strictEqual(
Expand Down Expand Up @@ -291,7 +288,7 @@ describe('3-runtime-html/computed-observer.spec.ts', function () {

const observerLocator = ctx.container.get(IObserverLocator);
const namePropValueObserver = observerLocator
.getObserver(component.nameProp, 'value',) as ComputedObserver;
.getObserver(component.nameProp, 'value',) as ComputedObserver<any>;

assert.instanceOf(namePropValueObserver, ComputedObserver);
},
Expand Down Expand Up @@ -388,7 +385,7 @@ describe('3-runtime-html/computed-observer.spec.ts', function () {
// eslint-disable-next-line mocha/no-exclusive-tests
const $it = (title_: string, fn: Mocha.Func) => only ? it.only(title_, fn) : it(title_, fn);
$it(title, async function () {
const { ctx, component, testHost, tearDown } = await createFixture<any>(
const { ctx, component, testHost, tearDown } = createFixture<any>(
template,
ViewModel,
);
Expand All @@ -401,7 +398,7 @@ describe('3-runtime-html/computed-observer.spec.ts', function () {
);

it('works with two layers of getter', async function () {
const { appHost, tearDown } = await createFixture(
const { assertText } = createFixture(
`\${msg}`,
class MyApp {
public get one() {
Expand All @@ -417,13 +414,11 @@ describe('3-runtime-html/computed-observer.spec.ts', function () {
}
);

assert.html.textContent(appHost, 'One two');

await tearDown();
assertText('One two');
});

it('observers property in 2nd layer getter', async function () {
const { ctx, component, appHost, tearDown } = await createFixture(
const { component, assertText, flush } = createFixture(
`\${msg}`,
class MyApp {
public message = 'One';
Expand All @@ -440,46 +435,13 @@ describe('3-runtime-html/computed-observer.spec.ts', function () {
}
);

assert.html.textContent(appHost, 'One two');
assertText('One two');

component.message = '1';
ctx.platform.domWriteQueue.flush();
assert.html.textContent(appHost, '1 two');

await tearDown();
flush();
assertText('1 two');
});

async function createFixture<T>(template: string | Node, $class: Constructable<T> | null, ...registrations: any[]) {
const ctx = TestContext.create();
const { container, observerLocator } = ctx;
registrations = Array.from(new Set([...registrations]));
container.register(...registrations);
const testHost = ctx.doc.body.appendChild(ctx.createElement('div'));
const appHost = testHost.appendChild(ctx.createElement('app'));
const au = new Aurelia(container);
const App = CustomElement.define({ name: 'app', template }, $class);
const component = new App();

au.app({ host: appHost, component });
await au.start();

return {
ctx: ctx,
au,
container,
testHost: testHost,
appHost,
component: component as T,
observerLocator,
tearDown: async () => {
await au.stop();
testHost.remove();

au.dispose();
},
};
}

class Property {
private _value: string;
public readonly valueChanged: any;
Expand Down

0 comments on commit ba40b2d

Please sign in to comment.