How to update nested array (multi react-select) - javascript

I'm creating an app where I need to store selected values in array nested in object (category below). State looks like:
state = {
data: {
user: "",
title: "",
text: "",
category: [], // should store values
},
updateNoteId: null,
}
In my render() I have following form:
<form onSubmit={this.submitNote}>
<Select
name="category"
value={this.state.data.category}
options={options}
onChange={this.handleMultiChange}
multi
/>
<input type="submit" value="Save" />
</form>
Options are:
const options = [
{ value: 1, label: 'one' },
{ value: 2, label: 'two' },
{ value: 3, label: 'three' }
]
So the question is how this.handleMultiChange function should looks like to work. Category[] need to keep all values selected in Select which is react-select component (eg. it should be category = [1,3], when 'one' and 'three' were chosen). I tried many combination but none of them worked so far. I prefer to use ES6 without any external libraries/helpers to do that.

handleMultiChange(selectedOptions) {
this.setState({
data: {
...this.state.data,
categories: selectedOptions
}
})
}

Related

How to $set a property to multiple objects in an array but have retain individual reactivity in vue js

In my case, I have data array with multiple objects
data() {
return {
selected: 0,
presetData: [true, true, true],
data: [
{
name: "name 1"
},
{
name: "name 2"
}
]
};
},
then I want to push inside each object in data like below
setNewData() {
this.data.forEach((o, i) => {
this.$set(this.data[i], "time", this.presetData);
});
},
now my with presetData pushed into data will look like this
data: [
{
name: "name 1",
time: [true, true, true]
},
{
name: "name 2",
time: [true, true, true]
}
]
and I want to change individual time property of each object, which I use something like below
$set(item.time,selected,true)
My Issue
my issue is, this going to change both objects time property. How do I first push/set correctly presetData to data, below is my entire code , I'm sorry I'm very new to programming, here is the link to jsfiddle
new Vue({
el: "#app",
data() {
return {
selected: 0,
presetData: [true, true, true],
data: [
{
name: "name 1",
},
{
name: "name 2",
}
]
};
},
methods: {
setNewData() {
this.data.forEach((o, i) => {
this.$set(this.data[i], "time", this.presetData);
});
},
}
})
<div id="app">
<button #click="setNewData">Set Data</button>
<br>
<br>
<select v-model="selected">
<option>0</option>
<option>1</option>
<option>2</option>
</select>
<div v-for="item in data" :key="item.id">
<p>{{item.name}}</p>
<p>{{item.time}}</p>
<button #click="$set(item.time,selected,true)">Change True</button>
<button #click="$set(item.time,selected,false)">Change False</button>
</div>
This is an object reference issue. Each of your time properties references the same array (presetData). You can break out of this problem by making shallow copies via spread syntax.
You can also avoid Vue.set() when assigning new data using the same technique
setNewData() {
this.data = this.data.map(d => ({
...d, // create a shallow copy of each data item
time: [...this.presetData] // add "time" as a shallow copy of presetData
}))
},
To change individual array elements within the time property, you need to continue using Vue.set(), ie
this.$set(item.time, selected, true)

Map a filtered array in React

