RankPermission.value in switchPermission function is changing from false to true, but MUI Switch is not updating in the browser. I don't know why isn't it updating, and I didn't try much. I don't have any ideas how can I fix it.
const [activeRank, setActiveRank] = useState<FactionRanks>();
export type FactionRanks = {
id: number;
name: string;
rankPermissions: FactionRanksPermissions[];
};
export type FactionRanksPermissions = {
label: string;
value: boolean;
};
const ActionMenu = () => {
const { activeRank } = useContext(FactionPanelContext);
const switchPermission = (rankPermission: FactionRanksPermissions) => {
rankPermission.value = !rankPermission.value;
console.log(rankPermission);
};
return (
<Wrapper>
<Buttons>
{activeRank?.rankPermissions.map(
(rankPermission: FactionRanksPermissions, index: number) => (
<Row key={index}>
<OptionDetails>{rankPermission.label}</OptionDetails>
<IOSSwitch
checked={rankPermission.value}
inputProps={{ 'aria-label': 'secondary checkbox' }}
onClick={() => switchPermission(rankPermission)}
/>
</Row>
)
)}
</Buttons>
</Wrapper>
);
};
You are changing rankPermission in place here
rankPermission.value = !rankPermission.value;
You should be setting state, with a new object of rankPermission, where it is defined instead.
Related
First, here's my "Parent Component" or "Factory", I'm not quite certain what the terminology is.
export interface ReadOnlyProps {
ignoreReadOnly?: boolean;
disabled?: boolean;
}
export const WithReadOnly = <T,>(Component: React.ComponentType<T>): React.ComponentType<T & ReadOnlyProps> => {
const ReadOnlyComponent = (props: T) => {
const { ...rest } = props as T & ReadOnlyProps;
return <Component {...(rest as T)} disabled={true} />;
};
return ReadOnlyComponent;
};
Here are the components I did for this:
const Dropdown = <T,>(props: { items: T[]; onChange: (data: T) => void }) => {
return (
<ul>
{props.items.map((item, index) => {
<li onClick={() => props.onChange(item)}>{index}</li>;
})}
</ul>
);
};
const DropdownReadOnly = WithReadOnly(Dropdown);
Then I did this two examples.
// Works
<Dropdown items={[{ name: 'Someone' }, { name: 'Someone else' }]} onChange={(item) => alert(item.name)} />;
// Doesn't work
<DropdownReadOnly items={[{ name: 'Someone' }, { name: 'Someone else' }]} onChange={(item) => alert(item.name)} />;
The first one is working, the second one is complaining that item is unknown on the onChange prop.
Anyone know what I am doing wrong here?
Thank you beforehand!
With a React Accordion I wanted to create a function that will only show the information of the belonging episode. But now it seems that all blocks are doing the same thing. How can I build that function so one block will be toggling instead of all?
Link to CodeSandbox
export const HomePage: React.FC<IProps> = () => {
const [open, setOpen] = useState(false);
return (
<div>{data.characters.results.map((character: { id: number, name: string, image: string; episode: any; }, index: number) => (
<div key={character.id}>
<CharacterHeading>{character.name}</CharacterHeading>
<CharacterImage src={character.image} alt={character.name} />
{character.episode.map((char: { name: string; air_date: string; episode: string; characters: any, id: number; }) => (
<div key={char.id}>
{char.name}
{char.air_date}
{char.episode}
<AccordionButton open={open}
onClick={() => setOpen(!open)}>
Check all characters
</AccordionButton>
<EpisodeListSection open={open}>
{char.characters.map((ep: { id: number, name: string; image: string; }, index: number) => (
<EpisodeInfo key={ep.id}>
<EpisodeInfoBlock>
<EpisodeName>{ep.name}</EpisodeName>
<EpisodeImage src={ep.image} alt={ep.name} />
</EpisodeInfoBlock>
</EpisodeInfo>
))}
</EpisodeListSection>
</div>
))}
</div>
))
}</div>
);
};
You only have one open variable that you are passing to every accordion. Thus, when one accordion changes that value with setOpen, it changes for all of them.
If AccordionButton is a component you built, I would suggest letting that component handle its own open/closed state. Your parent doesn't seem like it needs to control that. Otherwise, you will need open to be an array of values, you will need to only pass the correct open value to each accordion, and you will need to have your onClick function tell the parent which open value it needs to change.
Again, that seems like a lot of work for not a lot of utility. Better to just let the accordions keep track of whether they're open.
Here is the updated code that you can use:
import { useState } from "react";
import { gql, useQuery } from "#apollo/client";
import {
AccordionButton,
CharacterImage,
CharacterHeading,
EpisodeInfo,
EpisodeImage,
EpisodeInfoBlock,
EpisodeListSection,
EpisodeName
} from "./HomePage.styles";
export const GET_CHARACTER = gql`
query {
characters(page: 2, filter: { name: "rick" }) {
results {
name
image
gender
episode {
id
name
air_date
episode
characters {
id
name
image
}
}
}
}
location(id: 1) {
id
}
episodesByIds(ids: [1, 2]) {
id
}
}
`;
export interface Episodes {
name: string;
air_data: string;
episode: string;
characters: Array<any>;
}
export interface IProps {
episodeList?: Episodes[];
}
export const HomePage: React.FC<IProps> = () => {
const { data, loading, error } = useQuery(GET_CHARACTER);
const [inputValue, setInputValue] = useState("");
const [open, setOpen] = useState(false);
const [selectedId, setSelectedId] = useState(0);
const onChangeHandler = (text: string) => {
setInputValue(text);
};
const onSelectItem = (selectedItemId: number) => {
if (selectedId !== selectedItemId) {
setSelectedId(selectedItemId);
} else {
setSelectedId(-1);
}
};
if (loading) return <p>loading</p>;
if (error) return <p>ERROR: {error.message}</p>;
if (!data) return <p>Not found</p>;
return (
<div>
<input
type="text"
name="name"
onChange={(event) => onChangeHandler(event.target.value)}
value={inputValue}
/>
<div>
{data.characters.results.map(
(
character: {
id: number;
name: string;
image: string;
episode: any;
},
index: number
) => (
<div key={character.id}>
<CharacterHeading>{character.name}</CharacterHeading>
<CharacterImage src={character.image} alt={character.name} />
{character.episode.map(
(char: {
name: string;
air_date: string;
episode: string;
characters: any;
id: number;
}) => (
<div key={char.id}>
{char.name}
{char.air_date}
{char.episode}
<AccordionButton
onClick={() => onSelectItem(char.id)}
open={char.id === selectedId ? true : false}
>
Check all characters
</AccordionButton>
<EpisodeListSection
open={char.id === selectedId ? false : true}
>
{char.characters.map(
(
ep: { id: number; name: string; image: string },
index: number
) => (
<EpisodeInfo key={ep.id}>
<EpisodeInfoBlock>
<EpisodeName>{ep.name}</EpisodeName>
<EpisodeImage src={ep.image} alt={ep.name} />
</EpisodeInfoBlock>
</EpisodeInfo>
)
)}
</EpisodeListSection>
</div>
)
)}
</div>
)
)}
</div>
</div>
);
};
The following component should always only have at most one of the two checkboxes checked. Although the state checkedList always only contains at most one value the UI doesn't reflect this state.
What am I missing?
interface TeilnehmerProps {
rolle: Teilnehmerrolle,
teilnehmer: Teilnehmer,
readonly: boolean
}
export const TeilnehmerCard = (props: TeilnehmerProps) => {
const { rolle, teilnehmer, readonly } = props;
const [name, setName] = useState<string>(teilnehmer?.name);
const [checkedList, setCheckedList] = useState<CheckboxValueType[]>([teilnehmer?.abwesenheit]);
const handleChecked = (list: CheckboxValueType[]) => {
const set = new Set(list);
if (set.size === 2) {
set.delete(checkedList[0]);
setCheckedList(Array.from(set));
} else if (set.size === 1) {
setCheckedList(Array.from(set));
} else {
setCheckedList([]);
}
};
useEffect(() => {
console.log(checkedList);
}, [checkedList]);
return <Card title="" size="small" className={styles.teilnehmerCard}>
<Row key={teilnehmer?.rolle}>
<Col lg={4}>
<Form.Item name={`${teilnehmer?.rolle}.abwesenheit`}
initialValue={[teilnehmer?.abwesenheit]}>
<Checkbox.Group value={checkedList}
disabled={readonly}
options={[{ value: 'ENTSCHULDIGT', label: '' }, { value: 'UNENTSCHULDIGT', label: '' }]}
onChange={handleChecked}>
</Checkbox.Group>
</Form.Item>
</Col>
</Row>
</Card>;
};
Once wrapped in <Form.Item/> one cannot and shouldn't control the form values anymore, but use form.setFieldsValue().
Lately i discovered the react-hook-from plugin which on the first sight seem to be perfect to start using and replace other plugins due to its great performance.
After using the plugin for some really simple forms i came across a complicated form that i wish to handle with the plugin. My form is based on a nested object which has the following structure. (Typescript definition)
type model = {
tag: string;
visible: boolean;
columns?: (modelColumn | model)[];
}
type modelColumn = {
property: string;
}
So in order to handle to handle a n-level nested form i created the following components.
const initialData = {
...
datasource: {
tag: "tag1",
visible: true,
columns: [
{
property: "property",
},
{
property: "property1",
},
{
tag: "tag2",
visible: true,
columns: [
{
property: "property",
},
{
tag: "tag3",
visible: false,
columns: [
{
property: "property",
}
]
}
]
},
{
entity: "tag4",
visible: false,
}
],
},
...
}
export const EditorContent: React.FunctionComponent<EditorContentProps> = (props: any) => {
const form = useForm({
mode: 'all',
defaultValues: initialData,
});
return (
<FormProvider {...form}>
<form>
<Handler
path="datasource"
/>
</form>
</FormProvider>
);
}
In the above component the main form is created loaded with initial data. (I provided an example value). The Handler component has the login of recursion with the path property to call nested logic of the form data type. Here is the sample implementation.
...
import { get, isNil } from "lodash";
import { useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
...
export type HandlerProps = {
path: string;
index?: number;
...
}
export const Handler: React.FunctionComponent<HandlerProps> = (props) => {
const { path, index, onDelete, ...rest } = props;
const { control } = useFormContext();
const name = isNil(index)? `${path}` :`${path}[${index}]`;
const { fields, append, insert, remove } = useFieldArray({
control: control,
name: `${name}.columns`
});
...
const value = useWatch({
control,
name: `${name}`,
});
...
const addHandler = () => {
append({ property: null });
};
console.log(`Render path ${name}`);
return (
<React.Fragment>
<Row>
<Col>
<FormField name={`${name}.tag`} defaultValue={value.tag}>
<Input
label={`Tag`}
/>
</FormField>
</Col>
<Col>
<FormField defaultValue={value.visible} name={`${name}.visible`} >
<Switch />
</FormField>
</Col>
<Col>
<button onClick={addHandler}>Add Column Property</button>
</Col>
</Row>
{
fields && (
fields.map((field: any, _index: number) => {
if (field.property !== undefined) {
return (
<Column
path={`${name}.columns`}
index={_index}
control={control}
fields={fields}
onDelete={() => remove(_index) }
/>
)
} else {
return (
<Handler
path={`${name}.columns`}
index={_index}
/>
)
}
})
)
}
</React.Fragment>
);
}
Essentially the Handler Component uses the form's context and call itself if it should render a nested object of the form like datasource.columns[x] which the register to useFieldArray to get it's columns. Everything so far works fine. I render the complete tree (if i can say) like object form correctly.
For reference here is the code of the Column Component as also for the formField helper component FormField.
export const Column: React.FunctionComponent<ColumnProps> = (props) => {
const { fields, control, path, index, onDelete } = props;
const value = useWatch({
control,
name: `${path}[${index}]`,
defaultValue: !isNil(fields[index])? fields[index]: {
property: null,
}
});
console.log(`Render of Column ${path} ${value.property}`);
return (
<Row>
<Col>
<button onClick={onDelete}>Remove Property</button>
</Col>
<Col>
<FormField name={`${path}[${index}].property`} defaultValue={value.property}>
<Input
label={`Column Property name`}
/>
</FormField>
</Col>
</Row>
);
}
export type FormFieldProps = {
name: string;
disabled?: boolean;
validation?: any;
defaultValue?: any;
children?: any;
};
export const FormField: React.FunctionComponent<FormFieldProps> = (props) => {
const {
children,
name,
validation,
defaultValue,
disabled,
} = props;
const { errors, control, setValue } = useFormContext();
return (
<Controller
name={name}
control={control}
defaultValue={defaultValue? defaultValue: null}
rules={{ required: true }}
render={props => {
return (
React.cloneElement(children, {
...children.props,
...{
disabled: disabled || children.props.disabled,
value: props.value,
onChange: (v:any) => {
props.onChange(v);
setValue(name, v, { shouldValidate: true });
},
}
})
);
}}
/>
);
}
The problem is when i remove a field from the array. The Handler component re-renders having correct values on the fields data. But the values of useWatch have the initial data which leads to a wrong render with wrong fields being displayed an the forms starts to mesh up.
Is there any advice on how to render such a nested form the correct way. I guess this is not an issue from the react-hook-form but the implementation has an error which seems to cause the problem.
i am new to typescript and i am getting the error
"type Item[] | undefined is not assignable to Item[] type"
below is the code,
function Parent ({Items}: Props) {
return (
<Child subItems={Items.subItems}/> //error here
);
}
function Child({subItems}: Item[]) {
const sortedSubItems = subItems.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt));
return (
<>
{sortedSubItems.map((subItem: Item) => {
return (
<Card key={subItem.id}>
<CardHeader>
{subItem.name}
</CardHeader>
</Card>
);
})}
</>
);
};
here subItems has structure like below,
const subItems = [
{
id: '1',
title: 'subitem-one',
status: 'new',
createdAt: '2020-08-13T16:32:10.000Z',
orders: [
{
id: '1',
title: 'subitem1-order-one',
status: 'new',
},
{
id: '2',
title: 'subitem1-order-two',
status: 'new',
},
]
},
{
id: '2',
title: 'subitem-two',
status: 'new',
createdAt: '2020-08-16T12:02:06.000Z',
orders: [
{
id: '2',
title: 'subitem2-order-one',
status: 'new',
},
],
},
]
and the Item type is like below,
export interface Item {
id: string;
createdAt: string;
name?: string;
orders?: Order[];
subItems?: Item[];
}
could someone help me fix the error using typescript and react. thanks.
Because you have an optional type operator(the question mark) on your subItems property, that indicates to Typescript that that property can be undefined. You will need to type the subItems prop for your Child component to reflect that, and handle it appropriately in the code:
interface ChildProps {
subItems?: Item[]
}
function Child({subItems}: ChildProps) {
const sortedSubItems = subItems ? subItems.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt)) : [];
return (
<>
{sortedSubItems.map((subItem: Item) => {
return (
<Card key={subItem.id}>
<CardHeader>
{subItem.name}
</CardHeader>
</Card>
);
})}
</>
);
};
In your particular case it can be a good idea to provide an empty array as default value to subItems:
interface ChildProps {
subItems: Item[]
}
function Child(props: ChildProps) {
const { subItems = []} = props;
const sortedSubItems = subItems.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt));
return (
<>
{sortedSubItems.map((subItem: Item) => {
return (
<Card key={subItem.id}>
<CardHeader>
{subItem.name}
</CardHeader>
</Card>
);
})}
</>
);
};
You're facing this issue because the Child component is undefined in some cases, in order to fix this you will need to handle these cases.
The sortedSubItems array may be undefined, so you can compute the outside the Child component .ie. in the Parent component and do a && while passing it as a prop in the Child component.
function Parent ({Items}: Props) {
const sortedSubItems = Items?.subItems?.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt));
return (
<>
{sortedSubItems && <Child sortedSubItems={sortedSubItems} />}
</>
);
}
function Child({sortedSubItems}: Item[]) {
return (
<>
{sortedSubItems.map((subItem: Item) => {
return (
<Card key={subItem.id}>
<CardHeader>
{subItem.name}
</CardHeader>
</Card>
);
})}
</>
);
};
You can keep the same code as you have, just change
const sortedSubItems = subItems.sort((a: Item,b: Item) => b.createdAt.localeCompare(a.createdAt)) ?? [];
This will conditionally choose the empty array