core-api: add basic useRouteRef and RouteResolver without support for params

Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
Patrik Oldsberg
2020-11-27 14:47:28 +01:00
parent d8eebe10ac
commit 5596e8698e
2 changed files with 162 additions and 0 deletions
@@ -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 = (
<MemoryRouter initialEntries={['/foo/bar']}>
<Routes>
<Extension1 path="/foo">
<Extension2 path="/bar" />
</Extension1>
<Extension3 path="/baz" />
</Routes>
<MockRouteSource />
</MemoryRouter>
);
const { routes, routeParents } = traverseElementTree({
root,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routes: routeCollector,
routeParents: routeParentCollector,
},
});
render(
<RoutingProvider routes={routes} routeParents={routeParents}>
{root}
</RoutingProvider>,
);
expect.assertions(2);
});
});
+75
View File
@@ -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<RouteRef, string>,
private readonly routeParents: Map<RouteRef, RouteRef | undefined>,
) {}
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<RouteResolver | undefined>(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<RouteRef, string>;
routeParents: Map<RouteRef, RouteRef | undefined>;
children: ReactNode;
};
export const RoutingProvider = ({
routes,
routeParents,
children,
}: ProviderProps) => {
const resolver = new RouteResolver(routes, routeParents);
return (
<RoutingContext.Provider value={resolver}>
{children}
</RoutingContext.Provider>
);
};