Display react components in grid structure - reactjs

I have a React application that is displaying a number of buttons in a modal that form part of a button group.
The values for these buttons are set up in the state object as shown below.
At the moment these buttons display within my modal each on a separate line one after the other.
I would like to have them displayed in a grid type structure. As there are 5 of them this might be 2 on the first 2 lines and then the last one
on the third line perhaps centered if possible.
How could I go about doing this? Sorry I realise I've not offered a solution attempt for this but I'm not sure where to start. I've been searching online
and can't find any examples that match what I am trying to do i.e. where the actual data for the table is set in state. I will continue to do more research but thought I'd ask on here to see if someone could offer any tips.
const marginSelectionControls = [
{ label: '21+', type: '21plus' },
{ label: '16-20', type: '16to20' },
{ label: '11-15', type: '11to15' },
{ label: '6-10', type: '6to10' },
{ label: '1-5', type: '1to5' }
];
const MarginSelectionControls = (props ) => (
<div className="btn-group">
<ButtonGroup toggle={this.toggle}>
{marginSelectionControls.map( ctrl => (
<MarginSelectionControl
key={ctrl.label}
label={ctrl.label}
selectMargin={(winningMargin) => props.selectMargin(ctrl.label)}
selectedmargin={props.selectedmargin}
/>
))}
</ButtonGroup>
</div>
);
##########################################
const MarginSelectionControl = (props) => {
const MarginSelectionControlClasses = [classes.PredResSelectControl];
if (props.selectedmargin === props.label) {
MarginSelectionControlClasses.push(classes.Less);
}
else {
MarginSelectionControlClasses.push(classes.More);
}
return (
<div className={classes.MarginSelectionControl}>
<button
type="button"
label={props.label}
className={MarginSelectionControlClasses.join(' ')}
onClick={props.selectMargin}
selectedmargin={props.selectedmargin}
>{props.label}</button>
</div>
)
};

Related

How can animate between array filter methods with React?

Im currently building my personal Portfolio and I have an array of objects containing multiple projects, each one of the project object contain a property called category, which can have a value of 'Visual' or 'Complexity'.
My plan is to add two buttons, one for each category, but I would like to animate between the category changes, a simple fade out and fade in would be nice, any idea how can I do this ?
I tried using React Transition Group for this, but didn't manage to figure it out.
The first Component is the ProjectUl, this component will receive a filter prop, which is basically a string indicating which category should the filter method use.
const projects = [
{
name: "Limitless",
category: "visual",
dash: "- Landing Page",
img: imgList.limitless,
gif: imgList.limitlessGif,
},
{
name: "Spacejet",
category: "complexity visual",
dash: "- Landing Page / API Project; ",
img: imgList.spacejet,
},
];
export const ProjectsUl: FC<IProps> = ({ filter }) => {
const [filtered, setFiltered] = useState<IProject[]>([])
useEffect(() => {
const filteredProjects = projects.filter((i) => i.category.includes(filter));
setFiltered(filteredProjects);
}, [filter]);
return (
<ProjectsUlStyle>
{filtered.map((i) => (
<ProjectsLi filtered={i} />
))}
</ProjectsUlStyle>
);
};
Then, inside the ProjectsUl there will be the ProjectsLi, which are the list of projects, here is the code for the ProjectsLi
export const ProjectsLi: FC<any> = ({ filtered }) => {
return (
<LiStyle>
<img src={filtered.img} />
<div>
<span className="dash">{filtered.dash}</span>
<header>
<h1>{filtered.name}</h1>
<FAicons.FaGithub />
<FAicons.FaCode />
</header>
</div>
</LiStyle>
);
};

How does a parent know about the state of a list of child forms using formik?

