Skip to content

Commit

Permalink
feat: supports effect action (#53)
Browse files Browse the repository at this point in the history
* feat(core): supports effect action

supports effect action and change interceptor to middleware

* test: rewrite some test with react-hooks-testing-library

* chore: remove unexpect adding dependency

* feat: use the method's name as the default effect key

* fix: eslint

* docs: update README
  • Loading branch information
foreleven authored and JounQin committed Sep 6, 2019
1 parent 1c43b63 commit 77c2c5d
Show file tree
Hide file tree
Showing 37 changed files with 514 additions and 624 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
'@typescript-eslint/interface-name-prefix': 0,
'@typescript-eslint/no-floating-promises': 0,
'@typescript-eslint/no-type-alias': 0,
'@typescript-eslint/unbound-method': 0,
'react/jsx-handler-names': 0,
},
},
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,17 @@ Indicates that an annotated property is `Stated`. Its reassignment will be obser

The `PostProvided` decorator is used on a method that needs to be executed after the `StatedBean` be instanced to perform any initialization.

#### `@Effect(name?: string | symbol): MethodDecorator`

The `Effect` decorator is used on a method that can get the execution state by `useObserveEffect`.

### use Hooks

#### `useBean<T>(typeOrSupplier: ClassType<T> | () => T, name?: string | symbol): T`

The `useBean` will create an instance of the stated bean with a new `StatedBeanContainer` and listen for its data changes to trigger the re-rendering of the current component.

### `useInject<T>(type: ClassType<T>, option: UseStatedBeanOption<T> = {}): T`
#### `useInject<T>(type: ClassType<T>, option: UseStatedBeanOption<T> = {}): T`

The `useInject` will get the instance of the stated bean from the `StatedBeanContainer` in the context and listen for its data changes to trigger the re-rendering of the current component.

Expand Down Expand Up @@ -133,6 +137,27 @@ option = {
};
```

#### `useObserveEffect(bean: StatedBeanType, name: string | symbol): EffectAction`

observe the execution state of the method which with `@Effect`.

```tsx
@StatedBean
class UserModel {
@Effect()
fetchUser() {
// ...
}
}

