NcSelectUser doesn't get updated when user clicks on an item

I have been trying to get my feet wet in developing an extension for NC. The project is mostly empty, just a skeleton based on the tutorials.

In the sample view below I am trying to search contacts with the NcSelectUser component. But even with the static input from the sample code it doesn’t accept any selection.
Once clicked on an item in the list, the select input stays empty. No errors in the console . I am lost at that point and hope you can point me in the right direction.

<template>
	<div class="invite-content">
		<h2>
			TEST2
		</h2>
		<div class="grid">
			<div class="container">
				<NcSelectUser v-bind="selectprops"
					input-label="Select invitees" />
			</div>
		</div>
	</div>
</template>

<script>
import NcSelectUsers from '@nextcloud/vue/components/NcSelectUsers'

export default {
	name: 'MyInviteContent',

	components: {
		NcSelectUsers,
	},

	props: {
	},

	data() {
		return {
			selectprops: {
			options: [
				{
					id: '0-john',
					displayName: 'John',
					isNoUser: false,
					subname: 'john@example.org',
				},
				{
					id: '0-emma',
					displayName: 'Emma',
					isNoUser: false,
					subname: 'emma@example.org',
				},
				{
					id: '0-olivia',
					displayName: 'Olivia',
					isNoUser: false,
					subname: 'olivia@example.org',
				},
				{
					id: '0-noah',
					displayName: 'Noah',
					isNoUser: false,
					subname: 'noah@example.org',
				},
				{
					id: '0-oliver',
					displayName: 'Oliver',
					isNoUser: false,
					subname: 'oliver@example.org',
				},
			],
			},
		}
	},

	computed: {
	},

	watch: {
	},

	mounted() {
	},

	beforeDestroy() {
	},

	methods: {
	},
}
</script>

<style scoped lang="scss">
.invite-content {
	height: 100%;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
}
</style>

I tired various browsers with no luck.

Thanks!

You’re on the right track using NcSelectUsers, but from your description it sounds like you’re missing the v-model binding. That component requires a v-model to track the selected user(s) — otherwise, it will render the options but won’t store or reflect the selection anywhere.

:white_check_mark: Example fix:

Make sure you have something like this in your data():

data() {
	return {
		selectedUsers: [],
		selectProps: {
			options: [
				{ id: 'john', displayName: 'John Doe', isNoUser: false, subname: 'john@example.com' },
				{ id: 'emma', displayName: 'Emma Smith', isNoUser: false, subname: 'emma@example.com' },
			],
		},
	}
}

And then in your template:

vue

<NcSelectUsers
	v-model="selectedUsers"
	v-bind="selectProps"
	input-label="Select a contact"
/>

Just one remark:
Do not combine v-model with v-bind. They are mutually exclusive or might trigger strange behavior. v-model is a shorthand for v-bind plus v-on with a matching method to update the original data entry once the user interacts.

Chris

Thank you both for your reply! Coming from Yew/React with PHP, Vue is an interesting challenge.

I tried your suggestion, @destripador , but to no avail. Again, the select shows both contacts but when clicking on any the dropdown closes but the visible input field stays empty. (I am not so far into coding yet as to use any selected item, all I want to see first is that the dropdown works at all.)

@christianlupus This is interesting, as almost every example (even the official example from NC) are using both v-bind and v-model. As a complete newbie to Vue I do not understand the implications of either at the moment. Even if the two-way data binding doesn’t work correctly, shouldn’t the dropdown box at least keep the selected value visible to the user?

This is so strange, it must be something completely unrelated or too obvious. Maybe the NcSelectUsers component is filtering my input data, showing it correctly but doesn’t allow the object to be selected (which would be weird, as they shouldn’t be shown in the first place). Or it verifies, if the contact actually exists within NC. I am not sure.
The source code I found so far only shows the presentation layer, but there are missing pieces to it. For example, the avatar is fetched from NC and if it is a user, the presence state too. Can’t find those routines anywhere in the code. So maybe there is more going on.
However, I also tried to use NcSelect directly, adding a “label” property to each demo entry. Same issue, all shown but on click nothing gets put into the input field.

Which version of nextcloud-vue do you use? 8.x? There seems to be a bug in the component… You can also see it here: Nextcloud Vue Style Guide

According to package.json:

    "@nextcloud/vue": "^8.11.2",
    "@nextcloud/vue-richtext": "^2.1.0-beta.6",
    "pdf-lib": "^1.17.1",
    "vue": "^2.7.16",
    "vue-click-outside": "^1.1.0",
    "vue-material-design-icons": "^5.3.1"

