How do I preselect a vue-multiselect option when options is an array of objects? - arrays

I want to pre-select a particular value in a select drop-down generated by vue-multiselect.
I can get this to work fine if I have a simple array of strings like the following:
['Test 1', 'Test 2', 'Test 3']
However, when I use an array of objects, I can't get this to work. For example, if I have the following:
<v-multiselect :options="[{id: 1, name: 'Test 1'}, {id: 2, name: 'Test 2'}, {id: 3, name: 'Test 3'}]"
label="name"
track-by="id"
v-model="test">
</v-multiselect>
No matter what I set the test data property that v-model is connected to, it won't preselect the value. I've tried 1, 2, 3, '1', '2' and '3' for test when track-by is id and 'Test 1', etc. when track-by is name but nothing seems to work.
What am I doing wrong here? I looked at the docs at https://vue-multiselect.js.org/#sub-single-select-object, but they don't seem to provide an example when you want to preset a value for an array of objects for the options. Googling has also not returned what I'm looking for.
On a related topic, once I get this working, what would I have to change to select multiple values for when I set the component to multiple? Thank you.

track-by usage
The docs indicate that track-by is "Used to compare objects. Only use if options are objects."
That is, it specifies the object key to use when comparing the object values in options. The docs should actually state that track-by is required when the options are objects because <vue-multiselect> uses track-by to determine which options in the dropdown are selected and to properly remove a selected option from a multiselect.
Without track-by, you'd see two buggy behaviors for object-options: (1) the user would be able to re-select already selected options, and (2) attempting to remove selected options would instead cause all options to be re-inserted.
Setting initial values
<vue-multiselect> doesn't support automatically translating a value array, but you could easily do that from the parent component.
Create a local data property to specify track-by and initial multiselect values (e.g., named trackBy and initialValues, respectively):
export default {
data() {
return {
//...
trackBy: 'id',
initialValues: [2, 5],
}
}
}
Bind <vue-multiselect>.track-by to this.trackBy and <vue-multiselect>.v-model to this.value:
<vue-multiselect :track-by="trackBy" v-model="value">
Create a watcher on this.initialValues that maps those values into an object array based on this.trackBy, setting this.value to the result:
export default {
watch: {
initialValues: {
immediate: true,
handler(values) {
this.value = this.options.filter(x => values.includes(x[this.trackBy]));
}
}
}
}
Vue.component('v-multiselect', window.VueMultiselect.default);
new Vue({
el: '#app',
data () {
return {
trackBy: 'id',
initialValues: [5,2],
value: null,
options: [
{ id: 1, name: 'Vue.js', language: 'JavaScript' },
{ id: 2, name: 'Rails', language: 'Ruby' },
{ id: 3, name: 'Sinatra', language: 'Ruby' },
{ id: 4, name: 'Laravel', language: 'PHP' },
{ id: 5, name: 'Phoenix', language: 'Elixir' }
]
}
},
watch: {
initialValues: {
immediate: true,
handler(values) {
this.value = this.options.filter(x => values.includes(x[this.trackBy]));
}
}
}
})
<script src="https://unpkg.com/vue#2.6.6/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue-multiselect#2.1.0"></script>
<link rel="stylesheet" href="https://unpkg.com/vue-multiselect#2.1.0/dist/vue-multiselect.min.css">
<div id="app">
<v-multiselect :track-by="trackBy"
:options="options"
v-model="value"
label="name"
multiple>
</v-multiselect>
<pre>{{ value }}</pre>
</div>

Looks like a bug. The workaround is to use an actual reference to the object
Vue.component('v-multiselect', window.VueMultiselect.default);
let testOptions=[{id: 1, name: 'Test 1'}, {id: 2, name: 'Test 2'}, {id: 3, name: 'Test 3'}]
new Vue({
el: '#app',
data: function () {
return {
test: testOptions[1], // <- use an object ref here!
testOptions
};
}
});

The easiest way I found out is sending the whole object from BE, so it gets pre-selected. If you send the same object from BE will get pre-selected. But I don't know if your options are hard coded on FE or they are coming from a database or something. I had the same issue but my values were coming from my database, so it was easy to reproduce the object

In your question just :object="true" is missing actually they didn't know that it is of type string or object when we pass this it knows yes it is object and i need label="name" from v-model="test" and picks it and shows it as a preselected

Related

Merge and sort arrays of objects JS/TS/Svelte - conceptual understanding

