Formik checkbox with Semantic-UI styling - reactjs

Hi,
I've been trying to combine Semantic-UI (Semantic-UI React, to be specific) and Formik, but I keep running into problems. Currently I'm having a problem with checkboxes.
I'm trying to create a generic Formik-aware checkbox component with Semantic-UI styling. This is what I currently have:
const Checkbox = ({ name, title, toggle, fitted, inline }, context) => {
const { formik } = context;
const error = formik.errors[name];
const value = formik.values[name];
return (
<Form.Checkbox
inline={inline}
fitted={fitted}
label={title || (fitted ? null : name)}
name={name}
toggle={toggle}
checked={value ? true : false}
onChange={(e, { name, checked }) => formik.setFieldValue(name, !!checked)}
/>
);
};
Checkbox.contextTypes = { formik: PropTypes.object }
This kind of works. The only problem I'm currently having is with nested values. For example:
<Checkbox name="sections.0.enabled"/>
My Checkbox implementation will use formik.values["sections.0.enabled"] as value, while a formik Field would correctly pick the value from formik.values["sections"][0]["enabled"].
Is there a better way to do this, or should I just flatten the input values?

You can achieve this using lodash.get (https://www.npmjs.com/package/lodash.get) or simply use a function which does the exact same job:
function get( object, keys, defaultVal ){
keys = Array.isArray( keys )? keys : keys.split('.');
object = object[keys[0]];
if( object && keys.length>1 ){
return get( object, keys.slice(1) );
}
return object === undefined? defaultVal : object;
}
Then,
...
const error = get(formik.errors, name);
const value = get(formik.values, name);
...

Related

Getting an Empty array in props

I am trying to make table using react-table in which one of the columns will be a toggle-switch in typescript. I am using the following code to create a react-switch.
const tableInstance = useTable({
columns,
data},(hooks)=> {
console.log("setting buildings: ",data);
hooks.visibleColumns.push((columns) => columns.map((column)=>column.id === "cevacrunningstatus"?{...column,Cell:({row})=>
{return <BuildingStartStopSwitch row_={row} data__={buildings} setdata={setbuildings}/>}}:column));
}
)
I am using the following React function component
interface props{
row_:Row.Row<any>;
data__:BuildingControlStatus[];
setdata: React.Dispatch<React.SetStateAction<BuildingControlStatus[]>>;
}
const BuildingStartStopSwitch:React.FC<props> = ({row_,data__,setdata}) => {
const [state,setState] = useState<boolean>(row_.values.runningstatus);
const handleChange = (checked:boolean) => {
setState(checked);
console.log("Data before statechange: ",data__)
setdata(data__.map((data_)=>row_.values.ccid === data_.ccid?({...data_,runningstatus:checked}):data_))
}
console.log("Data after statechange: ",data__)
return (
<Switch onChange={handleChange} checked={state}/>
);
};
export default BuildingStartStopSwitch;
I have the following issue:
The array data__ is turning up as an empty array inside BuildingStartStopSwitch. The data variable which is assigned to data__ contains items, but the same is not reflected inside BuildingStartStopSwitch. I am trying to update the data variable(which is the table data) to reflect the status of toggle switch.
I have an input cell against each toggle switch in a row. When the switch is checked, the input should be enabled and when the switch is unchecked , it should be disabled. I am not sure how to implement.
Thanks in advance!

Chakra UI - Checkbox Group

I am using Chakra UI with React Typescript and implementing a checkbox group
The default values are controlled by an outside state that is passed down as a prop.
The problem is that the CheckboxGroup doesn't accept the default values from outside source
The code is as follows:
import React, {FC, useCallback, useEffect, useState} from "react";
import { CheckboxGroup, Checkbox, VStack } from "#chakra-ui/react";
interface IGroupCheckbox {
values: StringOrNumber[],
labels: StringOrNumber[],
activeValues: StringOrNumber[]
onChange: (value:StringOrNumber[])=> void
}
const GroupCheckbox:FC<IGroupCheckbox> = ({
values,
labels,
activeValues,
onChange
}) => {
const [currActiveValues, setCurrActiveValues] = useState<StringOrNumber[]>();
const handleChange = useCallback((value:StringOrNumber[]) => {
if(value?.length === 0) {
alert('you must have at least one supported language');
return;
}
onChange(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(()=>{
if(activeValues) {
setCurrActiveValues(['en'])
}
},[activeValues])
return (
<CheckboxGroup
onChange={handleChange}
defaultValue={currActiveValues}
>
<VStack>
{values && labels && values.map((item:StringOrNumber, index:number)=>
{
return (
<Checkbox
key={item}
value={item}
>
{labels[index]}
</Checkbox>
)
}
)}
</VStack>
</CheckboxGroup>
)
}
export default GroupCheckbox
When I change the defaultValue parameter, instead of the state managed, to be defaultValue={['en']} it works fine, but any other input for this prop doesn't work.
I checked and triple checked that the values are correct.
Generally, passing a defaultValue prop "from an outside source" actually does work. I guess that in your code, only ['en'] works correctly because you explicitly use setCurrActiveValues(['en']) instead of setCurrActiveValues(activeValues).
The defaultValue prop will only be considered on the initial render of the component; changing the defaultValue prop afterwards will be ignored. Solution: make it a controlled component, by using the value prop instead of defaultValue.
Note that unless you pass a parameter to useState(), you will default the state variable to undefined on that initial render.
Side note: You also don't need a separate state variable currActiveValues. Instead, you can simply use activeValues directly.
const GroupCheckbox = ({ values, labels, activeValues, onChange }: IGroupCheckbox) => {
const handleChange = useCallback(
(value: StringOrNumber[]) => {
if (value?.length === 0) {
alert("you must have at least one supported language")
return
}
onChange(value)
},
[onChange]
)
return (
<CheckboxGroup onChange={handleChange} value={activeValues}>
<VStack>
{labels && values?.map((item: StringOrNumber, index: number) => (
<Checkbox key={item} value={item}>
{labels[index]}
</Checkbox>
))}
</VStack>
</CheckboxGroup>
)
}

How to access input value inside `transformErrors`

When mapping errors inside transformErrors callback, I need to know the actual value of the input in question.
I need this to create a system for composing multiple existing formats into new composite formats. I want to match the input value against each of the "basic" formats and display the error for the one that fails. The allOf method of composing formats unfortunately doesn't work for me, for reasons very specific to my project.
I tried injecting the form data into my tranformErrors callback via currying and reading the data directly:
import _ from 'lodash'
import Form from '#rjsf/core'
const makeTransformErrors = formData => errors => {
errors.forEach(error => {
if (error.name === 'format') {
const value = _.get(formData, error.property)
// ...
}
})
}
const WrapedForm = (formData, ...rest) => {
const transformErrors = makeTransformErrors(formData)
return (
<Form
transformErrors={transformErrors}
formData={formData}
{...rest}
/>
)
}
but this way value lags one keystroke behind the actual state of the form, which is what I was expecting. Unfortunately this doesn't work even when I don't pass formData into makeTransformErrors directly, but instead I pass in an object containing formData and directly mutate it imeditately inside Forms onChange, which I was expecting to work.
What are other possible ways of accessing the field's value? Maybe it could be possible to configure (or patch) ajv validator to attatch the value to validation error's params?
Not sure exactly what kind of error validation you are trying todo, but have you tried using validate?
It can be passed as such :
<Form .... validate={validate} />
where validate is a function that takes as arguments formData and errors.
See documentation here
Ok, I found a way of achieving what I want, but it's so hacky I don't think I want to use it. I can get the up-to-date value when combining the above mentioned prop mutation trick with using a getter for the message, postponing the evaluation until the message is actually read, which happens to be enough:
import _ from 'lodash'
import Form from '#rjsf/core'
const makeTransformErrors = formDataRef => errors => {
return errors.map(error => {
if (error.name !== 'format') return error
return {
...error,
get message() {
const value = _.get(propPath, formDataRef.current) // WORKS! But at what cost...
}
}
})
}
const WrapedForm = (formData, onChange, ...rest) => {
const formDataRef = React.useRef(formData)
const transformErrors = makeTransformErrors(formDataRef)
handleChange = (params) => {
formDataRef.current = params.formData
onChange(params)
}
return (
<Form
transformErrors={transformErrors}
onChange={handleChange}
formData={formData}
{...rest}
/>
)
}

Keeping state of variable mapped from props in functional react component after component redraw

Recently I started learning react and I decided to use in my project functional components instead of class-based. I am facing an issue with keeping state on one of my components.
This is generic form component that accepts array of elements in order to draw all of necessary fields in form. On submit it returns "model" with values coming from input fields.
Everything working fine until I added logic for conditionally enabling or disabling "Submit" button when not all required fields are set. This logic is fired either on component mount using useEffect hook or after every input in form input. After re-render of the component (e.g. conditions for enabling button are not met, so button becomes disabled), component function is fired again and my logic for creating new mutable object from passed props started again, so I am finished with empty object.
I did sort of workaround to make a reference of that mutated object outside of scope of component function, but i dont feel comfortable with it. I also dont want to use Redux for that simple sort of state.
Here is the code (I am using Type Script):
//component interfaces:
export enum FieldType {
Normal = "normal",
Password = "password",
Email = "email"
}
export interface FormField {
label: string;
displayLabel: string;
type: FieldType;
required: boolean;
}
export interface FormModel {
model: {
field: FormField;
value: string | null;
}[]
}
export interface IForm {
title: string;
labels: FormField[];
actionTitle: string;
onSubmit: (model: FormModel) => void;
}
let _formState: any = null;
export function Form(props: IForm) {
let mutableFormModel = props.labels.map((field) => { return { field: field, value: null as any } });
//_formState keeps reference outside of react function scope. After coponent redraw state inside this function is lost, but is still maintained outside
if (_formState) {
mutableFormModel = _formState;
} else {
_formState = mutableFormModel;
}
const [formModel, setFormModel] = useState(mutableFormModel);
const [buttonEnabled, setButtonEnabled] = useState(false);
function requiredFieldsCheck(formModel: any): boolean {
let allRequiredSet = true;
formModel.model.forEach((field: { field: { required: any; }; value: string | null; }) => {
if (field.field.required && (field.value === null || field.value === '')) {
allRequiredSet = false;
}
})
return allRequiredSet;
}
function handleChange(field: FormField, value: string) {
let elem = mutableFormModel.find(el => el.field.label === field.label);
if (elem) {
value !== '' ? elem.value = value as any : elem.value = null;
}
let submitEnabled = requiredFieldsCheck({ model: mutableFormModel });
setFormModel(mutableFormModel);
setButtonEnabled(submitEnabled);
}
useEffect(() => {
setButtonEnabled(requiredFieldsCheck({ model: mutableFormModel }));
}, [mutableFormModel]);
function onSubmit(event: { preventDefault: () => void; }) {
event.preventDefault();
props.onSubmit({ model: formModel })
}
return (
<FormStyle>
<div className="form-container">
<h2 className="form-header">{props.title}</h2>
<form className="form-content">
<div className="form-group">
{props.labels.map((field) => {
return (
<div className="form-field" key={field.label}>
<label>{field.displayLabel}</label>
{ field.type === FieldType.Password ?
<input type="password" onChange={(e) => handleChange(field, e.target.value)}></input> :
<input type="text" onChange={(e) => handleChange(field, e.target.value)}></input>
}
</div>
)
})}
</div>
</form>
{buttonEnabled ?
<button className={`form-action btn btn--active`} onClick={onSubmit}> {props.actionTitle} </button> :
<button disabled className={`form-action btn btn--disabled`} onClick={onSubmit}> {props.actionTitle} </button>}
</div>
</FormStyle >
);
}
So there is quite a lot going on with your state here.
Instead of using a state variable to check if your button should be disabled or not, you could just add something render-time, instead of calculating a local state everytime you type something in your form.
So you could try something like:
<button disabled={!requiredFieldsCheck({ model: formModel })}>Click me</button>
or if you want to make it a bit cleaner:
const buttonDisabled = !requiredFieldsCheck({model: formModel});
...
return <button disabled={buttonDisabled}>Click me</button>
If you want some kind of "caching" without bathering with useEffect and state, you can also try useMemo, which will only change your calculated value whenever your listeners (in your case the formModel) have changes.
const buttonDisabled = useMemo(() => {
return !requiredFieldsCheck({model: formModel});
}, [formModel]);
In order to keep value in that particular case, I've just used useRef hook. It can be used for any data, not only DOM related. But thanks for all inputs, I've learned a lot.

Why toggleSelection and isSelected method are receiving different key parameter?

I am using react-table in version 6.10.0. with typescript.
There is an easy way to add checkbox with hoc/selectTable
However toggleSelection an isSelected method you need to provide to manage selection are receiving different key.
toggleSelection method is receiving extra "select-" at the beginning.
I could not found any example which such a problem.
I know there is a simple workaround for this problem, but still I could not found any example which extra string at the beginning. I am new in react and it seems that I do it incorrectly.
import "bootstrap/dist/css/bootstrap.min.css";
import ReactTable, { RowInfo } from "react-table";
import "react-table/react-table.css";
import checkboxHOC, { SelectType } from "react-table/lib/hoc/selectTable";
const CheckboxTable = checkboxHOC(ReactTable);
....
render() {
...
<CheckboxTable
data={this.getData()}
columns={this.columnDefinitions()}
multiSort={false}
toggleSelection={(r,t,v) => this.toggleSelection(r,t,v)}
isSelected={(key)=> this.isSelected(key)}
/>
}
...
toggleSelection = (key: string, shiftKeyPressed: boolean, row: any): any => {
...
//implementation -over here key is always like "select-" + _id
...}
isSelected = (key: string): boolean => {
// key received here is only _id
return this.state.selection.includes(key);
}
In all examples I have seen the methods are provided with the same key.
Looking at the source, it seems like it's working as intended, or there's a bug. If you haven't found any other mention of this, it's probably the former.
This is where the SelectInputComponents are created:
rowSelector(row) {
if (!row || !row.hasOwnProperty(this.props.keyField)) return null
const { toggleSelection, selectType, keyField } = this.props
const checked = this.props.isSelected(row[this.props.keyField])
const inputProps = {
checked,
onClick: toggleSelection,
selectType,
row,
id: `select-${row[keyField]}`
}
return React.createElement(this.props.SelectInputComponent, inputProps)
}
The two handlers of interest are onClick (which maps to toggleSelection) and checked, which maps to isSelected. Notice the id here.
The SelectInputComponent looks like this:
const defaultSelectInputComponent = props => {
return (
<input
type={props.selectType || 'checkbox'}
aria-label={`${props.checked ? 'Un-select':'Select'} row with id:${props.id}` }
checked={props.checked}
id={props.id}
onClick={e => {
const { shiftKey } = e
e.stopPropagation()
props.onClick(props.id, shiftKey, props.row)
}}
onChange={() => {}}
/>
)
In the onClick (i.e. toggleSelection) handler, you can see that props.id is passed in as the first argument. So this is where the additional select- is being added.
I'm not familiar with this package so I can't tell you if it's a bug or a feature, but there is a difference in how these callback arguments are being passed. Due to the maturity of the package, it strongly suggests to me that this is intended behaviour.

Resources