Skip to content

Commit

Permalink
feat(core): add react-hook form resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-jonas committed Nov 27, 2024
1 parent 60bc748 commit cf664eb
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`case={"method":"code","code":"","email":""} 1`] = `
{
"errors": {
"code": {
"context": {
"property": "code",
},
"id": 4000002,
"text": "Property code is missing",
"type": "error",
},
},
"values": {
"code": "",
"email": "",
"method": "code",
},
}
`;

exports[`case={"method":"code","code":"123456","email":""} 1`] = `
{
"errors": {},
"values": {
"code": "123456",
"email": "",
"method": "code",
},
}
`;

exports[`case={"method":"code","code":"123456","email":"[email protected]"} 1`] = `
{
"errors": {},
"values": {
"code": "123456",
"email": "[email protected]",
"method": "code",
},
}
`;

exports[`case={"method":"code"} 1`] = `
{
"errors": {
"code": {
"context": {
"property": "code",
},
"id": 4000002,
"text": "Property code is missing",
"type": "error",
},
},
"values": {
"method": "code",
},
}
`;

exports[`case={"method":"password"} 1`] = `
{
"errors": {},
"values": {
"method": "password",
},
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

import { UiNode, UiNodeGroupEnum } from "@ory/client-fetch"
import { PropsWithChildren } from "react"
import { useOryFlow } from "../../context"
import { FormProvider, useForm } from "react-hook-form"
import { useOryFlow } from "../../context"
import { computeDefaultValues } from "./form-helpers"
import { useOryFormResolver } from "./form-resolver"

export function OryFormProvider({
children,
Expand All @@ -21,6 +22,8 @@ export function OryFormProvider({
const methods = useForm({
// TODO: Generify this, so we have typesafety in the submit handler.
defaultValues: computeDefaultValues(defaultNodes),
resolver: useOryFormResolver(),
})

return <FormProvider {...methods}>{children}</FormProvider>
}
58 changes: 58 additions & 0 deletions packages/elements-react/src/components/form/form-resolver.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { renderHook } from "@testing-library/react"
import { FormValues } from "../../types"
import { useOryFormResolver } from "./form-resolver"
import { OryFlowProvider } from "../../context/flow-context"
import { defaultConfiguration } from "../../tests/jest/test-utils"
import { PropsWithChildren } from "react"
import { FlowType, LoginFlow } from "@ory/client-fetch"

const testCases = [
{
method: "code",
code: "",
email: "",
},
{
method: "code",
},
{
method: "code",
code: "123456",
email: "",
},
{
method: "code",
code: "123456",
email: "[email protected]",
},
{
method: "password",
},
] satisfies FormValues[]

const wrapper = ({ children }: PropsWithChildren) => (
<OryFlowProvider
config={defaultConfiguration}
flow={
{
active: "code",
ui: { nodes: [], action: "", method: "" },
} as unknown as LoginFlow // Fine, we're just testing the resolver
}
flowType={FlowType.Login}
>
{children}
</OryFlowProvider>
)

for (const testCase of testCases) {
test("case=" + JSON.stringify(testCase), () => {
const formResolver = renderHook(() => useOryFormResolver(), {
wrapper,
})
expect(formResolver.result.current(testCase)).toMatchSnapshot()
})
}
44 changes: 44 additions & 0 deletions packages/elements-react/src/components/form/form-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { useOryFlow } from "../../context"
import { FormValues } from "../../types"

function isCodeResendRequest(data: FormValues) {
return data.email ?? data.resend
}

/**
* Creates a resolver for the Ory form
*
* The resolver does form validation for missing fields in the form.
*
* @returns a react-hook-form resolver for the Ory form
*/
export function useOryFormResolver() {
const flowContainer = useOryFlow()

return (data: FormValues) => {
if (flowContainer.formState.current === "method_active") {
if (data.method === "code" && !data.code && !isCodeResendRequest(data)) {
return {
values: data,
errors: {
code: {
id: 4000002,
context: {
property: "code",
},
type: "error",
text: "Property code is missing",
},
},
}
}
}
return {
values: data,
errors: {},
}
}
}
8 changes: 4 additions & 4 deletions packages/elements-react/src/components/form/nodes/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ export const NodeInput = ({
//
...attrs
} = attributes
const isResendNode = node.meta.label?.id === 1070008
const isScreenSelectionNode =
"name" in node.attributes && node.attributes.name === "screen"

const setFormValue = () => {
if (attrs.value) {
if (attrs.value && !(isResendNode || isScreenSelectionNode)) {
setValue(attrs.name, attrs.value)
}
}
Expand Down Expand Up @@ -64,9 +67,6 @@ export const NodeInput = ({
const isPinCodeInput =
(attrs.name === "code" && node.group === "code") ||
(attrs.name === "totp_code" && node.group === "totp")
const isResendNode = node.meta.label?.id === 1070008
const isScreenSelectionNode =
"name" in node.attributes && node.attributes.name === "screen"

switch (attributes.type) {
case UiNodeInputAttributesTypeEnum.Submit:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { FlowType, getNodeLabel, UiNode } from "@ory/client-fetch"
import {
FlowType,
getNodeLabel,
instanceOfUiText,
UiNode,
} from "@ory/client-fetch"
import {
OryNodeLabelProps,
messageTestId,
Expand Down Expand Up @@ -31,7 +36,7 @@ export function DefaultLabel({
const label = getNodeLabel(node)
const { Message } = useComponents()
const { config, flowType, flow } = useOryFlow()
const { setValue } = useFormContext()
const { setValue, formState } = useFormContext()

const isPassword = attributes.type === "password"

Expand All @@ -43,6 +48,8 @@ export function DefaultLabel({
}
}

const fieldError = formState.errors[attributes.name]

return (
<div className="flex flex-col antialiased gap-1">
{label && (
Expand Down Expand Up @@ -86,6 +93,9 @@ export function DefaultLabel({
{node.messages.map((message) => (
<Message.Content key={message.id} message={message} />
))}
{fieldError && instanceOfUiText(fieldError) && (
<Message.Content message={fieldError} />
)}
</div>
)
}

0 comments on commit cf664eb

Please sign in to comment.