I'm trying to create filters in react, where i manipulate the url to return me products based on colours, cost etc
its working so if you change ?color=red to ?color=white in the url it will display different products on the page
it's also working whereby if you select the colours in my checkbox filter component it will update the url and then display the new products. i.e click on red will change the url from /sport to /sport?color=red and then returns me just the products with red
however this is my problem
if I manually change the url, I then want the checkbox checked so I tried to do this:
checked={option.text === this.getParams() ? true : false}
this does actually work but then I lose the ability to actually select and deselect the checkbox. any ideas how I can get it to do both? I guess making it a controlled and uncontrolled component simultaneously??
You need to store the filters in the state. like in your constructor you can init your state with the query parameter and then change the state upon checkbox change.
You could try something like this. You will need to change this code according to your usage, here I am assuming, this.getParams('color') will return an array of all the selected colors.
constructor state init
constructor(props) {
super(props);
this.state = {
filters: this.getParams('color') // consedering, it will return array
}
}
default check the checkbox
defaultChecked ={this.state.filters.indexOf(option.text) === -1 ? false : true}
onChange={() => this.toggleCheckbox(option.text)}
for toggling it
// if not present, then add it
// else remove it
toggleCheckbox(option) {
if (this.state.filters.indexOf(option) === -1) {
this.setState({
filters: [...this.state.filters, option]
})
} else {
this.setState({
filters: this.state.filters.filter(text => text !== option)
})
}
}
You should set the state of the checkbox in the component state, and then update that state when it's clicked. You can set the initial state based on the url on construct or mount.
Something like this:
constructor(props) {
super(props);
const isChecked = this.props.match.params.checkbox === 'true';
this.state = {
checkbox: isChecked
}
}
And then in your checkbox:
<input type="checkbox" checked={this.state.checkbox} onChange={() => this._toggleCheckbox()} />
And the method to turn it on and off would be something like:
toggleCheckbox() {
this.setState({
checkbox: !this.state.checkbox // will toggle to the opposite of the current state
});
}
Note that this is has not been tested but has been written based on the information you gave. The principle behind this is what you need to do. It may also be useful to set the state of the checkbox initially within componentDidMount(), rather than constructor(), but that's up to you. The onChange function of the checkbox uses ES6, but you could bind the function if you prefer or do not use ES6 with this._toggleCheckbox().bind(this)
Edit
To update the checkbox when the url is changed, rather than updating it on click, you could change the toggle method to redirect the browser, and then update the checkbox within componentWillReceiveProps.
Taken from my own code with react-router you can use 'this.props.match.params' to find the url parameters. I use react-router-dom package to update the url. So for instance:
This will give you access to this.props.history.
import { withRouter } from 'react-router-dom';
toggleCheckbox() {
// Check the current state of the checkbox and update the url to the opposite
let toCheck = this.props.match.params.checkbox === 'true' ? 'false' : 'checked';
this.props.history.push('/?checkbox=' + toCheck);
}
componentWillReceiveProps(newProps) {
// Check the new url and update the checkbox if it is different from the checkbox state
if(newProps.match.params.checkbox != this.state.checkbox) {
this.setState({checkbox: newProps.match.params.checkbox});
}
}
Related
I'm in trouble to create a component like mat-button-toggle-group of material
I create a simple container with an ng-content that wraps buttons and inside it some customized buttons. THe number of components-button can change...
<container-buttons-wrapper>
<component-button>Test 1</component-button>
<component-button>Test 2</component-button>
<component-button>Test 3</component-button>
</container-buttons-wrapper>
component-button has inside only a button tag
<button (click)="setActive()" [ngClass]="active? 'active-class' : 'no-active-class'"><ng-content></button>
I defined a function setActive() that toggle active value
setActive() {
this.active = !this.active
}
But I can't find a solution to control the other buttons into container. I want to reproduce exactly what mat-button-toggle-group. Is possible to define an eventEmitter inside the template html?
Problem: when a button changes its status to "active", container should change the state of the remaining buttons to "inactive".
Solution: Implement two-way communication between buttons and the container:
a button should be able to notify the container that it became active
a container should be able to set the remaining buttons to inactive state (or, a button should be able to know whether it is active or not from the container)
Basically, the container becomes a holder of the shared state for itself and all nested buttons. This state is made available to nested buttons via DI. State can be handled by a separate service, or it can be a part of the container component itself for simplicity (the latter approach is implemented in Material):
const CONTAINER = new InjectionToken<ContainerComponent>();
#Directive({
providers: [{provide: CONTAINER, useExisting: forwardRef(() => ContainerComponent)}]
})
class ContainerComponent {
private selectedButton: ButtonComponent | null = null
toggleButton(button: ButtonComponent) {
if (this.selectedButton = button) {
this.selectedButton = null
} else {
this.selectedButton = button;
}
}
isSelected(button: ButtonComponent): boolean {
return this.selectedButton = button
}
}
#Component({template: `
<button [class.selected]="isSelected()" (click)="onClick()">
<ng-content></ng-content>
</button>
`})
class ButtonComponent {
constructor(#Inject(CONTAINER) private container: ContainerComponent) {}
isSelected() {
return this.container.isSelected(this)
}
onClick() {
this.container.toggleButton(this)
}
}
Update: Pre-selecting a button
How do we set some button as "selected" initially?
Approach 1
One way is doing smth similar to what Material does.
Add "selected" input to a button componentn
In the Container we would read all buttons via ContentChildren
Whenever the input changes, button should update the state in container.
A challenge is that now we have two sources of truth for "selected" flag in a button (button's input and the state coming from the container) - so we need to reconcile them, and the overall code becomes more involved.
Approach 2
Alternatively, let's assume your toggle component has some sort of "value" property. Kind of like html <select> element - each option has value property, and the selected property of the <select> is derived based on that.
In this case, we would have an input in ContainerComponent that allows to set initial value:
class ContainerComponent {
#Input() selected: any
toggleButton(value: any) {
if (this.selected !== value) {
this.selected = value
} else {
this.selected = null
}
}
}
class ButtonComponent {
#Input() value: any
onClick() {
this.container.toggleButton(this.value)
}
}
// usage
<container selected="option-1">
<my-button value="option-1"><my-button>
<my-button value="option-2"><my-button>
</container>
i'm trying to develop an App with React using the Open trivia Api. I have mapped a button component (using material ui) to show the different answers for each question. I'm struggling now to target only the clicked one to apply a css property: if the answer is correct should become green, else red. The problem is the fact that once i click, all button become red or green. I tried to store the index in a state and compare the real index, but it doesn't work. here is my code:
in the main APP.js
const [clickedOne, setClickedOne] = useState({
clickedIndex: null,
});
useEffect(() => {
grabData();
}, []);
const handleClick = (choice, ke) => {
setChoice(choice);
if (choice === data.correct_answer) {
setIsCorrect(true);
} else {
setIsCorrect(false);
}
setClickedOne({ clickedIndex: ke });
grabData();
};
The mapped button inside the Render:
{answers.map((answer, index) => {
return (
<ContainedButtons
choice={handleClick}
answer={answer}
correct={data.correct_answer}
isCorrect={isCorrect}
key={index}
id={index}
clicked={clickedOne}
/>
);
})}
Inside the Button component:
const backStyle = () => {
if (clicked === id) {
if (isCorrect) {
return "green";
} else if (isCorrect === false) {
return "red";
} else {
return null;
}
}
};
return (
<div className={classes.root}>
<Button
style={{ backgroundColor: backStyle() }}
value={answer}
onClick={() => choice(answer, id)}
variant="contained"
>
{decodeURIComponent(answer)}
</Button>
When i check now inside the backstyle function if the clicked===id, now nothing happens anymore. Without that if check, i would have all buttons red or green.
Thank you guys for the help!
I have looked at your codesandbox demo, there are alot of other problems apart from the one your question is about.
First of all, each time you make a request to the API to fetch next question, you are making a request to get 10 questions instead of 1. API request URL contains a query parameter named amount which determines how many questions will be fetched on each request. Change its value to 1.
"https://opentdb.com/api.php?amount=1&encode=url3986"
Secondly, there is a lot of unnecessary code and unnecessary use of useState hook. You only need 2 things to be stored in the state, data and answers
const [data, setData] = useState({});
const [answers, setAnswers] = useState([]);
Now, coming to the original problem of detecting which button is clicked and correctly updating its background color.
To achieve the desired functionality, take following steps:
create couple of CSS classes as shown below
button.bgGreen {
background-color: green !important;
}
button.bgRed {
background-color: red !important;
}
pass a handleClick function from App component to ContainedButtons component. When a button is clicked, this click handler will be invoked. Inside the handleClick function, get the text and the button that was clicked using Event.target and depending on whether user answered correctly or not, add appropriate CSS class, created in step 1, on the button that was clicked.
Instead of using index as key for ContainedButtons in map function, use something that will be unique each time. This is needed because we want React to not re-use the ContainedButtons because if React re-uses the ContainedButtons component, then CSS classes added in step 2 will not be removed from the button.
Here's a working codesanbox demo of your app with the above mentioned steps.
In this demo, i have removed the unnecessary code and also changed the key of ContainedButtons inside map function to key={answer.length * Math.random() * 100}. You can change it to anything that will ensure that this key will be unique each time.
I have a textBox which will allow me to enter the building name and after I click on 'Add Building' button, it takes the value and passes it to the next page.So, in my screenshot below, the values 'AbsBuildingOne' and 'AbsBuildingTwo' comes from the textBox input.So, currently I am able to pass a single value from the textBox to the next page, but I am not able to understand if I want to enter the second building how do I save the previous value in this page and add the second value below it. I guess I have to do it with arrays, but I am not able to figure it out how to proceed.
Currently, my code looks as shown below for passing the single value.I have added a state:
this.state = {
textBoxValue: '',
};
So, in my textBox the onUpdateHandler function looks like this:
onClickAddBuildingTextBoxHandler = (inputData) => {
this.setState({ textBoxValue: inputData.value});
}
My textBox component looks like this.Its a custom textBox designed for the project:
<SceTextBox
placeholder='Enter Building Name'
id='AddBuilding'
type='buildingName'
maxLength='40'
onTextUpdatedHandler={this.onClickAddBuildingTextBoxHandler.bind(this)}
forceValidate={this.state.forceValidate}
validator={app.appUtil.validator}
isError={this.state.textBoxEmptyError}
errorMsg={this.errorMessage}
/>
So, currently I am passing the value of this.state.textBoxValue to my next component where the building names are displayed.
So, how do I proceed with multiple values and how do I save all the building names? Also, I have to delete the building name when i click on close icon. So do I have to work with push/pop in array? Someone please guide me with this.
Edit 1:
My new state looks like this:
this.state = {
buildingNames: {
[id++]: ''
}
};
Both my functions are:
onClickAddBuildingButtonHandler = () => {
const { buildingNames } = this.state;
this.setState({
buildingNames: {
...buildingNames,
[id++]:''
}
});
if (!this.state.selectedOptions) {
this.setState({ isRadioEmptyError: true });
if (!this.state.buildingNames.id) {
this.setState({ textBoxEmptyError: true });
}
}
if (this.state.selectedOptions && this.state.buildingNames) {
this.props.navigateToAddBuilding(this.state.buildingNames);
}
}
onClickAddBuildingTextBoxHandler = (inputData) => {
const { buildingNames } = this.state;
this.setState({
buildingNames:{
...buildingNames,
[inputData.id] : inputData.value
}
});
}
To get an array of values from a textarea you can use the this.state.value.split('\n') to separate split the string on each new line producing an array for you.
Below is a very simple example demonstrating its effectiveness. Just type in the textarea and look at the console. Once you make a new line you'll see a second element appears in the array.
function handleKeyUp() {
const textarea = document.getElementById('my-ta');
console.log(textarea.value.split('\n'));
}
<textarea id='my-ta' onKeyUp='handleKeyUp()'></textarea>
EDIT #1:
Here is a Code Sandbox Example 1 demonstrating how it is done. Type in the textarea, make a new line and type some more and then hit the toggle button. You'll see the values displayed and if you toggle back they're in the textarea still.
EDIT #2:
So here is another example of how something can be done. This will show you how to add an item to a list. Code Sandbox Example 2
I'm going to leave it up to you to combine the two methods.
hi I have a react app and this is my code :
this.state = {
bars: [{
tom1: Array(16).fill('0'),
}],
}
tom1Click= (i, j) => {
let bar = this.state.bars.slice();
if(bar[i].tom1[j] === '1'){
bar[i].tom1[j] = '0';
}
else
bar[i].tom1[j] = '1';
this.setState({bars: bar})
}
I have 16 component of Tom in my page and I want when one of them clicked that state value change to 0 or 1 but when I click on one of the Toms the whole state updated and whole page reRendered
what should I do to handle this, and when I click on component just that component reRender
A better way to make this work is to have 2 components a parent one that will render 16 child component, the child component will have a boolean state and a click handler that will have a simple line of code setState({bar:!this.state.bar}); for example. Hope it helps!
I am using react-selectize component for customizable dropdown which allows users to add new options.
<Dropdown
options={myOptions}
value={selectedValue}
onValueChange={value => {
this.valueUpdated(emptyStringToNull(value));
}}
createFromSearch={this.createFromSearch}
/>
My createFromSearch and onValueChange functions are as below;
createFromSearch: function(options, search){
if (search.length === 0 || (options.map(function(option){
return option.label;
})).indexOf(search) > -1)
return null;
else {
return {'label': search, 'value': search};
}
},
onValueChange: function(text) {
// update the value in state
},
Everything works fine other than this small UI issue. It shows duplicate options soon after I click .
When I click anywhere in the screen it removes this duplicate layover and showing properly. Can anyone please suggest is it styling issue or any other thing I need to do?
I able to fix this issue by trying several things. I was overriding onValueChange method of the component and passed only the value to the actual onValueChange method as below;
const onValueChangeInDropdown = props => value => {
if (value) {
props.onValueChange(value.value);
} else {
props.onValueChange(null);
}
};
This cause the above styling issue since component couldn't find out item.newOption attribute. So solution is when adding newly created item for the option list add it as item.newOption = 'true' and pass the whole item object to onValueChange method.