I have a form consisting of a list of building materials which can be materials already registered for the construction and new materials:
The MaterialBar:
function MaterialBar({
materialBarId,
material,
onDelete,
onChange,
}: MaterialBarProps) {
const { values, handleChange } = useFormik<MaterialBar>({
initialValues: material,
onSubmit: console.log,
enableReinitialize: true,
});
const updateField = (event) => {
handleChange(event);
onChange(materialBarId, values);
};
return (
<DropdownWrapper>
<Dropdown
label="Material"
items={/* A list of available materials */}
selectedItem={values.material}
onSelect={updateField}
/>
.... Other fields
<TextField
name="amount"
id="material-amount"
label="Amount"
type="number"
onChange={updateField}
/>
<DeleteButton onClick={() => onDelete(materialBarId)}>
<img src={remove} />
</DeleteButton>
</DropdownWrapper>
);
}
The parent (ConstructionMaterials):
function ConstructionMaterials({
project,
materials,
attachMaterial,
}: ProjectMaterialsProps) {
const [allMaterials, setAllMaterials] = useState<IProjectMaterial[]>(
getProjectMaterialsFrom(project.materials)
);
const saveAllMaterials = () => {
allMaterials.forEach((newMaterial) => {
if (newMaterial.isNew) {
attachMaterial(newMaterial);
}
});
};
const updateNewMaterial = (
materialBarId: number,
updatedMaterial: MaterialBar
): void => {
const updatedList: IProjectMaterial[] = allMaterials.map((material) => {
if (material.projectMaterialId === materialBarId) {
return {
...material,
materialId: materials.find(
(currentMaterial) =>
currentMaterial.name === updatedMaterial.material
)?.id,
type: updatedMaterial.type,
length: updatedMaterial.length,
width: updatedMaterial.width,
value: updatedMaterial.amount,
};
}
return material;
});
setAllMaterials(updatedList);
};
// Adds a new empty material to the list
const addMaterial = (): void => {
setAllMaterials([
...allMaterials,
{
projectMaterialId: calculateMaterialBarId(),
projectId: project.id,
isNew: true,
},
]);
};
return (
<>
{allMaterials.map((material) => {
const materialBar: MaterialBar = {
material: material.name || "",
type: material.type || "",
amount: material.value || 0,
length: material.length || 0,
width: material.width || 0,
};
return (
<AddMaterialBar
key={material.projectMaterialId}
materialBarId={material.projectMaterialId!}
materials={materials}
onDelete={removeMaterial}
onChange={updateNewMaterial}
/>
);
})}
<Button onClick={() => saveAllMaterials()}>
{texts.BUTTON_SAVE_MATERIALS}
</Button>
</>
);
}
I have a hard time figuring out how to manage the list of materials. I use Formik (the useFormik hook) in the MaterialBar to take care of the values of each field.
My challenge is how to keep all the data clean and easily pass it between the components while knowing which materials are new and which already exist. If I just use Formik in the MaterialBar, then ConstructionMaterials does not know about the changes made in each field and it needs the updated data because it calls the backend with a "save all" action (the "Save"-buttons in the image should not be there, but they are my temporary fix).
To circumvent this, I also keep track of each material in ConstructionMaterials with the onChange on MaterialBar, but that seems redundant, since this is what Formik should take care of. I have also added a isNew field to the material type to keep track of whether it is new, so I don't two lists for existing and new materials.
I have had a look at FieldArray in ConstructionMaterials, but shouldn't Formik be used in the child, since the parent should just grab the data and send it to the API?
So: is there a clever way to handle a list of items where the parent can know about the changes made in the childs form, to make a bulk create request without the parent having to also keep track of all the objects in the children?
Sorry about the long post, but I don't know how to make it shorter without loosing the context.

Updating Formik State whilst using Headless UI "Listbox" (which manages its own state, internally)