I made a component containing two dropdown lists. The options in the second dropdown menu is supposed to be filtered depending on the selected option from the first dropdown menu.
Now, I want to map a filtered array that is stored in a const similary to the way i map options1:
render() {
const options1 = [
{value: 'one', label: 'One'},
{value: 'two', label: 'Two'}
];
const options2 = [
{value: 'one-a', label: 'One A', link: 'one'},
{value: 'one-b', label: 'One B', link: 'one'},
{value: 'two-a', label: 'Two A', link: 'two'},
{value: 'two-b', label: 'Two B', link: 'two'}
];
const filteredOptions = options2.filter(o => o.link === this.state.selectedOption.value);
return (
<div style={style}>
<div>
<label>Select one</label>
<select
value={this.state.selectedOption.value}
onChange={this.handleChange1}
>
{options1.map(tag => <option>{tag.value}</option>)}
</select>
</div>
<div>
<label>Then the other</label>
<select
value={this.state.selectedOption2.value}
onChange={this.handleChange2}
>
{filteredOptions.map(tag => <option>{tag.value}</option>)}
</select>
</div>
</div>
)
}
The first mapping of options1 works just fine. However, my select tag gets rendered empty for the mapping of filteredOptions.
I have no idea why it won't work. Anyone happen to have an idea?
Full code: https://www.codepile.net/pile/evNqergA
Here is a working example for what you're trying to do.
import React, { Component } from "react";
const options1 = [
{ value: "one", label: "One" },
{ value: "two", label: "Two" }
];
const options2 = [
{ value: "one-a", label: "One A", link: "one" },
{ value: "one-b", label: "One B", link: "one" },
{ value: "two-a", label: "Two A", link: "two" },
{ value: "two-b", label: "Two B", link: "two" }
];
export default class SelectsComponent extends Component {
handleChange1 = e => {
console.log(e);
this.setState({
selectedOption: { value: e.target.value }
});
};
handleChange2 = e => {
this.setState({
selectedOption2: { value: e.target.value }
});
};
constructor(props) {
super(props);
this.state = {
selectedOption: { value: "one" },
selectedOption2: { value: "one-a" }
};
}
render() {
const filteredOptions = options2.filter(
o => o.link === this.state.selectedOption.value
);
return (
<div>
<div>
<label>Select one</label>
<select
value={this.state.selectedOption.value}
onChange={this.handleChange1}
>
{options1.map(tag => (
<option>{tag.value}</option>
))}
</select>
</div>
<div>
<label>Then the other</label>
<select
value={this.state.selectedOption2.value}
onChange={this.handleChange2}
>
{filteredOptions.map(tag => (
<option>{tag.value}</option>
))}
</select>
</div>
</div>
);
}
}
In your scenario filteredOptions would be an empty Array.
The check for o.link === this.state.selectedOption.value is doing something wrong.
Check the value of this.state.selectedOption.value, this is not set correctly.
The best way to do this wouldn't be inside of the render method.
1) move your arrays into state or other instance members
2) make sure to only trigger the sorting once
this.setState(lastState => ({
...lastState,
options2: lastState.options2.filter(yourFilterFn)
}))
3) map the filtered array into JSX inside of your render method
Side-note: this uses immutable setState (which I gather is important given you create a new filtered array from the options2 in your example). If you want to follow an even more functional pattern, you can do the filtering inside of your render method (although I don't recommend it). If you decided to filter inside of your render method, consider using a memoization technique from React 16.7 (which is currently in Alpha).

How to add limit to iview ui multiple select?

I am trying to add limit to iView ui Multiple select. Here is the code
<Select
v-model="data.category"
:multiple="true"
filterable
remote
:remote-method="remoteMethod2"
:loading="loading2">
<Option v-for="(option, index) in options2" :value="option.value" :key="index">{{option.label}}</Option>
</Select>
I want to add something like this max="3" to limit the selected items
Couldn't find anything in api doc.
There's no property with that functionality, but we could do it ourselves by watching the length of our model that contains the selected items and if it's equal to the fixed max in data object properties we change the disabled property state to true and if remove an item from the selected ones we could also enable the options drop down, check th following example that explains itself :
var Main = {
data() {
return {
disable:false,
max: 2,
cityList: [{
value: 'New York',
label: 'New York'
},
{
value: 'London',
label: 'London'
},
{
value: 'Sydney',
label: 'Sydney'
},
{
value: 'Ottawa',
label: 'Ottawa'
},
{
value: 'Paris',
label: 'Paris'
},
{
value: 'Canberra',
label: 'Canberra'
}
],
model10: []
}
},
watch: {
model10(val) {
if (val.length == this.max) this.disable=true
else this.disable=false
},
}
}
var Component = Vue.extend(Main)
new Component().$mount('#app')
#import url("//unpkg.com/iview/dist/styles/iview.css");
#app {
padding: 32px;
}
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/iview/dist/iview.min.js"></script>
<div id="app">
<i-select v-model="model10" multiple style="width:260px">
<i-option :disabled="disable" v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
</i-select>
</div>

Make a Tree view from JSON data using React JS

