Skip to content

Commit

Permalink
feat(elements): Add <Link /> component (#4456)
Browse files Browse the repository at this point in the history
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
  • Loading branch information
alexcarpenter and LauraBeatris authored Nov 4, 2024
1 parent 0800fc3 commit 1a0c8fe
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .changeset/short-mails-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@clerk/elements': patch
---

Add Elements `<Link />` component.

```tsx
import * as Clerk from '@clerk/elements/common';
import NextLink from 'next/link';

function SignInPage() {
return (
<>
<Clerk.Link navigate='sign-up'>Sign up</Clerk.Link>

<Clerk.Link navigate='sign-up'>{url => <NextLink href={url}>Sign up</NextLink>}</Clerk.Link>
</>
);
}
```
8 changes: 8 additions & 0 deletions .changeset/weak-hornets-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
'@clerk/types': patch
---

Expose internal `__internal_getOption` method from Clerk.
4 changes: 4 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ export class Clerk implements ClerkInterface {
return this.#options.standardBrowser || false;
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] {
return this.#options[key];
}

public constructor(key: string, options?: DomainOrProxyUrl) {
key = (key || '').trim();

Expand Down
1 change: 1 addition & 0 deletions packages/elements/src/react/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'client-only';
export { Field, FieldError, FieldState, GlobalError, Input, Label, Submit } from '~/react/common/form';
export { Connection, Icon } from '~/react/common/connections';
export { Loading } from '~/react/common/loading';
export { Link } from '~/react/common/link';

export type {
FormFieldErrorProps,
Expand Down
60 changes: 60 additions & 0 deletions packages/elements/src/react/common/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useClerk } from '@clerk/shared/react';
import { useClerkRouter } from '@clerk/shared/router';
import type { ClerkOptions } from '@clerk/types';
import React from 'react';

type Destination = 'sign-in' | 'sign-up';
export interface LinkProps extends Omit<React.HTMLAttributes<HTMLAnchorElement>, 'children'> {
navigate: Destination;
children: React.ReactNode | ((props: { url: string }) => React.ReactNode);
}

const paths: Record<Destination, keyof Pick<ClerkOptions, 'signInUrl' | 'signUpUrl'>> = {
'sign-in': 'signInUrl',
'sign-up': 'signUpUrl',
};

/**
* The `<Link>` component is used to navigate between sign-in and sign-up flows.
*
* @param {Destination} navigate - The destination to navigate to.
*
* @example
* ```tsx
* <Link navigate="sign-in">Sign in</Link>
* ```
* @example
* ```tsx
* <Link navigate="sign-in">
* {({ url }) => (
* <NextLink href={url}>Sign in</NextLink>
* )}
* </Link>
*/

export function Link({ navigate, children, ...rest }: LinkProps) {
const router = useClerkRouter();
const clerk = useClerk();
const destinationUrl = router.makeDestinationUrlWithPreservedQueryParameters(
clerk.__internal_getOption(paths[navigate])!,
);

if (typeof children === 'function') {
return children({ url: destinationUrl });
}

return (
<a
onClick={e => {
if (router) {
e.preventDefault();
router.push(destinationUrl);
}
}}
href={destinationUrl}
{...rest}
>
{children}
</a>
);
}
5 changes: 5 additions & 0 deletions packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
AuthenticateWithMetamaskParams,
Clerk,
ClerkAuthenticateWithWeb3Params,
ClerkOptions,
ClientResource,
CreateOrganizationParams,
CreateOrganizationProps,
Expand Down Expand Up @@ -254,6 +255,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
return this.#proxyUrl || '';
}

public __internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K] | undefined {
return this.clerkjs?.__internal_getOption(key);
}

constructor(options: IsomorphicClerkOptions) {
const { Clerk = null, publishableKey } = options || {};
this.#publishableKey = publishableKey;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const PRESERVED_QUERYSTRING_PARAMS = ['after_sign_in_url', 'after_sign_up
* Internal Clerk router, used by Clerk components to interact with the host's router.
*/
export type ClerkRouter = {
makeDestinationUrlWithPreservedQueryParameters: (path: string) => string;
/**
* The basePath the router is currently mounted on.
*/
Expand Down Expand Up @@ -132,6 +133,7 @@ export function createClerkRouter(router: ClerkHostRouter, basePath: string = '/
}

return {
makeDestinationUrlWithPreservedQueryParameters,
child,
match,
mode: router.mode,
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export interface Clerk {
*/
loaded: boolean;

__internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K];

frontendApi: string;

/** Clerk Publishable Key string. */
Expand Down

0 comments on commit 1a0c8fe

Please sign in to comment.