How do i make a component like this in react? - reactjs

What I needed was something like that shown in the picture. I need to show certain names and if the names list exceeds more than 2 rows I need to show +n others. If the user clicks +n others the list needs to be expanded to show all the others.
Is there any component available in react to get this result? I have seen it on a number of websites but don't know what they are called.
I could write the component myself but the difficult part would be how many names to show before i show the +n others. I can only show 2 rows initially and each name can be of variable length. So in one case, a single name may take up the entire 1st row and in others, i may be able to fit 3 names.

You have to store the state of the list see it's expanded or not.
Something like this should help.
import React, { useEffect, useState } from "react";
const myList = ({ list }) => {
const MAX_COUNT = 5;
const [isExpended, setIsExpended] = useState(false);
useEffect(() => {
if (list.length <= MAX_COUNT) {
setIsExpended(true);
}
}, [list]);
const expend = () => {
setIsExpended(true);
};
return (
<>
<div>
{list.map((item, i) =>
(isExpended === false && i < MAX_COUNT) || isExpended === true
? item.text
: null
)}
</div>
{isExpended === false ? (
<button onClick={expend}>+ {list.length - MAX_COUNT} others</button>
) : null}
</>
);
};
export default myList;
If you want to stick with 2 rows on any conditions there will be 2 approach to set the dynamic MAX_COUNT:
1: if you have a constant value for box-sizing and fonts and etc:
You can compute outerWidth of each elements (with box-sizing and elements length) and set a real MAX_COUNT based on that.
2: if you have responsive design:
you can render component with initial MAX_COUNT but hide contents with visibility: hidden and then computing outerWidth of each elements would be more realistic and much more easier.
in this scenario you have to fix the container height to prevent it from extending too much, just set a constant height to it, also you can change it after you get a real MAX_COUNT. now you can show contents with no worries.

Related

Keen Slider React Component Always One Step Behind

I installed the keen-slider library in my React project, and used the code from the App.js file in this example to set up a slider with page dots and navigation arrows. I am trying to modify that code, by passing in an array of React components, the size of which can be changed when the user selects or deselects options.
The problem is, the slider's dot count and arrow configuration always lags one step behind. If I move from 1 (default) to 2 pages selected, the rendered dot count stays at 1. When I increase to 3, it moves to 2. If I then decrease to 2, it goes to 3. It only catches up if I interact with the slider. In my App component's return, I place the slider as {keenSlider(outputComponentArray)}. To get outputComponentArray, I have some divs with onClick functions that toggle each page type's selected state. This array:
var selectedResultsConfig = [
['Proposal', outputs.proposal, resultSelectorProposal, setResultSelectorProposal],
['Map', outputs.map, resultSelectorMap, setResultSelectorMap],
['Front Page', outputs.frontPage, resultSelectorFrontPage, setResultSelectorFrontPage],
['Collage', outputs.collage, resultSelectorCollage, setResultSelectorCollage],
['Price Letter', outputs.priceLetter, resultSelectorPriceLetter, setResultSelectorPriceLetter],
['Line Items', outputs.lineItems, resultSelectorLineItems, setResultSelectorLineItems]
]
establishes what name, page component (in the 'outputs' object), and toggle state/setting function correspond to each other, then these buttons are rendered with .map on this array, like so:
{selectedResultsConfig.map((item, index) => {
return <>
{(index === 0) ? null : <> </>}
<div className={item[2] ? 'resultSelectorButton selectedButton' : 'resultSelectorButton'} onClick={() => { resultSelectToggle(item[0]) }}>
<Icon path={item[2] ? mdiCheckboxMarked : mdiCheckboxBlankOutline} size={1} color='#ecd670' />
<h2>{item[0]}</h2>
</div>
</>
})}
and their onClick function does the toggling like this:
function resultSelectToggle(button) {
if (screen === 'proposals') {
for (let i = 0; i < selectedResultsConfig.length; i++) {
if (button === selectedResultsConfig[i][0]) {
selectedResultsConfig[i][3](!selectedResultsConfig[i][2])
}
}
}
}
and then I have a useEffect hook that goes off after those toggles and sets up the final component array, which is fed to keen-slider:
//after the result selector button is toggled
useEffect(() => {
var tempComponentArray = [];
if (screen === 'proposals') {
for (let i = 0; i < selectedResultsConfig.length; i++) {
if (selectedResultsConfig[i][2]) {
tempComponentArray.push(selectedResultsConfig[i][1])
}
}
}
setOutputComponentArray(tempComponentArray);
}, [resultSelectorProposal, resultSelectorMap, resultSelectorFrontPage, resultSelectorCollage, resultSelectorPriceLetter, screen])
I'm not the most experienced with React, and I already know there are better ways of doing some of this, but it's not clear to me what is causing my issue. I was having a similar issue once that was fixed with useEffect, but I've already implemented that here. Any help would be appreciated, thanks.
I have made significant modifications to my original code to simplify it, and I originally passed keenSlider a functional component, but still the problem persists.

React - toggle css class but do not rerender everything

