What to use instead of window.getLocation() in a textarea in React? - javascript

I have a simple textarea where I want the user to be able to select/highlight text to make it bold, italicized, etc., much like the one I'm writing in now.
I actually get it working using a onKeyDown method and then using window.getSelection(), but I feel it's the wrong approach to use window in React. Correct? If so, what do I use instead?
handleKeyDown(event) {
const highlightedText = window.getSelection().toString()
console.log(highlightedText)
},
render() {
return (
<textarea onKeyDown={this.handleKeyDown} />
)
}

The time when things can go wrong is when you modify the DOM outside of React. But this is not the case.
I don't mind calling window.getSelection in my react code. I've been using document methods/attrs like document.activeElement and window methods/attr like window.innerHeight and setTimeout. I have come across no problem and I did not find any other better way to get what I want.
This does need some attention in test though. You need to set up the window object and make it available to your test code.

It's ok to use window.getSelection in React. The only issue I see here in terms of breaking the component model and getting unexpected results, is the case where you call the method inside your component, but the selection is actually on a different element on the page.
In your case since you are calling getSelection inside the onKeyDown handler, you know the selection must have come from this component instance, since it needs to be focused to receive the event.
But if you would like to be able to get the selection at any time inside a component in a safe way, you could create a member function on the component to check if it's focused using refs:
getSelection() {
return (this.refs.container === document.activeElement) ?
window.getSelection() : null;
}
render() {
return (
<textarea onKeyDown={this.handleKeyDown} ref="container" />
)
}
Here getSelection() will return the selection if it's within this component instance, otherwise null.

Related

React ref null after re-rendering element

I have a to-do list where tasks can be either a single big textarea (called dataArea here) or a list of those textareas. Those textareas should grow in height as content is added, which I do by setting the height to its scrollHeight on input (via handleInput). What I want to do is let folks toggle between that plain textarea and list of textareas (via toggleChecklist), using state to store the content.
However, when the content is set via state—not direct user input—the handleInput function isn't reached and I must set that from a different function or manually fire onInput. Either way, I believe I must use a ref (blobRef) to access that element to set its height. However, blobRef is null after toggling to/from checklist. Why is that?
Here's where that's [not] happening in full context (I think only the Form.js file is what needs looking at): https://github.com/werdnanoslen/tasks/blob/help/src/components/Form.js#L85
And here's some code previews:
const blobRef = useRef(null)
...
function handleInput(e, i) {
const element = e.target
if (checklist) {
let checklistDataCopy = [...checklistData]
checklistDataCopy[i] = { ...checklistDataCopy[i], data: element.value }
setChecklistData(checklistDataCopy)
} else {
setData(element.value)
}
element.style.height = '0'
element.style.height = element.scrollHeight + 'px'
}
function toggleChecklist() {
setChecklist((prevChecklist) => !prevChecklist)
if (checklist) {
const n = String.fromCharCode(13, 10) //newline character
setData(checklistData.reduce((p, c) => p.concat(c.data + n), ''))
blobRef.current && blobRef.current.dispatchEvent(new Event('input'))
}
}
function dataArea(item?, index?) {
return (
<textarea
id={item ? item.id : 'add-task'}
name="data"
className="input"
value={item ? item.data : data}
onKeyDown={(e) => addChecklistItem(e, index)}
onInput={(e) => handleInput(e, index)}
onFocus={() => setEditing(true)}
placeholder={inputLabel}
rows="1"
ref={item ? (item.id === newItemId ? lastRef : undefined) : blobRef}
// HELP ^^^ blobRef is null after toggling to/from checklist
/>
)
}
...
return (
<>
<form onSubmit={handleSubmit} onBlur={blurCancel}>
<label htmlFor="add-task" className="visually-hidden">
{inputLabel}
</label>
{checklist ? checklistGroup : dataArea()}
{isEditing ? editingTemplate : ''}
</form>
</>
)
On first render the ref is null. Only on the second render will it be populated. This is because during the first render pass, the HTML is not yet loaded, so there is no HTML element reference.
This applies to when you switch to the other component and back to dataArea, as when you switch away react sets it to null when it unmounts since the element reference is gone from the DOM. So when it remounts again, on first render it will be null.
You can sometimes get around this sort of problem with useLayoutEffect which only runs when the DOM has rendered.
Your code is a little odd in that you also are conditionally applying a ref. Normally, you want this to be stable, I would expect it to be simply:
ref={blobRef}
And any branching would be done when you consume the ref, not set it. It will be quite hard to manage otherwise.
However, before I go further I should warn you grabbing the element and setting its height is not actually a good idea. This violates React, because you shouldn't change a DOM nodes properties such that they no longer can be reproduced by React. There is now a different between reality and React's internal VDOM implementation.
This is quite tricky though, I'd recommend just using a library: https://github.com/Andarist/react-textarea-autosize/tree/main/src. These libraries get around the problem by rendering a cloned off-screen text area outside of React's remit and taking the heights from that, then applying them use the style prop on the "real" textarea.
A few notes around your implementation:
On the first render, the ref is null and not undefined because of component mounting or unmounting and setting the state.
Your ref is assigned on further renders.
You are doing DOM manipulation by directly assigning heights to DOM element which is not a correct way of doing it.
You can check by console logging that you are not getting the ref in actual as blobRef.current got nothing/undefined and thus handleInput is not triggered, thats why you added an extra && check there. You can handle the case of undefined too. If you don't get any ref then?
You cannot achieve this directly, either use a package or take the height from actual DOM instead of VDOM and assign on the actual DOM element.
A NPM packages can be helpful in your case as mentioned by #adam-thomas.
Example is: react-textarea-autosize

