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>
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
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.
I am trying to create a simple shopping cart using ReactJS and I figured a potential way out but whenever I click on the remove button I've set it doesn't really remove the items from the cart..
So those are my state managers right here:
let[product, setProduct] = useState([])
//The function bellow is what I use to render the products to the user
const[item] = useState([{
name: 'Burger',
image: '/static/media/Burger.bcd6f0a3.png',
id: 0,
price: 16.00
},
{
name: 'Pizza',
image: '/static/media/Pizza.07b5b3c1.png',
id: 1,
price: 20.00
}])
and I have a function that adds the objects in item to the product array, then I have a function that is supposed to remove them that looks like this:
const removeItem=(idx)=>
{
// let newProduct = product.splice(idx,1)
// setProduct([product,newProduct])
// $('.showItems').text(product.length)
// product[idx]=[]
product.splice(idx,1)
if(product.length<=0)
{
$('.yourCart').hide()
}
}
This function is called from here:
{product.map((item, idx)=>
<div className='yourCart' key={idx}>
<hr></hr>
<div>
<img src ={item.image}></img>
<h3 className='burgerTitle'>{item.name}</h3>
<h4><strong>$ {item.price}.00</strong></h4>
<Button variant='danger' onClick={()=>removeItem(idx)}>Remove</Button>
</div>
<br></br>
</div>)}
The problem is that I've tried to use splice, setState, I tried to even clear the entire array and add the elements that are left after applying the filter function to it but it was all to no avail.
How can I make it so that when I click on the remove button it removes the specific item from the array??
You need to use the mutation method setProduct provided from the useState hook to mutate product state.
const removeItem = (id) => {
const index = product.findIndex(prod => prod.id === id); //use id instead of index
if (index > -1) { //make sure you found it
setProduct(prevState => prevState.splice(index, 1));
}
}
usage
<Button variant='danger' onClick={()=>removeItem(item.id)}>Remove</Button>
as a side note:
Consider using definite id values when working with items in an array, instead of index in array. the index of items can change. Use the item.id for a key instead of the index when mapping. Consider using guids as identification.
{product.map((item, idx)=>
<div className='yourCart' key={`cartItem_${item.id}`}> //<-- here
<hr></hr>
<div>
<img src ={item.image}></img>
<h3 className='burgerTitle'>{item.name}</h3>
<h4><strong>$ {item.price}.00</strong></h4>
<Button variant='danger' onClick={()=>removeItem(item.id)}>Remove</Button>
</div>
<br></br>
</div>)}
You can define removeItem as a function, which gets an id (instead of an index, since that's safer) and setProduct to the subset which should remain. This could be achieved in many ways, in this specific example I use .filter() to find the subset of product whose elements differ in their id from the one that is to be removed and set the result as the new value of product.
removeItem = (id) => {
setProduct(product.filter((i)=>(i.id !== id)))
}
The application renders a table of an array of values in table rows, the mission is to detect the new table rows that are rendered after doing an action 'REFRESH' the table and pass them a new class name for the new rows.
As a new to react and redux , I can do it right , There are many methods I know.
We can create an array called oldResult = []
pass the old rows to it
loop over oldResult[] to detect the new values
here's the function :
let oldOrders = []
const getNewOrders = (orders) => {
oldOrders.push(OrderList.orders);
for (let i in orders){
if (orders.i != oldOrders[i]){
return this.order.state = 'newClickablerow'
}
}
}
here's rendering of the table rows:
<tr key={i.toString()}
onClick={_onClick}
className={'clickableRow'}
onMouseLeave={() => this.props.selectOrder(null)}
onMouseEnter={() => this.props.selectOrder(order)}>
<td data-label='Bewertung'>
{this._renderScore(score, order.creation_date)}
</td>
<td data-label='Börse'>
<img src={TranseuLogo} className="ordertable-logo"/>
</td>
<td data-label='Von'>
{renderLocation(order.pickup.location)}
</td>
<td data-label='Nach'>
{renderLocation(order.delivery.location)}
</td>
<td data-label='Vor-/Haupt-/Nachlauf'>
{renderRoute(deadhead, order.route, bobtail)}
</td>
<td data-label='Fracht'>
<CargoSummary
content={{bodytypes: this.props.content.bodytypes}}
cargo={order.cargo}/>
</td>
</tr>)
and this is the reducer which pass the values to the table :
function orders(state = [], action) {
switch(action.type) {
case RECEIVE_ORDERS:
return action.orders;
case RECEIVE_ORDERS_ERROR:
return []
case REFRESH:
default:
return state;
}
}
I dont know how to fire the function and use it when the REFRESH action is fired , what should i do in case of refresh?
and how to change the state (class name of the table row) ?
I searched and found that I can do that by changing the row state
constructor(props) {
super(props);
this.state = {
className : 'clickablerow'
}
}
Avoid redux state mutation to prevent bugs. Check out the Redux documentation for explanation and code examples: http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html#inserting-and-removing-items-in-arrays
Depending on your exact use case (like update / add / remove items) you could map over and compare every item using the array index, and add a boolean property like "isNew" on the order object. If you want to also check for updated items and if items can be removed then it would be better to use an object with unique IDs as the key to loop over every item.
You don't need to use local "state" on the order if you just want to render the className when updated from redux state, unless you also want to change it afterwards (like clicking it or removing the className again after a few seconds).
If you want to compare the whole object properties you could use utility methods like lodash.isEqual and lodash.omit to remove the "isNew" property before comparing to the new value, otherwise if you just want to add/remove items you can only compare the ID for example...
Didn't test this code but with using arrays of order objects and just checking if the object at the same position has the same ID to add "isNew" boolean value it would look something like this:
function orders(state = [], action) {
switch(action.type) {
case RECEIVE_ORDERS:
return action.orders;
case RECEIVE_ORDERS_ERROR:
return []
case REFRESH:
return refreshOrders(state.orders, action.orders)
default:
return state;
}
}
function refreshOrders (oldOrders, newOrders) {
return newOrders.map((newOrder, i) => {
// compare ID for same index and add a property isNew if it's different
const oldOrder = oldOrders[i] || {} // default to empty object if not found
const isNew = oldOrder.id === newOrder.id
return { ...newOrder, isNew }
})
}
Updated and simplified my answer, since I don't fully understand your requirements based on the explanation, so if this is enough to understand please mark this as answer or further explain what exactly you need to do (if you need to add / remove and also update items and if this class will only change after refreshing etc.)
I have a very simple Laravel /Vue js website, I have a list of product which I would like to filter.
const app = new Vue({
el: '#main-content',
data: {
forfaits: window.forfaits,
},
methods: {
filterData: function (val) {
console.log(val)
this.forfaits = this.forfaits.filter(item => {
return item.internet >=val[0] && item.internet <= val[1] ;
});
return this.forfaits;
}
HTML
<div class="product-item offre" v-for="forfait in forfaits">
.....
.....
.....
In this case it works but the original product array (forfaits) is mutated.
How can I filter without mutating the original value?
You want to have two properties:
A source-of-truth property with all unfiltered items, which is never consumed by the UI.
A computer property with the actual list to display, which at any point will return the complete list if there is no filter, or a filtered list if there is a filter. This is what you bind the UI to.
You don't need any methods; the computed property will automatically update as the filter changes.