I have a component to display list of tags, and the user can select tags to follow them. The tags are displaying fine. I would like to get the selected tag and store it inside a new array tagsSelectedList. So, when the user clicks a Tag, I would like to get that tag and push it to tagsSelectedList. However I am getting an error after I placed an onClick inside the li of the map function.
return (
<li id={Tag.tagName} class="tag" key={Tag.id} onClick={this.selectTag}>{Tag.tagName}</li>
);
This is the error:
Uncaught TypeError: Cannot read property 'selectTag' of undefined
Component.js:
let tags = [
{id: "1", tagName: "Arts"},
...
...
{id: "59", tagName: "Writing"}
}];
var tagsSelectedList = [];
export default class SignUpSubscribeTags extends React.Component {
constructor(props){
super(props);
}
selectTag = (e) => {
console.log(e.target.id);
}
render() {
let tagList = tags.map(function(Tag){
var i = 0;
return (
<li id={Tag.tagName} class="tag" key={Tag.id} onClick={this.selectTag}>{Tag.tagName}</li>
);
});
return(
<div id="signup-process-wrapper-addTags">
<div id="add_tags">
<ul id="tag_list">
{tagList}
</ul>
</div>
</div>
);
}
}
However if I remove onClick={this.selectTag} from the return statement of tagList,
<li id={Tag.tagName} class="tag" key={Tag.id}>{Tag.tagName}</li>
and place a li with an onClick={this.selectTag} inside the ul,
<ul id="tag_list">
<li id="tagName" class="tag" onClick={this.selectTag}>tagName</li>
{tagList}
</ul>
it works fine! I get no error.
What am I doing wrong? Please help me. Thank you.
You need to scope this so it references the React component context
There are a couple of ways to do this:
Option 1:
Using bind()
render() {
let tagList = tags.map(function(Tag){
var i = 0;
return (
<li id={Tag.tagName} class="tag" key={Tag.id} onClick={this.selectTag}>{Tag.tagName}</li>
);
}.bind(this));
return(
<div id="signup-process-wrapper-addTags">
<div id="add_tags">
<ul id="tag_list">
{tagList}
</ul>
</div>
</div>
);
}
Option 2:
You can use the ES6 arrow function to scope it
let tagList = tags.map((Tag) => {
var i = 0;
return (
<li id={Tag.tagName} class="tag" key={Tag.id} onClick={this.selectTag}>{Tag.tagName}</li>
);
});
Option 3:
The map function also takes a second argument that specifies what this refers to
let tagList = tags.map(function(Tag) {
var i = 0;
return (
<li id={Tag.tagName} class="tag" key={Tag.id} onClick={this.selectTag}>{Tag.tagName}</li>
);
}, this);
Related
I have a react component which renders a list item with individual OnClick.
In order to find out which item was clicked, the handler accepts a parameter. The handler does get invoked - but no matter which item is clicked - console always logs item3 (as if item3 is clicked). What am I doing wrong here?
class Item {
constructor(props) {
super(props);
this.onItemClickHandler = this.onItemClickHandler.bind(this)
}
onItemClickHandler (itemName) {
console.log("Clicked " + itemName)
}
render() {
this.items = ["item1", "item2", "item3"]
var lis = []
for (var liName in this.items) {
var liName2 = this.items[liName]
console.log("Adding " + this.items[liName])
lis.push(<li className="item-ListItem" key={this.items[liName]} onClick={() => this.onItemClickHandler(this.items[liName])}><span>{this.items[liName]}</span></li>)
}
return (
<div className="item">
<label className="item-Header"><u>items</u></label>
<ul className="item-List">
{lis}
</ul>
</div>
);
}
This line:
onClick={() => this.onItemClickHandler(this.items[liName])}>
appears to be correct.
The issue is that you are not capturing the value of this.items[liName] correctly because by the time you reach the third item iteration the onClick handler will always have the value of this.items[liName] set to the third item.
The solution for that is using closure to capture the value correctly, i edited your code and created a fully working example in this link
https://codesandbox.io/s/3xrp6k9yvp
Also the example code is written below with the solution
class App extends Component {
constructor(props) {
super(props);
this.onItemClickHandler = this.onItemClickHandler.bind(this);
}
onItemClickHandler(itemName) {
console.log("Clicked " + itemName);
}
render() {
this.items = ["item1", "item2", "item3"];
var lis = [];
for (var liName in this.items) {
var liName2 = this.items[liName];
console.log("Adding " + this.items[liName]);
//the clickHandler function here is the solution we created a function that get executed immediately each iteration and return a new function that has the correct value of `this.items[liName]` saved
var clickHandler = (item => {
return event => {
this.onItemClickHandler(item);
};
})(this.items[liName]);
lis.push(
<li
className="item-ListItem"
key={this.items[liName]}
onClick={clickHandler} // here we use the clickHandler function directly
>
<span>
{this.items[liName]}
</span>
</li>
);
}
return (
<div className="item">
<label className="item-Header">
<u>items</u>
</label>
<ul className="item-List">{lis}</ul>
</div>
);
}
}
For more info and examples about closures check this link
Edit We can use let in ES6 instead of var in our for loop as mentioned by #ArchNoob because using let will make the liName block scoped
Please take care of indentation while posting the code. Its very difficult to understand without that. You have to make use of closure. whenever the loop gets over liName variable gets set to last index as scope chain will keep the liName value to last one. The solution is not make a new scope between handler and click handler function where it is callled.
Here is the solution:
class Test extends React.Component {
constructor(props) {
super(props)
this.onItemClickHandler =
this.onItemClickHandler.bind(this)
}
onItemClickHandler(itemName) {
debugger
console.log("Clicked " + itemName)
}
render() {
this.items = ["item1", "item2", "item3"]
var lis = []
for (var liName in this.items) {
var liName2 = this.items[liName]
console.log("Adding " + this.items[liName])
debugger
lis.push( <li className = "item-ListItem"
key = {
this.items[liName]
}
onClick = {
((item) => {
return () => this.onItemClickHandler(item)
})(this.items[liName])
}
>
<span>
{this.items[liName]}
</span>
</li>
)
}
return (
<div>
<div className="item">
<label className="item-Header">
<u>items</u>
</label>
<ul className="item-List" >
{lis}
</ul>
</div>
</div>
)
}
}
ReactDOM.render(<Test />, document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I will do it the recommended way.
First make a separate component of list-item.
To render list in react Js
//Handler
clickHandler=(id)=>{
console.log(`Clicked on Item with Id ${id}`);
}
//render method
render(){
let items = null;
if(this.state.isAnyItem){
<div className="items-list">
{
this.state.items.map((item)=>{
<Item key={item.id} item={item} click={(item.id)=>this.clickHandler(item.id)}/>
})
}
</div>
}
return (
<div>
{/*Some JSX here*/}
{items}
</div>
)
}
And Now the Item Component like
<div onClick={props.click}>
{/*Some JSX Here*/}
<h3>{item.name}</h3>
</div>
My console says that all keys are not unique, and that's why my removeItem function doesn't work. I'm trying to make a to-do list with React.js, and now I'm adding remove button. Can you help me? Here's the code:
var TodoItems = React.createClass({ //This is the removeItem function, that takes the key as a parameter and compares i.key to parameter.
removeItem: function(key){
var itemArray = this.props.entries;
for (var i = 0; i < itemArray.length; i++)
if (itemArray[i.key] === key) {
itemArray.splice(i, 1);
break;
}
},
render: function() {
var todoEntries = this.props.entries;
var _removeItem = this.removeItem;
function createTasks(item) {
return (
<div>
<li key={item.key}>{item.text}</li>
<button onClick = {_removeItem(item.key)} className= "remove"> Remove </button>
</div>
);
}
var listItems = todoEntries.map(createTasks);
return (
<ul className="theList">
{listItems}
</ul>
);
}
});
var TodoList = React.createClass({
getInitialState: function() {
return {
items: []
};
},
addItem: function(e) {
var itemArray = this.state.items;
//Here I create the key:
itemArray.push(
{
text: this._inputElement.value,
key: Math.random().toString(36).substring(7)
}
);
this.setState({
items: itemArray
});
this._inputElement.value = "";
e.preventDefault();
},
render: function() {
return (
<div className="todoListMain">
<div className="header">
<form onSubmit = {this.addItem}>
<input ref={(a) => this._inputElement = a}
placeholder="enter task">
</input>
<button type="submit">add</button>
</form>
</div>
<TodoItems entries={this.state.items}/>
</div>
);
}
});
The problem lies within your createTasks function.
function createTasks(item) {
return (
<div>
<li key={item.key}>{item.text}</li>
<button onClick = {_removeItem(item.key)} className= "remove"> Remove </button>
</div>
);
}
You're return an array of divs to populate an unordered list. Firstly, you should be populating any ul element with li, and any content within that li element.
The reason reactjs is giving you can error is because you're adding your key to a child of the element you're returning instead of the root node, in this case a div.
Also, your removeItem function isn't working because it looks like it's being invoked when you're building each task, I've edited my answer to resolve this.
The following should work without issue:
function createTasks(item) {
return (
<li key={item.key}>
{item.text}
<button onClick = {this.removeItem.bind(this, item.key)} className= "remove"> Remove </button>
</li>
);
}
Edited: I misread a portion of the question, and have edited my answer.
I'm trying to build a Jeopardy like game using React and Redux. I currently have an onClick event set to each li, but whenever I click on it, I get every Modal to pop up instead of the one that is attached to that li item. I have my code separated in different files but I believe these two files are the only ones I need to show:
const ModalView = React.createClass({
pass: function(){
console.log('pass clicked');
store.dispatch({type:"MODAL_TOGGLE"})
},
submit: function(){
console.log('submit clicked');
store.dispatch({type:"MODAL_TOGGLE"})
},
render: function(){
let question = this.props.question
let category = this.props.category
let answer = this.props.answer
let val = this.props.value
return (
<div>
<div className="modal">
<p>{category}</p>
<p>{question}</p>
<p>{answer}</p>
<p>{val}</p>
<input
type="text"
placeholder="type in your answer">
</input>
<button onClick={this.submit}>Submit</button>
<button onClick={this.pass}>Pass</button>
</div>
</div>
)
}
})
and ValuesView
const ValuesView = React.createClass({
modalPopUp: function(value){
store.dispatch({type:"MODAL_TOGGLE"})
},
render: function(){
let showClass = "show-content"
let hideClass = "hide-content"
if (this.props.modal){
showClass = "hide-content"
hideClass = "show-content"
}
return (<div>
<ul className="list">
{this.props.datum.clues.slice(0,5).map((data, i) => {
if (data.value === null){
return <div>
<div className={hideClass}>
<ModalView
category = {this.props.category}
question = {data.question}
answer = {data.answer}
value ={data.value} />
</div>
<li onClick={this.modalPopUp} key={i}>$600</li>
</div>
}
return <div>
<div className={hideClass}>
<ModalView
category = {this.props.category}
question = {data.question}
answer = {data.answer}
value ={data.value}/>
</div>
<li
category = {this.props.category}
onClick={this.modalPopUp} key={i}>${data.value}</li>
</div>
})}
</ul>
</div>
)
}
})
How would I go about only getting the corresponding Modal to display instead of every one? Thanks!!
If you just want to code real Modal I suggest you to use some already implemented component like https://github.com/reactjs/react-modal (I'm not saying is mandatory, nor even whit the example I suggest, could be other)
I think that store.dispatch({type:"MODAL_TOGGLE"}) in some way toggle modal prop between true and false. Then you just use that flag to toggle a class to show or hide content I guess (I would need to see you css).
The problem with this approach is (apart that is not the best way to do this for many reasons) that you are using the same class for every item in your clues array.
In some way you need to store which is the "modal" you want to show, and then in the render, just apply the show class to this item.
Maybe:
const ValuesView = React.createClass({
modalPopUp: function(index){
return function (event) {
store.dispatch({
type:"MODAL_TOGGLE",
payload: {
modalIndex: index // Save modalIndex prop
}
})
}
},
render: function(){
return (<div>
<ul className="list">
{this.props.datum.clues.slice(0,5).map((data, i) => {
if (data.value === null){
return <div>
<div className={(this.prosp.modal && this.props.modalIndex === i) ? "show-content" : "hide-content"}>
<ModalView
category = {this.props.category}
question = {data.question}
answer = {data.answer}
value ={data.value} />
</div>
<li onClick={this.modalPopUp(i)} key={i}>$600</li>
</div>
}
return <div>
<div className={(this.prosp.modal && this.props.modalIndex === i) ? "show-content" : "hide-content"}>
<ModalView
category = {this.props.category}
question = {data.question}
answer = {data.answer}
value ={data.value}/>
</div>
<li
category = {this.props.category}
onClick={this.modalPopUp(i)} key={i}>${data.value}</li>
</div>
})}
</ul>
</div>
)
}
})
The following code works, but I think there's room for improvement. The index check is there because after the first element is removed the next element looks like it has an index of -1, but is actually the previously removed element. Then it iterates again and finds the clicked element and removes it. BUT since the index is -1 on the first go around the wrong group gets deleted.
How do I keep the zombie elements from being iterated on more efficiently? This is in a backbone view with an in page confirmation.Thanks.
EDIT: To add HTML
Group section always has a default group that shouldn't be deleted.
<div class="section-border-top--grey js-favorite-group">
<h4 class="expandable__cta cta--std-teal js-expand-trigger"><span class="icon icon-plus--teal expandable__cta-icon"></span>All Doctors</h4>
<div class="expandable__content js-favorite-doctor-row-container" aria-expanded="true">
<div class="location-section dr-profile">
<div class="section__content js-doctor-row">
<div class="favorite-doctor-manage__row">
DR info
</div>
</div><!--/section__content-->
</div><!--/location-section-->
</div><!--/expandable__content-->
Tag section to remove groups
<div class="js-favorite-doctor-manage-add-remove">
<div class="grid-construct">
<div class="expandable" data-controller="expandable">
<ul class="tag-list js-group-list" tabindex="-1">
<li class="tag tag--teal" >
Lauren's Doctors
<ul class="tag-sub">
<li><button class="tag-icon tag-icon--close-white js-group-remove">Remove group: Lauren's Doctors</button></li>
</ul>
</li>
<li class="tag tag--teal" >
Timmy's Doctors
<ul class="tag-sub">
<li><button class="tag-icon tag-icon--close-white js-group-remove">Remove group: Timmy's Doctors</button></li>
</ul>
</li>
</ul>
</div>
removeGroup: function( evt ) {
var deleteGroup = function() {
if ( $(evt.currentTarget).closest('.tag').hasClass('is-active')){
var clickedTag = $(evt.currentTarget).closest('.tag');
var groupList = this.$el.find('.js-group-list');
var groupTags = groupList.find('.tag');
var index = groupTags.index(clickedTag);
var groupSections = $('.js-favorite-group');
// add one to account for "All" section which is never removed
var groupToRemove = groupSections.eq(index + 1);
console.log(groupToRemove);
var removedGroupName = this.getGroupNameForSection(groupToRemove);
var allDoctors = groupSections.eq(0);
var allDoctorsContainer = allDoctors.find('.js-favorite-doctor-row-container');
if ( index > -1 ){
groupToRemove.find('.js-favorite-doctor-row').appendTo(allDoctorsContainer);
clickedTag.remove();
groupToRemove.remove();
this.updateSectionDropdowns();
this.ariaAlert('Group ' + removedGroupName + ' removed');
this.hideConfirm(evt);
}
}
};
this.showAlert(evt, deleteGroup);
},
showAlert: function (evt, callback) {
that = this;
var clickedTag = '';
clickedTag = $(evt.currentTarget).closest('.tag');
clickedTag.addClass('is-active').attr('data-delete','true');
$('.delete-acct-message').show().focus();
$('.js-remove-yes').on('click', function(evt){
evt.preventDefault();
callback.apply(that);
});
$('.js-remove-no').on('click', function(evt){
evt.preventDefault();
this.hideConfirm(evt);
});
},
I would suggest that you should use custom attributes in your html, this will simplify your javascript logic and make it more effective and efficient.
I have modified your html and javascript to add the support for custom attribute data-doc-group. Have a look at your group sections div here
<div data-doc-group="lauren" class="section-border-top--grey js-favorite-group">
<h4 class="expandable__cta cta--std-teal js-expand-trigger"><span class="icon icon-plus--teal expandable__cta-icon"></span>Lauren's Doctors</h4>
<div class="expandable__content js-favorite-doctor-row-container" aria-expanded="true">
<div class="location-section dr-profile">
<div class="section__content js-doctor-row">
<div class="favorite-doctor-manage__row">
DR info
</div>
</div><!--/section__content-->
</div><!--/location-section-->
</div>
Here are the tags with custom attributes
<li data-doc-group="lauren" class="tag tag--teal">
Lauren's Doctors
<ul class="tag-sub">
<li><button class="tag-icon tag-icon--close-white js-group-remove">Remove group: Lauren's Doctors</button></li>
</ul>
</li>
<li data-doc-group="timmy" class="tag tag--teal">
Timmy's Doctors
<ul class="tag-sub">
<li><button class="tag-icon tag-icon--close-white js-group-remove">Remove group: Timmy's Doctors</button></li>
</ul>
</li>
Here is the javascript to handle this, (this may be a bit buggy, but will give you a general idea)
removeGroup: function(evt) {
this.showAlert(evt, function() {
var $clickedTag = $(evt.currentTarget).closest('.tag'),
dataGroupName,
$groupToRemove,
removedGroupName,
$allDoctors = $('.js-favorite-group').eq(0),
$allDoctorsContainer = $allDoctors.find('.js-favorite-doctor-row-container');
if ($clickedTag.hasClass('is-active')){
dataGroupName = $clickedTag.data('doc-group');
$groupToRemove = $allDoctors.siblings('[data-doc-group="' + docGroupName + '"]');
if ($groupToRemove.length > 0){
$groupToRemove.find('.js-favorite-doctor-row').appendTo($allDoctorsContainer);
$clickedTag.remove();
$groupToRemove.remove();
removedGroupName = this.getGroupNameForSection($groupToRemove);
this.updateSectionDropdowns();
this.ariaAlert('Group ' + removedGroupName + ' removed');
this.hideConfirm(evt);
}
}
});
}
Basicaly, I have viewModel in KO having array of 2 values, I need to change css class prop of element (main) when <a> elemets are being clicked (when 1st li>a "Val1" clicked, <main class="stl1">... and so on). Strangely , nothing happends to <main>:
<script>
var mainViewModel = function () {
var self = this;
self.classArr = ['stl1', 'stl2'];
self.cssUsed = ko.observable(0);
self.getClass = function ( data, event ) {
var dat = event.target.value;
self.cssUsed = self.classArr[dat];
console.log( dat + ' : ' + self.cssUsed );
}
}
ko.applyBindings( new mainViewModel() );
</script>
<div class='page'>
<header>
<nav>
<ul >
<li>Val1</li>
<li>Val2</li>
</ul>
</nav>
</header>
<div id='maincontent'>
<main data-bind="css: cssUsed" >
<div class="center"></div>
</main>
</div>
</div>
You got it almost right. The problem was that your were assigning the value in the wrong way. Instead of
self.cssUsed = self.classArr[dat];
try
self.cssUsed(self.classArr[dat]);
Check it out here