I'm struggling to get a Headless UI Listbox implementation working successfully with Formik. After reading all the docs and scouring StackOverflow, I'm yet to find an answer.
I can get the Listbox to function perfectly well on its own, and Formik is working with other, less complex (but still custom) components. That said, I can't get them working in unison. Whenever I change the selection in the Select component, I can successfully update the Headless UI state (in as much as it updates to the correct value) and the Formik state, but for some reason the active and selected properties (within Headless UI) aren't working correctly (always false).
I assume what I need to do is to make use of the onChange handler so that it updates both the Headless UI state (to keep the active and selected properties updated) and the Formik value, but this doesn't seem to work as expected.
Does anyone have any suggestions? Please see a minimal representation of the code below:
export function TestForm() {
return (
<Formik
initialValues={{ testing: {} }}
onSubmit={ alert("Submitted"); }
>
<Form>
<Select
name="testing"
items={[
{ id: 1, name: "Test 1" },
{ id: 2, name: "Test 2" },
{ id: 3, name: "Test 3" },
]}
/>
</Form>
)
}
export function Select(props) {
// Get field properties
const [field, meta, helpers] = useField(props);
// Set initial value for `Select`
const [selectedItem, setSelectedItem] = useState(null);
// On change, update `Headless UI` and `Formik` values
const handleChange = (newValue) => {
setSelectedItem(newValue); // Update the Headless UI state
helpers.setValue(newValue); // This seems to "break" the Headless UI state
};
// Return `Select` structure
return (
<Listbox
value={selectedItem}
name={props.name}
onChange={handleChange}
// onBlur={field.onBlur} ...Commented out for now as trying to figure out issue with `onChange`
>
<Listbox.Label>Test Label:</Listbox.Label>
<Listbox.Button>
{selectedItem ? selectedItem.name : "-- Select --"}
</Listbox.Button>
<Listbox.Options>
{props.items.map((item) => {
return (
<Listbox.Option
className={({ active }) => (active ? "active" : "")}
key={item.id}
value={item}
>
{({ selected }) => (
<>
{item.name}
{selected ? <CheckIcon /> : null}
</>
)}
</Listbox.Option>
);
})}
</Listbox.Options>
</Listbox>
);
}
I managed to make it work like this:
import { useField } from "formik";
interface SelectProps {
name: string;
label: string;
options: string[];
}
export const Select: React.FC<SelectProps> = ({
name,
label,
options,
}) => {
const [field] = useField({ name });
return (
<Listbox
value={field.value}
onChange={(value: string) => {
field.onChange({ target: { value, name } });
}}
>
...
</Listbox>
);
};

Usestate hook with object arrays

For some reason I can't seem to find an answer to this rather simple task I believe. My challenge is that I have a usestate array which holds multiple objects depending on the user input. It can have as many as the user chooses or none. For some reason I can't get it to work right.
What I mean is that whenever I set the state (for example: setFinal(final => [...final, value]);) in any way that I've been able to come up with so far it doesn't work properly. It works fine when the user adds on click a new object to the array but deleting them and particularly coming from 1 to 0 to 1 again causes the state to be invalid. What I mean is that the last one before going to 0 doesn't get deleted and when you start to add new objects from 0 again it has already one value in state.
So, what is the right way to set state so that it works correctly no matter if you're adding or deleting objects from the state?
Hopefully I've made myself clear enough for a solution to this. It should be fairly straightforward but I don't seem to get it right and googling doesn't seem to do the trick. Thanks in advance for anyone helping me.
EDIT:
Something for clarification:
example objects:
0: Object { id: 484, data: [] }
1: Object { id: 524, data: [] }
2: Object { id: 170, data: (3) […] }
What I've tried so far in setting state:
setFinal(final => [...final, value]);
setFinal(value);
setFinal(final => value);
I would need it to work no matter whether you add or delete, delete them all and add again, it should work in all of these conditions.
Are you looking something like this? Live Demo
function App() {
const [arr, setArr] = useState({
numbers: [
{ id: 1, name: "Reactjs" },
{ id: 2, name: "Vuejs" },
{ id: 3, name: "Nodejs" }
]
});
const onclickHandler = event => {
setArr({
...arr,
numbers: [
...arr.numbers,
{
id: Math.floor(Math.random() * 100),
name: `${Math.random()} technology`
}
]
});
};
const deleteIt = item => {
let updated = arr.numbers.filter(a => a.id !== item.id);
setArr({ numbers: updated });
};
return (
<div className="App">
<button onClick={onclickHandler} value="4">
Add
</button>
<ul>
{arr.numbers.map(a => (
<li key={a.id}>
<span>
{a.id}-{a.name}
</span>
<button style={styles} onClick={() => deleteIt(a)}>
X
</button>
</li>
))}
</ul>
</div>
);
}
const styles = {
color: "white",
backgroundColor: "red"
};
You can't delete element from array via setFinal(final => [...final, value]); If value is undefined then setFinal just ignore it. Please see: Delete item from state array in react