So I assume 8.11. And yes, the Vue Style Guide examples work on all of my browsers, so it has to be something else. Appreciate your help!

OK, let me help you out a bit.

What is v-bind and v-model?

Ref: API — Vue.js and Components Basics — Vue.js

With v-bind you can pass data to a component (dynamically). That means, if you have a component that has a sub-component and the sub-component needs some infos, you can pass them down one-way. Changes in the sub-component are technically possible but will cause all sort of trouble. Don’t do it!

To bring back information from the sub-component to the component, Vue uses events. So, the sub-component can say something like “Hey, the user clicked on user John. Please update this!” The main component can then decide (or not) to update the state, which in turn will be dynamically forwarded to the sub-component.

This structure (from the view of the main component) is very common. Normally, you would add a v-bind (putting the info downwards) plus a v-on (receiving events). As a sort of better dev experience (mostly), v-model was introduced that does this under the hood. Therefore v-model is considered two-way binding as the sub-component can directly affect the main component’s data.

However, be careful when nesting v-models (so there is a sub-sub-component also with the same v-model). Things can get hairy there quickly.

What is v-bind and how does it work in detail?

Ref: API — Vue.js

There is more to the examples, namely the v-bind. But first you have to understand its twofold usage.

The trivial usage is like this (just to get the basic idea, does not make much sense here)

<NcSelectUsers v-bind:inputLabel="'foo'" placeholder="this" />

without v-bind you can assign static information to the (sub-) component. The placeholder attribute will get the string this. No modifications made.
If you prefix an attribute with v-bind:, you tell Vue that this value is to be updated during runtime and to update the component accordingly. The value (in this case it is 'foo', mind the single quotes(!)) is a JS string foo as inputLabel attribute. So, you can put arbitrary JS expressions in the value of such a v-bind entry. To use a static string foo, you have to double-quote. You could as well put a dynamic information like selectedprops[0].options[2].displayName (but then without double-quotes).

As you need v-bind: prefix very often, there is a shorthand for it: :. So, the above would be equivalent to

<NcSelectUsers :inputLabel="'foo'" placeholder="this" />

The NcSelectUsers component allows to define all sorts of attributes. You could add them all one by one using v-bind: or :. Vue has understood, however, that there might be the need to dynamically add or remove attributes or to control them in a more programmatic way. Thus, you can use the syntax v-bind="expr". (Note, there is no : and a attribute name here.)
This will look at the expression expr and take all keys of it (it should be an object) as names for attributes. The values are the values of the attributes. So, lets revisit the example (again). We could set

expr = {
  inputLabel: "foo",
}

and use

<NcSelectUsers v-bind="expr" placeholder="this" />

This is how the documentation is build: There is an array of possible setting to show and using the v-bind bind all parameters are pre-filled.

Why both v-bind and v-model

In the style guilde both are used. The v-bind is used to configure the component. The v-model is used to set (and get) the selected value.

The cause for it not working is that the value of v-model is not existing. It points to selectArray[i].props.value (i is just the index from the loop) which is not part of the data below. Therefore, I guess Vue gets a problem and cannot store the selected user anywhere (storing to undefined makes no sense). I have not tried it but this seems rather logical to me.

I hope this helps
Chris

Thank you @christianlupus for the detailed explanation. It’s interesting to see how Vue handles things. I guess one of my main issue is finding examples and docs using Vue 3 while apparently NC is still on Vue 2.

I modified my source based on your input and can now receive the selected element. But the select input still stays without a value.
My assumption is, that because the :option binding is static, the select component doesn’t get updated with the current selection. So I will further dig into this.

Here is the updated code so far. I find it a bit verbose, to be honest, as I was expecting the selection to be handled inside of the components and the v-model or in my case the @update:modelValue to be used for communicating the selected values back to me. But something is still missing there.

<template>
	<div class="invite-content">
		<h2>
			TEST2
		</h2>
		<div class="grid">
			<div class="container">
				<NcSelectUsers :options="contacts"
					@update:modelValue="selectedContact = $event"
					input-label="Select invitees" />
			</div>
		</div>
	</div>
</template>

<script>
import NcSelectUsers from '@nextcloud/vue/components/NcSelectUsers'

export default {
	name: 'MyInviteContent',

	components: {
		NcSelectUsers,
	},

	props: {
	},

	data() {
		return {
			selectedContact: null,
			contacts: [
				{
					id: '0-john',
					displayName: 'John',
					isNoUser: false,
					subname: 'john@example.org',
				},
				{
					id: '0-emma',
					displayName: 'Emma',
					isNoUser: false,
					subname: 'emma@example.org',
				},
			],
		}
	},

	computed: {
	},

	watch: {
		selectedContact: function(val) {
			console.log('contact selected: ' + val.displayName)
		},
	},

	mounted() {
	},

	beforeDestroy() {
	},

	methods: {
	},
}
</script>

