ReactJS loosing reference of an element - javascript

I am forcing element to be focused like this
/**focusing the element if the element is active */
componentDidUpdate() {
console.log(this.activeElementContainer);
if(this.activeElementContainer!==undefined && this.activeElementContainer!==null) {
/** need to focus the active elemnent for the keyboard bindings */
this.activeElementContainer.focus();
}
}
My render has conditional rendering the elements are being rendered dynamically from the array,
Let say I have one element in div and I am adding another from the toolbox. In that case I need to focus the last element I dragged.
render() {
let childControl= <span tabIndex="-1" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>;
if(this.props.activeItem){
childControl=<span ref={ (c) => this.activeElementContainer = c }
tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
}
//later I ma using childControl to array and it works fine.
The logs says, first time it works fine
But, second time the this.activeElementContainer is undefined
Is there any alternative way or possible solution to this?
The thing is I need to focus only one element at the time.
Remember: Activecontrol has too many things to do like it can have right click menu, drag etc. so, I need to render it separately.

After reading this one github:
This is intended (discussed elsewhere) but rather unintuitive
behavior. Every time you render:
<Value ref={(e) => { if (e) { console.log("ref", e); }}} /> You are
generating a new function and supplying it as the ref-callback. React
has no way of knowing that it's (for all intents and purposes)
identical to the previous one so React treats the new ref callback as
different from the previous one and initializes it with the current
reference.
PS. Blame JavaScript :P
Source
I changed my code to
<span id={this.props.uniqueId} ref={(c)=>
{if (c) { this.activeElementContainer=c; }}
} tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
Adding if was the real change. now, it has a ref.
For others who face this problem:
I need to write custom function too, in the componentDidUpdate I am still getting old reference,
ref={(c)=>{if (c) { this.activeElementContainer=c; this.ChangeFocus(); }}
Adding this was the perfect solution for me

Related

How can I use React Material UI's transition components to animate adding an item to a list?

I have this class.
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
items: []
};
this.add = this.add.bind(this);
this.clear = this.clear.bind(this);
}
add() {
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
}
clear() {
this.setState({ items: [] });
}
render() {
return (
<div>
<div>
<button onClick={this.add}>Add</button>
<button onClick={this.clear}>Clear</button>
</div>
{/* This is wrong, not sure what to do though... */}
<Collapse in={this.state.items.length > 0}>
<ul>{this.state.items}</ul>
</Collapse>
</div>
);
}
}
Sandbox link: https://codesandbox.io/s/material-demo-ggv04?file=/Demo.js
I'm trying to make it so that every time I click the "add" button, a new item gets animated into existence at the top of the list and the existing items get pushed down. Not sure how to proceed though.
Extra Resources
Example of what I'm trying to achieve: https://codeburst.io/yet-another-to-do-list-app-this-time-with-react-transition-group-7d2d1cdf37fd
React Transition Group Transition docs: http://reactcommunity.org/react-transition-group/transition (which seem to be used internally by Collapse)
I updated your Sandbox code to achieve what you wanted, but I don't think MaterialUI is the best library for that (I could be missing a better way to do it).
The challenge is that when you add a new item, that doesn't exist in the DOM yet. And most of those animation libraries/components require the element to be in the DOM and they just "hide" and "show" it with a transition time.
I had a similar situation and after some research, the better library I found that can handle animation for elements that are not yet in the DOM, was the Framer Motion. (You can check their documentation for mount animations)
Anyway, here is the link for the new Code Sandbox so you can take a look. The changes I made:
Removed random key
In the map function that creates your list using the <Collapse /> component, there was a function to get a random integer and assign that as a key to your component. React needs to have consistent keys to properly do its pretenders, so removing that random number fixes the issue where your "Toggle" button wasn't animating properly. (If your list of items doesn't have an unique ID, just use the index of the map function, which is not a good solution, but still better than random numbers).
<Collapse key={i} timeout={this.state.collapseTimeout} in={this.state.open}>
{it}
</Collapse>
Added a new function to control the toggle
The approach here was: add the item in your list and, after the element is in the DOM, close the <Collapse />, wait a little bit and open it again (so you can visually see the animation). In order to do that, we needed a new "toggle" function that can explicit set the value of the collapse.
toggleValue(value) {
this.setState(() => {
return {
open: value
};
});
}
Added a variable timeout for the collapse
The last issue was that, closing the <Collapse /> when the new item is added, was triggering the animation to close it. The solution here was to dynamically change the timeout of the collapse, so you don't see that.
setCollapseTimeout(value) {
this.setState(() => {
return {
collapseTimeout: value
};
});
}
When adding the element to the list, wait to trigger the animation
Again, to work around the issue with elements not yet in the DOM, we need to use a setTimeout or something to wait to toggle the <Collapse />. That was added in your add() function.
add() {
this.toggleValue(false);
this.setCollapseTimeout(0);
this.setState(prev => {
const n = prev.items.length;
return {
items: [<li key={n}>Hello, World {n}!</li>, ...prev.items]
};
});
setTimeout(() => {
this.setCollapseTimeout(300);
this.toggleValue(true);
}, 100);
}
Again, this is a hacky solution to make <Collapse /> from MaterialUI work with elements that are not yet in the DOM. But, as mentioned, there are other libraries better for that.
Good luck :)
Ended up here earlier on and then came back to create a sandbox showing hopefully a simple method for this scenario. The material-ui docs are a bit (lot) light in this area and I was fighting with a very similar situation, but I tried something with TransitionGroup from react-transition-group, crossed my fingers and it seemed to work.
Forked CodeSandbox with TransitionGroup
The gist is that you
wrap all of the components you want to transition in the <TransitionGroup> component
Inside the TransitionGroup, put in the "condition" (logic or loop output) for the data you want to render
Wrap the individual components you want to transition with transition component of your choice - <Collapse> in this example
e.g. In its most simple setup where "items" is an array of unique numbers coming from either props, state or a redux store
<TransitionGroup>
{items.map(item => (
<Collapse key={item}>
I am item {item}
</Collapse>
))}
</TransitionGroup>
With this setup I have found that I didn't need to put any props on the TransitionGroup or Collapse, and the TransitionGroup handled all the mounting and unmounting in the loop rendering. Material UI doesn't produce the lightest of HTML output, but I guess it's all rendered on the fly so maybe that makes it better (unless you have thousands of elements, then things start to drag).
You can even go a step further and wrap the whole thing in another TransitionGroup to cover situations where you want to remove the whole thing without transitioning all of the individual items - in this instance I switched it to a <Slide>. I was absolutely certain that this wouldn't work, but it seemed to not care. You can also try and be semantic and use the "component" property rather than wrapping in another element e.g.
{items.length > 0 && (
<TransitionGroup>
<Slide>
<TransitionGroup component="ul">
{items.map((item) => (
<Collapse component="li" key={item}>I am item {item}</Collapse>
))}
</TransitionGroup>
</Slide>
</TransitionGroup>
)}
I have changed the sandbox in the following ways
Included TransitionGroup from react-transition-group
Changed the "add" logic so that the components aren't part of the "items" array - the array only contains the data required to render the components
I have added a simple "count" and pushed that to the array to give the items a unique index (had originally used Math.random, but I wanted a "prettier" output). Generally your items will probably be coming from a database somewhere where a unique id will already be set.
Rendered the components in a loop based on the data in the array (this could be done in a separate function, but the gist is that the components aren't being stored in the array)
added a "delete" function to show the removal of single items
wrapped the whole group in a second <TransitionGroup> to show that the unmounting can happen in a group level
Put in some simple styling to get a better idea of the effect. You could use Material UI components here, but just wanted to keep it simple.
Hope this helps someone in the future.

draft.js: making editor content multipage

I have a difficult problem to solve.
Here is my sample project
Basically I want to create a editable book-like container and since each book page is another container, I can't figure how to make an editor transition from one page to another, once the page is filled with text.
Here is what I'm trying to achieve:
My first thought was to use a shared state for multiple editor instances, but as you can see in the example code, it doesn't work as expected and same text appears in two pages.
How can I achieve a multi-container transition when using draft.js?
The logic in general is to listen to changes of the first editor, count characters / word / lines (or more advanced calculation, if the editor has scroll for example) and finally focus the second editor if needed.
This answer covers, listen, dummy calculation and focus on the next editor.
This answer doesn't cover the calculation itself nor dynamic number of pages nor moving back from second editor when deleting content etc. But I believe that it's a direction to your final goal.
Listen
First of all, I moved the state creation and handling back to the Editor component itself. What "interests" the parent component ("App") is to get notified when its content changed.
But now, App needs to access the ("draft-js") editor in order to call focus. This will solve by creating "ref" in App and propagate it using forwardRef.
const editorRef = React.useRef();
const Editor = React.forwardRef((props, ref) => {
return <MyEditor name="editor-2" forwardRef={ref} />;
});
...
return (
...
<Editor ref={editorRef} />
)
and in Editor.js
function MyEditor({ onChange, forwardRef }) {
...
return (
<Editor
ref={forwardRef}
)
});
Next thing is to add onChange to Editor so App get notified when the content has been changed.
<Editor
ref={forwardRef}
stripPastedStyles
editorState={editorState}
onChange={editorState => {
setEditorState(editorState);
onChange && onChange(editorState); // <---
}}
/>
Handle the change and focus
The first editor is now
<MyEditor name="editor-1" onChange={onEditorChange} />
and onEditorChange is
const onEditorChange = editorState => {
const text = editorState.getCurrentContent().getPlainText();
if (text.length >= 5) {
setTimeout(() => {
editorRef.current.focus();
});
}
};
Currently it checks if the content's length is 5. You probably want to calculate it in a more sophisticated way.
The last question is, why setTimeout? Well, the simple answer is that without it, "draft-js" throw an error. I believe that it's something with their implementation of release / delete / clear global variables or something.
And the most important part, the code and live demo :)
https://codesandbox.io/s/young-shape-t6kcc?file=/src/App.js