useState element not rerendering when triggered by an event handler in a child component

I have a parent component with a useState that is being displayed. What I want to do is have a child component be able to update this state to change what the parent is displaying. I currently have the following:
function Parent() {
const [myWindow, setMyWindow] = useState(null)
useEffect(() => {
setMyWindow(<Child updateWindowFunc={() => setMyWindow(someNewWindow)} />)
}, []}
return (
<div>
{myWindow}
</div>
)
}
function Child({updateWindowFunc}) {
//Somehow calls updateWindowFunc
}
(Note that my actual code is set up somewhat differently, so don't mind the syntax as much as the general concept.)
I've found that I can get the value of myWindow to change, but the actual display doesn't show any change. I've tried forcing a re-render after the change, and adding a useRef to display useRef.current, but nothing seems to update which window is actually being rendered.
What am I doing wrong?
Edit:
I've found that it works if I switch to a different type of component, but if its just a different element of the same component then there is no re-render. I've been using React.createElement(), so I would think the 'objects' are distinct, but maybe I just misunderstand how this works.
Can you provide more details or working\not working code example that has more info?
Currently it's hard to get, cause that doesn't make any sense since you can just render it like below and that code will have same result as one that you are trying to run
function Parent() {
return (
<div>
<Child />
</div>
)
}
So for better understanding and fixing a problem \ finding a different approach, please, provide more details

Access $refs from other components not in the same level as current component

I'm working on a Vue application.
It has a header and then the main content.
Nesting and structure as below
TheHeader.vue -> TheLogin.vue
MainContent.vue -> ShoppingCart.vue -> OrderSummary.vue
I need to access an element in TheLogin.vue from OrderSummary.vue
this.$refs.loginPopover.$emit('open')
gives me an error "Cannot read property '$emit' of undefined" so obviously I am not able to access $refs from other components.
The question is how do I get hold of refs from other components?
Thanks in advance!
Edit 1 - Found out $refs works with only child components.
How do I access elements across components in different level?
You definitely don't want to be reaching through the hierarchy like that. You are breaking encapsulation. You want a global event bus.
And here's a secret: there's one built in, called $root. Have your OrderSummary do
this.$root.emit('openPopup');
and set up a listener in your TheLogin's created hook:
this.$root.on('openPopup', () => this.$emit('open'));
In general, you should try to avoid using refs.
For anyone who comes here later and wants to access $refs in parent component, not in this particular case for emitting events since event bus or a store would suffice but let's just say you want to access some element in parent to get it's attributes like clientHeight, classList etc. then you could access them like:
this.$parent.$parent.$refs //you can traverse through multiple levels like this to access $ref property at the required level
You can put a function like this on your component to do this. I put mine in a Mixin:
public findRefByName(refName) {
let obj = this
while (obj) {
if (obj.$refs[refName]) {
return obj.$refs[refName]
}
obj = obj.$parent
}
return undefined
}
I also added some accessors to help:
get mycomponent() {
return this.findRefByName('mycomponent')
}
And once that exists, you can access your component by simply doing:
this.mycomponent
Thanks for that tip Abdullah!
In my case I was looking for a sibling, so in case someone comes looking for that, here's an example:
var RefName='MyCoolReferenceName';
var MyRef,x;
for(x=0;x<this.$parent.$children.length;x++)
{
if(typeof this.$parent.$children[x].$refs[RefName] !='undefined')
MyRef=this.$parent.$children[x].$refs['LABEL_'+bGroupReady.ChildID];
}
if(typeof MyRef !='undefined')
MyRef.error=true;
PS - The reason I'm doing MyRef.error=true is because I was having ZERO luck with Quasar inputs and lazy-rules="ondemand". Turns out you can just set .error=true to activate the error message and the red highlighting and .clearValidation() event to clear it back out. In case someone is trying to do that as well!

Access DOM when using hyper.Component

When using HyperHTMLElement it's possible to access the contents of the component by simply using this.children or this.querySelector(), since it's an element.
But how would I achieve similar behavior when using hyper.Component?
The hypothetical example I have in mind is from React docs: https://facebook.github.io/react/docs/refs-and-the-dom.html - I'd like to focus a specific node inside my DOM.
I have a codepen sandbox where I'm trying to solve this: https://codepen.io/asapach/pen/oGvdBd?editors=0010
The idea is that render() returns the same Node every time, so I could save it before returning and access it later as this.node:
render() {
this.node = this.html`
<div>
<input type="text" />
<input type="button" value="Focus the text input" onclick=${this} />
</div>
`;
return this.node;
}
But that doesn't look clean to me. Is there a better way to do this?
The handleEvent pattern is there to help you. The idea behind that pattern is that you never need to retain DOM references when the behavior is event-driven, 'cause you can always retrieve nodes via event.currentTarget, always pointing at the element that had the listener attached, or event.target, suitable for clicks happened in other places too within a generic click handler attached to the wrap element, in your demo case the div one.
If you'd like to use these information, you can enrich your components using an attribute to recognize them, like a data-is="custom-text-input" on the root element could be, and reach it to do any other thing you need.
onclick(e) {
var node = e.target.closest('[data-is=custom-text-input]');
node.querySelector('[type=text]').focus();
}
You can see a working example in a fork of your code pen:
https://codepen.io/WebReflection/pen/RLNyjy?editors=0010
As alternative, you could render your component and address its content once as shown in this other fork:
https://codepen.io/WebReflection/pen/LzEmgO?editors=0010
constructor() {
super().node = this.render();
}
at the end of the day, if you are not using custom elements but just basic, good'ol DOM nodes, you can initialize / render them whenever you want, you don't need to wait for any upgrade mechanism.
What is both nice and hopefully secure here, is that there's no way, unless you explicitly expose it, to address/change/mutate the instance related to the DOM element.
I hope these possibilities answered your question.
This is something I've worked on in the past via https://github.com/joshgillies/hypercomponent
The implementation is actually quite trivial.
class ElementalComponent extends hyper.Component {
constructor () {
super()
const _html = super.html
this.html = (...args) => {
this.node = _html.apply(this, args)
return this.node
}
}
}
class HelloWorld extends ElementalComponent {
render () {
return this.html`<div>Hello World!</div>`
}
}
This works really well and is inline with your question. However, it's worth noting hyperHTML can render not only a single node but also multiple nodes. As an example:
hyper`<div>Hello World!</div>` // returns a single DOM Node
hyper`<div>Hello</div> <div>World!</div>` // returns multiple DOM Nodes as an Array.
So this.node in the above ElementalComponent can be either a DOM Node, or Array based on what the renderer is doing.

Call methods on React children components

I want to write a Form component that can export a method to validate its children. Unfortunately a Form does not "see" any methods on its children.
Here is how I define a potential children of Form:
var Input = React.createClass({
validate: function() {
...
},
});
And here is how I define Form class:
var Form = React.createClass({
isValid: function() {
var valid = true;
this.props.children.forEach(function(component) {
// --> This iterates over all children that I pass
if (typeof component.validate === 'function') {
// --> code never reaches this point
component.validate();
valid = valid && component.isValid();
}
});
return valid;
}
});
I noticed that I can call a method on a child component using refs, but I cannot call a method via props.children.
Is there a reason for this React behaviour?
How can I fix this?
The technical reason is that at the time you try to access the child component, they do not yet really exist (in the DOM). They have not been mounted yet. They have been passed to your<Form> component as a constructor prop or method as a react class. (hence the name class in React.createClass()).
As you point out, this can be circumvented by using refs, but I would not recommend it. In many cases, refs tend to be shortcuts for something that react wasn't intended for, and therefore should be avoided.
It is probably by design that react makes it hard/ impossible for parents to access a child's methods. They are not supposed to. The child's methods should be in the child if they are private to the child: they do something inside the child that should not directly be communicated upward to the parent. If that were the case, than handling should have been done inside the parent. Because the parent has at least all info and data the child has.
Now in your case, I imagine each input (child) component to have some sort of specific validation method, that checks the input value, and based on outcome, does some error message feedback. Let's say a red outline around incorrect fields.
In the react way, this could be achieved as follows:
the <Form> component has state, which includes a runValidation boolean.
as soon as runValidation is set to true, inside a setState( { runValidation: true }); react automatically re-renders all children.
if you include runValidation as a prop to all children.
then each child can check inside their render() function with something like if (this.props.runValidation) { this.validate() }
which will execute the validate() function in the child
the validate function can even use the child's state (state is not changed when new props come in), and use that for the validation message (e.g. 'please add more complicated symbols to your password`)
Now what this does not yet fix, is that you may want to do some checking at form level after all children have validated themselves: e.g. when all children are OK, submit the form.
To solve that, you could apply the refs shortcut to the final check and submit. And implement a method in your <Form> inside a componentDidUpdate() function, to check if each child is OK (e.g. has green border) AND if submit is clicked, and then submit. But as a general rule, I strongly recommend against using refs.
For final form validation, a better approach is:
add a non-state variable inside your <Form> which holds booleans for each child. NB, it has to be non-state, to prevent children from triggering a new render cycle.
pass a validateForm function as a (callback) prop to each child.
inside validate() in each child, call this.props.validateForm(someChildID) which updates the corresponding boolean in the variable in the Form.
at the end of the validateForm function in the Form, check if all booleans are true, and if so, submit the form (or change Form state or whatever).
For an even more lengthy (and way more complicated) solution to form validation in react (with flux) you could check this article.
I'm not sure if i'm missing something, but after trying what #wintvelt suggested i ran into a problem whenever i called the runValidation method inside the render method of React, since in my case runValidation changes the state by calling setState in it, thus triggering the render method which obviously is a bad practice since render method must be pure, and if i put the runValidation in willReceiveProps it won't be called the first time because the if condition is not true yet (this condition is changed in the parent component using setState, but in the first call of willReceiveProps it's still false).

Categories

Resources