VueJS -- Select form input when initial value is null - vuejs2

I have a locker object, with a bunch of fields, and a lots array of objects -- both passed in via props. For the locker object, one of the fields holds the id of a specific lot in the lots array. Like so:
locker: { locker_num: 'AH3', part_of_lot: '235', allocation: '' , etc...}
The lots object, is an array of objects, with the structure of:
lots: [
{ id: '234', sl_num: '567', etc... },
{ id: '235', sl_num: '567', etc... },
{ id: '236', sl_num: '567', etc... },
{ id: '237', sl_num: '567', etc... },
... etc
]
I'm trying to present a form to the user to edit the locker object. For the part_of_lot field, I want to present a select input. Here's my code for that:
<select class='form-control' v-model='locker.part_of_lot.id'>
<option disabled value=''>Choose a Lot</option>
<option v-for='l in lots' :selected="l.id === locker.part_of_lot.id" :value='l.id'>{{ l.sl_num }}</option>
</select>
This works as expected -- but only when there's already a value in the part_of_lot field of locker. When I have a locker that has the part_of_lot field empty, then the above fails. The error is Cannot read property 'id' of null.
So how would I setup that select input to handle both cases: 1) When part_of_lot already has a value and 2) when part_of_lot is null?
EDIT
If I created a method that simply checks if the part_of_lot value is null and, if so, changes it to the string "" then the above code works. But this seems like a clumsy work around. Shouldn't there be a more elegant way to handle this? I feel like there could be many many other fields on other models with a value of null that need to be dealt with in the template.

You could make a computed property partOfLotID with get and set methods referencing the locker.part_of_lot object and handling the case of a null value:
computed: {
partOfLotID: {
get() {
let part = this.locker.part_of_lot;
return (part) ? part.id : '';
},
set(value): {
let part = this.locker.part_of_lot;
if (part && typeof part === 'object') {
this.locker.part_of_lot.id = value;
} else {
this.locker.part_of_lot = { id: value };
}
}
}
}
Then just use partOfLotID in the select instead:
<select class='form-control' v-model='partOfLotID'>
<option disabled value=''>Choose a Lot</option>
<option
v-for='l in lots'
:selected="l.id === partOfLotID"
:value='l.id'
>
{{ l.sl_num }}
</option>
</select>

