@@ -12,6 +12,10 @@ import {
12
12
useEffect ,
13
13
useMemo ,
14
14
useState ,
15
+ Component ,
16
+ forwardRef ,
17
+ useImperativeHandle ,
18
+ useRef ,
15
19
} from 'react' ;
16
20
import {
17
21
ActiveUIView ,
@@ -23,16 +27,14 @@ import {
23
27
ViewConfig ,
24
28
ViewContext ,
25
29
applyPairs ,
30
+ UIRouter ,
26
31
} from '@uirouter/core' ;
27
32
import { useParentView } from '../hooks/useParentView' ;
28
33
import { useRouter } from '../hooks/useRouter' ;
29
- import { useTransitionHook } from '../hooks/useTransitionHook' ;
30
34
import { ReactViewConfig } from '../reactViews' ;
31
35
32
36
/** @internalapi */
33
- let id = 0 ;
34
- /** @hidden */
35
- let keyCounter = 0 ;
37
+ let viewIdCounter = 0 ;
36
38
37
39
/** @internalapi */
38
40
export interface UIViewAddress {
@@ -114,17 +116,22 @@ function useResolvesWithStringTokens(resolveContext: ResolveContext, injector: U
114
116
}
115
117
116
118
/* @hidden These are the props are passed to the routed component. */
117
- function useChildProps (
119
+ function useRoutedComponentProps (
120
+ router : UIRouter ,
121
+ stateName : string ,
122
+ viewConfig : ViewConfig ,
118
123
component : React . FunctionComponent < any > | React . ComponentClass < any > | React . ClassicComponentClass < any > ,
119
124
resolves : TypedMap < any > | { } ,
120
125
className : string ,
121
126
style : Object ,
122
- transition : any ,
123
- key : string ,
124
- setComponentInstance : ( instance : any ) => void
125
- ) : UIViewInjectedProps {
126
- return useMemo ( ( ) => {
127
- const componentProps : UIViewInjectedProps & { key : string } = {
127
+ transition : any
128
+ ) : UIViewInjectedProps & { key : string } {
129
+ const keyCounterRef = useRef ( 0 ) ;
130
+ // Always re-mount if the viewConfig changes
131
+ const key = useMemo ( ( ) => ( ++ keyCounterRef . current ) . toString ( ) , [ viewConfig ] ) ;
132
+
133
+ const baseChildProps = useMemo (
134
+ ( ) => ( {
128
135
// spread each string resolve as a separate prop
129
136
...resolves ,
130
137
// if a className prop was passed to the UIView, forward it
@@ -135,63 +142,99 @@ function useChildProps(
135
142
transition,
136
143
// this key updates whenever the state is reloaded, causing the component to remount
137
144
key,
138
- } ;
145
+ } ) ,
146
+ [ component , resolves , className , style , transition , key ]
147
+ ) ;
148
+
149
+ const maybeRefProp = useUiCanExitClassComponentHook ( router , stateName , component ) ;
150
+
151
+ return useMemo ( ( ) => ( { ...baseChildProps , ...maybeRefProp } ) , [ baseChildProps , maybeRefProp ] ) ;
152
+ }
139
153
140
- const maybeComponent = component as any ;
141
- if ( maybeComponent ?. prototype ?. render || ! ! maybeComponent ?. render ) {
142
- // for class components, add a ref to grab the component instance
143
- return { ...componentProps , ref : setComponentInstance } ;
154
+ function useViewConfig ( ) {
155
+ const [ viewConfig , setViewConfig ] = useState < ReactViewConfig > ( ) ;
156
+ const viewConfigRef = useRef ( viewConfig ) ;
157
+ viewConfigRef . current = viewConfig ;
158
+ const configUpdated = ( newConfig : ViewConfig ) => {
159
+ if ( newConfig !== viewConfigRef . current ) {
160
+ setViewConfig ( newConfig as ReactViewConfig ) ;
161
+ }
162
+ } ;
163
+ return { viewConfig, configUpdated } ;
164
+ }
165
+
166
+ function useReactHybridApi ( ref : React . Ref < unknown > , uiViewData : ActiveUIView , uiViewAddress : UIViewAddress ) {
167
+ const reactHybridApi = useRef ( { uiViewData, uiViewAddress } ) ;
168
+ reactHybridApi . current . uiViewData = uiViewData ;
169
+ reactHybridApi . current . uiViewAddress = uiViewAddress ;
170
+ useImperativeHandle ( ref , ( ) => reactHybridApi . current ) ;
171
+ }
172
+
173
+ // If a class component is being rendered, wire up its uiCanExit method
174
+ // Return a { ref: Ref<ClassComponentInstance> } if passed a component class
175
+ // Return an empty object {} if passed anything else
176
+ // The returned object should be spread as props onto the child component
177
+ function useUiCanExitClassComponentHook ( router : UIRouter , stateName : string , maybeComponentClass : any ) {
178
+ const ref = useRef < any > ( ) ;
179
+ const isComponentClass = maybeComponentClass ?. prototype ?. render || maybeComponentClass ?. render ;
180
+ const componentInstance = isComponentClass && ref . current ;
181
+ const uiCanExit = componentInstance ?. uiCanExit ;
182
+
183
+ useEffect ( ( ) => {
184
+ if ( uiCanExit ) {
185
+ const deregister = router . transitionService . onBefore ( { exiting : stateName } , uiCanExit . bind ( ref . current ) ) ;
186
+ return ( ) => deregister ( ) ;
144
187
} else {
145
- setComponentInstance ( undefined ) ;
146
- return componentProps ;
188
+ return ( ) => undefined ;
147
189
}
148
- } , [ component , resolves , className , style , transition , key ] ) ;
190
+ } , [ uiCanExit ] ) ;
191
+
192
+ return useMemo ( ( ) => ( isComponentClass ? { ref } : undefined ) , [ isComponentClass , ref ] ) ;
149
193
}
150
194
151
- export function UIView ( props : UIViewProps ) {
195
+ const View = forwardRef ( function View ( props : UIViewProps , forwardedRef ) {
152
196
const { children, render, className, style } = props ;
153
197
154
198
const router = useRouter ( ) ;
155
199
const parent = useParentView ( ) ;
200
+ const creationContext = parent . context ;
156
201
157
- // If a class component is being rendered, this is the component instance
158
- const [ componentInstance , setComponentInstance ] = useState ( ) ;
159
- const [ viewConfig , setViewConfig ] = useState < ReactViewConfig > ( ) ;
202
+ const { viewConfig, configUpdated } = useViewConfig ( ) ;
160
203
const component = useMemo ( ( ) => viewConfig ?. viewDecl ?. component , [ viewConfig ] ) ;
161
204
162
205
const name = props . name || '$default' ;
206
+ const fqn = parent . fqn ? parent . fqn + '.' + name : name ;
207
+ const id = useMemo ( ( ) => ++ viewIdCounter , [ ] ) ;
163
208
164
209
// This object contains all the metadata for this UIView
165
210
const uiViewData : ActiveUIView = useMemo ( ( ) => {
166
- return {
167
- $type : 'react' ,
168
- id : ++ id ,
169
- name,
170
- fqn : parent . fqn ? parent . fqn + '.' + name : name ,
171
- creationContext : parent . context ,
172
- configUpdated : config => setViewConfig ( config as ReactViewConfig ) ,
173
- config : viewConfig as ViewConfig ,
174
- } ;
175
- } , [ name , parent , viewConfig ] ) ;
176
-
177
- const viewContext : ViewContext = uiViewData ?. config ?. viewDecl ?. $context ;
178
- const uiViewAddress : UIViewAddress = { fqn : uiViewData . fqn , context : viewContext } ;
179
- const stateName : string = uiViewAddress ?. context ?. name ;
211
+ return { $type : 'react' , id, name, fqn, creationContext, configUpdated, config : viewConfig as ViewConfig } ;
212
+ } , [ id , name , fqn , parent , creationContext , viewConfig ] ) ;
213
+ const viewContext : ViewContext = viewConfig ?. viewDecl ?. $context ;
214
+ const stateName : string = viewContext ?. name ;
215
+ const uiViewAddress : UIViewAddress = { fqn, context : viewContext } ;
180
216
const resolveContext = useMemo ( ( ) => ( viewConfig ? new ResolveContext ( viewConfig . path ) : undefined ) , [ viewConfig ] ) ;
181
217
const injector = useMemo ( ( ) => resolveContext ?. injector ( ) , [ resolveContext ] ) ;
182
218
const transition = useMemo ( ( ) => injector ?. get ( Transition ) , [ injector ] ) ;
183
219
const resolves = useResolvesWithStringTokens ( resolveContext , injector ) ;
184
- const key = useMemo ( ( ) => ( ++ keyCounter ) . toString ( ) , [ viewConfig ] ) ;
185
- const childProps = useChildProps ( component , resolves , className , style , transition , key , setComponentInstance ) ;
220
+
221
+ const childProps = useRoutedComponentProps (
222
+ router ,
223
+ stateName ,
224
+ viewConfig ,
225
+ component ,
226
+ resolves ,
227
+ className ,
228
+ style ,
229
+ transition
230
+ ) ;
231
+
232
+ // temporarily expose a ref with an API on it for @uirouter /react-hybrid to use
233
+ useReactHybridApi ( forwardedRef , uiViewData , uiViewAddress ) ;
186
234
187
235
// Register/deregister any time the uiViewData changes
188
236
useEffect ( ( ) => router . viewService . registerUIView ( uiViewData ) , [ uiViewData ] ) ;
189
237
190
- // Handle component class with a 'uiCanExit()' method
191
- const canExitCallback = componentInstance ?. uiCanExit ;
192
- const hookMatchCriteria = canExitCallback ? { exiting : stateName } : undefined ;
193
- useTransitionHook ( 'onBefore' , hookMatchCriteria , canExitCallback ) ;
194
-
195
238
const childElement =
196
239
! component && isValidElement ( children )
197
240
? cloneElement ( children , childProps )
@@ -201,13 +244,22 @@ export function UIView(props: UIViewProps) {
201
244
const ChildOrRenderFunction =
202
245
typeof render !== 'undefined' && component ? render ( component , childProps ) : childElement ;
203
246
return < UIViewContext . Provider value = { uiViewAddress } > { ChildOrRenderFunction } </ UIViewContext . Provider > ;
204
- }
247
+ } ) ;
205
248
206
- UIView . displayName = 'UIView' ;
207
- UIView . __internalViewComponent = UIView ;
208
- UIView . propTypes = {
249
+ View . displayName = 'UIView' ;
250
+ View . propTypes = {
209
251
name : PropTypes . string ,
210
252
className : PropTypes . string ,
211
253
style : PropTypes . object ,
212
254
render : PropTypes . func ,
213
255
} as ValidationMap < UIViewProps > ;
256
+
257
+ // A wrapper class for react-hybrid to monkey patch
258
+ export class UIView extends Component < UIViewProps > {
259
+ static displayName = 'UIView' ;
260
+ static propTypes = View . propTypes ;
261
+ static __internalViewComponent : ComponentType < UIViewProps > = View ;
262
+ render ( ) {
263
+ return < View { ...this . props } /> ;
264
+ }
265
+ }
0 commit comments