From 5596e8698e64ed59dbe0237a4e0b41c67e6f524d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Nov 2020 14:47:28 +0100 Subject: [PATCH] core-api: add basic useRouteRef and RouteResolver without support for params Co-authored-by: blam --- packages/core-api/src/routing/hooks.test.tsx | 87 ++++++++++++++++++++ packages/core-api/src/routing/hooks.tsx | 75 +++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 packages/core-api/src/routing/hooks.test.tsx create mode 100644 packages/core-api/src/routing/hooks.tsx diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx new file mode 100644 index 0000000000..c08891c8cc --- /dev/null +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +import { render } from '@testing-library/react'; +import React, { PropsWithChildren } from 'react'; +import { MemoryRouter, Routes } from 'react-router-dom'; +import { createRoutableExtension } from '../extensions'; +import { + childDiscoverer, + routeElementDiscoverer, + traverseElementTree, +} from '../extensions/traversal'; +import { createPlugin } from '../plugin'; +import { routeCollector, routeParentCollector } from './collectors'; +import { useRouteRef, RoutingProvider } from './hooks'; +import { createRouteRef } from './RouteRef'; + +const mockConfig = () => ({ path: '/unused', title: 'Unused' }); +const MockComponent = ({ children }: PropsWithChildren<{}>) => <>{children}; + +const plugin = createPlugin({ id: 'my-plugin' }); + +const ref1 = createRouteRef(mockConfig()); +const ref2 = createRouteRef(mockConfig()); +const ref3 = createRouteRef(mockConfig()); + +const MockRouteSource = () => { + const routeFunc = useRouteRef(ref2); + expect(routeFunc?.()).toBe('/foo/bar'); + return null; +}; + +const Extension1 = plugin.provide( + createRoutableExtension({ component: MockComponent, mountPoint: ref1 }), +); +const Extension2 = plugin.provide( + createRoutableExtension({ component: MockRouteSource, mountPoint: ref2 }), +); +const Extension3 = plugin.provide( + createRoutableExtension({ component: MockComponent, mountPoint: ref3 }), +); + +describe('discovery', () => { + it('should handle all react router Route patterns', () => { + const root = ( + + + + + + + + + + ); + + const { routes, routeParents } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routes: routeCollector, + routeParents: routeParentCollector, + }, + }); + + render( + + {root} + , + ); + + expect.assertions(2); + }); +}); diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx new file mode 100644 index 0000000000..8a2aa72c92 --- /dev/null +++ b/packages/core-api/src/routing/hooks.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +import React, { createContext, ReactNode, useContext } from 'react'; +import { RouteRef } from './types'; + +export type RouteFunc = () => string; + +class RouteResolver { + constructor( + private readonly routes: Map, + private readonly routeParents: Map, + ) {} + + resolve(routeRef: RouteRef): RouteFunc { + let currentRouteRef: RouteRef | undefined = routeRef; + let fullPath = ''; + + while (currentRouteRef) { + const path = this.routes.get(currentRouteRef); + if (!path) { + throw new Error(`No path for ${currentRouteRef}`); + } + fullPath = `${path}${fullPath}`; + currentRouteRef = this.routeParents.get(currentRouteRef); + } + + return () => { + return fullPath; + }; + } +} + +const RoutingContext = createContext(undefined); + +export function useRouteRef(routeRef: RouteRef): RouteFunc | undefined { + const resolver = useContext(RoutingContext); + if (!resolver) { + throw new Error('No route resolver found in context'); + } + + return resolver.resolve(routeRef); +} + +type ProviderProps = { + routes: Map; + routeParents: Map; + children: ReactNode; +}; + +export const RoutingProvider = ({ + routes, + routeParents, + children, +}: ProviderProps) => { + const resolver = new RouteResolver(routes, routeParents); + return ( + + {children} + + ); +};