diff options
Diffstat (limited to 'sdnr/wt/odlux/framework/src/flux')
-rw-r--r-- | sdnr/wt/odlux/framework/src/flux/action.ts | 26 | ||||
-rw-r--r-- | sdnr/wt/odlux/framework/src/flux/connect.ts | 161 | ||||
-rw-r--r-- | sdnr/wt/odlux/framework/src/flux/middleware.ts | 107 | ||||
-rw-r--r-- | sdnr/wt/odlux/framework/src/flux/store.ts | 98 |
4 files changed, 392 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/framework/src/flux/action.ts b/sdnr/wt/odlux/framework/src/flux/action.ts new file mode 100644 index 000000000..76890257e --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/action.ts @@ -0,0 +1,26 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +/** + * Represents an action in the odlux flux architecture. + */ +export abstract class Action { } + +export interface IActionHandler<TState, TAction extends Action = Action> { + (state: TState | undefined, action: TAction): TState; +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/flux/connect.ts b/sdnr/wt/odlux/framework/src/flux/connect.ts new file mode 100644 index 000000000..f54e4e0f0 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/connect.ts @@ -0,0 +1,161 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; + +import { Dispatch } from '../flux/store'; + +import { ApplicationStore, IApplicationStoreState } from '../store/applicationStore'; + +interface IApplicationStoreContext { + applicationStore: ApplicationStore; +} + +export interface IDispatcher { + dispatch: Dispatch; +} + +interface IApplicationStoreProps { + state: IApplicationStoreState; +} + +interface IDispatchProps { + dispatch: Dispatch; +} + +type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> + +type ComponentDecoratorInfer<TMergedProps> = { + <TProps>(wrappedComponent: React.ComponentType<TProps & TMergedProps>): React.ComponentClass<Omit<TProps & TMergedProps, keyof TMergedProps>>; +}; + +export type Connect<TMapProps extends ((...args: any) => any) | undefined = undefined, TMapDispatch extends ((...args: any) => any) | undefined = undefined> = + (TMapProps extends ((...args: any) => any) ? ReturnType<TMapProps> : IApplicationStoreProps) & + (TMapDispatch extends ((...args: any) => any) ? ReturnType<TMapDispatch> : IDispatchProps); + +export function connect(): ComponentDecoratorInfer<IApplicationStoreProps & IDispatchProps>; + +export function connect<TStateProps>( + mapStateToProps: (state: IApplicationStoreState) => TStateProps +): ComponentDecoratorInfer<TStateProps & IDispatchProps>; + +export function connect<TStateProps, TDispatchProps>( + mapStateToProps: (state: IApplicationStoreState) => TStateProps, + mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps +): ComponentDecoratorInfer<TStateProps & TDispatchProps>; + + +export function connect<TDispatchProps>( + mapStateToProps: undefined, + mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps +): ComponentDecoratorInfer<IApplicationStoreProps & TDispatchProps>; + + +export function connect<TProps, TStateProps, TDispatchProps>( + mapStateToProps?: ((state: IApplicationStoreState) => TStateProps), + mapDispatchToProps?: ((dispatcher: IDispatcher) => TDispatchProps) +): + ((WrappedComponent: React.ComponentType<TProps & (IApplicationStoreProps | TStateProps) & IDispatchProps>) => React.ComponentType<TProps>) { + + const injectApplicationStore = (WrappedComponent: React.ComponentType<TProps & (IApplicationStoreProps | TStateProps) & IDispatchProps>): React.ComponentType<TProps> => { + + class StoreAdapter extends React.Component<TProps, {}> { + public static contextTypes = { ...WrappedComponent.contextTypes, applicationStore: PropTypes.object.isRequired }; + context: IApplicationStoreContext; + + render(): JSX.Element { + + if (isWrappedComponentIsVersion1(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, dispatch: this.store.dispatch.bind(this.store) }); + return element; + } else if (mapStateToProps && isWrappedComponentIsVersion2(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), dispatch: this.store.dispatch.bind(this.store) }); + return element; + } else if (mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion3(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store) }) as any) }); + return element; + } else if (!mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion4(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store) }) as any) }); + return element; + } + throw new Error("Invalid arguments in connect."); + } + + componentDidMount(): void { + this.store && this.store.changed.addHandler(this.handleStoreChanged); + } + + componentWillUnmount(): void { + this.store && this.store.changed.removeHandler(this.handleStoreChanged); + } + + private get store(): ApplicationStore { + return this.context.applicationStore; + } + + private handleStoreChanged = () => { + this.forceUpdate(); + } + } + + return StoreAdapter; + } + + + return injectApplicationStore; + + /* inline methods */ + + function isWrappedComponentIsVersion1(wrappedComponent: any): wrappedComponent is React.ComponentType<TProps & IApplicationStoreProps & IDispatchProps> { + return !mapStateToProps && !mapDispatchToProps; + } + + function isWrappedComponentIsVersion2(wrappedComponent: any): wrappedComponent is React.ComponentType<TProps & TStateProps & IDispatchProps> { + return !!mapStateToProps && !mapDispatchToProps; + } + + function isWrappedComponentIsVersion3(wrappedComponent: any): wrappedComponent is React.ComponentType<TProps & TStateProps & TDispatchProps> { + return !!mapStateToProps && !!mapDispatchToProps; + } + + function isWrappedComponentIsVersion4(wrappedComponent: any): wrappedComponent is React.ComponentType<TProps & TStateProps & TDispatchProps> { + return !mapStateToProps && !!mapDispatchToProps; + } +} + +interface ApplicationStoreProviderProps extends React.Props<ApplicationStoreProvider> { + applicationStore: ApplicationStore; +} + +export class ApplicationStoreProvider extends React.Component<ApplicationStoreProviderProps> + implements /* React.ComponentLifecycle<ApplicationStoreProviderProps, any>, */ React.ChildContextProvider<IApplicationStoreContext> { + + public static childContextTypes = { applicationStore: PropTypes.object.isRequired }; + + getChildContext(): IApplicationStoreContext { + return { + applicationStore: this.props.applicationStore + }; + } + + render(): JSX.Element { + return React.Children.only(this.props.children) as any; //type error, fix when possible + } +} + +export default connect;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/flux/middleware.ts b/sdnr/wt/odlux/framework/src/flux/middleware.ts new file mode 100644 index 000000000..de6505c4f --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/middleware.ts @@ -0,0 +1,107 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import { Action, IActionHandler } from './action'; +import { Store, Dispatch, Enhancer } from './store'; + +export interface MiddlewareArg<T> { + dispatch: Dispatch; + getState: () => T; +} + +export interface Middleware<T> { + (obj: MiddlewareArg<T>): Function; +} + +class InitialisationAction extends Action { }; +const initialisationAction = new InitialisationAction(); + +export type ActionHandlerMapObject<S extends { [key: string]: any }, A extends Action = Action> = { + [K in keyof S]: IActionHandler<S[K], A> +} + +export const combineActionHandler = <TState extends { [key: string]: any }, TAction extends Action = Action>(actionHandlers: ActionHandlerMapObject<TState, TAction>) : IActionHandler<TState, TAction> => { + const finalActionHandlers = {} as { [key: string]: any }; // https://github.com/microsoft/TypeScript/issues/31808 + Object.keys(actionHandlers).forEach(actionHandlerKey => { + const handler = actionHandlers[actionHandlerKey]; + if (typeof handler === 'function') { + finalActionHandlers[actionHandlerKey] = handler; + } + }); + + // ensure initialisation + Object.keys(finalActionHandlers).forEach(key => { + const actionHandler = finalActionHandlers[key]; + const initialState = actionHandler(undefined, initialisationAction); + if (typeof initialState === 'undefined') { + const errorMessage = `Action handler ${ key } returned undefiend during initialization.`; + throw new Error(errorMessage); + } + }); + + return function combination<TAction extends Action>(state: TState = ({} as TState), action: TAction) { + let hasChanged = false; + const nextState = {} as { [key: string]: any }; // https://github.com/microsoft/TypeScript/issues/31808 + Object.keys(finalActionHandlers).forEach(key => { + const actionHandler = finalActionHandlers[key]; + const previousState = state[key]; + const nextStateKey = actionHandler(previousState, action); + if (typeof nextStateKey === 'undefined') { + const errorMessage = `Given ${ action.constructor } and action handler ${ key } returned undefiend.`; + throw new Error(errorMessage); + } + nextState[key] = nextStateKey; + hasChanged = hasChanged || nextStateKey !== previousState; + }); + return (hasChanged ? nextState : state) as TState; + }; +}; + +export const chainMiddleware = <TStoreState>(...middlewares: Middleware<TStoreState>[]): Enhancer<TStoreState> => { + return (store: Store<TStoreState>) => { + const middlewareAPI = { + getState() { return store.state }, + dispatch: <TAction extends Action>(action: TAction) => store.dispatch(action) // we want to use the combinded dispatch + // we should NOT use the flux dispatcher here, since the action would affect ALL stores + }; + const chain = middlewares.map(middleware => middleware(middlewareAPI)); + return compose(...chain)(store.dispatch) as Dispatch; + } +}; + +/** + * Composes single-argument functions from right to left. The rightmost + * function can take multiple arguments as it provides the signature for + * the resulting composite function. + * + * @param {...Function} funcs The functions to compose. + * @returns {Function} A function obtained by composing the argument functions + * from right to left. For example, compose(f, g, h) is identical to doing + * (...args) => f(g(h(...args))). + */ +const compose = (...funcs: Function[]) => { + if (funcs.length === 0) { + return (arg: any) => arg + } + + if (funcs.length === 1) { + return funcs[0] + } + + return funcs.reduce((a, b) => (...args: any[]) => a(b(...args))); +}; + diff --git a/sdnr/wt/odlux/framework/src/flux/store.ts b/sdnr/wt/odlux/framework/src/flux/store.ts new file mode 100644 index 000000000..dd80ce7ba --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/store.ts @@ -0,0 +1,98 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import { Event } from "../common/event" + +import { Action } from './action'; +import { IActionHandler } from './action'; + +export interface Dispatch { + <TAction extends Action>(action: TAction): TAction; +} + +export interface Enhancer<TStoreState> { + (store: Store<TStoreState>): Dispatch; +} + +class InitialisationAction extends Action { }; +const initialisationAction = new InitialisationAction(); + +export class Store<TStoreState> { + + constructor(actionHandler: IActionHandler<TStoreState>, enhancer?: Enhancer<TStoreState>) + constructor(actionHandler: IActionHandler<TStoreState>, initialState: TStoreState, enhancer?: Enhancer<TStoreState>) + constructor(actionHandler: IActionHandler<TStoreState>, initialState?: TStoreState | Enhancer<TStoreState>, enhancer?: Enhancer<TStoreState>) { + if (typeof initialState === 'function') { + enhancer = initialState as Enhancer<TStoreState>; + initialState = undefined; + } + + this._isDispatching = false; + + this.changed = new Event<void>(); // sollten wir hier eventuell sogar den state mit übergeben ? + + this._actionHandler = actionHandler; + + this._state = initialState as TStoreState; + if (enhancer) this._dispatch = enhancer(this); + + this._dispatch(initialisationAction); + } + + public changed: Event<void>; + + private _dispatch: Dispatch = <TAction extends Action>(payload: TAction): TAction => { + if (payload == null || !(payload instanceof Action)) { + throw new Error( + 'Actions must inherit from type Action. ' + + 'Use a custom middleware for async actions.' + ); + } + + if (this._isDispatching) { + throw new Error('ActionHandler may not dispatch actions.'); + } + + const oldState = this._state; + try { + this._isDispatching = true; + this._state = this._actionHandler(oldState, payload); + } finally { + this._isDispatching = false; + } + + if (this._state !== oldState) { + this.changed.invoke(); + } + + return payload; + } + + public get dispatch(): Dispatch { + return this._dispatch; + } + + public get state() { + return this._state + } + + private _state: TStoreState; + private _isDispatching: boolean; + private _actionHandler: IActionHandler<TStoreState>; + +} + |