React: Get a list of tags by attribute values

I am trying to make a text which displays some information upon mouse hover. For example, I have three tags with following information
<div class="body main-seq" style="display: inline;">
<span prob="67.8">
Foo
</span>
<span prob="67.8;34.6">
Bar
</span>
<span prob="67.8;34.6;52.7">
Hello
</span>
</div>
On a browser, it will look something like this
FooBarHello
Basically, when user hovers a mouse on first bit of the text (the one that corresponds to "Bar"), I want to bold all the span tags that contain "34.6" in its "prob" attribute. In this case, it would have to bold "BarHello", but leave "Foo" as it is.
After doing some Google search, this task seems pretty trivial in Javascript or jQuery, and can be done by doing something like so,
$("span[prob*='34.6']") (along with onMouseOver event or something similar)
Find an element in DOM based on an attribute value
However, I've seen many posts saying I should absolutely try to avoid using jQuery in React because React and jQuery has conflicting philosophy (React renders DOM every time the data changes whereas jQuery directly manipulates DOM). Please correct me if I am wrong though.
So my question is, how can I achieve this in React?
You could perhaps do something like this in React:
import React, { useState } from 'react';
function MyComponent(props) {
const [ isHovered, setIsHovered ] = useState(false);
const onMouseEnter = () => {
setIsHovered(true);
}
const onMouseLeave = () => {
setIsHovered(false);
}
const spans = [
{ text: 'Foo', probs: [67.8] },
{ text: 'Bar', probs: [67.8, 34.6] },
{ text: 'Hello', probs: [67.8, 34.6, 52.7] }
];
return (
<div
class="body main-seq"
style="display: inline;"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{spans.map(({ text, probs }) => {
const isBold = isHovered && probs.includes(34.6);
return (
<span style={{ fontWeight: isBold ? 'bold' : 'normal'; }}>
{text}
</span>
);
}}
</div>
);
}
I hope this helps.
I think you're approaching this the wrong way, but it's a great exercise in "thinking in React".
You basically have these span tags that store number values. Each of them have the same responsibilities:
Store a list of number values
When clicked, set the new "criteria" values
let the others know that they should check if they should be bold or not
So let's call this component SpanComponent. We want a structure like this:
export const SpanComponent = ({ values, activeValues, setActiveValues, children }) => {
const isBold = checkForMatchingValues(values, activeValues) // returns true or false
const onMouseEnter = event => {
setActiveValues(values)
}
return (
<span style={{ fontWeight: isBold ? 'bold' : 'normal' }} onMouseEnter={onMouseEnter}>{children}</span>
)
}
Then in our main component we can manage these as so:
export const MainComponent = () => {
const [activeValues, setActiveValues] = useState([]) // empty array as default value
return (
<SpanComponent values={[68.8]} activeValues={activeValues} setActiveValues={setActiveValues}/>
<SpanComponent values={[68.8, 34.6]} activeValues={activeValues} setActiveValues={setActiveValues}/>
<SpanComponent values={[68.8, 34.6, 52.7]} activeValues={activeValues} setActiveValues={setActiveValues}/>
)
}

Resources