Reputation: 6953
I am creating a registration form, and along with that, I made a form context provider for handling validation, and an InputText component to handle display.
The context from FormContextProvider however is not being passed down to the form InputText components.
Here are the three components:
Register.tsx
import Joi from "joi";
import FormContextProvider from "../../components/forms/FormContextProvider/FormContextProvider";
import InputText from "../../components/forms/InputText/InputText";
const registrationSchema = Joi.object({
email: Joi.string().email({ tlds: false }).required(),
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")),
repeat_password: Joi.ref("password"),
}).with("password", "repeat_password");
function registrationSchemaMessageRewrite(validationResult: Joi.ValidationResult) {
if (validationResult.error?.details?.length) {
validationResult.error.details.forEach((error) => {
if (error.context?.key === "repeat_password") {
error.message = "Passwords do not match";
}
});
}
return validationResult;
}
export default function Register() {
const initialData = { username: "", password: "", repeat_password: "" };
return (
<FormContextProvider initialData={initialData} validation={registrationSchema} postValidation={registrationSchemaMessageRewrite}>
<InputText label="Username" name="username" type="text" />
<InputText label="Email" name="email" type="email" />
<InputText label="Password" name="password" type="password" />
<InputText label="Repeat Password" name="repeat_password" type="password" />
<div>
<input type="checkbox" name="terms" /> I agree to the terms and conditions
</div>
<input type="submit" value="Submit" />
</FormContextProvider>
);
}
FormContextProvider.tsx
import type Joi from "joi";
import { Accessor, createContext, createSignal } from "solid-js";
export const FormContext = createContext<Form>({
data: {},
set: () => undefined,
validationResult: () => undefined,
touched: () => ({}),
setFieldValue: () => undefined,
inputChangeHandler: () => () => undefined,
});
export interface Form {
data: any;
set: (data: any) => void;
validationResult: Accessor<Joi.ValidationResult<any> | undefined>;
touched: Accessor<TouchedData>;
setFieldValue: (field: string, value: string) => void;
inputChangeHandler: (name: string) => (e: Event) => void;
}
export interface FormContextProviderProps {
children: any;
initialData: { [key: string]: any };
validation: Joi.ObjectSchema<any>;
postValidation: (validationResult: Joi.ValidationResult<any>) => Joi.ValidationResult<any>;
}
interface FormData {
[key: string]: string;
}
interface TouchedData {
[key: string]: boolean;
}
function defaultPostValidation(validationResult: Joi.ValidationResult<any>) {
return validationResult;
}
declare module "solid-js" {
namespace JSX {
interface Directives {
formDecorator: boolean;
}
}
}
// wrap children in provider
export default function FormContextProvider(props: FormContextProviderProps) {
const { children, initialData, validation, postValidation = defaultPostValidation } = props;
const [formData, setFormData] = createSignal<FormData>(initialData || {});
const initialTouchData: TouchedData = { test: true };
const [touched, setTouched] = createSignal<TouchedData>(initialTouchData);
const [validationResult, setValidationResult] = createSignal<Joi.ValidationResult<any> | undefined>();
function validate(): boolean {
setValidationResult(postValidation(validation.validate(formData(), { abortEarly: false })));
if (validationResult()?.error) {
return false;
} else {
return true;
}
}
function formDecorator(element: HTMLFormElement, _: () => any): void {
element.addEventListener("submit", async (e) => {
e.preventDefault();
if (validate()) {
console.log("validation passes");
} else {
console.log("validation fails");
}
});
}
true && formDecorator; // hack to prevent unused variable error
const form = {
data: formData,
set: setFormData,
validationResult,
touched,
setFieldValue: (field: string, value: string) => {
setFormData({ [field]: value });
},
inputChangeHandler: (name: string) => (e: Event) => {
const target = e.target as HTMLInputElement;
setFormData({ ...formData(), [name]: target.value });
setTouched({ ...touched(), [name]: true });
validate();
},
};
validate();
return (
<FormContext.Provider value={form}>
<form use:formDecorator>{children}</form>
</FormContext.Provider>
);
}
InputText.tsx
import { createEffect, createSignal, useContext } from "solid-js";
import { FormContext } from "../FormContextProvider/FormContextProvider";
interface InputTextProps {
label: string;
name: string;
type: string;
}
export default function InputText(props: InputTextProps) {
const { label, name, type } = props;
const form = useContext(FormContext);
console.log(form);
if (form === undefined) {
return <div>FormContextProvider not found</div>;
}
const [error, setError] = createSignal("");
createEffect(() => {
let updatedError = "";
const errorDetails = form.validationResult()?.error?.details;
if (!errorDetails || !errorDetails.length) return setError(updatedError);
errorDetails.forEach((error) => {
if (error.context?.key === name) {
updatedError = error.message;
}
});
return setError(updatedError);
});
return (
<div>
<input type={type} name={name} id={name} placeholder={label} value={form.data[name] || ""} onInput={form.inputChangeHandler(name)} />
<div class="error">{form.touched()?.[name] && error()} </div>
</div>
);
}
The console.log(form) just returns the default value from createContext<Form>(/*defaults*/)
in FormContextProvider, but I want it to return the values set in <FormContext.Provider value={form}>
Upvotes: 1
Views: 757
Reputation: 13698
That is because component's props.children
is executed before the component itself when the props object is destructured.
Normally Solid runs props.children first, so that it can be passed to its parent component. But that causes children not to receive the provided value when written as direct children of a context provider.
To workaround this issue Solid uses a getter function to pass props.children
to the parent component. The getter function reverses the execution order:
const Card = (props) => {
return (
<Context.Provider value={'blue'}>
{props.children}
</Context.Provider>
);
}
The code above will produce the following code when compiled:
const Card = props => {
return _$createComponent(Context.Provider, {
value: 'blue',
get children() {
return props.children;
}
});
};
But if you destructure the props, compiler will not produce the getter function:
const Card = ({ children }) => {
return (
<Context.Provider value={'blue'}>
{children}
</Context.Provider>
);
}
Here is the output code for the desctructured value:
const Card = ({
children
}) => {
return _$createComponent(Context.Provider, {
value: 'blue',
children: children
});
};
Now provider receives children and returns it. The value will not be used at all.
Here is a simple application demonstrating the issue:
import { render } from "solid-js/web";
import { createContext, useContext } from "solid-js";
const Context = createContext(0);
const Title = () => {
const value = useContext(Context);
return <div>Value: {value}</div>
}
const Card = (props) => {
return (
<Context.Provider value={30}>
{props.children}
</Context.Provider>
);
}
const DestructuredCard = ({ children }) => {
return (
<Context.Provider value={20}>
{children}
</Context.Provider>
);
}
const App = () => {
return (
<>
<Card>
<Title />
</Card>
<DestructuredCard>
<Title />
</DestructuredCard>
</>
);
}
render(App, document.body);
Live Demo: https://playground.solidjs.com/anonymous/5f1fe27f-24a3-4ebb-80f9-ab0fa2516d1e
If you do not use prop destructuring everything work as expected. But if you do, you should call the provider before the children is executed:
const App = () => {
return (
<CardProvider>
<Card>
<Card.Title title="Title" />
<Card.Body body="Body" />
</Card>
</CardProvider>
);
};
This also works:
const App = () => {
return (
<Card>
<CardProvider>
<Card.Title title="Title" />
<Card.Body body="Body" />
</CardProvider>
</Card>
);
};
Upvotes: 2
Reputation: 6953
This is caused by the destructuring of props in FormContextProvider:
const { children, initialData, validation, postValidation = defaultPostValidation } = props;
By removing this line and referring to individual props by prefixing with "props." like "props.children" this problem was resolved. This is because by destructuring props you lose the reactivity (updates to the variable won't update the page content).
Most likely only children needed to remain on props as the other ones are static.
If so, something like this could be done:
const [{ initialData, validation, postValidation }, local] = splitProps(props, ["initialData", "validation", "postValidation"]);
and refer to children by local.children
Upvotes: 1