First of all i am very new to React JS. So that i am writing this question. I am trying this for three days.
What I have to do, make a list of category, like-
Category1
->Sub-Category1
->Sub-Category2
Categroy2
Category3
.
.
.
CategoryN
And I have this json data to make the listing
[
{
Id: 1,
Name: "Category1",
ParentId: 0,
},
{
Id: 5,
Name: "Sub-Category1",
ParentId: 1,
},
{
Id: 23,
Name: "Sub-Category2",
ParentId: 1,
},
{
Id: 50,
Name: "Category2",
ParentId: 0,
},
{
Id: 54,
Name: "Category3",
ParentId: 0,
},
];
I have tried many open source examples, but their json data format is not like mine. so that that are not useful for me. I have build something but that is not like my expected result. Here is my jsfiddle link what i have done.
https://jsfiddle.net/mrahman_cse/6wwan1fn/
Note: Every subcategory will goes under a category depend on "ParentId",If any one have "ParentId":0 then, it is actually a category, not subcategory. please see the JSON
Thanks in advance.
You can use this code jsfiddle
This example allows to add new nested categories, and do nested searching.
code with comments:
var SearchExample = React.createClass({
getInitialState: function() {
return {
searchString: ''
};
},
handleChange: function(e) {
this.setState({
searchString: e.target.value.trim().toLowerCase()
});
},
isMatch(e,searchString){
return e.Name.toLowerCase().match(searchString)
},
nestingSerch(e,searchString){
//recursive searching nesting
return this.isMatch(e,searchString) || (e.subcats.length && e.subcats.some(e=>this.nestingSerch(e,searchString)));
},
renderCat(cat){
//recursive rendering
return (
<li key={cat.Id}> {cat.Name}
{(cat.subcats && cat.subcats.length) ? <ul>{cat.subcats.map(this.renderCat)}</ul>:""}
</li>);
},
render() {
let {items} = this.props;
let {searchString} = this.state;
//filtering cattegories
if (searchString.length) {
items = items.filter(e=>this.nestingSerch(e,searchString))
console.log(items);
};
//nesting, adding to cattegories their subcatigories
items.forEach(e=>e.subcats=items.filter(el=>el.ParentId==e.Id));
//filter root categories
items=items.filter(e=>e.ParentId==0);
//filter root categories
return (
<div>
<input onChange={this.handleChange} placeholder="Type here" type="text" value={this.state.searchString}/>
<ul>{items.map(this.renderCat)}</ul>
</div>
);
}
});

Angular select all checkboxes from outside ng-repeat

