I have an array of non-homogeneous objects that are each rendered in a loop using a <svelte:component this={type}> component, and I would like to group adjacent things that are of the same type within a div.
For example, I have some code similar to this:
<script>
let things = [
{type: A, content: "One"},
{type: B, content: "Two"},
{type: B, content: "Three"},
{type: A, content: "Four"}
];
</script>
{#each things as thing, i}
{()=>someMagicHere1(things, thing, i)}
<svelte:component this={thing.type}>
{()=>someMagicHere2(things, thing, i)}
{/each}
And I want the output to group the things like so:
<div class="group type-A">
<div class="thing type-A">One</div>
</div>
<div class="group type-B">
<div class="thing type-B">Two</div>
<div class="thing type-B">Three</div>
</div>
<div class="group type-A">
<div class="thing type-A">Four</div>
</div>
In the things array, the things are not sorted (actually, they are, but by date, unrelated to their type), but the idea is to visually group together the ones that are the same type. Ideally, I'd be able to group only certain types (like group all of type A, but type B's would remain separate), but I feel like I would be able to derive a separate solution if I could group at all. There are also more than two types; this is just a minimal sample.
In Svelte, the individual A and B components can't have partial HTML elements like this inside, because Svelte won't allow conditionals around unclosed elements:
{#if groupStart}
<div class="group">
{/if}
From within each someMagicHereX() I could output some HTML with {#html customTags} to get the DOM output that I want, but then I lose the style encapsulation and other Svelte component benefits.
What I'd really like is a more "sveltian" solution. Perhaps I need to create something new with use? Anyone have any good ideas?
Update: A key feature I seem to have left out is that any controls must ultimately bind to the original dataset. So even if the data is transformed somehow, the original data must be updated at runtime and vice-versa on any bound controls.
Not absolutely sure if the following satisfies what you are looking for, as I can't tell from your question if you want to keep the order of values untouched or if you want to reorder them by group first.
The idea is to re-arrange your data in order to loop through it in the way you want to. It's always easier to manipulate data in order to fit a layout than the other way around.
The following would turn your initial array into an array with the following structure:
[
{
cssClass: 'type-A',
values: [ /* all objects of type 'A' */ ],
},
{
cssClass: 'type-B',
values: [ /* all objects of type 'B' */ ],
},
// etc.
]
The data transformation is pretty straightforward:
let groups = things.reduce((curr, val) => {
let group = curr.find(g => g.cssClass === `type-${val.type}`)
if (group)
group.values.push(val)
} else {
curr.push({ cssClass: `type-${val.type}`, values: [ val ] })
}
return curr
}, [])
With this new data structure available, it's fairly easy to achieve the layout you had in mind:
{#each groups as group}
<div class="group {group.cssClass}">
{#each group.values as value}
<div class="thing {group.cssClass}">
{value.content}
</div>
{/each}
</div>
{/each}
Demo REPL
Edit: If you prioritize the order of objects as it stands in your initial array, the data transformation would be slightly different. Basically, you'd want to create a new 'group' every time the type changes.
Something like the following would do the trick (note that the svelte #each structure would remain the same, only the data transformation changes):
let groups = things.reduce((curr, val) => {
let group = curr.length ? curr[curr.length - 1] : undefined
if (group && group.cssClass === `type-${val.type}`) {
group.values.push(val)
} else {
curr.push({ cssClass: `type-${val.type}`, values: [ val ] })
}
return curr
}, [])
Option 2 Demo REPL
Related
What is the best and clean way to alter Object Arrays?
I have a code that look´s like this.
const [get_post, set_post] = useState([
{name: "First"},
{name: "Second"},
{name: "Third"},
])
I would like to add and edit keys, on a certain index. So I do it like this:
<button onClick={()=>
set_post([...get_post, get_post[0].content = "This is index zero" ])
}> Manipulate! </button>
My result is this:
[
{"name": "First", "content": "This is index zero"},
{"name": "Second"},
{"name": "Third"},
"This is index zero" <-- This line is the problem
]
I have googled this a lot and this seems to be a common subject, however.
This post describe the same problem and solution with a keyed object, which doesn't help me.
React Hooks useState() with Object
This post support 3rd party libs and/or deep copying, which I suspect isn't the "right" way of doing it either.
Whats the best way to update an object in an array in ReactJS?
This thread also support a lot of deep copys and maps, which I suppose I don't need (It's an array, I'm should be able to adress my object by index)?
How do I update states `onChange` in an array of object in React Hooks
Another deep copy solution
https://codereview.stackexchange.com/questions/249405/react-hooks-update-array-of-object
The list goes on...
Basically I want the result I got without the extra line,
and if even possible:
Without deep copying the state to inject back in.
Without 3rd party libraries.
Without using a keyed object.
Without running a map/filter loop inside set_post.
Edit: The reason why map should be unnecessary in setPost.
In my particular scenario the Module that renders the getPost already is a map-loop. Trying to avoid nested loops.
(My logic simplified)
const [get_post, set_post] = useState([
{name: "First"},
{name: "Second"},
{name: "Third"},
])
//Render module
//Fixed numbers of controllers for each Object in Array.
get_post.map((post, index)=> {
<>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 1" ])}
}>
Controller 1
</button>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 2" ])}
}>
Controller 2
</button>
<button onClick={()=>
set_post([...get_post, get_post[index].content = "Content 3" ])}
}>
Controller 3
</button>
//...
</>
})
If you just want to alter the first property, extract it from the array first.
You can use the functional updates method to access the current state value and return a new one with the changes you want.
set_post(([ first, ...others ]) => [{
...first,
content: "This is index zero"
}, ...others])
To alter any particular index, you can map the current array to a new one, creating a new object for the target index when you reach it
let x = the_target_index
set_post(posts => posts.map((post, i) => i === x ? {
...post,
content: `This is index ${x}`
} : post))
A slightly different version of this that matches what you seem to want to do in your answer would be
set_post(posts => {
posts[x] = { ...posts[x], content: `This is index ${x}` }
// or even something like
// posts[x].content = `This is index ${x}`
return [...posts] // clone the array
})
I have found a solution that works! I haven't seen this posted elsewhere so it might be interesting to look into.
To change or add a key/value to an object in an array by index just do this:
<button onClick={() => {
set_post( [...get_post , get_post[index].content = "my content" ] )
set_post( [...get_post ] ) //<-- this line removes the last input
}
}>
As Phil wrote, the earlier code is interpreted as:
const val = "This is index zero";
get_post[0].content = val;
get_post.push(val);
This seems to remove the latest get_post.push(val)
According to the React Docs, states can be batched and bufferd to for preformance
https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
When React batches set_post it should behave like this.
If the one-line command
set_post( [...get_post , get_post[index].content = "my content" ] )
gives
const val = "This is index zero";
get_post[0].content = val;
get_post.push(val);
The double inject would trigger the buffer and reinject the state at the end insted of array.push(). Something like this.
var buffer = get_post
buffer[0].content = val
//Other updated to get_post...
get_post = buffer //(not push)
There for, this should be a perfectly good solutions.
actually no biggie but how would a computed property filter function look like that always returns the current array + 5 more elements?
more in detail:
Template:
<span class="box-content" v-for="item in activeItems" :key="item.id">
<img class="item" :src="item.filename" />
</span>
Script
data: function() {
return {
items: [],
limit: 1,
};
},
computed: {
activeItems: function() {
return this.items.filter( function(s) {
if(s.length > this.limit) {
return s;
}
});
// return this.limit ? this.items : this.items;
}
},
on page load , an axios post request gets an object of items, whose response is pushed into the items array which is empty upon component declaration.
so axios -> get object with items -> push into empty array.
now i want to display ,like, 5 items and make a show more button.
The problem now is, my activeItems function is invalid, it does not know "this.limit" and i doubt anyway that it returns the correct result as i just made it return itself and not a set of objects / arrays.
What I would do next is trying around with splice and slice, array copies and pushing elements into it until a certain condition is met but.. is there a better way ?
Thanks in advance
The filter function should be used to filter based on the internal values of an array. Say you have an array of objects with persons, and each Person as an age, then you could use the Array.prototype.filter function to filter based on that age of each entry.
The filter function therefore goes through every entry in your array and determines whether an item should be included or excluded.
If you, on the other hand, want to limit the amount of entries based on a maximum number of entries, I would suggest you use Array.prototype.slice, as you mentioned already.
Your computed function could be rewritten to:
activeItems: function() {
return this.items.slice(0, this.limit)
}
First, in your code, this.limit is undefined because this is referencing the anonymous function. If you want to access the component, you will better use arrow functions syntax.
Also, s references an element of your array, so s.length will be undefined too I guess...
Now, filter does not seem to be the best choice for your need. I'll go with slice instead. Somthing like:
computed: {
activeItems() {
return this.items.splice(0, this.limit)
}
}
Where limit is increased by 5 when you click the show more button.
Of course you could do it. You just missed some code on it. Here how you fix it
activeItems: function() {
let limit = this.limit
return this.items.filter( function(item, s) {
return s <= limit
});
}
If you don't mind using filter, here are some way to do it.
First : put condition in your for loop, this one
<span class="box-content" v-for="(item, index) in items" :key="item.id" v-if="index <= limit">
<img class="item" :src="item.filename" />
</span>
Second is to slice your array on you desired length, this one
<span class="box-content" v-for="(item, index) in items.slice(0, limit)" :key="item.id">
<img class="item" :src="item.filename" />
</span>
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>
I have and Array of Objects whose elements are randomly ordered. I would like to list the values in a specific order (of the keys).
As an example, the iteration below just lists them:
var vm = new Vue({
el: "#root",
data: {
all: [{
second: 2,
third: 3,
first: 1
},
{
third: 30,
first: 10,
second: 20
}
],
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.js"></script>
<div id="root">
<div v-for="a in all">
<div v-for="(v, k) in a">{{v}}</div>
</div>
</div>
Is this possible to drive the iteration so that it is ordered according to a list of keys (["first", "second", third"]) which would yield
1
2
3
10
20
30
I don't know vue but you can do it like this in javascript.
<div v-for="k in Object.keys(a).sort()">{{k}}:{{a[k]}}</div>
Also note that alphabetic sorting accidentally fits into your need, but you might need a custom sort function like sort((a,b)=>order.indexOf(a)-order.indexOf(b)) with your custom order order: ["first","second","third","fourth"] which may not be alphabetic.
var vm = new Vue({
el: "#root",
data: {
all: [{
second: 2,
third: 3,
first: 1,
fourth: 4
},
{
third: 30,
first: 10,
second: 20,
fourth: 40
}
],
order: ["first","second","third","fourth"]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.js"></script>
<div id="root">
<div v-for="a in all">
<div v-for="k in Object.keys(a).sort((a,b)=>order.indexOf(a)-order.indexOf(b))">{{k}}:{{a[k]}}</div>
<hr/>
</div>
</div>
You can put your list of sorted keys in an array and v-for over that instead.
<div v-for="a in all">
<div v-for="key in presortedListOfKeys">
{{a[key]}}
</div>
</div>
You can use computed properties and get the all sorted first before iteration through them.
In Module 5 of my Vue.js Training course, I actually discuss this. At first glance, the Object.keys approach may seem like the correct approach. However, different JavaScript engines return the properties in different orders. For that reason, you can't rely on it.
In Vue.js, you could either a) sort your Array when the Array gets populated b) Create a "computed property" or c) Create a sort method that you can pass your property into.
In my opinion, you should use option b. The reason why is because computed properties get cached. By doing this, you'll only run your sort code once. However, if you were to use option c the sort would execute during each iteration. Option a is a possibility, but I don't know enough about your scenario to know if this is a real option or not.
I have an array, keywords, printing with an ng-repeat:
<li ng-repeat="keyword in keywords"> {{ keyword }} </li>
Which when it's sorted alphabetically would display, e.g:
Apples
Cucumbers
Daikon
Turnip
I want that when a user searches a specific keyword, that keyword gets "pinned" to the top of the list, no matter how else the list is sorted. So if the user searches "Turnip", Turnip is first in the list, while the rest remains sorted alphabetically:
Turnip
Apples
Cucumbers
Daikon
I am wondering if this functionality is possible with ng-repeat, or if I will need to construct it by inserting an additional element at the top and then filtering just that one from the array.
I'm adding another answer, as I think both could be used, but this one with sorting is much slicker!
Here, I just do a sort of the array of objs on the pinned first then on the name value as you wanted it:
<li ng-repeat="obj in array | orderBy:['pinned','name']:reverseSort ">{{ obj.name }} [<label>Pin</label><input type="checkbox" ng-model="obj.pinned" ng-click="pinObj(obj)" />]</li>
http://plnkr.co/edit/8NGW3b?p=info
Cheers
You can create a custom angular filter that handles the sorting. Then you could just use
<li ng-repeat="keyword in keywords|my_sort"> {{ keyword }} </li>
http://docs.angularjs.org/guide/filter
good luck!
I could imagine that you could have instead of just a key in your array, you could have an array of objects for example:
array {
[ key: "Turnip",
pinned: true],
[ key: "Apples",
pinned: false] }
And then, in your ng-repeat, then you could have a filter that splits out the pinned versus unpinned as required.
app.filter('pinned', function() {
return function (list, pinned, scope) {
var test = (pinned == 'true' ? true : false);
var returnArray = [];
for (var i = 0; i < list.length; i++) {
if (list[i].pinned === test) {
returnArray.push(list[i]);
}
}
return returnArray;
};
});
I've created this plunk to show what I mean above. A potentially slicker solution would be to sort your array by the pinned attribute.
http://plnkr.co/edit/onFG7K61gLLqX31CgnPi?p=preview