Skip to content

Commit

Permalink
feat(core): new api useBean & useInject (#45)
Browse files Browse the repository at this point in the history
* feat(core): new api useBean & useInject

* chore(npm): version 0.2.0-alpha

* fix: type coverage

* docs: code comment

* test: update the test snapshot
  • Loading branch information
foreleven authored and JounQin committed Sep 1, 2019
1 parent efcc4af commit 3a1ce82
Show file tree
Hide file tree
Showing 29 changed files with 384 additions and 236 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,11 @@ function CounterDisplay() {

#### `@StatedBean(name?: string | symbol): ClassDecorator`

Indicates that an annotated class is a `StatedBean`. The `name` may indicate a suggestion for the bean name. Its default value is `Class.name`
MethodDecorator
Indicates that an annotated class is a `StatedBean`. The `name` may indicate a suggestion for the bean name. Its default value is `Class.name`.

#### `@Stated(): PropertyDecorator`

Indicates that an annotated property is `Stated`. Its reassignment will be observed and will be notified to update with components of `useStatedBean(Model)`.
Indicates that an annotated property is `Stated`. Its reassignment will be observed and notified to the container.

#### `@PostProvided(): MethodDecorator`

Expand Down
20 changes: 4 additions & 16 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
EffectContext,
StatedInterceptor,
NextCaller,
useBean,
} from '../src';

import { Counter } from './src/components/Counter';
Expand Down Expand Up @@ -42,26 +43,13 @@ class LoggerInterceptor implements StatedInterceptor {
}
}

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

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

app.setBeanFactory(beanFactory);
app.setInterceptors(new LoggerInterceptor(), new LoggerInterceptor2());
app.setInterceptors(new LoggerInterceptor());