The goal is to display a recent activity overview.
As an example: I would like it to display posts, comments, users.
A post, comment and user object live in its corresponding arrays. All of the objects have a timestamp (below createdAt), but also keys that the objects from the different arrays don't have. The recent activites should be sorted by the timestamp.
(Ultimately it should be sortable by different values, but first I would like to get a better general understanding behind merging and sorting arrays / objects and not making it to complicated)
I thought of somehow merging the arrays into something like an activity array, then sorting it and looping over it and conditionally output an object with its keys depending on what kind of object it is?
If someone is willing to deal with this by giving an example, it would make my day. The best thing I could imagine would be a svelte REPL that solves this scenario. Anyway I'm thankful for every hint. There probably already are good examples and resources for this (I think common) use case that I didn't find. If someone could refer to these, this would also be superb.
The example I'm intending to use to get this conceptual understanding:
const users = [
{ id: 'a', name: 'michael', createdAt: 1 },
{ id: 'b', name: 'john', createdAt: 2 },
{ id: 'c', name: 'caren', createdAt: 3 }
]
const posts = [
{ id: 'd', topic: 'food', content: 'nonomnom' createdAt: 4 },
{ id: 'e', name: 'drink', content: 'water is the best' createdAt: 5 },
{ id: 'f', name: 'sleep', content: 'i miss it' createdAt: 6 }
]
const comments = [
{ id: 'g', parent: 'd', content: 'sounds yummy' createdAt: 7 },
{ id: 'h', parent: 'e', content: 'pure life' createdAt: 8 },
{ id: 'i', parent: 'f', content: 'me too' createdAt: 9 }
]
Edit: it would have been a bit better example with more descriptive id keys, like userId and when a post and comment object contains the userId. However, the answers below make it very understandable and applicable for "real world" use cases.
This is fun to think about and it's great that you're putting thought into the architecture of the activity feed.
I'd say you're on the right track with how you're thinking of approaching it.
Think about:
How you want to model the data for use in your application
How you process that model
Then think about how you display it
You have 3 different types of data and you have an overall activity feed you want to create. Each type has createdAt in common.
There's a couple of ways you could do this:
Simply merge them all into one array and then sort by createdAt
const activities = [...users, ...posts, ...comments];
activities.sort((a,b) => b.createdAt - a.createdAt); // Sort whichever way you want
The tricky part here is when you're outputting it, you'll need a way of telling what type of object each element in the array is. For users, you can look for a name key, for posts you could look for the topic key, for comments you could look for the parent/content keys to confirm object type but this is a bit of a brittle approach.
Let's try to see if we can do better.
Give each activity object an explicit type variable.
const activities = [
...users.map((u) => ({...u, type: 'user'})),
...posts.map((u) => ({...u, type: 'post'})),
...comments.map((u) => ({...u, type: 'comment'}))
];
Now you can easily tell what any given element in the whole activities array is based on its type field.
As a bonus, this type field can also let you easily add a feature to filter the activity feed down to just certain types! And it also makes it much simpler to add new types of activities in the future.
Here's a typescript playground showing it and logging the output.
As a typesafe bonus, you can add types in typescript to reinforce the expected data types:
eg.
type User = {
type: 'user';
name: string;
} & Common;
type Post = {
type: 'post';
topic: string;
content: string;
} & Common;
type UserComment = {
type: 'comment';
parent: string;
content: string;
} & Common;
type Activity = User | Post | UserComment;
To expand on the other answers, eventually you will want to show each element also differently, while you could do this with an if block testing the type that has been added to the object, this is not very scalable as a new type of block would require at least two changes, one to add the type to the activities array and one to add this new type to the if blocks.
Instead if we change our activities array as follows:
const activities = [
...users.map((u) => ({...u, component: UserCompomnent})),
...posts.map((u) => ({...u, component: PostComponent})),
...comments.map((u) => ({...u, component: CommentComponent}))
];
where UserComponent, PostComponent and CommentComponent are the different ways of presenting this data.
Then when you loop over your data to display them, we can use svelte:component and leverage that we already defined which component should be shown:
{#each acitivities as activity}
<svelte:component this={activity.component} {...activity} />
{/each}
Here's an approach using simple 'helper classes' so that the different objects can be distinguished when displayed REPL
<script>
class User {
constructor(obj){
Object.assign(this, obj)
}
}
class Post {
constructor(obj){
Object.assign(this, obj)
}
}
class Comment {
constructor(obj){
Object.assign(this, obj)
}
}
const users = [
{ id: 'a', name: 'michael', createdAt: 1652012110220 },
{ id: 'b', name: 'john', createdAt: 1652006110121 },
{ id: 'c', name: 'caren', createdAt: 1652018110220 }
].map(user => new User(user))
const posts = [
{ id: 'd', topic: 'food', content: 'nonomnom', createdAt: 1652016900220 },
{ id: 'e', topic: 'drink', content: 'water is the best', createdAt: 1652016910220 },
{ id: 'f', topic: 'sleep', content: 'i miss it', createdAt: 1652016960220 }
].map(post => new Post(post))
const comments = [
{ id: 'g', parent: 'd', content: 'sounds yummy', createdAt: 1652116910220 },
{ id: 'h', parent: 'e', content: 'pure life', createdAt: 1652016913220 },
{ id: 'i', parent: 'f', content: 'me too', createdAt: 1652016510220 }
].map(comment => new Comment(comment))
const recentActivities = users.concat(posts).concat(comments).sort((a,b) => b.createdAt - a.createdAt)
</script>
<ul>
{#each recentActivities as activity}
<li>
{new Date(activity.createdAt).toLocaleString()} -
{#if activity instanceof User}
User - {activity.name}
{:else if activity instanceof Post}
Post - {activity.topic}
{:else if activity instanceof Comment}
Comment - {activity.content}
{/if}
</li>
{/each}
</ul>

How to use Vuelidate for an Object of an Array?

I have the following vue array;
server: [
{ id: 1, name: 'Name A', ipaddress: '192.168.1.1', status: true, applied: true, modal: false },
{ id: 1, name: 'Name A', ipaddress: '192.168.1.1', status: true, applied: true, modal: false },
]
I use this array to show these information on a data table. Users can add new rows to this table, that is, they can push the array. In addition, they can delete the rows they want from the table with the splice method. Finally, each row has an edit button. Since these buttons are connected to the elements in the array with the v-model, users can make changes on the row they want in the modal window that opens.
Adding and editing operations are carried out with two different modalities that open when the button is pressed.
In line with all this information, there is a question I want to ask. How can I write validation with Vuelidation to an array where new rpws can be added continuously? Here is my vuelidation functions;
validations: {
server: {
required,
$each: {
name: {
required
},
ipaddress: {
required
}
}
}
}
As an example, I just defined the required attribute for two elements. And here is how I use them in my add and edit modals;
<div>
<div">
<label>Name</label>
</div>
<div>
<label>:</label>
</div>
<div>
<input v-if="server[i].modal" v-model="server[i].name" type="text"/>
</div>
<small class="error-msg" v-if="!$v.server[i].name.required && $v.server[i].name.$dirty">Name is required.</small>
</div>
Here is how I add a new row to the table;
addNewRow(){
this.server.push({
name: "",
ipaddress: "",
status: true,
applied: false,
modal: false
});
},
And now I have this error;
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
I think I'm missing an important part here so how can I make this correct? Thanks in advance.
Your add modal might be delay with the template so no server[i] item would be defined. I would suggest using a different variable for add (newItem = {name: '', ...})

AngularJS TypeScript Kendo UI DropDownList - bug with default value selected

I have list of objects for items and everything works fine, but now I want to add default value selected, but I have issues with it.
Here's the HTML code for the drop-down:
<select kendo-drop-down-list
k-options="selectItems"
k-ng-model="selectedItem">
</select>
And in AngularJS (using TypeScript) I'm making the drop-down:
this.itemsToSelect = [{id: 1, name: "One"}, {id: 2, name: "Two"}];
this.selectItems = {
optionLabel: "Select items...",
dataTextField: "name",
dataValueField: "id",
dataSource: new kendo.data.DataSource({
transport: {
read: (options) => {
options.success(this.itemsToSelect);
}
}
}),
value: this.itemsToSelect[0]
};
I don't have default value selected with this.
I tried the code from their documentation and it works with items as strings, but with items as objects works when I say something like:
value: this.itemsToSelect[0].id
So, their code looks like this:
<script>
let items = [{id: 1, name: "one"}, {id: 2, name: "two"}];
$("#dropdownlist").kendoDropDownList({
dataTextField: "name",
dataValueField: "id",
dataSource: items,
value: items[1].id
});
</script>
I tried this in my code and it doesn't work.
Code doesn't have any errors. Any suggestions?
EDIT
What I can do is this:
this.selectedItem = this.itemsToSelect[0];
I've set k-ng-model in the HTML and I can just set the selected value like this.
But, is there a way to do it through value in this.selectItems object?
Make a local object representing your default value, like
{
id: 1,
name: "bob"
}
then on the control set autoBind to false.
https://docs.telerik.com/kendo-ui/api/javascript/ui/dropdownlist/configuration/autobind

AngularJS: refresh ng-options when property source object changes

Full description:
I have list of options with multiple properties (not just key-value pair):
[
{
id: 1,
name: "111",
some: "qqq"
},
{
id: 2,
name: "222",
some: "www"
}
];
And select defined as:
<select name="mySelect" id="mySelect"
ng-options="option.name for option in opts track by option.id"
ng-model="mod1"></select>
Depending on app logic, extra property may change:
{
id: 2,
name: "222",
some: "www1"
}
But actual list in Select doesn't change!
However, if name changes, then entire optionList will be refreshed.
In short you can see demo on JSFiddle OR JSFiddle. I prepared 2 very similar examples:
When button is clicked only extra property updates
When button is clicked - both extra property and key receive new value
Does anybody know solution?
UPDATE
For now I'm solving that issue with update + delay + update solution:
this.click = function(){
$scope.opts = {};
$timeout(function() {
$scope.opts = { /* NEW OBJECT */};
}, 0);
}
OK, so I think I understand what you want, which is to be able to select an option whose nested values may have changed since the list was rendered in the DOM.
Based on that understanding, I believe that the plunker I have created illustrates a solution for you. If you select one of the options, and change the child value in the input field, two-way binding will update the model.
Basically, it is taking the users selection, and on select change, re-assigning the selected object to reference the original option in the options array. This will allow two-way binding to occur. If you view the code, you will see that the input fields are updating the option list itself ($scope.options), where-as the model that is being displayed is $scope.formData.model.
https://plnkr.co/edit/DLhI7t7XBw9EbIezBCjI?p=preview
HTML
<select
name="mySelect"
id="mySelect"
ng-model="formData.model"
ng-change="onChange(formData.model)"
ng-options="option.name for option in options track by option.id"></select>
SELECTED CHILD: {{formData.model.child.name}}
<hr>
<div ng-repeat="option in options">
Child Name for {{ option.name }}: <input ng-model="option.child.name">
</div>
JS
$scope.onChange = function(option) {
angular.forEach($scope.options,function(optionItem){
if (optionItem.id == option.id){
$scope.formData.model = optionItem;
}
})
}
$scope.options = [
{
id: 1,
name: "111",
child: {
id: 11,
name: "111-1"
}
},
{
id: 2,
name: "222",
child: {
id: 22,
name: "222-1"
}
}
];
$scope.formData = {
model: $scope.options[0]
};
Call $apply whenever you want to apply changes made.
$scope.$apply();
This will tell AngularJS to refresh.

Angular-Formly Nested Model Not Updating

I am having an interesting issue with angular-formly. I am attempting to use the 'model' tag as shown below because my model is not flat.
{
'key': 'last',
'model': 'model.name',
'templateOptions: {}
}
However, I cannot update the model in a clean manner. Simply replacing model or even model.name with a matching model that contains the updated value does not cause the model to update the view.
var newModel = {
name: {
first: 'Gandalf',
last: 'The White'
}
};
self.model = {
name: {
first: 'Gandalf',
last: 'The Grey'
}
};
function setNewLastName() {
self.model = newModel;
}
setNewLastName();
However if I drill down to the specific property, it works as expected.
self.model.name.last = self.newModel.name.last;
Here is a link to a JSBin where the value updates using the drill-down method immediately above.
Drill-down JSBin
Another JSBin that attempts to update the model by assigning a new model that does not update.
Assign Model JSBin
Has anyone ran into this issue or can you see where I'm doing something wrong?
You replace the model for each key, therefore you never see the changes.
What you need to do is to match the model in the key itself.
vm.fields = [
{
key: 'name.first', // <-- HERE
type: 'input',
//model: vm.model.name, //Wrong
templateOptions: {
label: 'First Name'
}
},
{
key: 'name.first', // <-- AND HERE
type: 'input',
//model: vm.model.name, //Wrong
templateOptions: {
label: 'Last Name'
}
},
//...
];
See corrected example: http://jsbin.com/pupijoc/1/edit?js,console,output
UPDATE: Nested properties are also handled by fieldGroups
Se updated example: http://jsbin.com/pupijoc/3/edit?js,console,output

Resources