Create wt-odlux directory
[ccsdk/features.git] / sdnr / wt-odlux / odlux / framework / src / flux / connect.tsx
1 /**
2  * ============LICENSE_START========================================================================
3  * ONAP : ccsdk feature sdnr wt odlux
4  * =================================================================================================
5  * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
6  * =================================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8  * in compliance with the License. You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software distributed under the License
13  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14  * or implied. See the License for the specific language governing permissions and limitations under
15  * the License.
16  * ============LICENSE_END==========================================================================
17  */
18 import React, { FC, useContext, createContext, useState, useEffect, useRef } from 'react';
19
20 import { Dispatch } from './store';
21
22 import { ApplicationStore, IApplicationStoreState } from '../store/applicationStore';
23
24 const LogLevel = +(localStorage.getItem('log.odlux.framework.flux.connect') || 0);
25
26 interface IApplicationStoreContext {
27   applicationStore: ApplicationStore;
28 }
29
30 export interface IDispatcher {
31   dispatch: Dispatch;
32 }
33
34 interface IApplicationStoreProps {
35   state: IApplicationStoreState;
36 }
37
38 interface IDispatchProps {
39   dispatch: Dispatch;
40 }
41
42 type ComponentDecoratorInfer<TMergedProps> = {
43   <TProps>(wrappedComponent: React.ComponentType<TProps & TMergedProps>): React.ComponentClass<Omit<TProps & TMergedProps, keyof TMergedProps>>;
44 };
45
46 const ApplicationStoreContext = createContext<IApplicationStoreContext | undefined>(undefined);
47
48 export type Connect<TMapProps extends ((...args: any) => any) | undefined = undefined, TMapDispatch extends ((...args: any) => any) | undefined = undefined> =
49   (TMapProps extends ((...args: any) => any) ? ReturnType<TMapProps> : IApplicationStoreProps) &
50   (TMapDispatch extends ((...args: any) => any) ? ReturnType<TMapDispatch> : IDispatchProps);
51
52 export function connect(): ComponentDecoratorInfer<IApplicationStoreProps & IDispatchProps>;
53
54 export function connect<TStateProps>(
55   mapStateToProps: (state: IApplicationStoreState) => TStateProps
56 ): ComponentDecoratorInfer<TStateProps & IDispatchProps>;
57
58 export function connect<TStateProps, TDispatchProps>(
59   mapStateToProps: (state: IApplicationStoreState) => TStateProps,
60   mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps
61 ): ComponentDecoratorInfer<TStateProps & TDispatchProps>;
62
63
64 export function connect<TDispatchProps>(
65   mapStateToProps: undefined,
66   mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps
67 ): ComponentDecoratorInfer<IApplicationStoreProps & TDispatchProps>;
68
69
70 export function connect<TProps, TStateProps, TDispatchProps>(
71   mapStateToProps?: ((state: IApplicationStoreState) => TStateProps),
72   mapDispatchToProps?: ((dispatcher: IDispatcher) => TDispatchProps)
73 ):
74   ((WrappedComponent: React.ComponentType<TProps & (IApplicationStoreProps | TStateProps) & IDispatchProps>) => React.ComponentType<TProps>) {
75
76   const injectApplicationStore = (WrappedComponent: React.ComponentType<TProps & (IApplicationStoreProps | TStateProps) & IDispatchProps>): React.ComponentType<TProps> => {
77
78     class StoreAdapter extends React.Component<TProps, {}> {
79       
80       render(): JSX.Element {
81
82         if (isWrappedComponentIsVersion1(WrappedComponent)) {
83           const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, dispatch: this.store.dispatch.bind(this.store) });
84           return element;
85         } else if (mapStateToProps && isWrappedComponentIsVersion2(WrappedComponent)) {
86           const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), dispatch: this.store.dispatch.bind(this.store) });
87           return element;
88         } else if (mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion3(WrappedComponent)) {
89           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) });
90           return element;
91         } else if (!mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion4(WrappedComponent)) {
92           const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store) }) as any) });
93           return element;
94         }
95         throw new Error("Invalid arguments in connect.");
96       }
97
98       componentDidMount(): void {
99         this.store && this.store.changed.addHandler(this.handleStoreChanged);
100       }
101
102       componentWillUnmount(): void {
103         this.store && this.store.changed.removeHandler(this.handleStoreChanged);
104       }
105
106       private get store(): ApplicationStore {
107         return this.context.applicationStore;
108       }
109
110       private handleStoreChanged = () => {
111         this.forceUpdate();
112       }
113     }
114     StoreAdapter.contextType = ApplicationStoreContext;
115     return StoreAdapter;
116   }
117
118
119   return injectApplicationStore;
120
121   /* inline methods */
122
123   function isWrappedComponentIsVersion1(wrappedComponent: any): wrappedComponent is React.ComponentType<TProps & IApplicationStoreProps & IDispatchProps> {
124     return !mapStateToProps && !mapDispatchToProps;
125   }
126
127   function isWrappedComponentIsVersion2(wrappedComponent: any): wrappedComponent is React.ComponentType<TProps & TStateProps & IDispatchProps> {
128     return !!mapStateToProps && !mapDispatchToProps;
129   }
130
131   function isWrappedComponentIsVersion3(wrappedComponent: any): wrappedComponent is React.ComponentType<TProps & TStateProps & TDispatchProps> {
132     return !!mapStateToProps && !!mapDispatchToProps;
133   }
134
135   function isWrappedComponentIsVersion4(wrappedComponent: any): wrappedComponent is React.ComponentType<TProps & TStateProps & TDispatchProps> {
136     return !mapStateToProps && !!mapDispatchToProps;
137   }
138 }
139
140 type ApplicationStoreProviderProps = {
141   applicationStore: ApplicationStore;
142 }
143
144 export const ApplicationStoreProvider: FC<ApplicationStoreProviderProps> = (props) => {
145   const { applicationStore, children } = props;
146
147   return (
148     <ApplicationStoreContext.Provider value={{ applicationStore }}>
149       {children}
150     </ApplicationStoreContext.Provider>
151   );
152 };
153
154 export const useApplicationStore = (): ApplicationStore => {
155   const context = useContext(ApplicationStoreContext);
156   if (context == null || context.applicationStore == null) {
157     throw new Error("Requires application store provider!")
158   }
159   return context.applicationStore
160 };
161
162 export const useSelectApplicationState = <TProp extends unknown >( selector: (state: IApplicationStoreState) => TProp, eqFunc = (a: TProp, b: TProp) => a === b ): TProp => {
163   const context = useContext(ApplicationStoreContext);
164   if (context == null || context.applicationStore == null) {
165     throw new Error("Requires application store provider!")
166   }
167   
168   const [propState, setPropState] = useState<TProp>(selector(context.applicationStore.state));
169   
170   const selectorRef = useRef(selector);
171   selectorRef.current = selector;
172
173   const propStateRef = useRef({propState});
174   propStateRef.current.propState = propState;
175
176   useEffect(() => {
177     if (context == null || context.applicationStore == null) {
178       throw new Error("Requires application store provider!")
179     }
180
181     const changedHandler = () => {
182       const newState = selectorRef.current(context.applicationStore.state);
183       if (!eqFunc(newState, propStateRef.current.propState)) {
184         setPropState(newState);
185       }
186     };
187
188     if (LogLevel > 3) {
189       console.log("useSelectApplicationState: adding handler", changedHandler);
190     }
191
192     context.applicationStore.changed.addHandler(changedHandler);
193
194     return () => {
195       if (LogLevel > 3) {
196         console.log("useSelectApplicationState: removing handler", changedHandler);
197       }
198
199       context.applicationStore.changed.removeHandler(changedHandler);
200     }
201   }, [context]);
202
203   return propState;
204   
205 };
206
207 export const useApplicationDispatch = (): Dispatch => {
208   const context = useContext(ApplicationStoreContext);
209   if (context == null || context.applicationStore == null) {
210     throw new Error("Requires application store provider!")
211   }
212   return context.applicationStore.dispatch;
213 };