const UserInfo = () => {
const model = useBean(() => new UserModel());
const { loading, error } = useObserveEffect(model, 'fetchUser');

return; //...;
};
```

### Provider

#### `<StatedBeanProvider {...props: StatedBeanProviderProps} />`
Expand Down
39 changes: 14 additions & 25 deletions example/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import '@abraham/reflection';
import { ReflectiveInjector } from 'injection-js';

import {
StatedBeanProvider,
StatedBeanApplication,
IBeanFactory,
EffectContext,
StatedInterceptor,
NextCaller,
useBean,
} from '../src';
import { ReflectiveInjector } from 'injection-js';

import { Counter } from './src/components/Counter';
import { TodoApp } from './src/components/Todo';
import { TodoModel } from './src/models/TodoModel';
import { TodoService } from './src/services/TodoService';

import {
EffectEvent,
IBeanFactory,
NextCaller,
StatedBeanApplication,
StatedBeanProvider,
useBean,
} from 'stated-bean';
import ReactDOM from 'react-dom';
import React from 'react';

Expand All @@ -29,22 +28,12 @@ const beanFactory: IBeanFactory = {
},
};

class LoggerInterceptor implements StatedInterceptor {
async stateInit(context: EffectContext, next: NextCaller) {
console.log('1. before init', context.toString());
await next();
console.log('1. after init', context.toString());
}

async stateChange(context: EffectContext, next: NextCaller) {
console.log('1. before change', context.toString());
await next();
console.log('1. after change', context.toString());
}
}

app.setBeanFactory(beanFactory);
app.setInterceptors(new LoggerInterceptor());
app.use(async (event: EffectEvent, next: NextCaller) => {
console.log('1. before change', event.type, event.name);
await next();
console.log('1. after change', event.type, event.name);
});

const App = () => {
const model = useBean(() => beanFactory.get(TodoModel));
Expand Down
6 changes: 5 additions & 1 deletion example/src/components/Todo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TodoModel } from '../../models/TodoModel';
import { Todo } from '../../services/TodoService';

import { useInject } from 'stated-bean';
import { useInject, useObserveEffect } from 'stated-bean';
import React from 'react';

function TodoList(props: { items: Todo[] }) {
Expand All @@ -16,10 +16,14 @@ function TodoList(props: { items: Todo[] }) {

export const TodoApp = () => {
const todo = useInject(TodoModel);
const { loading } = useObserveEffect(todo, 'fetchTodo');

console.log(loading);

return (
<div>
<h3>TODO</h3>
{loading && 'loading'}
<TodoList items={todo.todoList} />
<form
onSubmit={e => {
Expand Down
7 changes: 4 additions & 3 deletions example/src/models/CounterModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from 'injection-js';

import { StatedBean, Stated } from 'stated-bean';
import { StatedBean, Stated, Effect } from 'stated-bean';

@StatedBean()
@Injectable()
Expand All @@ -12,9 +12,10 @@ export class CounterModel {
this.count = count;
}

increment = () => {
@Effect('11')
increment() {
this.count++;
};
}

decrement = () => {
this.count--;
Expand Down
3 changes: 2 additions & 1 deletion example/src/models/TodoModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable, Inject } from 'injection-js';

import { TodoService, Todo } from '../services/TodoService';

import { StatedBean, Stated, PostProvided } from 'stated-bean';
import { StatedBean, Stated, PostProvided, Effect } from 'stated-bean';

@StatedBean()
@Injectable()
Expand All @@ -16,6 +16,7 @@ export class TodoModel {
constructor(@Inject(TodoService) private readonly todoService: TodoService) {}

@PostProvided()
@Effect('fetchTodo')
async fetchTodo() {
this.todoList = await this.todoService.fetchTodoList();
}
Expand Down
2 changes: 1 addition & 1 deletion example/src/services/TodoService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class TodoService {
state: 'todo',
},
]);
}, 1);
}, 1000);
});
}
}
7 changes: 2 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stated-bean",
"version": "0.2.0",
"version": "0.3.0-alpha",
"repository": "git@github.com:mjolnirjs/stated-bean.git",
"license": "MIT",
"main": "./lib/cjs/index.js",
Expand Down Expand Up @@ -42,16 +42,13 @@
"@babel/core": "^7.5.5",
"@commitlint/cli": "^8.1.0",
"@commitlint/config-conventional": "^8.1.0",
"@types/enzyme": "^3.10.3",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@testing-library/react-hooks": "^2.0.1",
"@types/jest": "^24.0.18",
"@types/node": "^12.7.3",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/react-test-renderer": "^16.9.0",
"cross-env": "^5.2.1",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"eslint": "^6.3.0",
"eslint-formatter-friendly": "^7.0.0",
"husky": "^3.0.5",
Expand Down
86 changes: 86 additions & 0 deletions src/core/AutoBind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
export function boundMethod(
target: Function,
key: string | number | symbol,
descriptor: PropertyDescriptor,
) {
let fn = descriptor.value;

if (typeof fn !== 'function') {
throw new TypeError(
`@boundMethod decorator can only be applied to methods not: ${typeof fn}`,
);
}

// In IE11 calling Object.defineProperty has a side-effect of evaluating the
// getter for the property which is being replaced. This causes infinite
// recursion and an "Out of stack space" error.
let definingProperty = false;

return {
configurable: true,
get() {
// eslint-disable-next-line no-prototype-builtins
if (
definingProperty ||
this === target.prototype ||
Object.hasOwnProperty.call(this, key) ||
typeof fn !== 'function'
) {
return fn;
}

const boundFn = fn.bind(this);
definingProperty = true;
Object.defineProperty(this, key, {
configurable: true,
get() {
return boundFn;
},
set(value) {
fn = value;
delete this[key];
},
});
definingProperty = false;
return boundFn;
},
set(value: unknown) {
fn = value;
},
};
}

export function boundClass(target: Function) {
// (Using reflect to get all keys including symbols)
let keys: Array<string | symbol | number>;
// Use Reflect if exists
if (typeof Reflect !== 'undefined' && typeof Reflect.ownKeys === 'function') {
keys = Reflect.ownKeys(target.prototype);
} else {
keys = Object.getOwnPropertyNames(target.prototype);
// Use symbols if support is provided
// eslint-disable-next-line @typescript-eslint/unbound-method
if (typeof Object.getOwnPropertySymbols === 'function') {
keys = keys.concat(Object.getOwnPropertySymbols(target.prototype));
}
}

keys.forEach(key => {
// Ignore special case target method
if (key === 'constructor') {
return;
}

const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key);

// Only methods need binding
if (descriptor !== undefined && typeof descriptor.value === 'function') {
Object.defineProperty(
target.prototype,
key,
boundMethod(target, key, descriptor),
);
}
});
return target;
}
54 changes: 0 additions & 54 deletions src/core/EffectContext.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/core/EffectEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export enum EffectEventType {
StateChanged = 'StateChanged',
EffectAction = 'EffectAction',
}

export class EffectEvent<Bean = unknown, Value = unknown> {
constructor(
readonly target: Bean,
readonly type: EffectEventType,
readonly name: string | symbol,
readonly value: Value,
) {
this.target = target;
this.type = type;
this.name = name;
this.value = value;
}
}
Loading

0 comments on commit 77c2c5d

Please sign in to comment.