My requirement is as follows
In parent component, i am passing an array of Child Components(array can be 1 or more than 1)
As the image shows, a child component consists of elements like, input[type=range], input[type=number], dropdown menu, etc
Parent component has a button
<button>Search Location</button>
When I click on Search button in Parent, I need the value of every single elements in each Child Component,
for eg. structure can be as follows
let finalObj={
child1: {
dropValue: "Room1",
cond: "AND"
},
child2: {
inputVal: 50,
cond: "OR"
},
child[n]: {
rangeVal: 1,
cond: ""
}
}
Also, we can change the value again(before clicking search), and Search button should always pickup, the current set value of each component.
I am not sure how to go ahead with this. Any pointers will be really helpful. Please help
So you need to change an array of components into an object of... well first of all that's a .reduce use.
const almostFinalObj = components.reduce((retval, each, i) => {
retval['child'+i] = each;
return retval;
}, {});
That will give an object like
almostFinalObj = {
child1: component1,
child2: component2,
childN: componentN,
}
Now we can .forEach through it, transforming each child component into whatever format you're looking for. (I'm unclear on that part but maybe you can figure out the rest.)
Object.keys(almostFinalObj).forEach((each, i, array) => {
let component = array[i];
array[i] = {};
component.querySelectorAll('input').forEach(e => {
array[i][e.name] = e.value;
});
});
This assumes the name attribute exists on each element in each child row-component. (As in, each radio button in your example has <input type='radio' name='cond' .... />.) You could use id or even a data-XXX attribute as well instead of e.name.
Related
I currently dynamically render the same component when clicking a button and the latest component is rendered on the top of the list.
Now, I want to delete the component. Each component has a cancel button to delete the rendered component. So I should be able to delete the component no matter where it is in the list.
Here's what I have so far:
local state:
state = {
formCount: 0
}
add and cancel:
onAddClicked = () => {
this.setState({formCount: this.state.formCount + 1});
}
onCancelButtonClicked = (cancelledFormKey: number) => {
const index = [...Array(this.state.formCount).keys()].indexOf(cancelledFormKey);
if (index > -1) {
const array = [...Array(this.state.formCount).keys()].splice(index, 1);
}
}
Parent component snippet:
{ [...Array(this.state.formCount).keys()].reverse().map(( i) =>
<Form key={i}
onCancelButtonClicked={() => this.onCancelButtonClicked(i)}
/>)
}
The only thing is I'm not sure how to keep track of which form was cancelled/deleted. I think I would need to create a new object in my local state to keep track but how do I know which index of the array I deleted as part of state? I'm not sure how do that? As I am using the count to make an array above.
Usually, this isn't how you'd generate a list of items. You're not storing the form data in the parent, and you're using index based keys which is a no-no when you're modifying the array. For example, I have an array of size 5 [0, 1, 2, 3, 4], when I remove something at position 2, the index of all the items after it changes causing their key to change as well, which will make react re-render them. Since you're not storying the data in the parent component, you will lose them.
Just to humor you, if we want to go with indexed based keys, we may have to maintain a list of removed indexes and filter them out. Something like this should do the trick:
state = {
formCount: 0,
deletedIndex: []
}
onCancelButtonClick = (cancelledIndex: number) => setState((prevState) => ({
deletedIndex: [...prevState.deletedIndex, cancelledIndex]
});
And your render would look like:
{
[...Array(this.state.formCount)].keys()].reverse().map((i) => (
if (deletedIndex.includes(i) {
return null;
} else {
<Form key={i} ... />
}
))
}
As a rule of thumb though, avoid having index based keys even if you don't care about performance. It'll lead to a lot of inconsistent behavior, and may also cause the UI and the state to be inconsistent. And if you absolutely want to for fun, make sure the components that are being rendered using index based keys have their data stored at the parent component level
this.state = {
myArray = [
{
name:"cat",
expand:false
}
]
}
clickItem(item){
item.expand = true;
this.setState({})
}
this.state.myArray.map((item) =>{
return <div onClick={()=>this.clickItem(item)}>{item.name}</div>
})
In React, i have a simple array of objects,
when i click on one of theses object, i want to change their prop and update the state, what is the proper way of doing this.
i feel like there could be a better way
You need to copy your state, update the copied state and the set the state.
this.state = {
myArray = [
{
name:"cat",
expand:false
}
]
}
clickItem(key){
let items = this.state.myArray;
items[key].expand = true;
this.setState({items})
}
this.state.myArray.map((key, item) =>{
return <div onClick={()=>this.clickItem(key)}>{item.name}</div>
})
Okay, a couple of things.
You're mutating the state directly which is going to fail silently and you're also missing the key prop on your <div.
This is easily resolved though by using the data you have available to you. I don't know whether each name is unique but you can use that as your key. This helps React decide which DOM elements to actually update when state changes.
To update your item in state, you need a way to find it within the state originally, so if name is unique, you can use Array.prototype.find to update it.
clickItem(item) {
const targetIndex = this.state.items.find(stateItem => stateItem.name === item.name)
if (targetIndex === -1)
// Handle not finding the element
const target = this.state.items[targetIndex]
target.expand = !target.expand // Toggle instead of setting so double clicking works as expected.
this.setState({
items: this.state.items.splice(targetIndex, 1, target) // This replaces 1 item in the target array with the new one.
})
}
This will update state and re-render your app. The code is untested but it should work.
I am trying to dynamically create/remove a Vue component. I have figured out how to dynamically add the component, but I am having some troubles with allowing the users to remove the specific component.
Consider below two Vue files:
TableControls.vue
<a v-on:click="addColumn">Add Column</a>
<script>
export default {
methods: {
addColumn: function () {
Event.$emit('column-was-added')
}
}
};
</script>
DocumentViewer.vue:
<div v-for="count in columns">
<VueDragResize :id="count">
<a #click="removeColumn(count)">Remove Column</a>
</VueDragResize>
</div>
<script>
import VueDragResize from 'vue-drag-resize';
export default {
components: {
VueDragResize
},
data() {
return {
columns: [1],
}
},
created() {
Event.$on("column-was-added", () => this.addColumn())
},
methods: {
addColumn: function () {
this.columns.push(this.columns.length + 1)
},
removeColumn: function (id) {
this.columns.splice(id, 1)
}
}
};
</script>
As you can see, whenever a user clicks on <a v-on:click="addColumn">Add Column</a>, it will submit an event, and the DocumentViewer.vue file will pick up it, firing the addColumn method. This will ultimately create a new <VueDragResize></VueDragResize> component.
This works great.
The problem is when I want to remove the component again. My removeColumn method simply removes an id from the columns array:
removeColumn: function (id) {
this.columns.splice(id, 1)
}
This results in that a column is in fact removed. However, consider below example. When user clicks on the remove icon for the first column, it will remove the 2nd column instead. (And when there is only one column present, it cannot be removed).
I believe this is due to the fact that I splice() the array, but I cannot see how else I can remove the component dynamically?
I see, Array on Vue does not re render when you modify them.
You need to use the
Vue.set(items, indexOfItem, newValue)
if you want to modify
and use
Vue.delete(target, indexOfObjectToDelete);
If you want to delete an item from an array
You may read the additional info here
https://v2.vuejs.org/v2/api/#Vue-delete
If you want to delete an item from array. Using this will cause the component to rerender.
In this case it will be intuitive to do this
removeColumn: function (id) {
Vue.delete(this.columns, id)
}
Note that id should be the index. Vue.delete ensures the re-render of the component.
EDIT, you must use the index, instead of the count here.
<div v-for="(count, index) in columns">
<VueDragResize :id="index">
<a #click="removeColumn(index)">Remove Column</a>
</VueDragResize>
</div>
I would recommend reshaping your data, each element should be an object with an id and whatever other properties you want. Not simply an id then you would need something like this:
removeColumn(id) {
const elToRemove = this.columns.findIndex(el => el.id === id)
let newArr = [elToRemove, ...this.columns]
this.columns = newArr
}
Also make another computed property for columns like this to make sure they change dynamically (when you add/remove):
computed: {
dynColumns(){ return this.columns}
}
I have same problem, and I found the solution of this problem. It is need to set #key with v-for. This is Built-in Special Attributes.
By default, if you do not set "#key", array index is set to#key. So if array length is 3, #key is 0,1,2. Vue identify eash v-for elements by key. If you remove second value of array, then array index is 0 and 1, because array length is 2. Then Vue understand that #key==2 element removed, So Vue remove 3rd component. So if you remove second value of array, if no #key, third component will be removed.
To avoid this, need to set #key to identify component like this:
let arr = [
{ id: 'a', ...},
{ id: 'b', ...},
{ id: 'c', ...}
];
<div v-for="obj in arr" :key="obj.id">
<someYourComponent>
...
</someYourComponent>
</div>
Situation 1 - I check my individual checkboxes, my header checkbox gets checked. This is method 1 and works fine.
Code for same
index.vue
<VCheckbox
checked={this.checkSelections[idx]}
nativeOnChange={e => {
this.$set(this.checkSelections, idx, e.target.checked);
let allSelected = true;
for (let i = 0; i < this.checkSelections.length; i++) {
allSelected = this.checkSelections[i];
if (!allSelected) break;
}
this.$root.$emit("checkmarkHead", { allSelected });
}}
/>
Head.vue
mounted() {
this.$nextTick(() => {
this.$root.$on("checkmarkHead", ({ allSelected }) => {
console.log("checkmarkHead", allSelected);
this.allSelected = allSelected;
});
}
},
Situation 2 -
I check my header checkbox and all my checkboxes are checked. Vice-versa is true as well. So method 2 corresponding to this works fine too.
Code for same -
Head.vue
<HeadItem>
<VCheckbox
checked={this.allSelected}
nativeOnChange={e => {
this.allSelected = e.target.checked;
this.$root.$emit("selectAll", {
allSelected: this.allSelected
});
}}
/>
</HeadItem>
index.vue
mounted() {
this.$nextTick(() => {
this.$root.$on("selectAll", ({ allSelected }) => {
this.checkSelections = Array(this.sortedData.length).fill(allSelected);
});
});
}
Problem - When I do situation 2 after Situation 1, the same methods don't work as expected. The view isn't updated. Similarly, executing Situation 1 after Situation 2 won't work either.
Here's the link to
Code Link - https://codesandbox.io/s/vmwy3v4203
I'm clueless now after handling all mutations caveats etc.
I owe you an apology. This is indeed another reactivity issue which could be solved by providing a key attribute ..
The key special attribute is primarily used as a hint for Vue’s
virtual DOM algorithm to identify VNodes when diffing the new list of
nodes against the old list. Without keys, Vue uses an algorithm that
minimizes element movement and tries to patch/reuse elements of the
same type in-place as much as possible. With keys, it will reorder
elements based on the order change of keys, and elements with keys
that are no longer present will always be removed/destroyed.
You can assign a unique key value to an element, which if changed will force Vue to re-render that element. In your case you can assign keys to your VCheckbox elements equal to their checked value, forcing Vue to re-render them when they're checked/unchecked. For example ..
<HeadItem>
<VCheckbox
key={this.allSelected}
checked={this.allSelected}
nativeOnChange={e => {
this.$emit('change', !this.allSelected)
}}
/>
</HeadItem>
I've taken the liberty to re-write your allSelected property as a computed property and removed the event listener you set up on the root instance. I think it's much cleaner this way ..
I want to write a custom element like this.
<dom-module id="custom-element">
<template>
<!-- This part I want to use document.createElement -->
<template is="dom-repeat" items="[[data]]">
<div>
<span>[[item.name]]</span>
<span>[[item.count]]</span>
</div>
</template>
</template>
</dom-module>
The count value may be changed by other element.
Is it possible to bind properties count to a document.createElement element?
class CustomElement extend Polymer.Element {
static get is () { return "custom-element" }
static get properties () {
return {
data: {
type: Array,
value: [{
"name": "item1",
"count": 0
}, {
"name": "item2",
"count": 0
}, {
"name": "item3",
"count": 0
}],
notify: true,
observer: "_dataChanged"
}
}
}
_dataChanged: (data) => {
data.map((item) => {
let div = document.createElemnt("div")
let itemName = document.createElement("span")
itemName.textContent = item.name
let itemCount = document.createElement("span")
// I want to bind value count here
itemCount.textContent = item.count
div.appendChild(itemName)
div.appendChild(itemCount)
this.shadowRoot.appendChild(div)
})
}
}
window.customElements.define(CustomElement.is, CustomElement)
When other element change the count value, the element which create by document.createElement's count will be change.
Is this possible?
So, data binding works, between two custom elements
Whether you use a dom-repeat with the bind, or bind it to an element like an input in the shadow DOM,
OR,
you use document.createElement to append an element to your shadowDOM, with the bound value,
does not matter
you use {{}} for a two way binding, and a [[]] for a one way binding.
So, If other-element changes adata property bound to custom-element, and you want custom-element to accommodate them, use one way
If you want the changes made by custom-elemnt to also change values in other-element, use two way.
If you are using the console to mutate data of one element, I am afraid, those mutations aren't reflected to the custom element.
If however, you are having a binding between custom-element and other-element on the property data, and
other-element mutates the property data as the result of an event, then, these changes are notified to all communicating elements.
Edit: I see that you have a subsequent query in the comments, which asks why is the plunk you made not working
While you are now correctly binding the elements two-way via data ,
there is a bug in polymer, that doesn't notify observers on data mutation if data is an array.
Solution for now: Shallow clone your data using slice after you change it
like so:
add (e) {
this.set(`data.${e.target.index}.count`, (this.data[e.target.index].count + 1))
this.data = this.data.slice();
}