<style scoped lang="scss">
.invite-content {
	height: 100%;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
}
</style>

The source code of NcSelectUsers does define a model:

but so far trying to use v-model="..." doesn’t yield any effect.

At least, you should set the modelValue value dynamically. So, the event just tells you “hey, the user clicked here”. It does not update the state or similar.

By doing so (in the outer component), you set the inner component to update its state (again?) to the final value. The again here means, this depends on the implementation of the component. That way, ideally, the inner component is completely stateless and just a dumb viewer of the data in the surrounding component.

That was it! The select box now updates perfectly. Thank you all for getting me started.

Here is the updated code. Still looks a bit weird to me, but at least I have something to go on.

<template>
	<div class="invite-content">
		<h2>
			TEST2
		</h2>
		<div class="grid">
			<div class="container">
				<NcSelectUsers :options="contacts"
					:model-value="selectedContacts"
					:multiple="true"
					:keep-open="true"
					input-label="Select invitees"
					@update:modelValue="selectedContacts = []; $event.forEach((e) => selectedContacts.push({
						id: e.id,
						displayName: e.displayName,
						isNoUser: e.isNoUser,
						subname: e.subname,
					}))" />
			</div>
		</div>
	</div>
</template>

<script>
import NcSelectUsers from '@nextcloud/vue/components/NcSelectUsers'

let selectedContacts = []

export default {
	name: 'MyInviteContent',

	components: {
		NcSelectUsers,
	},

	props: {
	},

	data() {
		return {
			selectedContacts,
			contacts: [
				{
					id: '0-john',
					displayName: 'John',
					isNoUser: false,
					subname: 'john@example.org',
				},
				{
					id: '0-emma',
					displayName: 'Emma',
					isNoUser: false,
					subname: 'emma@example.org',
				},
			],
		}
	},

	computed: {
	},

	watch: {
		selectedContacts: function(val) {
		},
	},

	mounted() {
	},

	beforeDestroy() {
	},

	methods: {
	},
}
</script>

<style scoped lang="scss">
.invite-content {
	height: 100%;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
}
</style>

I am now getting a warning from Vue in the browser console, as this is a multiple selection and the model of modelValue is defined as an object, not an array in NcSelectUsers. I will ask on GitHub if thats a bug or a usage error.

1 Like

Those props are usually defaulting to false, so you can also just write:

					:multiple
					:keep-open

The defaults are listed in the style guide, too :slight_smile:

What exactly looks weird?

Drop that. It brings only confusion.

Unless you need them, you can drop them as well.

This looks a bit verbose to me, to be honest. I would suggest you put this into methods and just call the method. This is more compact in the template section above.

Thank you, I will shorten that. Good to know :slight_smile:

That I couldn’t use v-model for the bi-directional communication, but had to implement the emit handler, which isn’t even listed as emit on NcSelectUsers. But from what I read on GitHub it might be a bug in the component.

I thought I need that, in case I move the @update:modelValue code into a function (as you suggested, which I will do). Wouldn’t I need to declare this variable outside the export?

Could be just me. If I add the entire object it carries a lot of internal data with it (observer etc), so I tried to extract only the values according to the contacts schema.

I did find the event in the docs… But yeah, there might be an issue in general. Also, I thought (but I would have to check it once more) that v-model is hard-coded to the property/attribute named value. But this is just some ringing in the head…

No, all properties reside inside the component. Otherwise you run into problems if you instantiate the component twice.

Also I said method, not function. See below.

I would replace this by something like (indentation needs fixing)

methods: {
  selectUsers(ev) {
    // Vue2 does not allow overwriting an array, we must inplace drop the entries
    // https://v2.vuejs.org/v2/guide/reactivity.html#For-Arrays
    this.selectedContacts.splice(0);

    // Add the entries we want back
    ev.forEach((e) => this.selectedContacts.push({
      id: e.id,
      displayName: e.displayName,
      isNoUser: e.isNoUser,
      subname: e.subname,
    }))
  },
},

Then, @update:modelValue="selectUsers" is sufficient in the template. This is what I meant by verbose. You know best what information you will need later on.

Ahh, thanks. I’ll change that.

I like that, as it simplifies the code and makes it much more readable. The splice comment is another interesting nugget, I wouldn’t even have thought about looking for something like that.

Thank you again for all your assistance.

1 Like