-
Notifications
You must be signed in to change notification settings - Fork 283
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(elements): Introduce router, scaffold sign in components (#2426)
* feat(elements): Spike out routing * feat(elements): Move some files around, create initial package structure * chore(elements): Adjust example usage of the package * chore(elements): Add comments * chore(elements): Tweak error message to make it more accurate * chore(repo): Add changeset
- Loading branch information
Showing
7 changed files
with
218 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
--- | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { SignIn, SignInFactorOne, SignInFactorTwo, SignInSSOCallback, SignInStart } from '@clerk/elements' | ||
|
||
export default function SignInPage() { | ||
return ( | ||
<SignIn> | ||
<SignInStart> | ||
Start child | ||
</SignInStart> | ||
<SignInFactorOne> | ||
Factor one child | ||
</SignInFactorOne> | ||
<SignInFactorTwo> | ||
Factor two child | ||
</SignInFactorTwo> | ||
<SignInSSOCallback /> | ||
</SignIn> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
export function HelloWorld(): JSX.Element { | ||
return <p>Hello World!</p>; | ||
} | ||
'use client'; | ||
import { useNextRouter } from './internals/router'; | ||
|
||
export * from './sign-in'; | ||
|
||
export { useNextRouter }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
'use client'; | ||
import { createContext, useContext } from 'react'; | ||
|
||
import type { ClerkHostRouter, ClerkRouter } from './router'; | ||
import { createClerkRouter } from './router'; | ||
|
||
export const ClerkRouterContext = createContext<ClerkRouter | null>(null); | ||
|
||
function useClerkRouter() { | ||
return useContext(ClerkRouterContext); | ||
} | ||
|
||
export function Router({ | ||
children, | ||
router, | ||
basePath, | ||
}: { | ||
router: ClerkHostRouter; | ||
children: React.ReactNode; | ||
basePath?: string; | ||
}) { | ||
const clerkRouter = createClerkRouter(router, basePath); | ||
|
||
return <ClerkRouterContext.Provider value={clerkRouter}>{children}</ClerkRouterContext.Provider>; | ||
} | ||
|
||
type RouteProps = { path?: string; index?: boolean }; | ||
|
||
export function Route({ path, children, index }: RouteProps & { children: React.ReactNode }) { | ||
// check for parent router, if exists, create child router, otherwise create one | ||
const parentRouter = useClerkRouter(); | ||
|
||
if (!path && !index) { | ||
return children; | ||
} | ||
|
||
if (!parentRouter?.match(path, index)) { | ||
return null; | ||
} | ||
|
||
return children; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; | ||
|
||
/** | ||
* This type represents a generic router interface that Clerk relies on to interact with the host router. | ||
*/ | ||
export type ClerkHostRouter = { | ||
push: (path: string) => void; | ||
replace: (path: string) => void; | ||
pathname: () => string; | ||
searchParams: () => URLSearchParams; | ||
}; | ||
|
||
/** | ||
* Internal Clerk router, used by Clerk components to interact with the host's router. | ||
*/ | ||
export type ClerkRouter = { | ||
/** | ||
* Creates a child router instance scoped to the provided base path. | ||
*/ | ||
child: (childBasePath: string) => ClerkRouter; | ||
/** | ||
* Matches the provided path against the router's current path. If index is provided, matches against the root route of the router. | ||
*/ | ||
match: (path?: string, index?: boolean) => boolean; | ||
/** | ||
* Navigates to the provided path via a history push | ||
*/ | ||
push: ClerkHostRouter['push']; | ||
/** | ||
* Navigates to the provided path via a history replace | ||
*/ | ||
replace: ClerkHostRouter['replace']; | ||
/** | ||
* Returns the current pathname (including the base path) | ||
*/ | ||
pathname: ClerkHostRouter['pathname']; | ||
/** | ||
* Returns the current search params | ||
*/ | ||
searchParams: ClerkHostRouter['searchParams']; | ||
}; | ||
|
||
/** | ||
* Ensures the provided path has a leading slash and no trailing slash | ||
*/ | ||
function normalizePath(path: string) { | ||
const pathNoTrailingSlash = path.replace(/\/$/, ''); | ||
return pathNoTrailingSlash.startsWith('/') ? pathNoTrailingSlash : `/${pathNoTrailingSlash}`; | ||
} | ||
|
||
/** | ||
* Factory function to create an instance of ClerkRouter with the provided host router. | ||
* | ||
* @param router host router instance to be used by the router | ||
* @param basePath base path of the router, navigation and matching will be scoped to this path | ||
* @returns A ClerkRouter instance | ||
*/ | ||
export function createClerkRouter(router: ClerkHostRouter, basePath: string = '/'): ClerkRouter { | ||
const normalizedBasePath = normalizePath(basePath); | ||
|
||
function match(path?: string, index?: boolean) { | ||
const pathToMatch = path ?? (index && '/'); | ||
|
||
if (!pathToMatch) { | ||
throw new Error('[clerk] router.match() requires either a path to match, or the index flag must be set to true.'); | ||
} | ||
|
||
const normalizedPath = normalizePath(pathToMatch); | ||
|
||
return normalizePath(`${normalizedBasePath}${normalizedPath}`) === normalizePath(router.pathname()); | ||
} | ||
|
||
function child(childBasePath: string) { | ||
return createClerkRouter(router, `${normalizedBasePath}/${normalizePath(childBasePath)}`); | ||
} | ||
|
||
return { | ||
child, | ||
match, | ||
push: router.push, | ||
replace: router.replace, | ||
pathname: router.pathname, | ||
searchParams: router.searchParams, | ||
}; | ||
} | ||
|
||
/** | ||
* Framework specific router integrations | ||
*/ | ||
|
||
export const useNextRouter = (): ClerkHostRouter => { | ||
const router = useRouter(); | ||
const pathname = usePathname(); | ||
const searchParams = useSearchParams(); | ||
|
||
return { | ||
push: (path: string) => router.push(path), | ||
replace: (path: string) => router.replace(path), | ||
pathname: () => pathname, | ||
searchParams: () => searchParams, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
'use client'; | ||
import { type ClerkHostRouter, useNextRouter } from '../internals/router'; | ||
import { Route, Router } from '../internals/router-react'; | ||
|
||
export function SignIn({ children }: { router?: ClerkHostRouter; children: React.ReactNode }): JSX.Element { | ||
// TODO: eventually we'll rely on the framework SDK to specify its host router, but for now we'll default to Next.js | ||
const router = useNextRouter(); | ||
|
||
return ( | ||
<Router | ||
router={router} | ||
basePath='/sign-in' | ||
> | ||
{children} | ||
</Router> | ||
); | ||
} | ||
|
||
export function SignInStart({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<Route index> | ||
Start | ||
{children} | ||
</Route> | ||
); | ||
} | ||
|
||
export function SignInFactorOne({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<Route path='factor-one'> | ||
Factor One | ||
{children} | ||
</Route> | ||
); | ||
} | ||
|
||
export function SignInFactorTwo({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<Route path='factor-two'> | ||
Factor Two | ||
{children} | ||
</Route> | ||
); | ||
} | ||
|
||
export function SignInSSOCallback() { | ||
return <Route path='sso-callback'>SSOCallback</Route>; | ||
} |