Description
I have a small product order system, where a user can add order lines, and on each order line add one or more products. (I realise it's quite unusual for more than one product to be on the same order line, but that's another issue).
The products that can be selected on each line is based on a hierarchy of products. For example:
Example product display
T-Shirts
V-neck
Round-neck
String vest
JSON data
$scope.products = [
{
id: 1,
name: 'T Shirts',
children: [
{ id: 4, name: 'Round-neck', children: [] },
{ id: 5, name: 'V-neck', children: [] },
{ id: 6, name: 'String vest (exclude)', children: [] }
]
},
{
id: 2,
name: 'Jackets',
children: [
{ id: 7, name: 'Denim jacket', children: [] },
{ id: 8, name: 'Glitter jacket', children: [] }
]
},
{
id: 3,
name: 'Shoes',
children: [
{ id: 9, name: 'Oxfords', children: [] },
{ id: 10, name: 'Brogues', children: [] },
{ id: 11, name: 'Trainers (exclude)', children: []}
]
}
];
T-Shirts isn't selectable, but the 3 child products are.
What I'm trying to achieve
What I'd like to be able to do, is have a 'select all' button which automatically adds the three products to the order line.
A secondary requirement, is that when the 'select all' button is pressed, it excludes certain products based on the ID of the product. I've created an 'exclusion' array for this.
I've set up a Plunker to illustrate the shopping cart, and what I'm trying to do.
So far it can:
Add / remove order lines
Add / remove products
Add a 'check' for all products in a section, excluding any that are in the 'exclusions' array
The problem
However, although it adds the check in the input, it doesn't trigger the ng-change on the input:
<table class="striped table">
<thead>
<tr>
<td class="col-md-3"></td>
<td class="col-md-6"></td>
<td class="col-md-3"><a ng-click="addLine()" class="btn btn-success">+ Add order line</a></td>
</tr>
</thead>
<tbody>
<tr ng-repeat="line in orderHeader.lines">
<td class="col-md-3">
<ul>
<li ng-repeat="product in products" id="line_{{ line.no }}_product_{{ product.id }}">
{{ product.name }} <a ng-click="selectAll(product.id, line.no)" class="btn btn-primary">Select all</a>
<ul>
<li ng-repeat="child in product.children">
<input type="checkbox"
ng-change="sync(bool, child, line)"
ng-model="bool"
data-category="{{child.id}}"
id="check_{{ line.no }}_product_{{ child.id }}"
ng-checked="isChecked(child.id, line)">
{{ child.name }}
</li>
</ul>
</li>
</ul>
</td>
<td class="col-md-6">
<pre style="max-width: 400px">{{ line }}</pre>
</td>
<td class="col-md-3">
<a ng-click="removeLine(line)" class="btn btn-warning">Remove line</a>
</td>
</tr>
</tbody>
</table>
Javascript
$scope.selectAll = function(product_id, line){
target = document.getElementById('line_'+line+'_product_'+product_id);
checkboxes = target.getElementsByTagName('input');
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].type == 'checkbox') {
category = checkboxes[i].dataset.category;
if($scope.excluded.indexOf(parseInt(category)) == -1)
{
checkboxes[i].checked = true;
// TODO: Check the checkbox, and set its bool parameter to TRUE
}
}
}
}
Update with full solution
There were a couple of issues with the above code. Firstly, I was trying to solve the problem by manipulating the DOM which is very much against what Angular tries to achieve.
So the solution was to add a 'checked' property on the products so that I can track if they are contained on the order line, and then the view is updated automatically.
One drawback of this method is that the payload would be significantly larger (unless it is filtered before being sent to the back-end API) as each order line now has data for ALL products, even if they aren't selected.
Also, one point that tripped me up was forgetting that Javascript passes references of objects / arrays, not a new copy.
The solution
Javascript
var myApp = angular.module('myApp', []);
myApp.controller('CartForm', ['$scope', function($scope) {
var inventory = [
{
id: 1,
name: 'T Shirts',
checked: false,
children: [
{ id: 4, name: 'Round-neck', checked: false, children: [] },
{ id: 5, name: 'V-neck', checked: false, children: [] },
{ id: 6, name: 'String vest (exclude)', checked: false, children: [] }
]
},
{
id: 2,
name: 'Jackets',
checked: false,
children: [
{ id: 7, name: 'Denim jacket', checked: false, children: [] },
{ id: 8, name: 'Glitter jacket', checked: false, children: [] }
]
},
{
id: 3,
name: 'Shoes',
checked: false,
children: [
{ id: 9, name: 'Oxfords', checked: false, children: [] },
{ id: 10, name: 'Brogues', checked: false, children: [] },
{ id: 11, name: 'Trainers (exclude)', checked: false, children: []}
]
}
];
$scope.debug_mode = false;
var products = angular.copy(inventory);
$scope.orderHeader = {
order_no: 1,
total: 0,
lines: [
{
no: 1,
products: products,
total: 0,
quantity: 0
}
]
};
$scope.excluded = [6, 11];
$scope.addLine = function() {
var products = angular.copy(inventory);
$scope.orderHeader.lines.push({
no: $scope.orderHeader.lines.length + 1,
products: products,
quantity: 1,
total: 0
});
$scope.loading = false;
}
$scope.removeLine = function(index) {
$scope.orderHeader.lines.splice(index, 1);
}
$scope.selectAll = function(product){
angular.forEach(product.children, function(item){
if($scope.excluded.indexOf(parseInt(item.id)) == -1) {
item.checked=true;
}
});
}
$scope.removeAll = function(product){
angular.forEach(product.children, function(item){
item.checked=false;
});
}
$scope.toggleDebugMode = function(){
$scope.debug_mode = ($scope.debug_mode ? false : true);
}
}]);
Click here to see the Plunker
You are really over complicating things first by not taking advantage of passing objects and arrays into your controller functions and also by using the DOM and not your data models to try to update states
Consider this simplification that adds a checked property to each product via ng-model
<!-- checkboxes -->
<li ng-repeat="child in product.children">
<input ng-model="child.checked" >
</li>
If it's not practical to add properties to the items themselves, you can always keep another array for the checked properties that would have matching indexes with the child arrays. Use $index in ng-repeat for that
And passing whole objects into selectAll()
<a ng-click="selectAll(product,line)">
Which allows in controller to do:
$scope.selectAll = function(product, line){
angular.forEach(product.children, function(item){
item.checked=true;
});
line.products=product.children;
}
With angular you need to always think of manipulating your data models first, and let angular manage the DOM
Strongly suggest reading : "Thinking in AngularJS" if I have a jQuery background?
DEMO
Why ng-change isn't fired when the checkbox is checked programatically?
It happens because
if($scope.excluded.indexOf(parseInt(category)) == -1)
{
checkboxes[i].checked = true;
// TODO: Check the checkbox, and set its bool parameter to TRUE
}
only affects the view (DOM). ng-change works alongside ngModel, which can't be aware that the checkbox really changed visually.
I suggest you to refer to the solution I provided at How can I get angular.js checkboxes with select/unselect all functionality and indeterminate values?, works with any model structure you have (some may call this the Angular way).

Categories