I have a Gatsby website and I want to use it to show flashcards. I have found multiple existing solutions online, but I would like to try it a different way. However, I am completely stuck.
Desired result:
Show one random word at a time. When the users clicks on a button 'show', the meaning of the word should be shown.
Issue:
I can generate a random combination of the words. But when the user clicks on 'show', a new combination is loaded instead of the answer. I understand why this happens. However, I do not understand how to fix this.
I have tried this (stripped to the bare essentials):
export const Flashcard = () => {
const [flip, setFlip] = useState(false)
const onButtonClick = () => {
setFlip(!flip)
}
let word = words[GenerateRandomNumber(0, words.length - 1)] // words is an array containing all the possible combinations
return (
<div className={`card ${flip ? "flip" : ""}`}>
<div className="front">
{word["language1"]}
</div>
<div className="back">
{word["language2"]}
</div>
<button onClick={onButtonClick}>Show answer</button>
</div>
)
}
export default Flashcard
I feel like I am on a totally wrong path, but don't see how to fix it. Should I structure my logic different? Should I use some other hooks? Or anything else?
In addition, I would also like the user having the possibility to say he knew the meaning (clicking on a button 'correct') or not (button 'wrong'). If correct, a new word is shown, if wrong, the word is stored to be shown later again. How should I trigger this logic?
I think you should add a state to store the picked word
const [word, setWord] = useState("");
and When you do a click you need to get a random word and store in word
useEffect(() => {
const randomNumber = GenerateRandomNumber(0, words.length - 1);
const newWord = words[randomNumber];
setWord(newWord);
}, [flip]); //This useEffect will call everytime when flip change.
in return you can refer the word
{word["language1"]}
here is a demo

react-virtualized: how to use a separate rowRenderer for height measurement

