ref callback is being applied to first matching class - reactjs

I have a simple component that has a render like this:
render: function(){
return (
<textarea className="wmd-input" id="wmd-input" ref={this.initiatePagedown}></textarea>
)
}
initiatePagedown: function(input){
//code that initiates markdown editor.
attr = $(input).attr('id').split('wmd-input')[1];
converter = new Markdown.Converter();
Markdown.Extra.init(converter, {highlighter: "highlight"});
editor = new Markdown.Editor(converter, attr);
return editor.run();
},
The component mounts n different times, hence creating multiple textareas.
The problem is the ref callback is running with the input of the first component, so it's always the first component that's manipulated, not the one that I select. So let's say this component was mounted twice, then the ref callback will be called on the first component instance twice, not once of each component instance. How do I solve this issue?

You're using the Pagedown editor to point to a specific element ID on the page, because you're passing in the second argument, attr:
new Markdown.Editor(converter, attr);
Check out the documentation for the second argument of the Markdown.Editor constructor:
If given, this argument is a string, appended to the HTML element ids of the three elements used by the editor...so you may create the second input box as <textarea id="wmd-input-2"> and pass the string "-2" as the second argument to the constructor.
Right now you're always creating an editor with the same ID:
id="wmd-input"
So the editor constructor will always match every existing instance of that ID on the page.
This is a very poor API forcing you to point to element IDs. As a workaround I would probably make the id a prop you pass in, so that the wrapping component/page can decide how many editors should be there, and you can build the ID like this
return (
<textarea id={ `wmd-input-${ this.props.id }` } ... />
)
Then you can instantiate the unfortunate API with something like
editor = new Markdown.Editor(converter, `-${ this.props.id }`);

Related

Dynamic, unique values for DOM elements

I want advice on if my solution is conventional and/or if there are better, more common approaches using React for this situation:
I have a comments section. I retrieve a list of comments from the server as a prop to my functional component. Within the JSX, I map through each comment and, for each, return a TextArea to display the comment. One of the functionalities is that if you created the comment an edit button will appear, rendering the textarea editable.
This means that I need a unique identifier for each textarea, because once the user edits the comment's content and clicks the save button, I need to post to my server with the correct _id of the comment in the DB, along with the updated content.
My most idiomatic solution thus far is to use refs, code below:
I create a ref containing an array, a function to pass objects mapping the comment._id to the element, and add that to the commentRefs array whenever the function is called. The function to add to the ref array is the ref proeprty's value for the textarea:
const addCommentRef = _id => el => {
if(el && !commentRefs.current.includes(el) && el) {
const commentRef = {
_id,
el,
content: '',
}
commentRefs.current.push(commentRef);
}
}
<textarea ref={addCommentRef(comment._id)}
On the textarea onChange, I call a function called editCommentRef, which maps through the commentRefs, compares the ID's passed as param with each _id value in the array and if found, updates the content to what has been typed into the textarea:
const editCommentViaRef = _id => event => {
for(let i = 0; i < commentRefs.current.length; i++) {
const commentRef = commentRefs.current[i];
if(_id == commentRef._id) {
commentRefs.current[i] = {
...commentRefs.current[i],
content: event.target.value
}
}
}
}
<textarea onChange= {editCommentRefs(comment._id)}...
Lastly, once the save button is clicked, its onClick handler calls a 3rd function named "saveCommentEdit", where it passes the _id of the comment. We again traverse through the commentRefs array and find the _id param matching the _id of the saved object. Once found, I have what I need to post to the server/db and update the comment:
const saveCommentEdit = _id => {
commentRefs.current.forEach((comment) => {
if(_id == comment._id) {
console.log('found comment to save to db ', comment._id + ' saving content - \n' + comment.content);
}
});
}
<button onSubmit={saveCommentEdit(comment._id)} ...
This works, but is this too complicated, and are there any better and common solutions? Other solutions I have tried/considered:
States - I imagine I would have the same issue with states as the state names need to be referred to in code, so I'd need a different variable name for each state. Also, states would require the entire component to re-render with every character typed into the textarea which seems more costly than refs.
Document selectors - The simplest solution is to call a function which targets the DOM for the elements using "findElementById" and etc.. in which case I provide a unique identifier by making the ID of each textarea contain the comment._id. The obvious issue here is that targeting the DOM directly in a React app is discouraged, and fundamentally contradicting to the React framework.
Thanks in advance!
You don't need to use Ref - it's meant for something else (to communicate with the DOM) and your task is different. You can store the edit state and record id from the database in each component. You can store the author's id to know exactly who can edit. On the server, update the record, provided that the id and author are the same. Also, you do not need to do any loops, a comment component can take care of sending data itself according to OOP.
For example you may have a table comments in your DB:
id
post_id
author_id
comment
1
1000
user100
cool
2
1001
user42
hello
Then get all comments by post_id if current user_id equal author_id you can pass to component property editable for show button edit/save (caption depends of mode)