React replace event.target with input

UPDATE
Here's are some demos
contentEditable demo - requires double click for H1 to become editable
replace with input demo - adopts event.target styles but makes the UI 'twitch' when rendered
So I have some functional components, let's say:
component1.js
import React from 'react';
const component1 = props => (
<div>
<h1>Title</h1>
</div>
);
export { component1 };
They are variable. event.target could be anything with text, so paragraph, heading, anything. I'm trying to let users edit content inline by clicking on it, so I'll pass a function editMode to these functional components, that'll update parent state with editing info, let's say like this:
<h1 onClick={event => {editMode(event, props.name, props.title, 'title')}}>title</h1>
This changes parent local state to have all the necessary information to grab the value from redux, define a target etc. For this example, props.name is the name of the component, props.title is the value, and 'title' is object key in redux.
So I'll add something to my component1.js and make it look a bit like this:
import React from 'react';
const component1 = props => (
<div>
{props.editState === 'true' &&
<EditLayout
name={props.name}
target={props.target}
value={props.value}
onChange={event => someFunc(event)}
/>
}
<h1>Title</h1>
</div>
);
export { component1 };
Now this works fine, except it doesn't scale. EditLayout, in this case, will just return an input with correct value. What I need it to do is to adapt to whatever is being clicked, get font size, background, padding, margin, position. Am I doing this right? Every way I try, I run into huge issues:
Idea 1 - move EditLayout component outside of the functional component
Issue: positioning
So I'll move EditLayout to parent component that contains both component1.js and EditLayout. This will allow me to manipulate it from inside the functional component, without having to include it everywhere. I'll then grab coordinates and other important information from event.target like so:
const coords = event.target.getBoundingClientRect();
const offsetX = coords.left;
const offsetY = coords.top;
const childHeight = coords.height;
const childWidth = coords.width;
const childClass = event.target.className;
I'll then wrap the EditLayout to return a container which contains an input, and apply size/coordinates to the absolutely positioned container. This'll present an issue of input being offset by a random amount of pixels, depending on how big/where is the event.target.
Idea 2 - pass relevant computed styles to EditLayout
Issue: twitching on render, and I have to add EditLayout for every possible event.target there is, as well as condition its' render
So I'll grab all important computed styles like this:
const computedTarget = window.getComputedStyle(event.target);
const childMargins = computedTarget.marginBottom;
const childPaddings = computedTarget.padding;
const childFontSize = computedTarget.fontSize;
const childTextAlign = computedTarget.textAlign;
And pass it to component1.js, and then pass it to EditLayout component inside the component1.js. I'll then condition theevent.target to hide if it's being edited like this:
<h1 className={ props.target === 'title' ? 'd-none' : ''}>Title</h1>
And condition the EditLayout to show only if it's needed:
{props.target === 'title' && <EditLayout />}
In this example, clicking h1 will show the input, but the layout itself with twitch on render. Input will have the exact same margin and font size as the h1, or event.target, but it'll appear bigger and extend the layout. Demo:
Idea 3 - Use conditional contentEditable
Issue: Requires double click to enable, doesn't work in safari, doesn't let me preselect the value
This is the weirdest of them all. I figured it'd be pretty simple, do something like this inside the functional component render:
<h1 contentEditable={props.target === 'title'} onClick={event => props.setTarget(event)}>Title</h1>
However, I have to double click to enable it. I have no idea why, if I attach a console log every time onClick is fired, I'll get correct outputs, I'll get the correct target value as well. I've tried numerous ways, but it simply requires double click. Even attempted to handle this inside the functional component, as most of the stuff is handled by a parent component, doesn't make a difference.
I have oversimplified the examples, so it's safe to assume/understand the following:
I am passing props in a correct fashion, they aren't undefined
I am using bootstrap
I am using styled components, and EditLayout is a styled component
which accepts props and turns them into CSS like: font-size: ${props
=> props.fontSize};
The values should be correct, I am not manipulating anything I get back from getComputedStyle() or getBoundingClientRect()
I am keen on keeping my functional components functional, and easy to
add. Functional components, in this case, are simple HTML structures,
and I'd like to keep them as simple as possible
So there's a neat solution to contentEditable requiring two clicks instead of one, instead of binding onClick and passing it to enable contentEditable, simply keep contentEditable true and handle the change however you like. Here's a working h1 that doesn't require two clicks to enable contentEditable, unlike the one in the demo
<h1
className="display-4 text-center"
contentEditable
suppressContentEditableWarning
onBlur={event => updateValues(event)}
>
Title
</h1>
The available methods for trigger update could be onBlur or onInput.

