bloc-react
Advanced tools
Comparing version 0.0.7 to 0.0.8
@@ -14,3 +14,26 @@ import { Subscription } from 'rxjs'; | ||
} | ||
interface ChangeEvent<T> { | ||
currentState: T; | ||
nextState: T; | ||
} | ||
interface TransitionEvent<T, E> { | ||
currentState: T; | ||
event: E; | ||
nextState: T; | ||
} | ||
interface BlocObserverOptions { | ||
onChange?: (bloc: BlocBase<any>, event: ChangeEvent<any>) => void; | ||
onTransition?: (bloc: BlocBase<any>, event: TransitionEvent<any, any>) => void; | ||
} | ||
declare class BlocObserver { | ||
onChange: (bloc: BlocBase<any>, event: ChangeEvent<any>) => void; | ||
onTransition: (bloc: BlocBase<any>, event: TransitionEvent<any, any>) => void; | ||
constructor(methods?: BlocObserverOptions); | ||
readonly addChange: (bloc: BlocBase<any>, state: any) => void; | ||
readonly addTransition: (bloc: BlocBase<any>, state: any, event: any) => void; | ||
private createTransitionEvent; | ||
private createChangeEvent; | ||
} | ||
interface ReactBlocOptions$1 { | ||
@@ -22,3 +45,3 @@ /** Enables debugging which calls BlocReact.observer every time a Subject is updated. Defaults to false */ | ||
declare class BlocConsumer { | ||
observer: null | ((bloc: BlocBase<any>, value: any) => void); | ||
observer: BlocObserver; | ||
debug: boolean; | ||
@@ -29,4 +52,5 @@ readonly blocListGlobal: BlocBase<any>[]; | ||
constructor(blocs: BlocBase<any>[], options?: ReactBlocOptions$1); | ||
notify(bloc: BlocBase<any>, state: ValueType<any>): void; | ||
addBlocObserver<T extends BlocBase<any>>(blocClass: BlocClass<T>, callback: (bloc: T, state: ValueType<T>) => unknown, scope?: BlocObserverScope): void; | ||
notifyChange(bloc: BlocBase<any>, state: any): void; | ||
notifyTransition(bloc: BlocBase<any>, state: any, event: any): void; | ||
addBlocObserver<T extends BlocBase<any>>(blocClass: BlocClass<T>, callback: (bloc: T, event: ChangeEvent<T>) => unknown, scope?: BlocObserverScope): void; | ||
addLocalBloc(key: string, bloc: BlocBase<any>): void; | ||
@@ -54,8 +78,5 @@ removeLocalBloc(key: string): void; | ||
onRegister: null | ((consumer: BlocConsumer) => void); | ||
onChange: null | ((change: { | ||
currentState: T; | ||
nextState: T; | ||
}) => void); | ||
onChange: null | ((change: ChangeEvent<T>) => void); | ||
constructor(initialValue: T, blocOptions?: BlocOptions); | ||
private _consumer; | ||
protected _consumer: BlocConsumer | null; | ||
set consumer(consumer: BlocConsumer); | ||
@@ -66,3 +87,3 @@ protected notifyChange: (state: T) => void; | ||
declare class Bloc<E, T> extends BlocBase<T> { | ||
protected onTransition: null | ((change: { | ||
onTransition: null | ((change: { | ||
currentState: T; | ||
@@ -75,3 +96,3 @@ event: E; | ||
add: (event: E) => void; | ||
protected notifyTransition: (value: T, event: E) => void; | ||
protected notifyTransition: (state: T, event: E) => void; | ||
} | ||
@@ -78,0 +99,0 @@ |
@@ -84,2 +84,3 @@ 'use strict'; | ||
this.notifyChange = (state) => { | ||
this._consumer?.notifyChange(this, state); | ||
this.onChange?.({ | ||
@@ -111,7 +112,8 @@ currentState: this.state, | ||
}; | ||
this.notifyTransition = (value, event) => { | ||
this.notifyTransition = (state, event) => { | ||
this._consumer?.notifyTransition(this, state, event); | ||
this.onTransition?.({ | ||
currentState: this.state, | ||
event, | ||
nextState: value | ||
nextState: state | ||
}); | ||
@@ -132,5 +134,32 @@ }; | ||
class BlocObserver { | ||
constructor(methods = {}) { | ||
this.addChange = (bloc, state) => { | ||
this.onChange(bloc, this.createChangeEvent(bloc, state)); | ||
}; | ||
this.addTransition = (bloc, state, event) => { | ||
this.onTransition(bloc, this.createTransitionEvent(bloc, state, event)); | ||
}; | ||
this.onChange = methods.onChange ? methods.onChange : () => { | ||
}; | ||
this.onTransition = methods.onTransition ? methods.onTransition : () => { | ||
}; | ||
} | ||
createTransitionEvent(bloc, state, event) { | ||
return { | ||
currentState: bloc.state, | ||
event, | ||
nextState: state | ||
}; | ||
} | ||
createChangeEvent(bloc, state) { | ||
return { | ||
currentState: bloc.state, | ||
nextState: state | ||
}; | ||
} | ||
} | ||
class BlocConsumer { | ||
constructor(blocs, options = {}) { | ||
this.observer = null; | ||
this._blocMapLocal = {}; | ||
@@ -140,12 +169,10 @@ this.blocObservers = []; | ||
this.debug = options.debug || false; | ||
this.observer = new BlocObserver(); | ||
for (const b of blocs) { | ||
b.consumer = this; | ||
b.subscribe((v) => this.notify(b, v)); | ||
b.onRegister?.(this); | ||
} | ||
} | ||
notify(bloc, state) { | ||
if (this.observer) { | ||
this.observer(bloc, state); | ||
} | ||
notifyChange(bloc, state) { | ||
this.observer.addChange(bloc, state); | ||
for (const [blocClass, callback, scope] of this.blocObservers) { | ||
@@ -155,6 +182,12 @@ const isGlobal = this.blocListGlobal.indexOf(bloc) !== -1; | ||
if (matchesScope && bloc instanceof blocClass) { | ||
callback(bloc, state); | ||
callback(bloc, { | ||
nextState: state, | ||
currentState: bloc.state | ||
}); | ||
} | ||
} | ||
} | ||
notifyTransition(bloc, state, event) { | ||
this.observer.addTransition(bloc, state, event); | ||
} | ||
addBlocObserver(blocClass, callback, scope = "all") { | ||
@@ -165,3 +198,3 @@ this.blocObservers.push([blocClass, callback, scope]); | ||
this._blocMapLocal[key] = bloc; | ||
bloc.subscribe((v) => this.notify(bloc, v)); | ||
bloc.consumer = this; | ||
} | ||
@@ -168,0 +201,0 @@ removeLocalBloc(key) { |
{ | ||
"name": "bloc-react", | ||
"version": "0.0.7", | ||
"version": "0.0.8", | ||
"scripts": { | ||
@@ -5,0 +5,0 @@ "dev": "vite", |
# BLoC React | ||
BLoC pattern implementation for react using rxjs and heavily inspired by flutter_react - https://bloclibrary.dev | ||
[Coverage: 100%] | ||
## This library is in early alpha and is not recommended being used for production. | ||
TypeScript BLoC pattern implementation for react using RxJS and heavily inspired by flutter_react - https://bloclibrary.dev | ||
The BLoC Pattern (**B**usiness **Lo**gic **C**omponent) is a battle-tested design pattern for state management coming from Flutter and Dart. It tries to separate business logic from UI as much as possible while still being simple and flexible. | ||
Everything revolves around [**subjects**](https://rxjs-dev.firebaseapp.com/guide/subject) which are native to Dart, for JS there is a solid implementation by RxJS. | ||
# Quickstart | ||
[TODO: Add plain JS examples] | ||
### 1. Create a new **Bloc/Cubit** | ||
```typescript | ||
// CounterCubit.ts | ||
export default class CounterCubit extends Cubit<number> { | ||
increment = (): void => { | ||
this.emit(this.state + 1); | ||
}; | ||
} | ||
``` | ||
### 2. Create a new **BlocReact** instance and export `useBloc` from it | ||
```typescript | ||
// state.ts | ||
const state = new BlocReact([new CounterCubit(0)]); | ||
export const { useBloc } = state; | ||
``` | ||
### 3. Use the hook to access the state and class methods | ||
```typescript | ||
// CounterButton.tsx | ||
import { useBloc } from "../state"; | ||
export default function CounterButton() { | ||
const [count, { increment }] = useBloc(CounterCubit); | ||
return <button onClick={() => increment()}>count is: {count}</button>; | ||
} | ||
``` | ||
# Documentation | ||
[TODO] | ||
## BlocReact | ||
[TODO] | ||
## Bloc | ||
[TODO] | ||
## Cubit | ||
[TODO] | ||
## BlocObserver | ||
[TODO] |
import Bloc from "./Bloc"; | ||
import mockConsole from "jest-mock-console"; | ||
import { AuthEvent, TestBloc } from "../helpers/test.fixtures"; | ||
@@ -7,28 +8,5 @@ describe("Bloc", () => { | ||
onChange: jest.fn(), | ||
onTransition: jest.fn(), | ||
onTransition: jest.fn() | ||
}; | ||
enum AuthEvent { | ||
authenticated = "authenticated", | ||
unauthenticated = "unauthenticated", | ||
} | ||
class TestBloc extends Bloc<AuthEvent, boolean> { | ||
constructor() { | ||
super(false); | ||
this.onChange = spy.onChange; | ||
this.onTransition = spy.onTransition; | ||
this.mapEventToState = (event) => { | ||
switch (event) { | ||
case AuthEvent.unauthenticated: | ||
return false; | ||
case AuthEvent.authenticated: | ||
return true; | ||
} | ||
}; | ||
} | ||
} | ||
beforeEach(() => { | ||
@@ -45,3 +23,6 @@ jest.resetAllMocks(); | ||
mockConsole(); | ||
class NotFullyImplemented extends Bloc<AuthEvent, boolean> {} | ||
class NotFullyImplemented extends Bloc<AuthEvent, boolean> { | ||
} | ||
const bloc = new NotFullyImplemented(false); | ||
@@ -62,2 +43,3 @@ expect(bloc.state).toBe(false); | ||
const bloc = new TestBloc(); | ||
bloc.onChange = spy.onChange; | ||
expect(spy.onChange).toHaveBeenCalledTimes(0); | ||
@@ -68,3 +50,3 @@ bloc.add(AuthEvent.authenticated); | ||
currentState: false, | ||
nextState: true, | ||
nextState: true | ||
}); | ||
@@ -75,2 +57,3 @@ }); | ||
const bloc = new TestBloc(); | ||
bloc.onTransition = spy.onTransition; | ||
expect(spy.onTransition).toHaveBeenCalledTimes(0); | ||
@@ -82,6 +65,13 @@ bloc.add(AuthEvent.authenticated); | ||
event: AuthEvent.authenticated, | ||
nextState: true, | ||
nextState: true | ||
}); | ||
}); | ||
it("should accept payload", () => { | ||
const bloc = new TestBloc(); | ||
expect(bloc.state).toBe(false); | ||
bloc.add(AuthEvent.authenticated); | ||
expect(bloc.state).toBe(true); | ||
}); | ||
}); | ||
}); |
@@ -5,3 +5,3 @@ import BlocBase from "./BlocBase"; | ||
export default class Bloc<E, T> extends BlocBase<T> { | ||
protected onTransition: | ||
onTransition: | ||
| null | ||
@@ -28,9 +28,10 @@ | ((change: { currentState: T; event: E; nextState: T }) => void) = null; | ||
protected notifyTransition = (value: T, event: E): void => { | ||
protected notifyTransition = (state: T, event: E): void => { | ||
this._consumer?.notifyTransition(this, state, event); | ||
this.onTransition?.({ | ||
currentState: this.state, | ||
event, | ||
nextState: value, | ||
}); | ||
nextState: state, | ||
}) | ||
}; | ||
} |
import { BlocConsumer } from "./BlocConsumer"; | ||
import StreamAbstraction from "./StreamAbstraction"; | ||
import { BlocOptions } from "./types"; | ||
import { BlocOptions, ChangeEvent } from "./types"; | ||
@@ -8,3 +8,3 @@ export default class BlocBase<T> extends StreamAbstraction<T> { | ||
onRegister: null | ((consumer: BlocConsumer) => void) = null; | ||
onChange: null | ((change: { currentState: T; nextState: T }) => void) = null; | ||
onChange: null | ((change: ChangeEvent<T>) => void) = null; | ||
@@ -15,3 +15,3 @@ constructor(initialValue: T, blocOptions: BlocOptions = {}) { | ||
private _consumer: BlocConsumer | null = null; | ||
protected _consumer: BlocConsumer | null = null; | ||
@@ -23,2 +23,4 @@ set consumer(consumer: BlocConsumer) { | ||
protected notifyChange = (state: T): void => { | ||
this._consumer?.notifyChange(this, state); | ||
this.onChange?.({ | ||
@@ -25,0 +27,0 @@ currentState: this.state, |
@@ -1,58 +0,35 @@ | ||
import { BlocConsumer, BlocObserverScope } from "./BlocConsumer"; | ||
import Cubit from "./Cubit"; | ||
import { BlocClass } from "./types"; | ||
import { BlocConsumer } from "./BlocConsumer"; | ||
import BlocObserver from "./BlocObserver"; | ||
import { AuthEvent, Listener, Test1, TestBloc } from "../helpers/test.fixtures"; | ||
class Test1 extends Cubit<number> { | ||
constructor(options: { register?: () => void } = {}) { | ||
super(1); | ||
if (options.register) { | ||
this.onRegister = options.register; | ||
} | ||
} | ||
increment = () => { | ||
this.emit(this.state + 1); | ||
}; | ||
} | ||
class Listener extends Cubit<number> { | ||
constructor( | ||
notify: (bloc: any, state: any) => void, | ||
listenFor: BlocClass<any>, | ||
scope?: BlocObserverScope | ||
) { | ||
super(1); | ||
this.onRegister = (consumer) => { | ||
consumer.addBlocObserver( | ||
listenFor, | ||
(bloc, state) => { | ||
notify(bloc, state); | ||
}, | ||
scope | ||
); | ||
}; | ||
} | ||
increment = () => { | ||
this.emit(this.state + 1); | ||
}; | ||
} | ||
describe("BlocConsumer", function () { | ||
it("should call function set to observer prop on any state change", function () { | ||
describe("BlocConsumer", function() { | ||
it("should call `onChange` on any state change", () => { | ||
const testCubit = new Test1(); | ||
const testBlocConsumer = new BlocConsumer([testCubit]); | ||
const fn = jest.fn(); | ||
testBlocConsumer.observer = fn; | ||
const onChange = jest.fn(); | ||
testBlocConsumer.observer = new BlocObserver({ onChange }); | ||
testCubit.increment(); | ||
expect(fn).toHaveBeenCalledTimes(1); | ||
expect(fn).toHaveBeenCalledWith(testCubit, 2); | ||
expect(onChange).toHaveBeenCalledTimes(1); | ||
expect(onChange).toHaveBeenCalledWith(testCubit, { currentState: 1, nextState: 2 }); | ||
}); | ||
it("should call `onRegister` when the class is registered", function () { | ||
it("should call `onTransition` on any state change for Blocs", () => { | ||
const testBloc = new TestBloc(); | ||
const testBlocConsumer = new BlocConsumer([testBloc]); | ||
const onTransition = jest.fn(); | ||
testBlocConsumer.observer = new BlocObserver({ onTransition }); | ||
testBloc.add(AuthEvent.authenticated); | ||
expect(onTransition).toHaveBeenCalledTimes(1); | ||
expect(onTransition).toHaveBeenCalledWith(testBloc, { | ||
currentState: false, | ||
event: AuthEvent.authenticated, | ||
nextState: true | ||
}); | ||
}); | ||
it("should call `onRegister` when the class is registered", function() { | ||
const register = jest.fn(); | ||
const testCubit = new Test1({ | ||
register, | ||
register | ||
}); | ||
@@ -63,4 +40,4 @@ new BlocConsumer([testCubit]); | ||
describe("observers", function () { | ||
it("should allow one bloc to listen to another bloc", function () { | ||
describe("observers", function() { | ||
it("should allow one bloc to listen to another bloc", function() { | ||
const notify = jest.fn(); | ||
@@ -71,13 +48,12 @@ const global = new Test1(); | ||
const consumer = new BlocConsumer([global, listener]); | ||
consumer.addLocalBloc('abc', local); // should trigger listener "all" | ||
consumer.addLocalBloc("abc", local); | ||
expect(notify).toHaveBeenCalledTimes(0); | ||
global.increment(); // should trigger listener "all" | ||
expect(notify).toHaveBeenCalledTimes(1); | ||
expect(notify).toHaveBeenCalledWith(local, 1); | ||
global.increment(); // should trigger listener "all" | ||
expect(notify).toHaveBeenCalledWith(global, { currentState: 1, nextState: 2 }); | ||
local.increment(); // should trigger listener "all" | ||
expect(notify).toHaveBeenCalledTimes(2); | ||
expect(notify).toHaveBeenCalledWith(global, 2); | ||
local.increment(); // should trigger listener "all" | ||
expect(notify).toHaveBeenCalledTimes(3); | ||
}); | ||
it("should allow filtering listener only for local blocs", function () { | ||
it("should allow filtering listener only for local blocs", function() { | ||
const notify = jest.fn(); | ||
@@ -88,14 +64,11 @@ const global = new Test1(); | ||
const consumer = new BlocConsumer([global, listener]); | ||
// local blocs trigger listeners when added | ||
consumer.addLocalBloc('abc', local); // should trigger listener "local" | ||
expect(notify).toHaveBeenCalledTimes(1); | ||
expect(notify).toHaveBeenCalledWith(local, 1); | ||
consumer.addLocalBloc("abc", local); // should trigger listener "local" | ||
global.increment(); // should not trigger listener "local" | ||
expect(notify).toHaveBeenCalledTimes(0); | ||
local.increment(); // should trigger listener "local" | ||
expect(notify).toHaveBeenCalledTimes(1); | ||
local.increment(); // should trigger listener "local" | ||
expect(notify).toHaveBeenCalledTimes(2); | ||
expect(notify).toHaveBeenCalledWith(local, 2); | ||
expect(notify).toHaveBeenCalledWith(local, { currentState: 1, nextState: 2 }); | ||
}); | ||
it("should allow filtering listener only for global blocs", function () { | ||
it("should allow filtering listener only for global blocs", function() { | ||
const notify = jest.fn(); | ||
@@ -106,7 +79,7 @@ const global = new Test1(); | ||
const consumer = new BlocConsumer([global, listener]); | ||
consumer.addLocalBloc('abc', local); // should not trigger listener "global" | ||
consumer.addLocalBloc("abc", local); // should not trigger listener "global" | ||
expect(notify).toHaveBeenCalledTimes(0); | ||
global.increment(); // should trigger listener "global" | ||
expect(notify).toHaveBeenCalledTimes(1); | ||
expect(notify).toHaveBeenCalledWith(global, 2); | ||
expect(notify).toHaveBeenCalledWith(global, { currentState: 1, nextState: 2 }); | ||
local.increment(); // should not trigger listener "global" | ||
@@ -116,3 +89,3 @@ expect(notify).toHaveBeenCalledTimes(1); | ||
it("should allow not notify changes after bloc has been removed", function () { | ||
it("should allow not notify changes after bloc has been removed", function() { | ||
const notify = jest.fn(); | ||
@@ -123,9 +96,7 @@ const global = new Test1(); | ||
const consumer = new BlocConsumer([global, listener]); | ||
consumer.addLocalBloc('abc', local); // should trigger listener "all" | ||
consumer.addLocalBloc("abc", local); // should trigger listener "all" | ||
global.increment(); // should trigger listener "all" | ||
expect(notify).toHaveBeenCalledTimes(1); | ||
expect(notify).toHaveBeenCalledWith(local, 1); | ||
global.increment(); // should trigger listener "all" | ||
expect(notify).toHaveBeenCalledTimes(2); | ||
expect(notify).toHaveBeenCalledWith(global, 2); | ||
consumer.removeLocalBloc('abc'); | ||
expect(notify).toHaveBeenCalledWith(global, { currentState: 1, nextState: 2 }); | ||
consumer.removeLocalBloc("abc"); | ||
local.increment(); // should trigger listener "all" | ||
@@ -132,0 +103,0 @@ expect(notify).toHaveBeenCalledTimes(2); |
import BlocBase from "./BlocBase"; | ||
import { BlocClass, ValueType } from "./types"; | ||
import { BlocClass, ChangeEvent } from "./types"; | ||
import BlocObserver from "./BlocObserver"; | ||
@@ -10,5 +11,5 @@ export interface ReactBlocOptions { | ||
export type BlocObserverScope = "local" | "global" | "all"; | ||
type BlocObserver = [ | ||
type BlocObserverList = [ | ||
BlocClass<any>, | ||
(bloc: any, state: any) => unknown, | ||
(bloc: any, event: ChangeEvent<any>) => unknown, | ||
BlocObserverScope | ||
@@ -18,7 +19,7 @@ ]; | ||
export class BlocConsumer { | ||
observer: null | ((bloc: BlocBase<any>, value: any) => void) = null; | ||
observer: BlocObserver; | ||
debug: boolean; | ||
readonly blocListGlobal: BlocBase<any>[]; | ||
protected _blocMapLocal: Record<string, BlocBase<any>> = {}; | ||
private blocObservers: BlocObserver[] = []; | ||
private blocObservers: BlocObserverList[] = []; | ||
@@ -28,6 +29,7 @@ constructor(blocs: BlocBase<any>[], options: ReactBlocOptions = {}) { | ||
this.debug = options.debug || false; | ||
this.observer = new BlocObserver(); | ||
for (const b of blocs) { | ||
b.consumer = this; | ||
b.subscribe((v: any) => this.notify(b, v)); | ||
// b.subscribe((v: any) => this.notifyChange(b, v)); | ||
b.onRegister?.(this); | ||
@@ -37,6 +39,4 @@ } | ||
notify(bloc: BlocBase<any>, state: ValueType<any>): void { | ||
if (this.observer) { | ||
this.observer(bloc, state); | ||
} | ||
notifyChange(bloc: BlocBase<any>, state: any): void { | ||
this.observer.addChange(bloc, state); | ||
@@ -50,3 +50,6 @@ for (const [blocClass, callback, scope] of this.blocObservers) { | ||
if (matchesScope && bloc instanceof blocClass) { | ||
callback(bloc, state); | ||
callback(bloc, { | ||
nextState: state, | ||
currentState: bloc.state | ||
}); | ||
} | ||
@@ -56,5 +59,9 @@ } | ||
notifyTransition(bloc: BlocBase<any>, state: any, event: any): void { | ||
this.observer.addTransition(bloc, state, event); | ||
} | ||
public addBlocObserver<T extends BlocBase<any>>( | ||
blocClass: BlocClass<T>, | ||
callback: (bloc: T, state: ValueType<T>) => unknown, | ||
callback: (bloc: T, event: ChangeEvent<T>) => unknown, | ||
scope: BlocObserverScope = "all" | ||
@@ -67,3 +74,3 @@ ) { | ||
this._blocMapLocal[key] = bloc; | ||
bloc.subscribe((v: any) => this.notify(bloc, v)); | ||
bloc.consumer = this; | ||
} | ||
@@ -70,0 +77,0 @@ |
@@ -18,1 +18,13 @@ import BlocBase from "./BlocBase"; | ||
} | ||
export interface ChangeEvent<T> { | ||
currentState: T, | ||
nextState: T, | ||
} | ||
export interface TransitionEvent<T, E> { | ||
currentState: T, | ||
event: E, | ||
nextState: T, | ||
} |
@@ -5,2 +5,3 @@ import CounterCubit from "./bloc/CounterCubit"; | ||
import { BlocReact } from "../lib"; | ||
import BlocObserver from "../lib/BlocObserver"; | ||
@@ -12,3 +13,3 @@ const state = new BlocReact( | ||
state.observer = console.log; | ||
state.observer = new BlocObserver(); | ||
@@ -15,0 +16,0 @@ export const { useBloc, BlocBuilder, BlocProvider } = state; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
465907
58
2189
51