Skip to content

Commit b9dcf51

Browse files
committed
feat(core): added new react hook *useEffect
Example: effect() { console.log('lol'); return () => console.log(`I'm died!`); } <ng-container *useEffect="effect; on [x]"> Hello world! </ng-container>
1 parent 6a242f1 commit b9dcf51

8 files changed

Lines changed: 189 additions & 24 deletions

File tree

projects/platform/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngxf/platform",
3-
"version": "6.0.7",
3+
"version": "6.0.8",
44
"description": "NGXF - Non-State Management for Angular",
55
"keywords": [
66
"non-ngxs",

projects/platform/src/lib/directives/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './set-props.directive';
1212
export * from './timeout.directive';
1313
export * from './use-reducer.directive';
1414
export * from './use-state.directive';
15+
export * from './use-effect.directive';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
Directive,
3+
DoCheck,
4+
EmbeddedViewRef,
5+
Input,
6+
OnChanges,
7+
OnDestroy,
8+
SimpleChanges,
9+
TemplateRef,
10+
ViewContainerRef
11+
} from '@angular/core';
12+
13+
@Directive({
14+
selector: '[useEffect]'
15+
})
16+
export class UseEffectDirective implements OnChanges, DoCheck, OnDestroy {
17+
private context: {} = {};
18+
19+
@Input() useEffect: Function;
20+
@Input() useEffectOn: [];
21+
22+
private onDestroyCallback: Function;
23+
24+
private viewRef: EmbeddedViewRef<{}>;
25+
26+
constructor(
27+
private templateRef: TemplateRef<{}>,
28+
private vcr: ViewContainerRef
29+
) {}
30+
31+
ngOnChanges(changes: SimpleChanges) {
32+
if (changes.useEffect && this.useEffect) {
33+
this.destroy();
34+
this.init();
35+
} else if (changes.useEffectOn) {
36+
if (Array.isArray(this.useEffectOn)) {
37+
const previous = changes.useEffectOn.previousValue || [];
38+
const current = changes.useEffectOn.currentValue || [];
39+
40+
const isChanged = !previous.every((item, index) => current[index] === item);
41+
42+
if (isChanged) {
43+
this.destroy();
44+
this.init();
45+
}
46+
} else {
47+
this.destroy();
48+
this.init();
49+
}
50+
}
51+
}
52+
53+
ngDoCheck() {
54+
if (!Array.isArray(this.useEffectOn)) {
55+
this.destroy();
56+
this.init();
57+
}
58+
}
59+
60+
ngOnDestroy() {
61+
this.destroy();
62+
}
63+
64+
private init() {
65+
this.viewRef = this.vcr.createEmbeddedView(this.templateRef, this.context);
66+
if (this.useEffect) {
67+
this.onDestroyCallback = this.useEffect();
68+
}
69+
}
70+
71+
private destroy() {
72+
if (this.onDestroyCallback) {
73+
this.onDestroyCallback();
74+
this.onDestroyCallback = null;
75+
}
76+
77+
this.vcr.clear();
78+
79+
if (this.viewRef) {
80+
this.viewRef.destroy();
81+
this.viewRef = null;
82+
}
83+
}
84+
}

projects/platform/src/lib/directives/use-state.directive.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,31 @@ interface UseStateContext<T = any> {
1212
selector: '[useState]'
1313
})
1414
export class UseStateDirective implements OnChanges, OnDestroy {
15-
@Input() useStateOf: any;
15+
@Input() useStateDefault: any;
1616

17-
private context: UseStateContext = {
18-
$implicit: {
19-
get: this.useStateOf,
20-
set(value: any) {
21-
this.get = value;
22-
this.detectChanges();
23-
},
24-
detectChanges: () => {
25-
this.embeddedViewRef.detectChanges();
26-
}
27-
}
28-
};
29-
private embeddedViewRef: EmbeddedViewRef<UseStateContext> =
30-
this.vcr.createEmbeddedView(this.templateRef, this.context);
17+
private context: UseStateContext = {} as any;
18+
private embeddedViewRef: EmbeddedViewRef<UseStateContext>;
19+
private value: any;
3120

3221
constructor(
3322
private templateRef: TemplateRef<UseStateContext>,
3423
private vcr: ViewContainerRef
35-
) {}
24+
) {
25+
Object.defineProperty(this.context, '$implicit', {
26+
get: () => this.value,
27+
set: (value) => {
28+
this.value = value;
29+
if (this.embeddedViewRef) {
30+
this.embeddedViewRef.detectChanges();
31+
}
32+
}
33+
});
34+
this.embeddedViewRef = this.vcr.createEmbeddedView(this.templateRef, this.context);
35+
}
3636

