I have an element and a child element, both have an event handler, however, the parent handler seems to block the child handler.
I have tried to use https://reactjs.org/docs/events.html#supported-events.
sadly its didn't work or I didn't use it correctly.
Where is some of my code:
classes:
Matrix and Tile
the Matrix render function contains:
<element onMouseDown={this._onPressDown} onMouseUp={this._onPressUp}><Matrix/></element>
Matrix include the Tile object as NxN matrix as following:
[[<Tile/>,<Tile/>],[<Tile/>,<Tile/>]]
the Tile render functioncontains:
<div onClick={this._onClick} onMouseEnter={this._onEnter}><Matrix/></div>
The child (tile) function _onClicked is not beening called.
Edit:
buildMatrix(){
let [tileWidth,tileHeight] = this.calculateTileSize();
this.reBuildMatrix = !this.reBuildMatrix;
return (<element onMouseDown={this._onPressDown} onMouseUp={this._onPressUp} stopPropagation>
<table className='matrix' key={this.reBuildMatrix}>{
this.state.tileMatrixMap.map((rowItem,row)=>(
<tr>
{
rowItem.map((tile,col)=>(
<th><Tile
width={tileWidth}
height={tileHeight}
isWall={tile.isWall}
isPath={tile.isPath}
onClick={tile.onClick}
position= {[row,col]}
backgroundColor={tile.isWall?this.wallColor:(tile.isPath? this.pathColor:this.backgroundColor)}
/></th>
))
}
</tr>
))}
</table>
</element>);
When this line is disabled this.reBuildMatrix = !this.reBuildMatrix;
the code work as expexted, how ever if I enable the line the function is not called
Related
I have bound click eventListeners to an up and down vote button.
Problem: When I click on different parts of the button I get the corresponding element I clicked on and not the parent element which contains relevant information for further processing.
What I already tried: I already tried ev.stopPropagation(); but the behaviour remained the same.
Question: How can I solve this problem?
My example code
const commentVotes = document.querySelectorAll('.comment-votes');
commentVotes.forEach((row) => {
const up = row.querySelector('.comment-vote-up');
const down = row.querySelector('.comment-vote-down');
up.addEventListener('click', async (ev) => {
// ev.stopPropagation();
const id = ev.target.getAttribute('data-item-id');
console.log({"target": ev.target, "ID": id})
});
down.addEventListener('click', async (ev) => {
// same as up
})
});
.comment .comment-vote-box {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.spacer {
margin-right:10px;
}
<div class="comment">
<div class="comment-vote-box comment-votes mt-10">
<div class="vote-up">
<button class="comment-vote-up"
data-item-id="11">
<span class="spacer">Like</span>
<span>0</span>
</button>
</div>
<div class="vote-down">
<button class="comment-vote-down"
data-item-id="12">
<span class="spacer">Dislike</span>
<span>1</span>
</button>
</div>
</div>
</div><!-- comment -->
Use the Event.currentTarget to get the attribute values from.
ev.target: is the element within the bubbling that triggered the event. So exactly what you are describing.
ev.currentTarget: is the element to which you have bound the listener.
* ev = event
https://medium.com/#etherealm/currenttarget-vs-target-in-js-2f3fd3a543e5
const commentVotes = document.querySelectorAll('.comment-votes');
commentVotes.forEach((row) => {
const up = row.querySelector('.comment-vote-up');
const down = row.querySelector('.comment-vote-down');
up.addEventListener('click', async (ev) => {
// ev.stopPropagation();
const id = ev.currentTarget.getAttribute('data-item-id');
console.log({"target": ev.target, "currentTarget": ev.currentTarget, "ID": id})
});
down.addEventListener('click', async (ev) => {
// same as up
})
});
.comment .comment-vote-box {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.spacer {
margin-right:10px;
}
<div class="comment">
<div class="comment-vote-box comment-votes mt-10">
<div class="vote-up">
<button class="comment-vote-up"
data-item-id="11">
<span class="spacer">Like</span>
<span>0</span>
</button>
</div>
<div class="vote-down">
<button class="comment-vote-down"
data-item-id="12">
<span class="spacer">Dislike</span>
<span>1</span>
</button>
</div>
</div>
</div><!-- comment -->
You probably meant to use event.currentTarget instead of event.target:
event.currentTarget is the target of the current listener (further: current target).
event.target is the target to which the event was dispatched (further: dispatching target).
Alternatively you can just reference the specific target directly since you use distinct listeners with distinct references (up, down) as a closure.
Instead of using distinct listeners for each element, you could also make use of event delegation (see below).
Also, see below for an explanation of event propagation and stopPropagation().
Event propagation
A single dispatched event may invoke multiple listeners.
The order of invocation for the listeners is specified to happen in phases:
Capturing phase:
Capturing listeners are invoked in tree-order; from root to dispatching target.
Target phase:
Non-capturing listeners of the dispatching target are invoked.
Bubbling phase:
Non-capturing listeners are invoked in reverse tree-order; from (excluding) dispatching target to root.
Additionally, listeners of a target are invoked in the order in which they are added.
This sequence (of listener invocations) is called event propagation.
At any point may a listener stop this propagation from reaching the next listener, e.g. via event.stopPropagation():
Example of stopping propagation early:
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
// A non-capturing listener
inner.addEventListener("click", evt => logId(evt.currentTarget));
outer.addEventListener("click", evt => {
evt.stopPropagation();
logId(evt.currentTarget);
}, { capture: true }); // Attach a capturing listener
function logId(element) {
console.log(element.id);
}
#outer {background-color: blue}
#inner {background-color: red}
div {
padding: .2rem;
padding-block-start: 3.8rem;
}
<div id="outer">
<div id="inner"></div>
</div>
Event delegation
Listeners of common ancestors of two distinct elements will be invoked due to event propagation (see above).
And events hold references to the dispatching target and the current target. This allows listeners to be more abstract than otherwise possible.
Together, these aspects allow events to be handled by one listener on a (but usually the first) common ancestor (further: delegator) for multiple distinct elements. This is called event delegation.
Sidenote: This is most easily realized if the relevant elements are siblings, but this is not a requirement.
(Any descendant of) the delegator may be the dispatching target. This also means that no relevant element may be a target. For example, if the common ancestor itself is the dispatching target, then no relevant element is targeted.
We need to assert that the event happened in a relevant element. Otherwise the event should not be handled.
In most cases that assertion can be done by querying for the relevant element with calling closest() on event.target.
Advantages of using event delegation:
"It allows us to attach a single event listener for elements that exist now or in the future":
Less memory usage.
Less mental overhead and simpler code when adding/removing elements.
Allows "behaviour pattern": Elements with e.g. certain attributes will automatically inherit some behaviour.
Allows separation of design (relevant elements + CSS) and application (delegator).
Less likely to cause significant memory leaks:
One listener means one closure at maximum. As opposed to potentially infinite listeners and therefore closures, this one closure is less likely to have a significant effect on memory usage.
Note that not all events bubble, meaning you cannot use event delegation to handle them.
Example
A typical listener for event delegation...
Finds the first common ancestor to be used as a delegator.
Attaches an abstract listener to the common ancestor:
Assert that event happened in a relevant element; otherwise abort.
Handles the event.
Let's say we have a table of products, and want to log the product row that was clicked on as an object. An implementation may look like this:
const tbody = document.querySelector("tbody"); // First common ancestor
tbody.addEventListener("click", evt => {
const tr = evt.target.closest("tr"); // Find reference to relevant element
if (tr === null) return; // Abort if event not in relevant element
// Usecase-specific code
logRow(tr);
});
function logRow(tr) {
const [idCell, nameCell, amountCell] = tr.children;
const row = {
id: idCell.textContent,
name: nameCell.textContent,
amount: Number(amountCell.textContent)
};
console.log(row);
}
<table>
<caption>Table of products</caption>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Amount in stock</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>Spaghetti</td>
<td>34</td>
</tr>
<tr>
<td>1</td>
<td>Peanuts</td>
<td>21</td>
</tr>
<tr>
<td>3</td>
<td>Crackers</td>
<td>67</td>
</tr>
</tbody>
</table>
Without event delegation (i.e. with distinct listeners), an implementation could look like this:
const tableRows = document.querySelectorAll("tbody>tr");
tableRows.forEach(tr => {
tr.addEventListener("click", () => {
// `event.currentTarget` will always be `tr`, so let's use direct reference
logRow(tr);
});
})
function logRow(tr) {
const [idCell, nameCell, amountCell] = tr.children;
const row = {
id: idCell.textContent,
name: nameCell.textContent,
amount: Number(amountCell.textContent)
};
console.log(row);
}
<table>
<caption>Table of products</caption>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Amount in stock</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>Spaghetti</td>
<td>34</td>
</tr>
<tr>
<td>1</td>
<td>Peanuts</td>
<td>21</td>
</tr>
<tr>
<td>3</td>
<td>Crackers</td>
<td>67</td>
</tr>
</tbody>
</table>
If you were to add or remove rows, then...
... the first example would just work.
... the second example would have to consider adding listeners to the new elements. If they are added e.g. via innerHTML or cloneNode(), then this may become complicated.
I'm new to javascript and its ecosystem. I'm trying to build some components using mithril.js. My goal is to have a component that shows some properties and provides a couple of button for each of them. Just to learn about mithril.js and jsx. Here is what I did so far:
const m = require("mithril");
var Something = {
_increase: function(category) {
console.log("increase category: "+category);
},
_decrease: function(category) {
console.log("decrease category: "+category);
},
view: function(vnode) {
return <div>
{Object.keys(vnode.attrs.categories).map((category)=> {
return <div>
<label for={category}>{category}</label>
<input type="number" id={category} value={vnode.attrs.categories[category]} />
<button type="button" onclick="{this._increase(category)}">MORE</button>
<button type="button" onclick="{this._decrease(category)}">LESS</button>
</div>
})}
</div>
}
}
export default Something;
Well, component seems to work fine, node doesn't complain and labels and buttons and fields are displayed on page, but, when I click on a button, nothing happen. It looks like event isn't fired. What's wrong?
Two things: (1) I think you should just put the function into the onclick handler braces instead of encoding the function in a string. (2) It looks like you're immediately invoking the function, not declaring that the onclick handler is a function that uses the category argument. Try passing in an anonymous function with no arguments, that way you when the onclick event is fired it can take in the category as a parameter:
onclick={() => this._increase(category)}
onclick={() => this._decrease(category)}
In my parent component, I used a component called List as follows.
render() {
return (
<div className="experiments">
<div className="experiments-list-container">
<List rowItems={this.state.employeeData} />
</div>
</div>
);
}
}
In my List component, I am trying to change the style whenever each item of the row is clicked. So what I did is:
render() {
const dateDisplay = moment(this.props.createdAt).format('MMM YYYY');
return (
<tr
className={this.state.isExpanded ? 'testclass' : "experiment-list__row"}
//className="experiment-list__row"
onClick={this.handleRowClick}
>
<td>
{this.props.rowItems.firstName + ' ' + this.props.rowItems.lastName}
</td>
<td>{this.props.rowItems.jobTitle}</td>
<td>{'Email#Email.com'}</td>
<td>{this.props.rowItems.employmentType}</td>
</tr>
);
}
whenever I click a row in the table, it will all a function that changes the this.state.isExpanded to True. However, the style that I actually want to change is <div className="experiments"> or <div className="experiments-list-container">. But I am not sure how to change the style of the upper-level component. Please help.
EDIT
Thanks for the reply. What I have tried is,
const List = props => {
return (
<table className="experiment-list">
<tbody>
<ListHeader />
{props.rowItems.map((data, i) => <ListRow
key={i}
rowItems={data}
onRowClicked={props.onRowClicked} />)}
</tbody>
</table>
);
};
and
toggleEmployerInfo(e) {
alert('dd')
}
in my parent component.
Whenever I click each row, it alerts "dd" correctly.
However, what I eventually want to do is pass in the info of the row clicked.
In my parent component, I use the List by doing
<div className="experiments-list-container">
<List
rowItems={this.state.employeeData}
onRowClicked={this.toggleEmployerInfo.bind(this)}
/>
</div>
This does render all data into each row correctly, but how can I make each row correctly read the id of the item that the row has?
You could pass an event handler to the List component and call it whenever a row is clicked. Here I've defined handleRowClick in the parent component as an ES6 arrow function. Then I pass this function as a callback to the child component via the onRowClicked prop.
// parent.jsx
handleRowClick = (id) => {
// Handle click event, update state, etc.
console.log(id);
}
render() {
return (
<div className="experiments">
<div className="experiments-list-container">
<List rowItems={this.state.employeeData} onRowClicked={this.handleRowClick} />
</div>
</div>
);
}
And then call the onRowClicked function on the onClick event for each element you want to react to.
// list.jsx
render() {
return (
// Extremely simplified example...
<div onClick={() => this.props.onRowClicked('row-id-goes-here')}>row content</div>
);
}
The arrow function syntax here allows us to specify parameters beside the default event parameter that you would get if you just used onClick={this.props.onRowClicked}.
Here is a post explaining this approach better than I can: https://medium.com/#machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb
I have several components that use basically the same table so I'm in the process of abstracting out that table. I have solved most of my dynamic table population needs but have yet to find a solution to the following.
In one of my table instances the rows need to be clickable. In the original table I simply added a click event in the row and had it call a function in my typescript file.
Now that the table is a child of any consuming component I am not sure how to dynamically add this click event. Here is an essentially what I am trying to achieve:
HTML:
<tr class="someClass" <!-- want click event here -->>
<td *ngFor="let column of row;"><div [innerHtml]="column"></div></td>
</tr>
This is the tables typescript file, where all the data is coming in on the visibleData object:
export class GenericTableComponent implements OnInit {
#Input() visibleData;
constructor() { }
ngOnInit() {
}
}
I implement the generic table in my parent HTML here
Parent HTML:
<oma-generic-table [visibleData]="visibleData"></oma-generic-table>
And here is a function in the parent which prepares the data. I have attempted to store the click event in a string and pass it but everything I've tried so far has failed (data binding with {{}}, square brackets, etc..).
transformData(visibleData) {
const ret: any = {};
ret.headings = visibleData.headings;
ret.action = '(click)="rowClicked([row.id])"';
ret.checkbox = this.checkBox; //add if the table needs checkboxes
ret.content = [];
for (let i = 0; i < visibleData.content.length; i++) {
ret.content.push(_.values(_.omit(visibleData.content[i], 'id')));
}
return ret;
}
However, even when hard coded into the child, the click event doesn't recognize the function in the parent and I get the following error:
EXCEPTION: Error in ./GenericTableComponent class GenericTableComponent - inline template:35:4 caused by: self.parentView.context.rowClicked is not a function
ORIGINAL EXCEPTION: self.parentView.context.rowClicked is not a function
I'm not sure if this is something simple or not. I'm new to Angular 2 so I apologize if this question is simplistic. Thanks in advance for any help.
Your generic table can emit custom events to which parent component can subscribe:
#Component({
selector: 'oma-generic-table',
template: `
<table>
<tr *ngFor="let row of visibleData" class="someClass" (click)="selectRow(row)">
<td *ngFor="let column of row;"><div [innerHtml]="column"></div></td>
</tr>
</table>
`
})
export class OmaGenericTable {
#Input() visibleData: VisibleDataRow[];
#Output() select = new EventEmitter<VisibleDataRow>();
selectRow(row: VisibleDataRow) {
this.select.emit(row);
}
}
Then in your parent component:
// in template
<oma-generic-table
[visibleData]="visibleData"
(select)="tableRowSelected($event)"
></oma-generic-table>
// in component
tableRowSelected(r: VisibleDataRow) {
console.log(`Selected row ${r}`);
}
Angular2 as far as I know will not bind to a "(click)" event that way. It may look ugly, but I would add the NgIf directive to the table on the row that the click event should act upon, and inversely apply the logic to another row that should not be affected by a click event. Ex:
<tr *ngIf="!isClickable" class="someClass">
<tr *ngIf="isClickable" class="someClass" (click)="rowClicked()>
<td *ngFor="let column of row;"><div [innerHtml]="column"></div></td>
</tr>
Then you can pass the isClickable variable as an input into your table from anywhere that you instantiate the table component, and act upon it.
Using this in HTML
<tr *ngIf="!isClickable" class="someClass">
<tr *ngIf="isClickable" class="someClass" #click>
<td *ngFor="let column of row;"><div [innerHtml]="column"></div></td>
</tr>
this in typescript
export class GenericTableComponent implements OnInit {
#Input() visibleData;
#ViewChild('click') protected _click:ElementRef;
constructor(enderer: Renderer) {
renderer.listen(this._click.nativeElement, 'click', (event) => {
// Do something with 'event'
})
}
}
Hope that help you
I am getting the following error when my react component is re-rendered after a click event:
Uncaught Error: Invariant Violation: processUpdates(): Unable to find child 2 of element. This probably means the DOM was unexpectedly mutated ...
This only happens when my table has a different number of rows than the previously rendered version. For example:
/** #jsx React.DOM */
React = require('react');
var _ = require("underscore");
var testComp = React.createClass({
getInitialState: function () {
return {
collapsed: false
};
},
handleCollapseClick: function(){
this.setState({collapsed: !this.state.collapsed});
},
render: function() {
var rows = [
<tr onClick={this.handleCollapseClick}><th>Header 1</th><th>Header 2</th><th>Header 3</th></tr>
];
if(!this.state.collapsed){
rows.push(<tr><th>Row1 1</th><th>Row1 2</th><th>Row1 3</th></tr>);
}
rows.push(<tr><th>Footer 1</th><th>Footer 2</th><th>Footer 3</th></tr>);
return <div>
<table>
{rows}
</table>
</div>
}
});
module.exports = testComp
If I render different content, but with the same number of rows, I don't get the error, so if I update the if statement to:
if(!this.state.collapsed){
rows.push(<tr><th>Row1 1</th><th>Row1 2</th><th>Row1 3</th></tr>);
}else{
rows.push(<tr><th>Row2 1</th><th>Row2 2</th><th>Row2 3</th></tr>);
}
... everything works.
Do I need to force react to re-render the entire component in this case, instead of just the 'changed' elements?
You should read the full error message (at least that's what I am seeing):
Unable to find child 2 of element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a <tbody> when using tables, nesting tags like <form>, <p>, or <a>, or using non-SVG elements in an parent.
Every table needs a <tbody> element. If it doesn't exist, the browser will add it. However, React doesn't work if the DOM is manipulated from the outside.
Related: Removing row from table results in TypeError
I encountered this when dealing with p tags. I had nested paragraphs within paragraphs.
To resolve the problem I replaced the wrapping p element with a div element.
Before:
render: function render() {
return (
<p>
<p>1</p>
<p>2</p>
</p>
);
}
After:
render: function render() {
return (
<div>
<p>1</p>
<p>2</p>
</div>
);
}
For people who are using react-templates:
You have to generate the <tbody> tag in the .jsx file. If you only add it in the .rt file you still get the error message.
this.tbody = <tbody>{tablerows}</tbody> // - in .jsx
In My case the fix is to remove spaces in Table render from
<tbody> {rows} </tbody>
to
<tbody>{rows}</tbody>
I think your problem is with using the <th> tag in multiple rows. <th> is reserved for the header row. Try replacing it with <td> everywhere except in the first row. Also you should wrap the header in a <thead> tag and the rest in a <tbody> tag:
var header = [
<tr onClick={this.handleCollapseClick}><th>Header 1</th><th>Header 2</th><th>Header 3</th></tr>
];
var body = [];
if(!this.state.collapsed){
body.push(<tr><td>Row1 1</td><td>Row1 2</td><td>Row1 3</td></tr>);
}
body.push(<tr><td>Footer 1</td><td>Footer 2</td><td>Footer 3</td></tr>);
return <div>
<table>
<thead>{header}</thead>
<tbody>{body}</tbody>
</table>
</div>;