I'm trying to display large text documents with complex markups using the react-virtualized List component. The document is broken up into chunks of text of varying lengths shorter than some maximum. The List component renders each of these chunks as a row.
Since I can't use fixed row heights because chunk lengths vary, I'd like to use CellMeasurer. The problem is that the parsing needed to generate the markups on each chunk is expensive -- this is part of the reason I want to use react-virtualized. Even if all the chunks are rendered in the background, it will still be too slow.
Since the markup does not affect height, I'd like to use a simpler rowRenderer function that renders text without markup only for measuring rows, and then provide a separate and more complete rowRenderer for the actual render of each chunk with markup. Is there a way to do this?
This is admittedly really hacky, and not best practices, but could you do something like:
<List
rowHeight={(index) => {
const rowData = data[index];
let div = document.getElementById('renderer');
if (!div) {
div = document.createElement('div');
div.id = 'renderer';
}
ReactDOM.render(div, <Row>{rowData}</Row>);
const height = div.offsetHeight;
ReactDOM.unmountComponentAtNode(div);
if (index === data.length - 1) {
div.parentNode.removeChild(div);
}
return height;
}}
/>
Or, actually, you could have two lists, one with visibility: hidden, where you just render each row without markup, get the height, and add it to an array. Once the length of the array is equal to your data length, you no longer show it, and then render the other one, with rowHeight={index => heights[index]}
After some trial and error I found a good way to do this is to make the rowRenderer function decide which way to render the row. You can do this by checking the _rowHeightCache property in the CellMeasurerCache instance use use in your list. Note that the keys of _rowHeightCache take the form: "index-0" where index is the row's index.
Here is how you can set up the row renderer:
TextRowRenderer(
{
key, // Unique key within array of rows
index, // Index of row within collection
// isScrolling, // The List is currently being scrolled
style, // Style object to be applied to row (to position it)
parent, // reference to List
},
) {
const { textArray } = this.props;
// get the cached row height for this row
const rowCache = this.listCache._rowHeightCache[`${index}-0`];
// if it has been cached, render it using the normal, more expensive
// version of the component, without bothering to wrap it in a
// CellMeasurer (it's already been measured!)
// Note that listCache.defaultHeight has been set to 0, to make the
// the comparison easy
if (rowCache !== null && rowCache !== undefined && rowCache !== 0) {
return (
<TextChunk
key={key}
chunk={textArray[index]}
index={index}
style={style}
/>
);
}
// If the row height has not been cached (meaning it has not been
// measured, return the text chunk component, but this time:
// a) it's wrapped in CellMeasurer, which is configured with the
// the cache, and
// b) it receives the prop textOnly={true}, which it tells it to
// to skip rendering the markup
return (
<CellMeasurer
cache={this.listCache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
{() => {
return (
<TextChunk
key={key}
chunk={textArray[index]}
index={index}
style={style}
textOnly
/>
);
}}
</CellMeasurer>
);
}
This is then passed to the List component in the ordinary way:
<List
height={pageHeight}
width={pageWidth}
rowCount={textArray.length}
rowHeight={this.listCache.rowHeight}
rowRenderer={this.TextRowRenderer}
deferredMeasurementCache={this.listCache}
style={{ outline: 'none' }}
ref={(r) => { this.listRef = r; }}
/>
By including a reference callback, we can now access the
measureAllRows method, which we can use to force the List
to render all rows in advance to get the heights. This will
ensure that the scroll bar functions properly, but even with
the textOnly flag can take a while longer. In my case, I believe
it is worth the wait.
Here is how you can call measureAllRows:
componentDidUpdate() {
const {
textArray,
} = this.props;
// We will only run measureAllRows if they have not been
// measured yet. An easy way to check is to see if the
// last row is measured
const lastRowCache = this
.listCache
._rowHeightCache[`${textArray.length - 1}-0`];
if (this.listRef
|| lastRowCache === null
|| lastRowCache === undefined
|| lastRowCache === 0) {
try {
this.listRef.measureAllRows();
} catch {
console.log('failed to measure all rows');
}
}
}
The try-catch block is needed because if it tries to measure
before the List is constructed it will throw an index-out-of
-bounds error.
FINAL THOUGHTS
Another possible way to do this would be to have the actual List
component be conditional on cached measurements rather than the
rowRenderer function. You could render an array of text as a a
regular list, wrapping each row in a <CellMeasurer>. When the
cache is filled, you would then render a virtual List configured
using the prefilled cache. This would obviate the need to call
measureAllRows in componentDidUpdate or a useEffect callback.

React Table using hooks expand and collapse rows

I am using react-table component inside my project. The row expansion property is something that my features utilized and it is working fine now.
I need the ability to collapse all the rows while I expand a row. ie Only one row should be open at a time. I did go through many documentation and stackoverflow links but none didn't work out. Please note that this implementation is using hooks. Just like the one mentioned here : https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/expanding
By default they allow to open more than one row at a time, but I need to implement the opposite.
The closest I came to is this : Auto expandable rows and subrows react table using hooks
But here its opening on initial load.
Thanks
I have only added a portion of App function here. Codesandbox: https://codesandbox.io/s/jolly-payne-dxs1d?fontsize=14&hidenavigation=1&theme=dark.
Note: I am not used to react-table library. This code is a sample that only works in the table with two levels of rows. You can optimize the code with recursion or some other way to make the code work in multi-level tables.
Cell: ({ row, rows, toggleRowExpanded }) =>
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter
// to build the toggle for expanding a row
row.canExpand ? (
<span
{...row.getToggleRowExpandedProps({
style: {
// We can even use the row.depth property
// and paddingLeft to indicate the depth
// of the row
paddingLeft: `${row.depth * 2}rem`
},
onClick: () => {
const expandedRow = rows.find(row => row.isExpanded);
if (expandedRow) {
const isSubItemOfRow = Boolean(
expandedRow && row.id.split(".")[0] === expandedRow.id
);
if (isSubItemOfRow) {
const expandedSubItem = expandedRow.subRows.find(
subRow => subRow.isExpanded
);
if (expandedSubItem) {
const isClickedOnExpandedSubItem =
expandedSubItem.id === row.id;
if (!isClickedOnExpandedSubItem) {
toggleRowExpanded(expandedSubItem.id, false);
}
}
} else {
toggleRowExpanded(expandedRow.id, false);
}
}
row.toggleRowExpanded();
}
})}
>
{row.isExpanded ? "👇" : "👉"}
</span>
) : null

React design question about creating remove functionality

This is a little long winded.
I would like advice on how to go about making the functionality of a remove button. I'm relatively new to react and should have planned this project before I embarked. Either way I am here now and looking for some advice before I remove functionality tomorrow.
In this component I create a method called "displayFood" in which I take an array from props that has in it string values of the names of foods that the user wanted to add to the refrigerator. For example: [yogurt, milk, egg, yogurt, yogurt]. I then create an object that maps this array to key value pairs based on name and quantity, for example: {"yogurt": 3, "milk": 1, "egg": 1}. After this I create an array that holds what I want to render to the user which is each item that they put in the fridge and the quantity of that item. I also have a remove button. I have been thinking about how to remove Items but do not know how to go about doing so.
If, for example the user deletes yogurt I want the value to decrease by one, and if the user deletes a item with quantity 1 then it should go away.
This is a pretty specific question thank you for your time.
import React from 'react';
import "./style.scss";
function InFrige(props) {
const handleRemove = (e, counts) => {
console.log(e.target.name)
}
const displayFood = () => {
var counts = {};
props.foodAddedByUser.forEach(function(x) { counts[x] = (counts[x] || 0) + 1; });
let foodItems = []
for(var index in Object.entries(counts)){
foodItems.push(
<div className="inFrige-food-item" key={index}>
<h3>{Object.keys(counts)[index]} x{Object.values(counts)[index]}</h3>
<button onClick={handleRemove} name={Object.keys(counts)[index]}>Remove</button>
</div>
)
}
return foodItems
}
return (
<div>
<div className="inFrige-food-container">
{displayFood()}
</div>
</div>
)
}
export default InFrige;
Your problem is you are trying to alter your props from within the component. You could either handle this inside the component with state or give a callback via props from the parent component and handle the deletion there, something like this:
<button onClick={()=>{this.props.handleRemove(Object.keys(counts)[index])}} name={Object.keys(counts)[index]}>Remove</button>
in parents render:
<InFridge handleRemove={(item)=>{foodAddedByUser.delete(item)} foodAddedByUser={foodAddedByUser} />

Resources