The solution I've gone with was to have a method that checks the value of part_of_lot and changes it to "" if it is null. Like so:
created () {
this.setToStrings()
},
methods: {
setToStrings () {
// Simply replaces null values with strings=''
if (this.locker.part_of_lot === null) {
this.locker.part_of_lot = ''
}
return this.locker
},

If the part_of_lot is passed in via props, you can set a default value for it:
props: {
part_of_lot: {
type: String,
default: ""
}
}
vuejs props doc
you can also add a new field in data:
{
data: function (){
return {
new_part_of_lot: this.locker.part_of_lot || "";
}
}
}
then use new_part_of_lot instead of part_of_lot.

Related

How to select specific instance of Vue component when multiple instances exist in DOM

I have a component as defined by the following template, assume it's called CompositeComponent:
<template>
<div class="item-and-qualifiers-selector">
<special-list
:props="someProps"
></special-list>
<another-special-list
:id="'anotherSpecialList1'"
:list-items="someListItems"
></another-special-list>
<another-special-list
:id="'anotherSpecialList2'"
:list-items="someMoreListItems"
></another-special-list>
</div>
</template>
The CompositeComponent in constructed of two different components, SpecialList and AnotherSpecialList, where AnotherSpecialList appears twice within the structure of the CompositeComponent. Assume the following basic component design for AnotherSpecialList:
export default {
name: 'AnotherSpecialList',
props: {
listItems: { type: Array, required: true },
id: { type: String, required: true }
},
data() {
return {
activeListItem: null
};
},
created() {
document.addEventListener('focusin', this.focusChanged);
},
methods: {
focusChanged() {
if (document.activeElement.id === 'anotherSpecialList1') {
console.log('Focussed on the first special list');
this.activeListItem = this.listItems[0];
} else if (document.activeElement.id === 'anotherSpecialList2') {
console.log('Focussed on the second special list');
this.activeListItem = this.listItems[0];
}
}
}
};
</script>
When the DOM's current focus is the SpecialList, I want to tab from the SpecialList into the first instance of AnotherSpecialList and assign the activeListItem of AnotherSpecialList where id === anotherSpecialList1 to listItems[0], i.e., this.activeListItem = listItems[0]. Then, similarly, when the DOM's current focus is the AnotherSpecialList where id === anotherSpecialList1, I want to tab from that instance into the second instance of AnotherSpecialList where id === anotherSpecialList2 and assign the activeListItem of AnotherSpecialList where id === anotherSpecialList2 to listItems[0].
Currently, I've got a listener attached to focusin which is created in mounted, which is bound to focusChanged. I'm fetching the document's activeElement and checking the id against the two values that have been specified, anotherSpecialList1 and anotherSpecialList2. The problem I'm facing with the logic of this.activeListItem = this.listItems[0]; is that it is happening for both instances of AnotherSpecialList. Is there a way where I can select a specific instance of AnotherSpecialList and only execute the logic on the instance that matches the one that is the current focus of the DOM?

how to append multiple query parameters in url - NuxtJS

I am creating a nuxt ecommerce application. I have a situation where I have more than 10,000 items in a category and i want to create related filters for the products.
My question is how do i append url (add & remove query parameters) so that i can filter products.
I have tried something like this by adding a change event to !
<ul>
<li>
<b-form-checkbox #change="filterProduct">
<label class="ui__label_checkbox">Apple</label>
</b-form-checkbox>
</li>
<li >
<b-form-checkbox #change="filterProduct">
<label class="ui__label_checkbox">Mango</label>
</b-form-checkbox>
</li>
</ul>
methods:{
filterProduct() {
this.$router.push({ query: Object.assign({}, this.$route.query, { random: "query" }) });
},
}
This approach does append the url only once but removes the checked state of the checkbox which i don't want
I want similar to below everytime i click checkbox, it must retain the state of the checkbox at the same time append to the url
www.foobar.com/?first=1&second=12&third=5
Here's what you should do. First of all, you should all your filters state in data()
data() {
return {
filter: {
first: this.$route.query.first || null,
second: this.$route.query.second || null,
third: this.$route.query.third || null
}
}
}
Then you set up a watcher that fires when any filter changes, obviusly you need to v-model the inputs in your <template> to the fields in data()
watch() {
filter: {
handler(newFilters) {
const q = complexToQueryString({
...this.filter,
})
const path = `${this.$route.path}?${q}`
this.$router.push(path)
}
}
}
The complexToQueryString function is a thing of mine which removes null values from the query and also works for filters that are arrays. I did this because my API reads null as String 'null'.
const complexToQueryString = (object, parentNode = null) => {
const query = Object.entries(object).map((item) => {
const key = parentNode ? `${parentNode}[${item[0]}]` : item[0]
const value = item[1]
if (Array.isArray(value)) {
return arrayToQueryString(value, key)
} else if (value instanceof Object) {
return complexToQueryString(value, key)
} else if (item[1] !== undefined) {
return [
Array.isArray(item[0]) ? `${key}[]` : key,
encodeURIComponent(item[1]),
].join('=')
}
return ''
})
.filter(empty => empty)
.join('&')
return query
}
Now it should work, if you change the filter value then the data.filter.first changes the value, which fires the watcher, which updates the URL.
The best thing about this aproach is that now you can copy & paste the URL and the filter is exactly the same and returns the same result.
Your approach is almost correct, except that on page request, router-level You should append all the query parameters to route params.
Then asign those params to data inside Your filter page, and mutate them, also updating the query like You're doing now. This way You'll have query updated, and checkboxes wont lose state as they will depend on data, rather than on params.
routes: [{
path: '/path',
component: Component,
props: (route) => ({
filter1: route.query.filter1,
filter2: route.query.filter2,
filter3: route.query.filter3
})
}]

Vue.js - v-model with a predefined text

I have an input attribute that I want to have text from a source and is two-way binded
// messages.html
<input type="textarea" v-model="newMessage">
// messages.js
data () {
newMessage: ''
},
props: {
message: {
type: Object,
required: true,
default () {
return {};
}
}
// the message object has keys of id, text, and hashtag
I would like the initial value of input to be message.text. Would it be appropriate to do something like newMessage: this.message.text?
EDIT
I tried adding :value="message.text" in input but that didn't really show anything
Yes, you can reference the props in the data function.
data(){
return {
newMessage: this.message.text
}
}

Only null or Array instances can be bound to a multi-select

I'm building a multi-step form in Aurelia where each page shows one question.
I use the same view for every question, with if statements determining what type of form field to show.
When I try to bind my question data to a multiple select element however, Aurelia throws errors and says "Only null or Array instances can be bound to a multi-select.".
What's really strange is that if the first question is a multiple select I don't get the error until I come to a non-multiselect question and then go back to the multiselect question.
I can solve this entire problem by setting activationStrategy: 'replace' for this route, but I really don't want that.
The important code follows:
import {inject} from 'aurelia-framework';
import {Router} from 'aurelia-router';
#inject(Router)
export class Form {
constructor (router) {
this.router = router;
this.active = 0;
this.field = null;
this.fields = [
{
type: 'text',
value: null
},
{
type: 'select',
value: [],
options: [
'foo',
'bar'
]
},
{
type: 'select',
value: [],
options: [
'foo',
'bar'
]
},
{
type: 'text',
value: null
},
];
}
activate (routeParams) {
this.active = routeParams.fieldIndex || 0;
this.active = parseInt(this.active);
this.field = this.fields[this.active];
}
prev () {
if (typeof this.fields[this.active - 1] !== 'undefined') {
this.router.navigateToRoute('form', {
fieldIndex: this.active - 1
});
return true;
}
else {
return false;
}
}
next () {
if (typeof this.fields[this.active + 1] !== 'undefined') {
this.router.navigateToRoute('form', {
fieldIndex: this.active + 1
});
return true;
}
else {
return false;
}
}
}
And the template:
<template>
<div class="select" if.bind="field.type == 'select'">
<select value.bind="field.value" multiple="multiple">
<option repeat.for="option of field.options" value.bind="option">${option}</option>
</select>
</div>
<div class="text" if.bind="field.type == 'text'">
<input type="text" value.bind="field.value">
</div>
<a click.delegate="prev()">Previous</a> | <a click.delegate="next()">Next</a>
</template>
But you'll probably want to check out the GistRun: https://gist.run/?id=4d7a0842929dc4086153e29e03afbb7a to get a better understanding.
Try setting the first question to a multiselect and you'll notice the error disappears (until you go back to it). You can also try activationStrategy in app.js like mentioned above.
Why is this happening and how can I solve it?
Also note that in my real app I'm actually using compose instead of ifs but have tried with both and both produce the same error. It almost seems as if the select values are bound before the if is evaluated, causing the error to show up because the text field type lacks the options array.
A little late but I wanted to give a suggestion -- for SELECT multi-selects, you should decouple the bound variable from the multi-selector to prevent those errors.
For example, if you in your custom elements that bind to 'selected', they should bind to:
<select multiple value.two-way="selectedDecoupled">
Then when the actual variable 'selected' changes, it only changes in the custom element if the bound value is an array:
selectedChanged( newV, oldV ){
if( typeof newV =='object' )
this.selectedDecoupled = newV;
else
this.selectedDecoupled = [];
$(this.SELECT).val(this.selectedDecoupled ).trigger('change');
}
Example of it in use with a custom select2 element:
https://github.com/codefreeze8/aurelia-select2
Ok so it turns out swapping the order of the HTML, and putting the select after the input solves this issue.
Jeremy Danyow explains it like this:
When Form.field changes, the bindings subscribing to that property's changes evaluate sequentially. Which means there's a period of time when the select AND the input are both on the page. The html input element coaleses null values to empty string which in turn causes field.value to be empty string, which makes the multi-select throw.
Very tricky to track down imo but I'm glad the Aurelia devs are so helpful over on Github.
Working Gist: https://gist.run/?id=3f88b2c31f27f0f435afe14e89b13d56

How to use domProps in render function?

here is a custom select component, it works, but I just can not understand some part of the code,
jsFiddle
Vue.component("myselect", {
props: ['option'],
render: function (createElement) {
var self = this
var items = []
for (var i = 0; i < 16; i++) {
items.push(createElement('option', { attrs: { value: i } }, i))
}
return createElement('select', {
domProps: { value: self.option.value }, // v-bind:value = this binds the default value
on: {
input: function (event) {
console.log(event.target.value)
}
}
}, items)
}
})
this sets the default value of select to option.value, is it <select value='2'>, but the html select tag uses <option selected>, looks like magic to me.
domProps refers to element properties, not attributes.
Think of it as something like this...
document.getElementById('mySelect').value = 'Two'
<select id="mySelect">
<option>One</option>
<option>Two</option>
<option>Three</option>
<option>Four</option>
</select>
When you set the value property on a select element, it selects the option with the corresponding value (at least in Firefox and Chrome).