@@ -12,6 +12,10 @@ import {
1212 useEffect ,
1313 useMemo ,
1414 useState ,
15+ Component ,
16+ forwardRef ,
17+ useImperativeHandle ,
18+ useRef ,
1519} from 'react' ;
1620import {
1721 ActiveUIView ,
@@ -23,16 +27,14 @@ import {
2327 ViewConfig ,
2428 ViewContext ,
2529 applyPairs ,
30+ UIRouter ,
2631} from '@uirouter/core' ;
2732import { useParentView } from '../hooks/useParentView' ;
2833import { useRouter } from '../hooks/useRouter' ;
29- import { useTransitionHook } from '../hooks/useTransitionHook' ;
3034import { ReactViewConfig } from '../reactViews' ;
3135
3236/** @internalapi */
33- let id = 0 ;
34- /** @hidden */
35- let keyCounter = 0 ;
37+ let viewIdCounter = 0 ;
3638
3739/** @internalapi */
3840export interface UIViewAddress {
@@ -114,17 +116,22 @@ function useResolvesWithStringTokens(resolveContext: ResolveContext, injector: U
114116}
115117
116118/* @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 ,
118123 component : React . FunctionComponent < any > | React . ComponentClass < any > | React . ClassicComponentClass < any > ,
119124 resolves : TypedMap < any > | { } ,
120125 className : string ,
121126 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+ ( ) => ( {
128135 // spread each string resolve as a separate prop
129136 ...resolves ,
130137 // if a className prop was passed to the UIView, forward it
@@ -135,63 +142,99 @@ function useChildProps(
135142 transition,
136143 // this key updates whenever the state is reloaded, causing the component to remount
137144 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+ }
139153
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 ( ) ;
144187 } else {
145- setComponentInstance ( undefined ) ;
146- return componentProps ;
188+ return ( ) => undefined ;
147189 }
148- } , [ component , resolves , className , style , transition , key ] ) ;
190+ } , [ uiCanExit ] ) ;
191+
192+ return useMemo ( ( ) => ( isComponentClass ? { ref } : undefined ) , [ isComponentClass , ref ] ) ;
149193}
150194
151- export function UIView ( props : UIViewProps ) {
195+ const View = forwardRef ( function View ( props : UIViewProps , forwardedRef ) {
152196 const { children, render, className, style } = props ;
153197
154198 const router = useRouter ( ) ;
155199 const parent = useParentView ( ) ;
200+ const creationContext = parent . context ;
156201
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 ( ) ;
160203 const component = useMemo ( ( ) => viewConfig ?. viewDecl ?. component , [ viewConfig ] ) ;
161204
162205 const name = props . name || '$default' ;
206+ const fqn = parent . fqn ? parent . fqn + '.' + name : name ;
207+ const id = useMemo ( ( ) => ++ viewIdCounter , [ ] ) ;
163208
164209 // This object contains all the metadata for this UIView
165210 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 } ;
180216 const resolveContext = useMemo ( ( ) => ( viewConfig ? new ResolveContext ( viewConfig . path ) : undefined ) , [ viewConfig ] ) ;
181217 const injector = useMemo ( ( ) => resolveContext ?. injector ( ) , [ resolveContext ] ) ;
182218 const transition = useMemo ( ( ) => injector ?. get ( Transition ) , [ injector ] ) ;
183219 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 ) ;
186234
187235 // Register/deregister any time the uiViewData changes
188236 useEffect ( ( ) => router . viewService . registerUIView ( uiViewData ) , [ uiViewData ] ) ;
189237
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-
195238 const childElement =
196239 ! component && isValidElement ( children )
197240 ? cloneElement ( children , childProps )
@@ -201,13 +244,22 @@ export function UIView(props: UIViewProps) {
201244 const ChildOrRenderFunction =
202245 typeof render !== 'undefined' && component ? render ( component , childProps ) : childElement ;
203246 return < UIViewContext . Provider value = { uiViewAddress } > { ChildOrRenderFunction } </ UIViewContext . Provider > ;
204- }
247+ } ) ;
205248
206- UIView . displayName = 'UIView' ;
207- UIView . __internalViewComponent = UIView ;
208- UIView . propTypes = {
249+ View . displayName = 'UIView' ;
250+ View . propTypes = {
209251 name : PropTypes . string ,
210252 className : PropTypes . string ,
211253 style : PropTypes . object ,
212254 render : PropTypes . func ,
213255} 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