How can i force a template-update in Polymer?

How is it possible to show changes of existing items in a dom-repeat template in polymer?
i tried really all i could think of and i could find in the polymer documentation or in the web. but nothing works. below you find a html-page that uses a small list and tries to change one entry in the list to another value when you click the change button. But the only thing that would change the item in the list next to the change-button is the line that is commented out. all the other lines try it, but fail.
i understand that re-rendering a template is a time-consuming task and that it only should take place when it is necessary and that polymer tries to avoid it as much as possible. but why is it not possible for me (from the view of the code the god of their world ^^) to force a render on purpose?
the method of creating a complete new object, deleting the old item from the list and inserting the new object (thats what the commented line does) is a workaround, but it is a really huge effort, when the items are more complex and have properties or arrays that are not even displayed.
What am i missing? What did i not try? I would be very glad if anybody could tell me what i could do to achieve such a (from my point of view) simple and very common task.
EDIT (solved):
the solution of Tomasz Pluskiewicz was partly successful. but i updated the code to show my current problem. the name of the item is bound using the method format(...). when a button is clicked the item will be removed from the list. this works good. but if you remove the last item of the list, then the new last item in the list should get the name "Last". this also works, when the name is bound directly to the property name. but if i want to do some formatting of the name (surround it with # for example) then the display of this item is not updated.
EDIT2 (partially solved):
The next example that doesn't work, occurs when a value inside the method that is called for displaying a value changes. This can be seen in the example if a change-button is clicked multiple times. It increases an internal counter and the corresponding text will display this value. But this is only true for the first change of the counter. Subsequent clicks won't change the display again. The display shows the value of the counter after the first click. But if another change button is clicked, then the text in front of this button shows the increased counter value. But also only once. It also doesn't display changes on subsequent clicks. The notifyPath-method seems to check if the value changed, but doesn't consider that inside the method that is used for displaying the value, something could have been changed to show the data in another way.
i included a partial solution in this example. If the method that gets called has a parameter that changes when something in the method is changed, then the update will be executed. This can be seen in the second variable that is bound with the parameter displayEnforcer - format(item.name,displayEnforcer). This variable is set to a random value everytime the counter is changed. And this triggers the display update.
But this is a really strange solution and should not be necessary. it would be great if someone has a better solution to this problem.
<link rel="import" href="components/bower_components/polymer/polymer.html">
<link rel="import" href="components/bower_components/paper-button/paper-button.html">
<dom-module id="polymer-test">
<template>
<table>
<template id="tpl" is="dom-repeat" items="{{list}}">
<tr>
<td>{{item.id}} - {{format(item.name)}}- {{format(item.name,displayEnforcer)}}</td>
<td><paper-button raised on-tap="tapDelete">delete</paper-button></td>
<td><paper-button raised on-tap="tapChange">change</paper-button></td>
</tr>
</template>
</table>
</template>
</dom-module>
<script>
Polymer(
{
is: "polymer-test",
properties:
{
count: {type: Number, value:0}
,list: {type: Array, value: [{id:0,name:"First"}
,{id:1,name:"Second"}
,{id:2,name:"Third"}
,{id:3,name:"Fourth"}
,{id:4,name:"Fifth"}
,{id:5,name:"Last"}
]}
,displayEnforcer: {type:Number,value:Math.random()}
},
format: function(name,dummy)
{
return "#" + name + " - " + this.count + "#";
},
tapChange: function(e)
{
this.count++;
this.displayEnforcer = Math.random();
this.notifyPath("list." + (e.model.index) + ".name","changed");
},
tapDelete: function(e)
{
if(this.list.length == 1)
return;
this.splice("list",e.model.index,1);
if(this.list.length > 0)
this.list[this.list.length-1].name = "Last";
this.notifyPath("list." + (this.list.length-1) + ".name",this.list[this.list.length-1].name);
}
});
</script>
You can use notifyPath to refresh binding of single list element's property:
tapChange: function(e) {
this.notifyPath('list.' + e.model.index + '.name', 'changed');
}
See: https://github.com/Polymer/polymer/issues/2068#issuecomment-120767748
This way does not re-render the entire repeater but simply updates the necessary data-bound nodes.
EDIT
Your format function is not getting updated because it doesn't use the notified path which is an item's name. When you remove an element, the index of that element within the rendered repeater doesn't change, even though the actual item changes. In other words, when you remove fifth element, the index 4 is still 4.
As a workaround you can add item.name to format call so that it is refreshed when you notify that path:
<td>{{item.id}} - {{format(index, item.name)}}</td>
You don't even have to use that value in the example. It's enough that the binding is reevaluated.

Why ReactCSSTransitionGroup generates multiple components though I expect only one?

I am using ReactCSSTransitionGroup to do some animation and I found an interesting thing which does not make any sense to me.
In the example below, when I click <div className="HeartControl">, it will update the height of the <div className="HeartFill"> which works fine. (I know to achieve the effect does not necessarily need ReactCSSTransitionGroup here though).
Interesting thing is that when I click, there will be another <div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div> with a new React component id added after the existing one.
But I expect there will always be only ONE <div className="HeartFill"> there.
Why this happened???
P.S.. after a few clicks, the result will look like:
<span data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2">
<div class="HeartFill HeartFill-enter HeartFill-enter-active" style="position:absolute;bottom:0;left:0;width:30px;height:3.5999999999999996px;background-color:#D64541;" data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2.$=1$6:0"></div>
<div class="HeartFill HeartFill-enter HeartFill-enter-active" style="position:absolute;bottom:0;left:0;width:30px;height:3px;background-color:#D64541;" data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2.$=1$5:0"></div>
var HEIGHT_HEART = 30;
var NUM_HEART_MAX = 50;
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
var Heart = React.createClass({
getInitialState: function() {
return {
heartHeight: 0
};
},
onClick: function(e) {
var currentHeartHeight = this.state.heartHeight;
this.setState({
heartHeight: currentHeartHeight + 1
});
},
render: function() {
var styleHeartFill = {
'position': 'absolute',
'bottom': 0,
'left': 0,
'width': 30,
'height': this.state.heartHeight / NUM_HEART_MAX * HEIGHT_HEART,
'background-color': '#D64541'
};
return (
<div className="Heart" >
<div className="HeartControl" onClick={this.onClick}>
<i className="fa fa-angle-up" />
</div>
<img src="heart.png" className="HeartOutline" />
<ReactCSSTransitionGroup transitionName="HeartFill">
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
</ReactCSSTransitionGroup>
</div>
);
}
});
React.renderComponent(<Heart />, document.getElementById('Heart'));
`
I suspect the reason your getting more than one is because your using the key prop
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
From React docs http://facebook.github.io/react/docs/multiple-components.html#dynamic-children
When React reconciles the keyed children, it will ensure that any child with key will be reordered (instead of clobbered) or destroyed (instead of reused).
Heres a jsfiddle using the key prop http://jsfiddle.net/kb3gN/3826/
Heres a jsfiddle not using the key prop http://jsfiddle.net/kb3gN/3827/
P.s I've made a few changes in the fiddle just to try and better demostrate the reasoning
I'm fairly late to the game with this answer, but I ran into this issue as well and want to provide a solution for others. Removing the key is not a sufficient solution since React relies on it to know when to animate items. The documentation now has a section discouraging this.
You must provide the key attribute for all children of
ReactCSSTransitionGroup, even when only rendering a single item. This
is how React will determine which children have entered, left, or
stayed.
- https://facebook.github.io/react/docs/animation.html
If you are only animating the entry/exit of a single item, a CSS hack can be used to fix flickering that may be seen from multiple items entering/exiting.
.HeartFill {
display: none;
}
.HeartFill:first-child {
display: block;
}
React will add new elements on top in most cases, but this isn't guaranteed. If your transitionEndTimeout prop is set to a relatively short time, this shouldn't be a huge concern. The timeout prop should also match the CSS transition time.
Here is the problem:
You are providing a value for key which is changing over time. Keys are used to decide if an element is the same or different.
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
When you do this the value for key changes and React thinks a new element is entering and an old element is leaving.
Usually you need a unique key which can either be sequential or be generated using Math.random(). (remember to generate them once with getInitialState or DefaultProps, not in render, as that would create a new key every time).
The order of elements is another thing that can be in trouble.
From React's documentation:
In practice browsers will preserve property order except for properties that can be
parsed as a 32-bit unsigned integers. Numeric properties will be ordered sequentially
and before other properties. If this happens React will render components out of
order. This can be avoided by adding a string prefix to the key:
ReactCSSTransitionGroup should create a second element
it will remove it when a specified css animation has finished
or print a warning when there is no animation in the css
maybe look at the Low-level API for better understanding: http://facebook.github.io/react/docs/animation.html (bottom of the page)
I used gluxon's advice as a starting point - what worked for me was removing the leave transition and making it display nothing:
.example-leave.example-leave-active { display: none; }

Categories