3737
ngOnChanges(changes: SimpleChanges) {
38-
if (changes.useStateOf) {
39-
this.context.$implicit.get = this.useStateOf;
38+
if (changes.useStateDefault) {
39+
this.value = this.useStateDefault;
4040
}
4141
}
4242

projects/platform/src/lib/platform.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
RouteDirective,
1414
SetPropsDirective,
1515
TimeoutDirective,
16+
UseEffectDirective,
1617
UseReducerDirective,
1718
UseStateDirective
1819
} from './directives';
@@ -32,7 +33,8 @@ const DIRECTIVES = [
3233
SetPropsDirective,
3334
TimeoutDirective,
3435
UseReducerDirective,
35-
UseStateDirective
36+
UseStateDirective,
37+
UseEffectDirective
3638
];
3739

3840
const PIPES = [
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Component } from '@angular/core';
2+
import { createHostComponentFactory, SpectatorWithHost } from '@netbasal/spectator';
3+
import { UseEffectDirective } from '../../lib/directives';
4+
5+
@Component({ selector: 'host', template: '' })
6+
class Host {
7+
a;
8+
x;
9+
10+
effect = () => {
11+
return () => this.destroy();
12+
}
13+
14+
destroy() {}
15+
}
16+
17+
describe('UseEffectDirective', () => {
18+
let host: SpectatorWithHost<UseEffectDirective, Host>;
19+
const create = createHostComponentFactory({
20+
component: UseEffectDirective,
21+
host: Host
22+
});
23+
24+
it('should recreate effect when x changed', () => {
25+
host = create(`
26+
<ng-container *useEffect="effect; on [x]">
27+
Hello world!
28+
</ng-container>
29+
`);
30+
31+
spyOn(host.hostComponent, 'effect').and.callThrough();
32+
spyOn(host.hostComponent, 'destroy').and.callThrough();
33+
34+
host.setHostInput({ a: 'a' });
35+
host.setHostInput({ x: 'a' });
36+
host.setHostInput({ a: 'v' });
37+
38+
expect(host.hostComponent.effect).toHaveBeenCalledTimes(2);
39+
expect(host.hostComponent.destroy).toHaveBeenCalledTimes(2);
40+
});
41+
42+
it('shouldn recreate effect when any param changed', () => {
43+
host = create(`
44+
<ng-container *useEffect="effect; on []">
45+
Hello world!
46+
</ng-container>
47+
`);
48+
49+
spyOn(host.hostComponent, 'effect').and.callThrough();
50+
spyOn(host.hostComponent, 'destroy').and.callThrough();
51+
52+
host.setHostInput({ a: 'a' });
53+
host.setHostInput({ x: 'a' });
54+
host.setHostInput({ a: 'v' });
55+
56+
expect(host.hostComponent.effect).toHaveBeenCalledTimes(1);
57+
expect(host.hostComponent.destroy).toHaveBeenCalledTimes(1);
58+
});
59+
60+
it('should recreate effect when any param changed', () => {
61+
host = create(`
62+
<ng-container *useEffect="effect">
63+
Hello world!
64+
</ng-container>
65+
`);
66+
67+
spyOn(host.hostComponent, 'effect').and.callThrough();
68+
spyOn(host.hostComponent, 'destroy').and.callThrough();
69+
70+
host.setHostInput({ a: 'a' });
71+
host.setHostInput({ x: 'a' });
72+
host.setHostInput({ a: 'v' });
73+
74+
expect(host.hostComponent.effect).toHaveBeenCalledTimes(4);
75+
expect(host.hostComponent.destroy).toHaveBeenCalledTimes(4);
76+
});
77+
78+
});

projects/platform/src/test/directives/use-state.directive.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ describe('UseStateDirective', () => {
1414

1515
it('should create state through template', () => {
1616
host = create(`
17-
<ng-container *useState="let x of 0">
18-
Count: {{ x.get }}
19-
<button id="increment" (click)="x.set(x.get + 1)">Increment</button>
17+
<ng-container *useState="let x default 0">
18+
Count: {{ x }}
19+
<button id="increment" (click)="x = x + 1">Increment</button>
2020
</ng-container>
2121
`);
2222

projects/socketio/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngxf/socket.io",
3-
"version": "6.0.7",
3+
"version": "6.0.8",
44
"description": "NGXF - Non-State Management for Angular",
55
"keywords": [
66
"non-ngxs",

0 commit comments

Comments
 (0)