Display a React Component as a string

I'm trying to display a helper string text with a react component. The string is part of an array of messages that get displayed on a screen. In this particular string, I'm using an icon for part of the string. The icon is a component. However, I keep getting back [object, object].
What I've tried...
`Click on the ${(<EyeIcon />)} to view the password you have entered.`,
Also just displaying the icon,
<EyeIcon />
I've even tried making the component into a export function.
EyeIcon()
But I can't seem to get it to appear.
Ok, there's some difference between a function, and a function component.
function
const eyeIcon = s => `Click on ${s}`
This is a function that you can plugin to output a new string based on s.
function component
const EyeIcon = ({ s }) => <span>Click on {s}</span>
This is a function component that you can use via
const Title = () => {
return <EyeIcon s="abc" />
}
A function component is a function, but it needs to be invoked to produce an element (not a string), therefore the return statement is not a string (Even you output a string there, it'll be wrapped into an element for you).
NOTE:
it's a bit confusing if it's your first time into functional programming, but just watch out for the input and output. The function component input argument is a props object, and it returns a DOM-like element (called React element). There's quite a bit ES6 syntax to confuse you further.
If this string is part of some conditions try this option:
<>Click on the (<EyeIcon />) to view the password you have entered.</>

cannot render a complex React Element in order to replace a DOM element by id

I am a beginner with react so please excuse me if this question does not make sense.
I have a complex div element in the html document with an id, it looks like this:
aaavote: I am comingChange My voteDelete Me
and it was originally created from rendering a react element that was returned from a component, like this:
let content = visitors_list.map((visitor) =>
)
{content} was returned from the react component.
At some stage I need to replace that div (with id="MyDiv2") with a different content.
I have a function that creates this new "content" exactly the same way I created the original one, however, now instead of return it from the component and have react do the rendering, I need to do it manually and call:
document.getElementById("MyDiv2").innerHTML = something
I cannot just pass content as something because : component is an array of [object Object]
I cannot pass it as a json structure because JSON.stringify(content) gives me:
[{"type":"div","key":"1","ref":null,"props":{"className":"flex-no-shrink w-full md:w-1/4 md:px-3","children":{"key":null,"ref":null,"props":{"visitor":{"id":"1","name":"aaa","vote":0,"imageUrl":"http://www.stone-guitar-picks.com/stoneguitarpicksshop/images/large/GP2046_LRG.JPG"}},"_owner":null,"_store":{}}},"_owner":null,"_store":{}}]
so I tried: ReactDOM.render(content, document.getElementById("MyDiv2"))
but ReactDOM.render does nothing, infact it breaks and exists the function.
I also tried using React.createElement(content) - also did not work...
How can I solve this problem. the only solution that I found to work is forcing the refreshing of the page which I do not want to do.
Dont try to handle the dom manually, react does this for you!, imagine you have your component MyInput that renders an input with some characteristics that may be passed by properties as well, then you could just change your component props and react will change your component automatically. i'll try to post a hardcoded example:
const myComp = ({text, visible}) = return (
<input text={text} visibility={visible}>
)
then, when you change your component text it will render it automatically.

How to modify state of a parent component from a child component which is a custom form I wrote. This might also effect other children of the parent

I wrote a custom form component called SingleFilterContainer
Within the parent component called FiltersContainer, I initialize a state filters which has an array of a single filter initially and respective setFilters function to modify the array of filters. And also has a button to add a filter. And in the render function I use filters.map to render SingleFilterContainer component multiple times. So far this works. But I want to add a delete filter button. I put this inside the SingleFilterContainer. When I click this it should update the state filters in the parent component and delete the ith filter and render the rest of the filters normally. Or render the whole map of filters again.
I'm new to react and hooks, and I feel like I'm missing something. I'm at it for the past three days and I'm a little bit lost.
Here is the sandbox https://codesandbox.io/s/editing-filters-j2qrp
I feel like how I'm handling state is completely wrong. And maybe I should use redux? But I want the SingleFilterContainer to be like an ephemeral form. Or should the delete filter button be within the parent component? and repeat it using map?
TL;DR Fixed fork
The problem in your code is that you call the function on render onClick={handleDeleteFilter(i)} the onClick expects a reference to a function, but if you want this code to work, then the method that you are passing from the parent to the child needs to return a function also.
Then your handleDeleteFilter will look like this:
function handleDeleteFilter(i) {
return function() {
filters.splice(i, i + 1);
setFilters(filters => filters);
}
}
Also in your case you don't need to pass i + 1 into splice as the second argument, as the second argument is the amount of items to remove. Which in your case is just 1. Docs
You pass the i to the first function and the second one will see it due to closure.
And then to the removing of an element.
Mutating the state is bad practice, so you can have a local variable with a copy of the state, which you can manipulate and then update the state. That way you can use whatever you want, but on the local variable.
So your updated handleDeleteFilter would look like
function handleDeleteFilter(i) {
return function() {
const clone = [...filters]
clone.splice(i, 1);
setFilters(clone);
}
}
or with .filter
function handleDeleteFilter(i) {
return function() {
const clone = filters.filter((item, index) => index !== i)
setFilters(clone);
}
}
this way you don't need a new variable as .filter returns a new array. Docs
Here's my fork.
The following is my changes.
function handleDeleteFilter(i) {
setFilters(filters => filters.filter((e, index) => index !== i));
}
Important notes:
Ideally, you need to assign an id property for each item in the array instead of comparing by index.
Don't directly mutate the state. .splice directly mutates the state. Learn to use .map, .filter & .reduce array functions

using valueLink, child input field unable to change

I'm starting to play with reactjs, and for this project, I have ->
an input field
a button that toggles a modal
the modal has another input field.
How do we make both input fields stay in sync?
What I though I would need to do use to use the LinkedState mixin.
So, I do something like this ->
MainFoo = React.createClass
mixins: [React.addons.LinkedStateMixin]
getInitialState: ->
searchTerm: ''
render: ->
input valueLink: #linkState('searchTerm') # this works
CustomReactChild
searchTermLink: #linkState('searchTerm') #passing into child.
CustomReactChild = React.createClass
renderModal:
unless #modal
$anchor = $('<div>').appendTo('body');
comp = (Modal
body: (CustomReactChildchild
searchTermLink: #props.searchTermLink)
) #custom react modal class
#modal = React.renderComponent comp, anchor
#modal.show()
render: ->
label
onClick: #renderModal
# Deep inside CustomReactChild
CustomReactChildsChild = React.createClass
render: ->
input valueLink: #props.searchTermLink # Am unable to change the value via this input
Am I using this incorrectly? How can I get the second input to change the value of the parent's input and vice versa?
#Douglas popped in an awesome answer in the comments -->
Looking at the logs here, jsfiddle.net/kb3gN/3602 - I think part of the problem is that the modal popup is re-rendering (or at least running the "Controlled Input" handler) before MainFoo has updated its state. I'd suggest merging the models together, so that there is only one React.renderComponent call, or factor out the state into a shared Store which both inputs update and get change events from.
```

Resources