ajwahjs
v1.3.9
Published
Framework agnostic reactive state management tools
Downloads
10
Maintainers
Readme
ajwahjs
Framework agnostic state management tools.
Reactive state management library. Manage your application's states, effects, and actions easy way. Make apps more scalable with a unidirectional data-flow.
Every StateController
has the following features:
- Dispatching actions
- Filtering actions
- Adding effects
- Communications among Controllers[
Although they are independents
]
CounterState
interface CounterState {
count: number;
loading: bool;
}
class CounterStateCtrl extends StateController<CounterState> {
constructor() {
super({ count: 0, loading: false });
}
onInit() {}
inc() {
this.emit({ count: this.state.count++ });
}
dec() {
this.emit({ count: this.state.count-- });
}
async asyncInc() {
this.emit({ loading: true });
await delay(1000);
this.emit({ count: this.state.count++, loading: false });
}
asyncIncBy = this.effect<number>((num$) =>
num$.pipe(
tap((_) => this.emit({ loading: true })),
delay(1000),
map((by) => ({ count: this.state.count + by, loading: false }))
)
);
}
Consuming State in
Vanilla js
const csCtrl = Get(CounterStateCtrl);
csCtrl.stream$.subscrie(console.log);
csCtrl.inc();
csCtrl.dec();
csCtrl.asyncInc();
csCtrl.asyncIncBy(5);
React
const CounterComponent = () => {
const csCtrl = Get(CounterStateCtrl);
const data = useStream(csCtrl.stream$, csCtrl.state);
return (
<p>
<button className="btn" onClick={() => csCtrl.inc()}>
+
</button>
<button className="btn" onClick={() => csCtrl.dec()}>
-
</button>
<button className="btn" onClick={() => csCtrl.asyncInc()}>
async(+)
</button>
{data.loading ? 'loading...' : data.count}
</p>
);
};
Angular
@Component({
selector: 'app-counter',
template: `
<p>
<button class="btn" (click)="csCtrl.inc()">+</button>
<button class="btn" (click)="csCtrl.dec()">-</button>
<button class="btn" (click)="csCtrl.asyncIn())">async(+)</button>
<span *ngIf="csCtrl.stream$ | async as state"
>{{ state.loading ? 'loading...' : state.count }}
</span>
</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
constructor(public csCtrl: CounterStateCtrl) {}
}
Vue
<template>
<p>
<button class="btn" @click="inc()">+</button>
<button class="btn" @click="dec()">-</button>
<button class="btn" @click="asyncInc()">async(+)</button>
{{ state.loading?'loading...':state.count }}
</p>
</template>
export default {
name: "Counter",
components: {},
setup() {
const csCtrl = Get(CounterStateCtrl);
const state = useStream(csCtrl.stream$, csCtrl.state);
function inc() {
csCtrl.inc();
}
function dec() {
csCtrl.dec();
}
function asyncInc() {
csCtrl.asyncInc();
}
return { inc, dec, asyncInc, state };
},
};
Effects
onInit() {
this.effectOnAction(
this.action$.isA(AsyncInc).pipe(
tap((_) => this.emit({ loading: true })),
delay(1000),
map((action) => ({ count: this.state.count + action.data, loading: false }))
));
}
asyncIncBy = effect<number>((num$) =>
num$.pipe(
tap((_) => this.emit({ loading: true })),
delay(1000),
tap((by) => this.emit({ count: this.state.count + by, loading: false }))
)
);
Combining States
get todos$() {
return combineLatest([
this.stream$,
this.remoteStream<SearchCategory>(SearchCategoryStateCtrl)
]).pipe(
map(([todos, searchCategory]) => {
switch (searchCategory) {
case SearchCategory.active:
return todos.filter(todo => !todo.completed);
case SearchCategory.completed:
return todos.filter(todo => todo.completed);
default:
return todos;
}
})
);
}
Todo Service
import { Injectable } from "@angular/core";
import { StateController } from './store';
import { getTodos, HasMessage, IAppService, Visibility, SearchTodo, Todo, tween } from './app.service.types'
import { delay, filter, tap, map, combineLatest, startWith, exhaustMap, repeat, takeUntil, endWith } from "rxjs";
@Injectable({ providedIn: 'root' })
export class AppService extends StateController<IAppService>{
constructor() {
super({
message: null,
todos: [],
visibility: 'all',
isSearching: false,
loading: false,
});
}
override onInit() {
this.emit({ todos: getTodos() })
this.effectOnAction(
this.action$.isA(HasMessage).pipe(
filter(_ => this.state.message !== null),
delay(3000),
map(_ => (<IAppService>{ message: null }))
)
)
}
setVisibility(visibility: Visibility) {
this.emit({ visibility })
}
toggleSearch() {
this.emit({ isSearching: !this.state.isSearching })
}
addTodo(task: string) {
if (this.state.isSearching) return
if (!task) {
this.emit({ message: { type: 'error', message: 'Task is required.' } })
return
}
const todos = this.state.todos.concat();
todos.push({ id: todos.length + 1, task, completed: false })
this.throttle({ todos, message: { type: 'info', message: 'Todo added successfully' } });
}
updateTodo(id: number) {
const todos = this.state.todos.map(todo => {
if (todo.id === id) {
todo = { ...todo, completed: !todo.completed }
}
return todo;
});
this.throttle({ todos, message: { type: 'info', message: 'Todo updated successfully' } });
}
removeTodo(id: number) {
const todos = this.state.todos.filter(todo => todo.id !== id);
this.throttle({ todos, message: { type: 'info', message: 'Todo removed successfully' } });
}
#loadingStart$ = this.select(state => state.loading).pipe(filter(val => val));
#loadingEnd$ = this.select(state => state.loading).pipe(filter(val => !val));
rotate$ = this.#loadingStart$.pipe(
exhaustMap(() => tween(0, 360, 1000).pipe(
repeat(),
takeUntil(this.#loadingEnd$),
endWith(0)
))
)
isSearching$ = this.select(state => state.isSearching)
message$ = this.select(state => state.message).pipe(
tap(msg => {
if (msg) { this.dispatch(new HasMessage()) }
}),
);
activeTodo$ = this.select(state => state.todos).pipe(
map(todos => todos.filter(todo => !todo.completed).length));
visibility$ = this.select(state => state.visibility)
todo$ = combineLatest([
this.select(state => state.todos),
this.select(state => state.visibility),
this.action$.isA(SearchTodo).pipe(
filter(_ => this.state.isSearching),
map(search => search.searchText),
startWith('')
)
]).pipe(
map(([todos, visibility, searchText]) => {
if (searchText) {
todos = todos.filter(todo => todo.task.toLowerCase().includes(searchText))
}
if (visibility === 'active') {
todos = todos.filter(todo => !todo.completed)
}
else if (visibility === 'completed') {
todos = todos.filter(todo => todo.completed)
}
return todos;
})
);
throttle = this.effect<Partial<IAppService>>(todo$ => todo$.pipe(
tap(_ => this.emit({ loading: true })),
delay(1300),
map(state => {
state.loading = false;
return state;
})
));
}