I am building an accordion in which accordion items will only open if onCLick the mapped key assigned to the item matches a state held key.
If matched, the class of accordion-item-opened is added to the class list by a ternary operator. This uses the max-height hack to open the item. Clicking a different accordion item changes the state held key and therefore opens the clicked item and closes the previous item.
The problem:
Logically speaking, this all works. Items open and close. The problem is that they open and close instantly. There is a transition applied to the class to smoothly transition between max-height:0; and max-height:17rem which is not firing.
My assumption as to why this is happening is something to do with the asynchronous nature of setState.
The state looks like this:
this.state = {
vpWidth: 0, //ignore
breakpoint: 797, //ignore
activeIndex: 0,
}
data comes from a graphQL query and then mapped to the functional component containing the accordion item div, for context, that looks like this:
{data.map((node, index) => {
return (
<>
<li {...{ className: "accordion-list-item", index }}>
<TestItem {...node} index={index} />
</li>
</>
)
})}
The functional component containing the accordion item div housing both the ternary operator and the onClick looks like one of these two ways I have tried it. The first looks like this:
const TestItem = ({ node, index }) => {
return (
<div
className={
"accordion-item " +
(index == this.state.activeIndex ? "accordion-item-opened" : "")
}
onClick={() => {
if (index != this.state.activeIndex) {
this.setState({ activeIndex: index })
}
}}
>
//the rest of the component
This works as far as assigning the class and opening the item. However the transition does not fire.
I've atempted something like the below but this does nothing at all. I assume that, because of setState being async, at the time the ternary operator tests its argument index does not equal test. However the console log will show as true when the onClick is fired.
const TestItem = ({ node, index }) => {
let test = null
return (
<div
className={
"accordion-item " + (index == test ? "accordion-item-opened" : "")
}
onClick={() => {
if (index != this.state.activeIndex) {
this.setState({ activeIndex: index }, () => {
test = index
//lets say index == 1
console.log(test) //1
console.log(index) //1
//yet the ternary operator fails.
})
}
}}
>
Perhaps there is some way to directly assign the element class in the setState callback. If so, it's not something I have seen or done before and would be very interested to see.
SCSS
Stripped down to only the relevant parts.
#mixin transition(
$property: all,
$duration: 0.25s,
$ease: cubic-bezier(0.65, 0, 0.076, 1)
) {
-webkit-transition: $property $duration $ease;
transition: $property $duration $ease;
}
.accordion-item {
$self: &;
&-opened {
#{ $self }-inner {
max-height: 17rem;
}
}
&-inner {
#include transition;
max-height: 0;
overflow: hidden;
z-index: 1;
position: relative;
}
Related
Currently I'm working on Alarm clock app and I want to do it in way so you can add multiple Alarms. Every Alarm is stored in alarms:[] state in App.js file. If alarm is added,I want to display that alarm as a li element under the clock and I want to be able to remove it by clicking on X icon. Also ,when current time === time set for alarm ,Ring.js component renders and alarm starts ringing. When alarm is ringing there is 'turn off' button. How is it possible to delete this specific alarm which is ringing ,from state array after click on turn off button?
I've tried to send removeAlarm function and alarm(which may help in determining which alarm from array delete)as a prop to this component when condition if fulfilled.
function checkTime(){
if(time.alarms[0]){
const alarms = time.alarms.map(function(alarm,i){
if(time.currentHour === alarm.hour && time.currentMinute === alarm.minute && time.currentSecond
>= 0){
return <Ring message={alarm.message} key={i} alarm={alarm} removeAlarm={removeAlarm} />
}
})
return alarms;
}
}
removeAlarm function:
function removeAlarm(alarm){
setTime(prevState => ({
...prevState,
alarms:[...prevState.alarms.filter(el => el !== alarm)]
}))
}
Ring.js file
let message = props.message;
function removeAlarm(alarm){
props.removeAlarm(alarm);
}
function turnOff(e,alarm){
e.preventDefault();
setShowRing(false);
removeAlarm(alarm);
}
<form>
<h3>{message}</h3>
<button onClick={turnOff}>TURN OFF</button>
</form>
I can't figure it out how to do that. I don't know how to use that passed function or how to determine in that onClick function that THIS is that button which has to be send to removeAlarm function ,map thru state and remove that specific one.
Also second problem which I've noticed is with React Spring Transitions. I'm using it in Ring.js,Alarm.js and want to use it also for listing active alarms in ListAlarms.js. I'm using it the exact same way as in first two components but for ListAlarms.js it's not working and I don't undestand why. My goal is to display those active alarms with transitions not just 'blink' there.
Thank you.
CodeSandBox link here
OK some corrections but you have to alter the transitions
First of all you need to filter your list by id, in order to remove correctly the alarm.
function removeAlarm(alarm){
setTime(prevState => ({
...prevState,
alarms:[...prevState.alarms.filter(el => el.id !== alarm.id)]
}))
}
Secondly, I have removed the from property from your transition, since every new object was positioned on top of others. Also, instead of null for the key I used mapping to each item's id.
(item) => item.id
Finally I corrected the order in map function
{listAlarmTransitions.map(({ item, props, key }) => (
So it became
const listAlarmTransitions = useTransition(props.alarms, (item) => item.id, {
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<ul>
{listAlarmTransitions.map(({ item, props, key }) => (
<animated.div key={key} style={props}>
<li
key={item.id}
id={item.id}
onClick={() => {
removeAlarm(item);
}}
>
<FontAwesomeIcon icon={faTimesCircle} className="listIcon" />
<h3>{item.message}</h3>
<span>
{item.hour}:{item.minute}
</span>
</li>
</animated.div>
))}
</ul>
);
Check this sandbox
https://codesandbox.io/s/broken-morning-upqwp
You are filtering out objects/references you should filter out by id.
Your passed alarm argument is an object and your alarms filter array contains objects, find a unique property which you can filter against, by looking at your code, it should be id.
Something like this:
function removeAlarm(alarm){
setTime(prevState => ({
...prevState,
alarms:[...prevState.alarms.filter(el => el.id !== alarm.id)]
}))
}
For reference, check out this StackBlitz Link
I have css animation #keyframes class..
/*
Animation classes.
*/
.example-enter {
animation: flasher 0.5s;
}
#keyframes flasher{
from {
opacity: 0.2;
}
to {
opacity: 1;
}
}
I am applying this .example-enter css class to div when array.map is having new added data. like below..
<div className="example-enter"> ... </div>
The question is when I added new Array to state, react will create new div, and gives animation to last added div only. But I want very first element to be animated, not last one when added new state to array.
You can check out demo in above stackblitz link to check out more details and see there when we add new list only last div is animated but i want to animate very first element to animate because that first div element is i added to array. using.. setTodoState( prev => [newTodoState, ...prev ]);
I guess the problem is on this line, you're using index as key for your LineItem.
todoState.map((item, index) => <LineItem key={index} todoList={item} index={index}/>)
React does not recomment using indexes for keys, read more here.
You need to have some sort of id to uniquely identify your todo items and use that id as key when you do your todoState.map((item) => .... So you'll have to make your todo item as an object and not a string.
In your TodoForm, we'll add a generateId and use this functions return value for our new todo's id.
TodoForm.tsx
function generateId() {
// https://stackoverflow.com/a/8084248/8062659
return Math.random().toString(36).substring(7);
}
const sendTodoItem = (e) => {
if(todoInput.trim().length > 0){
const text = todoInput.trim();
const id = generateId();
addNewTodoState({ text , id }); // notice here
setIsDisabled(false);
setTodoInput('');
}else{
setIsDisabled(true);
}
}
Now on you TodoListLineItem, we'll remove the index from map and use the current todo's id as key.
TodoListLineItem.tsx
todoState.map((item) => <LineItem key={item.id} todoList={item}/>)
We also have to update your Line-Item to display the text property of the current todo.
Line-Item.tsx
<p>{todoList.text}</p>
Check this link for a demo.
--
Because we're using now an object as a todo item, I believe you should also make changes on some parts of the app, like the model and the delete feature. But the changes above should be sufficient to achieve what you want as described on your post.
A simple approach is to add the animation class with useEffect and remove it after the duration of the animation.
import React, {useEffect, useState} from 'react'
const YourApp = props => {
const [animate, setAnimate] = useState(false)
const [todoState, setTodoState] = useState([])
useEffect(() => {
setAnimate( true )
setTimeout( () => { setAnimate( false ) }, 500 )
}, [todoState])
return <div className={ animate ? "example-enter" : null } > ... </div>
}
Each time you update todoState, it will apply the animation class for 0.5s basically.
I'm having an interesting issue that I cannot debug.
Goal
On a class component, inside of render function, iterate over an array of objects from state using this.state.items.map((item, index) => {}) and return a contentEditable paragraph element.
On each contentEditable paragraph element, listen for the onKeyUp event. If the key being used from e.which is the enter (13) key, add a new item to this.state.items using the index of the element that was keyed, in order to insert a new element after that index using splice.
Seeing Expected Result?
No. The newly added item is instead being put at the end of the loop when it is being rendered.
Example situation and steps to reproduce:
Type "test1" into the first P element
Hit enter (a new P element is created and focused)
Type "test2" into this second, newly created, P element
Refocus on the first P element, either by shift+tab or clicking
Hit enter
See observed results: a new P element is created and focused, but it is at the end of the list and not where it is intended to be, which is between the "test1" and "test2" P elements
Here is the code that I have so far:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
items: [this.paragraphTemplate()]
}
}
render() {
return (
<section>
<div>
{this.state.items.map((item, index) => {
return <p ref={item.ref}
key={index}
contentEditable
suppressContentEditableWarning
onKeyUp={e => this.handleParagraphKeyUp(e, index, item)}></p>
})}
</div>
</section>
)
}
handleParagraphKeyUp = (e, index, item) => {
if (e.which === 13) {
let addition = this.paragraphTemplate()
this.setState(state => {
state.items.splice(index + 1, 0, addition)
return {
blocks: state.items
}
}, () => {
addition.ref.current.focus()
/* clear out the br and div elements that the browser might auto-add on "enter" from the element that was focused when the "enter" key was used */
this.state.items[index].ref.current.innerHTML = this.state.items[index].ref.current.innerHTML.replace(/<br\s*[\/]?>/gi, '').replace(/<[\/]?div>/gi, '')
})
return false
}
}
paragraphTemplate = () => {
return {
ref: React.createRef()
}
}
}
export default MyComponent
Here is a jsfiddle with the code from above.
If you take the above steps, you will see the issue that I am having.
Let me know if you require any further information, thanks in advance!
P.S. Please let me know if there any improvements that I can make to the code. I have been working in React for a short amount of time, and would love any feedback on how to make it better/cleaner.
UPDATED
Added key={index} to the P element. Note: this does not reflect any answers, it was merely added to stay in line with ReactJS list rendering.
to render a list of items, React needs key to keep track of the element
see this: https://reactjs.org/docs/lists-and-keys.html
here is your updated fiddle that working..
<p ref={item.ref}
key={item.id}
contentEditable
suppressContentEditableWarning
onKeyUp={e => this.handleParagraphKeyUp(e,
I have a highscore list that updates automatically with current scores once every minute.
It then updates the component with this.setState({ data: newData });.
Below I sort the new data by score, run each item through map that updates their style.top.
Javascript
...
let current = this.state.data.sort((a,b) => {
if(a.score < b.score) return -1;
if(a.score > b.score) return 1;
return 0;
});
let items = current.map((item,i) => {
return (
<div
key={item.name}
className="item"
style={{ top: (i*30) + 'px', backgroundColor: item.color }}>
{item.name}
</div>
);
});
return (
<div>
{items}
</div>
);
CSS
.item {
position: absolute;
transition: top 1s;
}
I want the items to animate up and down to their new positions, but this doesn't happen. The browser seems to remember parts of the DOM, but not others.
React seems to understand which item is which by using the unique key, if I inspect using React Developer Tools, but the actual DOM doesn't.
Even stranger, only items moving up the list (higher score) seems to be remembered. Items moving down is recreated, and therefor the transition doesn't work. They just pop into existence without transitioning to their new position.
Here is a working example to demonstrate. (JSFiddle)
If you change it so that the items are not reordered between render cycles, but are always rendered in the same order with different top styles, it animates correctly. I think this is because changing the order of elements forces them to be removed from the DOM and re-added by react.
The relevant code is:
render() {
let current = this.state.data.slice().sort((a,b) => {
if(a.score < b.score) return -1;
if(a.score > b.score) return 1;
return 0;
});
let items = this.state.data.map(item => {
let position = current.indexOf(item);
return (
<div
key={item.name}
className="item"
style={{ top: (position*30) + 'px', backgroundColor: item.color }}>
{item.name}
</div>
);
});
return (
<div>
{items}
</div>
);
}
Updated demo
React redraws the DOM elements you update. This will resolve in really odd behaviours, such as blinking, jumping etc.
The React team offered a couple of solutions for this before they started working on Fiber instead. With the release of Fiber I hope we'll see better support for transitions and animations.
Until that time, I recommend you use external libs for animations such as:
https://github.com/FormidableLabs/react-animations For general animation.
And https://github.com/reactjs/react-transition-group for elements entering and exiting the DOM.
I have a list of elements created by using an array in react. On user click how can I make the clicked element active (by adding a CSS class) while making the other elements inactive (by removing the active class)?
My rendering of elements looks like this.
{this.props.people.map(function(person, i){
<div className='media' key={i} onClick={state.handleClick.bind(state,i,state.props)}>
<item className="media-body">{person.name}</item>
</div>
}
When the user clicks on one of these elements an active class will be added to the clicked 'media' element making the clicked element 'media active' while removing the 'active' class from the previously clicked element??
constructor(props) {
super(props);
this.state = { activeIndex: 0 };
}
handleClick(index, props) {
// do something with props
// ...
// now update activeIndex
this.setState({ activeIndex: index });
}
render() {
return (
<div>
{
this.props.people.map(function(person, index) {
const className = this.state.activeIndex === index ? 'media active' : 'media';
return (
<div className={className} key={index} onClick={handleClick.bind(this, index, this.props)}>
<item className="media-body">{person.name}</item>
</div>
);
}, this)
}
</div>
);
}
For the sake of clean code I personally would suggest you creating subcomponents to add functionality to mapped elements.
You could create a small subcomponent which simply returns the element which you want to add functionality to just like this :
...
this.state = {
active: false
}
...
return(
<div className=`media ${this.state.active ? 'active' : ''` onClick={()=>{this.setState({active: true})}}>
<item className="media-body">{this.props.name}</item>
</div>
)
...
And in your map function you simply pass the contents as properties:
{this.props.people.map(function(person, i){
<SubComponent key={i} {...person} />
}
That way you stay with clean code in the "root" component and can add complexity to your subcomponent.
In your handleClick method you could store in the component's state the clicked person (looks like the collection is of people). Then set the className conditionally based on, say, the person's id.
You can set the className using something like:
className={this.state.clickedPersonId === i ? 'media media--clicked' : 'media'}
(NB This is using i, the index of the item in the people array; you may want to use something a little more explicit, like the person's real Id.)