const App = () => {
const model = useBean(() => beanFactory.get(TodoModel));
return (
<StatedBeanProvider application={app} types={[TodoModel]}>
<StatedBeanProvider application={app} beans={[model]}>
<Counter />
<hr />
<Counter />
Expand Down
4 changes: 2 additions & 2 deletions example/src/components/Counter/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { CounterModel } from '../../models/CounterModel';

import { useStatedBean } from 'stated-bean';
import { useBean } from 'stated-bean';
import React from 'react';

export function Counter() {
const counter = useStatedBean(() => new CounterModel(10));
const counter = useBean(() => new CounterModel(10));

return (
<div>
Expand Down
4 changes: 2 additions & 2 deletions 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 { useStatedBean } from 'stated-bean';
import { useInject } from 'stated-bean';
import React from 'react';

function TodoList(props: { items: Todo[] }) {
Expand All @@ -15,7 +15,7 @@ function TodoList(props: { items: Todo[] }) {
}

export const TodoApp = () => {
const todo = useStatedBean(TodoModel);
const todo = useInject(TodoModel);

return (
<div>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "stated-bean",
"version": "0.1.5-beta",
"version": "0.2.0-alpha",
"repository": "git@github.com:mjolnirjs/stated-bean.git",
"license": "MIT",
"main": "dist/cjs",
Expand Down
6 changes: 4 additions & 2 deletions src/context/StatedBeanProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import { StatedBeanApplication } from '../core';
import { useContainer } from '../hooks';
import { ClassType, BeanProvider } from '../types';
import { BeanProvider, ClassType, StatedBeanType } from '../types';

import { getStatedBeanContext } from './StatedBeanContext';

import React from 'react';

export interface StatedBeanProviderProps {
types?: ClassType[];
beans?: Array<StatedBeanType<unknown>>;
beanProvider?: BeanProvider;
application?: StatedBeanApplication;
children: React.ReactNode | React.ReactNode[] | null;
}

export const StatedBeanProvider: React.FC<StatedBeanProviderProps> = ({
types,
beans,
beanProvider,
application,
children,
}) => {
// TODO: update container
const StatedBeanContext = getStatedBeanContext();
const container = useContainer({ types, beanProvider, application });
const container = useContainer({ types, beans, beanProvider, application });

return (
<StatedBeanContext.Provider value={{ container }}>
Expand Down
5 changes: 5 additions & 0 deletions src/core/StatedBeanApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { ClassType } from '../types';
import { IBeanFactory, DefaultBeanFactory } from './StatedBeanFactory';
import { EffectContext } from './EffectContext';

/**
*
* @export
* @class StatedBeanApplication
*/
export class StatedBeanApplication {
private _beanFactory: IBeanFactory = new DefaultBeanFactory();

Expand Down
70 changes: 61 additions & 9 deletions src/core/StatedBeanContainer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { Event } from '../event';
import { getMetadataStorage } from '../metadata';
import {
BeanRegisterOption,
ClassType,
StatedBeanMeta,
StatedBeanType,
StatedFieldMeta,
BeanRegisterOption,
} from '../types';
import { isFunction } from '../utils';

import { EffectContext } from './EffectContext';
import { ForceUpdate } from './ForceUpdate';
import { NoSuchBeanDefinitionError } from './NoSuchBeanDefinitionError';
import { StatedBeanApplication } from './StatedBeanApplication';
import { StatedBeanRegistry } from './StatedBeanRegistry';

import { ForceUpdate, StatedBeanSymbol } from './Symbols';

/**
* `StatedBeanContainer` is responsible for registering and managing `bean` and observing its `@Stated()` property changes.
*
* @internal
* @export
* @class StatedBeanContainer
* @extends {Event}
*/
export class StatedBeanContainer extends Event {
private readonly _parent?: StatedBeanContainer;

Expand All @@ -33,14 +43,44 @@ export class StatedBeanContainer extends Event {
}
}

getBean<T>(type: ClassType<T>, name?: string): T | undefined {
let bean = this.registry.getBean(type, name);
destroy() {
console.info('container destroyed.');
}

getBeanIdentity<T>(type: ClassType<T>, name?: string | symbol) {
return name || this.getBeanMetaName(type) || type.name;
}

getBeanMetaName<T>(type: ClassType<T>): string | symbol | undefined {
const beanMeta = this.getBeanMeta(type);

return beanMeta === undefined ? undefined : beanMeta.name;
}

getBeanMeta<T>(type: ClassType<T>): StatedBeanMeta | undefined {
const storage = getMetadataStorage();
return storage.getBeanMeta(type);
}

getBean<T>(
type: ClassType<T>,
name?: string | symbol,
): StatedBeanType<T> | undefined {
const beanIdentity = this.getBeanIdentity(type, name);
let bean = this.registry.getBean(type, beanIdentity);

if (bean == null && this.parent) {
bean = this.parent.getBean(type, name);
}

return bean as T;
return bean as StatedBeanType<T>;
}

addBean<T>(bean: StatedBeanType<T>) {
const { name, container } = bean[StatedBeanSymbol];
// TODO: if need off the listener.
container.on(bean, e => this.emit(bean, e));
this.registry.register(bean.constructor, bean, name);
}

hasBean<T>(type: ClassType<T>, name?: string): boolean {
Expand All @@ -64,7 +104,8 @@ export class StatedBeanContainer extends Event {
beanOrSupplier: T | (() => T),
options: BeanRegisterOption = {},
): Promise<void> {
if (this.registry.getBean(type, options.name) !== undefined) {
const identity = this.getBeanIdentity(type, options.name);
if (this.registry.getBean(type, identity) !== undefined) {
return;
}

Expand All @@ -80,14 +121,15 @@ export class StatedBeanContainer extends Event {
throw new Error(`bean ${bean} mast be an instance of ${type.name}`);
}

const beanMeta = this.registry.getBeanMeta(type);
const beanMeta = this.getBeanMeta(type);

if (beanMeta === undefined) {
throw new NoSuchBeanDefinitionError(type.name);
}

this.registry.register(type, bean, options.name);
this.registry.register(type, bean, identity);

this._defineStatedBean(bean, this.getBeanIdentity(type, options.name));
this._defineForceUpdate(bean, beanMeta);

const fields = beanMeta.statedFields || [];
Expand All @@ -105,6 +147,16 @@ export class StatedBeanContainer extends Event {
}
}

// @internal
private _defineStatedBean<T>(bean: T, name: string | symbol) {
Object.defineProperty(bean, StatedBeanSymbol, {
value: {
name,
container: this,
},
});
}

// @internal
private _defineForceUpdate<T>(bean: T, beanMeta: StatedBeanMeta) {
const self = this;
Expand Down
9 changes: 9 additions & 0 deletions src/core/StatedBeanFactory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { ClassType } from '../types';

/**
* BeanFactory interface
*
* @export
* @interface IBeanFactory
*/
export interface IBeanFactory {
get<T>(Type: ClassType<T>): T | undefined;
}

/**
* the default `BeanFactory` by the class `new` constructor.
*/
export class DefaultBeanFactory implements IBeanFactory {
get<T>(Type: ClassType<T>): T | undefined {
return new Type();
Expand Down
36 changes: 13 additions & 23 deletions src/core/StatedBeanRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { getMetadataStorage } from '../metadata';
import { ClassType, StatedBeanMeta } from '../types';

import { ClassType } from '../types';

/**
* The named and types bean storage with `Map<string | symbol, WeakMap<ClassType, unknown>>`.
*
* @export
* @class StatedBeanRegistry
*/
export class StatedBeanRegistry {
// @internal
private readonly beans = new Map<
string | symbol,
WeakMap<ClassType, unknown>
>();

getBean<T>(type: ClassType<T>, name?: string | symbol): T | undefined {
const beanName = name || this.getBeanMetaName(type) || type.name;
const typedBeans = this.beans.get(beanName);
getBean<T>(type: ClassType<T>, identity: string | symbol): T | undefined {
const typedBeans = this.beans.get(identity);

if (typedBeans === undefined) {
return undefined;
Expand All @@ -19,26 +23,12 @@ export class StatedBeanRegistry {
}
}

register<T>(type: ClassType<T>, bean: T, name?: string | symbol) {
const beanName = name || this.getBeanMetaName(type) || type.name;

console.log('register bean', beanName, type.name);
const typedBeans = this.beans.get(beanName);
register<T>(type: ClassType<T>, bean: T, identity: string | symbol) {
const typedBeans = this.beans.get(identity);
if (typedBeans === undefined) {
this.beans.set(beanName, new WeakMap().set(type, bean));
this.beans.set(identity, new WeakMap().set(type, bean));
} else {
typedBeans.set(type, bean);
}
}

getBeanMetaName<T>(type: ClassType<T>): string | symbol | undefined {
const beanMeta = this.getBeanMeta(type);

return beanMeta === undefined ? undefined : beanMeta.name;
}

getBeanMeta<T>(type: ClassType<T>): StatedBeanMeta | undefined {
const storage = getMetadataStorage();
return storage.getBeanMeta(type);
}
}
2 changes: 2 additions & 0 deletions src/core/ForceUpdate.ts → src/core/Symbols.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const StatedBeanSymbol = Symbol('stated-bean');

export const ForceUpdate = Symbol('stated-bean-force-update');
4 changes: 1 addition & 3 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ export * from './EffectContext';
export * from './StatedBeanApplication';
export * from './StatedBeanContainer';
export * from './StatedBeanFactory';
export * from './ForceUpdate';

export const StatedBeanSymbol = Symbol('stated-bean');
export * from './Symbols';
38 changes: 23 additions & 15 deletions src/decorator/PostProvided.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { getMetadataStorage } from '../metadata';

export const PostProvided = (): MethodDecorator => (
prototype,
propertyKey,
descriptor?: TypedPropertyDescriptor<any>,
) => {
if (descriptor === undefined) {
descriptor = Object.getOwnPropertyDescriptor(prototype, propertyKey)!;
}
getMetadataStorage().collectPostProvided({
name: propertyKey,
target: prototype.constructor,
descriptor,
});
return descriptor;
};
/**
* The `PostProvided` decorator is used on a method that needs to be executed after the StatedBean be instanced to perform any initialization.
*
* @export
* @returns {MethodDecorator}
*/
export function PostProvided(): MethodDecorator {
return (
prototype,
propertyKey,
descriptor?: TypedPropertyDescriptor<any>,
) => {
if (descriptor === undefined) {
descriptor = Object.getOwnPropertyDescriptor(prototype, propertyKey)!;
}
getMetadataStorage().collectPostProvided({
name: propertyKey,
target: prototype.constructor,
descriptor,
});
return descriptor;
};
}
Loading

0 comments on commit 3a1ce82

Please sign in to comment.