Ultimately I am trying to create a piano like application that you can control by click or key down. I want each keyboard key to control a certain note. Each "piano" key is a component which has a keycode property and a note property. When the event.keycode matches the keycode property I want the associated note to play. What is the best strategy to go about this?
I have tried using refs and playing around with focus on componentDidMount. I cant seem to wrap my head around how this should work.
class Pad extends React.Component {
constructor(props) {
super(props)
this.state = {
clicked: false
}
this.clickedMouse = this.clickedMouse.bind(this)
this.unClickedMouse = this.unClickedMouse.bind(this)
this.handleDownKeyPress = this.handleDownKeyPress.bind(this)
this.handleUpKeyPress = this.handleUpKeyPress.bind(this)
}
clickedMouse(e) {
this.setState({ clicked: true })
this.props.onDown(this.props.note)
console.log(e)
}
unClickedMouse(e) {
this.setState({ clicked: false })
this.props.onUp(this.props.note)
}
handleDownKeyPress(e) {
if (e.keyCode === this.props.keyCode && this.state.clicked === false) {
this.setState({ clicked: true })
this.props.onDown(this.props.note)
}
}
handleUpKeyPress(e) {
this.setState({ clicked: false })
this.props.onUp(this.props.note)
}
render() {
return (
<div
className='pad'
onMouseUp={this.unClickedMouse}
onMouseDown={this.clickedMouse}
onKeyDown={this.handleDownKeyPress}
onKeyUp={this.handleUpKeyPress}
tabIndex='0'
/>
);
}
}
export default Pad
class Pads extends React.Component {
constructor(props) {
super(props);
// tone.js build
this.synth = new Tone.Synth().toMaster()
this.vol = new Tone.Volume(0)
this.synth.chain(this.vol, Tone.Master)
// bindings
this.onDownKey = this.onDownKey.bind(this);
this.onUpKey = this.onUpKey.bind(this);
}
onDownKey(note) {
console.log(`${note} played`);
this.synth.triggerAttack(note);
}
onUpKey(note) {
this.synth.triggerRelease();
}
render() {
const { octave } = this.props
return (
<div className="pad-grid">
<Pad
keyCode={65}
note={`C${octave}`}
onDown={this.onDownKey}
onUp={this.onUpKey}
/>
<Pad
keyCode={70}
note={`Db${octave}`}
onDown={this.onDownKey}
onUp={this.onUpKey}
/>
<Pad
keyCode={83}
note={`D${octave}`}
onDown={this.onDownKey}
onUp={this.onUpKey}
/>
<Pad
keyCode={68}
note={`Eb${octave}`}
onDown={this.onDownKey}
onUp={this.onUpKey}
/>
</div>
);
}
}
export default Pads
Heres a codepen so you can check it out in action https://codepen.io/P-FVNK/pen/XopBgW
Final Code: https://codesandbox.io/s/5v89kw6w0n
So I made quite a number of changes to enable the functionality you wanted.
Is there a way to maybe loop through the repeated components and their properties to match event.keyCode to correct keycode property?
To enable this, I decided to create an object array containing the possible keys and their notes. I changed them from the now deprecated KeyboardEvent.keyCode to KeyboardEvent.key instead. You could use KeyboardEvent.code as well.
this.padCodes = [
{
key: "a",
note: "C"
},
{
key: "f",
note: "Db"
},
{
key: "s",
note: "D"
},
{
key: "d",
note: "Eb"
}
];
I moved the keydown event listeners from the individual <Pad />s to the parent component and attached the listener to the document instead.
document.addEventListener("keydown", e => {
let index = this.state.pads.findIndex(item => item.key === e.key);
let { octave } = this.context;
if (this.state.pressed === false) {
this.onDownKey(`${this.state.pads[index].props.note}${octave}`);
this.setState({
activeNote: this.state.pads[index].note,
pressed: true
});
}
});
document.addEventListener("keyup", e => {
this.onUpKey(this.state.activeNote);
this.setState({
activeNote: "",
pressed: false
});
});
You may also notice I decided to use the Context instead of props. That's just personal preference, but I rather have one main reference to the octave state rather than passing the prop to each child component.
After that it was just a matter of ensuring that the functions made the calls to the same function and passed both the note and octave.
To reduce some of the clutter I made an array of <Pad />s which I later rendered instead of typing them out one by one.
let newPadCodes = this.padCodes.map(x => (
<Pad
key={x.key.toString()}
note={`${x.note}`}
onDown={this.onDownKey}
onUp={this.onUpKey}
/>
));
this.setState({
pads: newPads
});
//And later
//Render if this.state.pads.length is > 0
{this.state.pads.length > 0 &&
this.state.pads}
That just about covers all I did. Here's the codesandbox with the modified code: https://codesandbox.io/s/5v89kw6w0n
Related
This question already has an answer here:
Issues with suggestion list in botframework Webchat React
(1 answer)
Closed 2 years ago.
I am currently doing bot-framework web chat application using react js.I want to know how can i take a key up/down. As i want to select or highlight through suggestion list using arrow keys. My code which i used for the react component is attached below, PFA,
async handleSuggestionClick(val) {
const newValue = val.currentTarget.textContent;
await this.setState({ typingChecking: "false" },
() => { console.log(this.state.typingChecking) });
await this.setState({ suggestions: [] },
() => { console.log(this.state.suggestions) });
this.state.suggestionCallback.dispatch({
type: 'WEB_CHAT/SET_SEND_BOX',
payload: {
text: newValue,
}
});
await this.setState({ typingChecking: "true" },
() => { console.log(this.state.typingChecking) });
}
<div className="react-container webchat" >
<ReactWebChat directLine={this.state.directLine}
webSocket={true}
store={this.state.storeValue}
sendTypingIndicator={true} />
<div className="SuggestionParent" id="Suggestion1">
{this.state.suggestions.map(suggestion => (
<div className="Suggestion"
onClick={this.handleSuggestionClick}
onKeyDown={this.handleKeyDown}>
{suggestion
.toLowerCase()
.startsWith(this.state.suggestionTypedText) ?
(<div>
<b>
{this.state.suggestionTypedText}
</b>
{suggestion
.toLowerCase()
.replace(this.state.suggestionTypedText, "")}
</div>) :
(<div>{suggestion}</div>
)
}
</div>
))
}
</div>
From the answer to your duplicate issue: Issues with suggestion list in botframework Webchat React
Keyboard events are generally handled in React using the onKeyDown
property. I've placed it on an element that contains both Web Chat and
your suggestions parent:
<div className={ROOT_CSS} onKeyDown={this.handleKeyDown.bind(this)}>
<div className={WEB_CHAT_CSS + ''}>
<ReactWebChat
That will handle all key presses, so you'll need a way to route to the
function for the correct key. You could use a switch statement but
the source code for [react-autocomplete][3] uses a lookup object and I
think that's smart.
keyDownHandlers = {
ArrowDown(event) {
this.moveHighlight(event, 1);
},
ArrowUp(event) {
this.moveHighlight(event, -1);
},
Enter(event) {
const {suggestions} = this.state;
if (!suggestions.length) {
// menu is closed so there is no selection to accept -> do nothing
return
}
event.preventDefault()
this.applySuggestion(suggestions[this.state.highlightedIndex]);
},
}
handleKeyDown(event) {
if (this.keyDownHandlers[event.key])
this.keyDownHandlers[event.key].call(this, event)
}
I've centralized the functionality for the up and down arrows into one
function: moveHighlight. You will need to define a new property in
your state to keep track of which suggestion has been selected by the
keyboard. I'm keeping the name highlightedIndex from
react-autocomplete.
moveHighlight(event, direction) {
event.preventDefault();
const { highlightedIndex, suggestions } = this.state;
if (!suggestions.length) return;
let newIndex = (highlightedIndex + direction + suggestions.length) % suggestions.length;
if (newIndex !== highlightedIndex) {
this.setState({
highlightedIndex: newIndex,
});
}
}
I have a header component where I need to render three buttons, so every three buttons have three props. One is the class name, click handler and text.
So out of three buttons, two buttons act as a toggle button, so based on the click the text should change.
See the below code:
class App extends Component(){
state = {
navigationList: [{
text: 'Signout',
onClickHandler: this.signoutHandler,
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode,
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay,
customClassName: 'buttonStyle'
}]
}
signoutHandler = () => {
// some functionality
}
viewMode = () => {
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay = () => {
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<Header navigationList={this.state.navigationList}/>
)
}
}
const Header = ({navigationList}) => {
return (
<>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</>
)
}
The other way is I can pass all the props one by one and instead of an array I can write three button elements render it, but I am thinking to have an array and render using a map.
So which method is better, the problem that I am facing is if use the array. map render
the approach I need to set the initial value as a variable outside and how can I set the state.
And I am getting the onClick method is undefined, is it because the function is not attached to the state navigation list array.
Update
I declared the functions above the state so it was able to call the function.
So in JS, before the state is declared in the memory the functions should be hoisted isn't.
class App extends React.Component {
constructor(props){
super();
this.state = {
isStudents:false,
activeWay:false,
}
}
createList(){
return [{
text: 'Signout',
onClickHandler: this.signoutHandler.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay.bind(this),
customClassName: 'buttonStyle'
}];
}
signoutHandler(){
}
viewMode(){
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay(){
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<div>
<div>ddd</div>
<Header navigationList={this.createList()} />
</div>
)
}
}
const Header = ({navigationList}) => {
console.log(navigationList);
return (
<div>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
https://jsfiddle.net/luk17/en9h1bpr/
Ok I will try to explain, If you see you are using function expressions in your class and as far as hoisting is concerned in JavaScript, functions expressions are not hoisted in JS only function declarations are hoisted, function expressions are treated as variables in JS.
Now for your case you don't have to shift your functions above the state, you can simply use constructor for initializing state as
constructor(props) {
super(props);
this.state = {
isStudents: false,
activeWay: false,
navigationList: [
{
text: "Signout",
onClickHandler: this.signoutHandler,
customClassName: "buttonStyle"
},
{
text: "Teachers",
onClickHandler: this.viewMode,
customClassName: "buttonStyle"
},
{
text: "Active Hidden",
onClickHandler: this.activeWay,
customClassName: "buttonStyle"
}
]
};
}
Now you will have your handlers available as it is
Sandbox with some modification just to show
EDIT:
You can have default text for buttons and change it when clicking,
Sandbox updated
Hope it helps
I'm working on a component that should be able to:
Search by input - Using the input field a function will be called after the onBlur event got triggered. After the onBlur event the startSearch() method will run.
Filter by a selected genre - From an other component the user can select a genre from a list with genres. After the onClick event the startFilter() method will run.
GOOD NEWS:
I got the 2 functions above working.
BAD NEWS:
The above 2 functions don't work correct. Please see the code underneath. The 2 calls underneath work, but only if I comment one of the 2 out. I tried to tweak the startSearch() method in various ways, but I just keep walking to a big fat wall.
//////Searching works
//////this.filter(this.state.searchInput);
//Filtering works
this.startFilter(this.state.searchInput);
QUESTION
How can I get the filter/search method working?. Unfortunately simply putting them in an if/else is not the solution (see comments in the code).
import { Component } from 'preact';
import listData from '../../assets/data.json';
import { Link } from 'preact-router/match';
import style from './style';
export default class List extends Component {
state = {
selectedStreamUrl: "",
searchInput: "",
showDeleteButton: false,
searchByGenre: false,
list: [],
}
startFilter(input, filterByGenre) {
this.setState({
searchByGenre: true,
searchInput: input,
showDeleteButton: true
});
alert("startFilter ")
console.log(this.state.searchByGenre)
/////////---------------------------------
document.getElementById("searchField").disabled = false;
document.getElementById('searchField').value = input
document.getElementById('searchField').focus()
// document.getElementById('searchField').blur()
document.getElementById("searchField").disabled = true;
console.log(input)
this.filter(input);
}
//search
startSearch(input) {
alert("startSearch ")
console.log(this.state.searchByGenre)
//komt uit render()
if (!this.state.searchByGenre) {
//check for input
this.setState({
searchInput: input.target.value,
showDeleteButton: true,
})
//Searching works
//this.filter(this.state.searchInput);
//Filtering works
this.startFilter(this.state.searchInput);
// DOESNT WORK:
// if (this.state.searchInput != "") {
// this.filter(this.state.searchInput);
// } else {
// this.startFilter(this.state.searchInput);
// }
}
}
setAllLists(allLists) {
console.log("setAllLists")
console.log(this.state.searchByGenre)
this.setState({ list: allLists })
//document.body.style.backgroundColor = "red";
}
filter(input) {
let corresondingGenre = [];
let filteredLists = listData.filter(
(item1) => {
var test;
if (this.state.searchByGenre) {
alert("--this.state.searchByGenre")
//filterByGenre
//& item1.properties.genre == input
for (var i = 0; i < item1.properties.genre.length; i++) {
if (item1.properties.genre[i].includes(input)) {
corresondingGenre.push(item1);
test = item1.properties.genre[i].indexOf(input) !== -1;
return test;
}
this.setState({ list: corresondingGenre })
}
} else {
//searchByTitle
alert("--default")
test = item1.title.indexOf(input.charAt(0).toUpperCase()) !== -1;
}
return test;
})
console.log("filterdLists:")
console.log(filteredLists)
console.log("corresondingGenre:")
console.log(corresondingGenre)
//alert(JSON.stringify(filteredLists))
this.setState({ list: filteredLists })
}
removeInput() {
console.log("removeInput ")
console.log(this.state.searchByGenre)
this.setState({ searchInput: "", showDeleteButton: false, searchByGenre: false })
document.getElementById("searchField").disabled = false;
this.filter(this.state.searchInput)
}
render() {
//alle 's komen in deze array, zodat ze gefilterd kunnen worden OBV title.
if (this.state.list === undefined || this.state.list.length == 0 && this.state.searchInput == "") {
//init list
console.log("render ")
console.log(this.state.searchByGenre)
this.filter(this.state.searchInput)
}
return (
<div class={style.list_container}>
<input class={style.searchBar} type="text" id="searchField" placeholder={this.state.searchInput} onBlur={this.startSearch.bind(this)} ></input>
{
this.state.searchByGenre ?
<h1>ja</h1>
:
<h1>nee</h1>
}
{
this.state.showDeleteButton ?
<button class={style.deleteButton} onClick={() => this.removeInput()}>Remove</button>
: null
}
{
this.state.list.map((item, index) => {
return <div>
<p>{item.title}</p>
</div>
})
}
</div>
);
}
}
SetState is an async operation that takes a callback function. I suspect that your second function runs before the first SetState is finished.
Also, you are modifying the DOM yourself. You need to let React do that for you just by modifying state. I don't have time to write up an example now, but hopefully this helps in the meantime.
can you modify your search func,
//search
startSearch(input) {
const { value } = input.target
const { searchInput } = this.state
if (!this.state.searchByGenre) {
this.setState(prevState => ({
searchInput: prevState.searchInput = value,
showDeleteButton: prevState.showDeleteButton = true,
}))
JSON.stringify(value) !== '' ? this.filter(value) : this.startFilter(searchInput)
}
}
Although finding loads of posts about this error, I can't seem to fix it in my specific case.
I have a set of items and I am trying to color these items (the div's) when you hover over them. The coloring happens when the state of these items (reserved) is false and the starting state is set to true.
So when an item is clicked (MouseDown event) I am setting the starting condition (state to true), then when I mouse over the next item, and the start condition is true and the item being moused over has it's reserved state property set to false, it should change it's background color and so on for the next items which are being moused over.
This is now being done as follows:
Defining the Items and start condition in the (initial) state:
constructor(props) {
super(props);
this.state = {
items: [
{
name: 'test1',
key: '#test1',
reserved: false,
},
{
name: 'test2',
key: '#test2',
reserved: false,
},
{
name: 'test3',
key: '#test3',
reserved: false,
},
{
name: 'test4',
key: '#test4',
reserved: false,
},
{
name: 'test5',
key: '#test5',
reserved: false,
},
],
startCondition: false
}
}
Render the List item:
render() {
return (
<FocusZone>
<List
items={this.state.items}
onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
In the _onRenderCell event I am rendering each cell (item div) and setting the onMouseDown, onMouseUp and onMouseOver events, I also check the reserved state of the item here so that when the item becomes reserved = true it gets an extra css class which contains the different background color:
_onRenderCell = (item, index) => {
let className = 'ms-ListGridExample-label';
if (item.reserved === true) {
className += ' reservation-creating';
}
return (
<div
className="ms-ListGridExample-tile"
data-is-focusable={false}
style={{
width: 100 / this._columnCount + '%',
height: this._rowHeight * 1.5,
float: 'left'
}}
>
<div className="ms-ListGridExample-sizer">
<div className="msListGridExample-padder">
<span className={className}
onMouseOver={() => this._onMouseOver(item.name)}
onMouseUp={() => this._onMouseUp(item)}
onMouseDown={() => this._onMouseDown(item.name)}
>
{item.name}
</span>
<span className="urenboeken-bottom"></span>
</div>
</div>
</div>
);
};
So when an item is clicked (MouseDown event) it sets the starting condition to true and also set it to reserved to give it the extra css class (with it's different background color):
_onMouseDown(name){
if (this.state.startCondition === false){
this.setState({startCondition: true});
this.setState(prevState => ({
items: prevState.items.map(item => {
if (item.name === name) {
if (item.reserved === true)
{
item.reserved = false
}
else if (item.reserved === false)
{
item.reserved = true
}
else
{
item.reserved = false
}
}
return item;
})
}))
}
}
After the MouseDown event has been fired and the Start Condition is being set, the moving will be tracked with the MouseOver event and setting the reserved state to true:
_onMouseOver(name) {
if (this.state.startCondition === true) {
this.setState(prevState => ({
items: prevState.items.map(item => {
if (item.reserved === false) {
if (item.name === name) {
item.reserved = true
}
return item;
}
})
}))
}
}
At this point, the _OnRenderCell is fired again and returns me the error:
MyClass.js:265 Uncaught TypeError: Cannot read property 'reserved' of undefined
at MyClass._this._onRenderCell
which is the following part in the _onRenderCell function:
if (item.reserved === true)
I am probably doing something obvious wrong but I cant seem to point out what it is.
P.s. the onMouseUp event reset the starting condition to false.
UPDATE
On request of #M.Fazio, who suspects something is going wrong with the items={this.state.items} part in the List rendering, I added a log to display what is inside the items just before rendering:
render() {
let tItems;
tItems = this.state.items.map(item => {
console.log('Item name = ' + item.name + ' item reserved = ' + item.reserved);
});
return (
<FocusZone>
<List
items={this.state.items}
onRenderCell={this._onRenderCell}
/>
</FocusZone>
);
}
Result:
Some things to point out:
The OnRenderCell gets called for every item in the array (which is logical).
The List rendering seem to happen three times, why?
When doing the process (onMouseDown + onMouseOver event) the following happens (error now actually already happens on the added logging):
Solution
#M.Fazio found the solution. In the end it was a small glitch (though so hard to find for myself).
The return item; in the _onMouseOver event was inside the if condition, moving it outside the if condition made it working!
In your ListComponent, when you're calling the onRenderCell prop, you have to pass your parameters. Did you ?
Maybe past the code of your ListComponent so we can help you ?
I am fetching some data from the server to populate a list of items, and each item got a onClick event binded to the items id, that changes the UI to be disabled when clicked.
My problem is that the UI changes to disabled perfectly on the first click, but when I go on to click on the next item it resets the first on, so there is only one button disabled at a time. How do I make it so I can disable all the items I want, without resetting the previous ones?
Here is my component:
class Video extends Component {
constructor () {
super()
this.state = {
isDisabled: false
}
}
handleClick(frag, voted, event){
event.preventDefault()
this.setState({
isDisabled: {
[frag]: true
}
})
}
Snippet of what I return in the UI that changes the disabled button
<button onClick={this.handleClick.bind(this, frags.id, frags.voted)} disabled={this.state.isDisabled[frags.id]} className="rating-heart-2">
<i className="fa fa-heart" aria-hidden="true"></i>
</button>
I would really appreciate all tips!
It seems that when you call setState, you are overriding the previous value of the isDisabled.
You can do something like this:
handleClick(frag, voted, event){
event.preventDefault()
this.setState({
isDisabled: {
...this.state.isDisabled,
[frag]: true
}
})
}
The code you provided is a bit confusing because in the jsx you have this.state.hasRated to disable the button and in the handleClick you have a isDisabled object.
I followed the jsx approach and I add the frag id the hasRated object with the value true to disable a button each it is clicked.
You can run the following snippet to see the output of the code:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
frags: [],
hasRated: {}
};
this.handleClick = this.handleClick.bind(this);
}
componentDidMount() {
setTimeout(() => {
this.setState({
frags: [{
id: 1,
voted: false
}, {
id: 2,
voted: false
}, {
id: 3,
voted: false
}]
});
}, 500);
}
handleClick(id, voted) {
return (event) => {
this.setState({
hasRated: {
...this.state.hasRated,
[id]: true
}
});
}
}
render() {
const items = this.state.frags.map(frag => ( <
button
key={frag.id}
onClick = {
this.handleClick(frag.id, frag.voted)
}
disabled = {
this.state.hasRated[frag.id]
}
className = "rating-heart-2" >
Button <
/button>
));
return (
<div>
{items}
</div>
);
}
}
ReactDOM.render( < Example / > , document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div id="container"></div>