How to use domProps in render function? - vue.js

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).

Related

Changes made to a computed array won't show after a prop binding

I'm quite new to vue and right now I'm trying to figure out how to make changes to a computed array and make an element react to this change. When I click the div element (code section 4), I want the div's background color to change. Below is my failed code.
Code section 1: This is my computed array.
computed: {
arrayMake() {
let used = [];
for (let i = 0; i < 5; i++) {
used.push({index: i, check: true});
}
return used;
Code section 2: This is where I send it as a prop to another component.
<test-block v-for="(obj, index) in arrayMake" v-bind:obj="obj" v-on:act="act(obj)"></card-block>
Code section 3: This is a method in the same component as code section 1 and 2.
methods: {
act(obj){
obj.check = true;
}
Code section 4: Another component that uses the three sections above.
props: ["obj"],
template: /*html*/`
<div v-on:click="$emit('act')">
<div v-bind:style="{ backgroundColor: obj.check? 'red': 'blue' }">
</div>
</div>
Easiest way to achieve this, store the object into another data prop in the child component.
child component
data() => {
newObjectContainer: null
},
onMounted(){
this.newObjectContainer = this.obj
},
methods: {
act(){
// you don't need to take any param. because you are not using it.
newObjectContainer.check = !newObjectContainer.check
}
}
watch: {
obj(val){
// updated if there is any changes
newObjectContainer = val
}
}
And if you really want to update the parent component's computed data. then don't use the computed, use the reactive data prop.
child component:
this time you don't need watcher in the child. you directly emit the object from the method
methods: {
act(){
newObjectContainer.check = !newObjectContainer.check
this.emits("update:modelValue", nextObjectContainer)
}
}
parent component:
data() => {
yourDynamicData: [],
},
onMounted(){
this.yourDynamicData = setAnArray()
},
methods(){
setAnArray(){
let used = [];
for (let i = 0; i < 5; i++) {
used.push({index: i, check: true});
}
return used;
}
}
okay above you created a reactive data property. Now you need the update it if there is a change in the child component,
in the parent first you need object prop so, you can update that.
<test-block v-for="(obj, index) in arrayMake" v-model="updatedObject" :obj="obj"></card-block>
data() => {
yourDynamicData: [],
updatedObject: {}
},
watch:{
updatedObject(val){
const idx = val.index
yourDynamicData[idx] = val
}
}

Prevent Vue Multiple Select to Store an Empty Array

I want this select multiple to pre-select one option, and not be able to deselect all options.
Whenever the last selected option is deselected it should be reselected. In other words when the user tries to deselect the last selected option it should visually not be deselected.
<template>
<b-select
if="Object.keys(doc).length !== 0 /* wait until firebase has loaded */"
:options="computedOptions"
v-model="model"
multiple
#input="onChange"
/>
</template>
<script>
//import Vue from 'vue'
import { fb } from "../fbconf";
export default {
name: "MyMultiSelect",
props: {
doc: Object, // firestore document
},
data() {
return {
options: []
};
},
firestore() {
var options = fb.db.collection("options");
return {
options: options
};
},
computed: {
computedOptions: function() {
return this.options.map(function(option) {
return {
text: option.name,
value: option.id
};
});
},
// to make sure mySelectedOptions is an array, before this.doc is loaded
// I use the following custom model
// because not using 'get' below causes a warning:
// [Vue warn]: <select multiple v-model="localValue"> expects an Array value for its binding, but got Undefined
model: {
get: function() {
if (!this.doc.hasOwnProperty('mySelectedOptions')) return []; // empty array before this.doc is loaded
else return this.doc['mySelectedOptions'];
},
set: function(newValue) {
// here I can prevent the empty array from being stored
// but visually the user can deselect all options, which is bad UX
//if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
}
},
},
methods: {
onChange: function(newValue){
// I can manually store the array as I want here
// but I cannot in any way prevent the user from deselecting all options
if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
else {
// none of these reselects the last selected option
var oldValue = this.doc['mySelectedOptions'];
this.doc['mySelectedOptions'] = this.doc['mySelectedOptions'];
//this.$forceUpdate();
//this.$emit("change", newValue);
//Vue.set(this.doc, 'mySelectedOptions', this.doc['mySelectedOptions']);
}
}
}
};
</script>
You could add watcher and when length becomes 0 just add previous value.
watch: {
model(val, oldVal) {
if(val.length == 0 && oldVal.length > 0) {
// take only one item in case there's clear button or etc.
this.model = [oldval[0]];
}
}
}

how to add a condition like a greater than equal to in filter search in vue js

Hi every one i am going to build a real-estate application
i want to add a condition in Bedrooms filter function like a "greater than equal "
Eg:- if i select 2 on Bedroom list i want to filter greater than equal 2 Bedrooms properties
How can i do this
export default {
data() {
return {
blogs: [],
minbed: this.$route.params.bed,
}
},
created() {
this.$http.get("https://test.json").then(function(data) {
console.log(data);
this.blogs = data.body;
});
},
computed: {
filteredList() {
const { blogs, search, UnitType } = this;
return this.blogs
.filter(blog => blog.Bedrooms.includes(this.minbed))
}
}
<select
v-model="minbed"
id="formInput202"
class="form-control"
value="MaxBedrooms"
>
<option>Max.Bedrooms</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>ST</option>
</select>
includes method is not working in this case. Only accepts string as the param (Ref-Link1).
The inline callback function will sort out this case,
Check the below code.
filteredList() {
const { blogs, search, UnitType } = this;
var MaxBedroomsVal = this.minbed;
return this.blogs.filter( function (blog) {
return blog.Bedrooms >= MaxBedroomsVal;
})
}
Ref: Link2

Vue v-for list not re-rendering after computed data update

I am implementing pagination for a huge list of cards, I display 10 cards at once and wish to show the 10 next (or 10 previous) by clicking on two buttons.
Here's how I do it:
export default {
...
data() {
return {
pois: [], // My list of elements
pageNumber: 0, // Current page number
};
},
props: {
size: {
type: Number,
required: false,
default: 10, // 10 cards per page
},
},
computed: {
pageCount() {
// Counts the number of pages total
const l = this.pois.length;
const s = this.size;
return Math.floor(l / s);
},
paginatedData() {
// Returns the right cards based on the current page
const start = this.pageNumber * this.size;
const end = start + this.size;
return this.pois.slice(start, end);
},
},
methods: {
nextPage() {
this.pageNumber += 1;
},
prevPage() {
this.pageNumber -= 1;
},
}
...
};
And my template:
<div v-for="poi in paginatedData" :key="poi.id">
<card :poi="poi"/>
</div>
Everything should work (and a page change does output the correct cards in the console) but my list is not updated even though the computed method is called on each click.
What is causing this issue? I've read it could be linked to a :key value missing, but it's there, and no data is being updated directly and manually in the array, only sliced out.
First, try this change, just for sure, and let me know in comment it works or not.
export default {
...
computed: {
paginatedData() {
...
const end = start + this.size - 1;
...
},
...
};
And yes: instead of id, try to use index:
<div v-for="(poi, idx) in paginatedData" :key="idx">
<card :poi="poi"/>
</div>

Vue 2 custom select2: why is #change not working while #input is working

I created a custom select2 input element for Vue 2.
My question is: why is
<select2 v-model="vacancy.staff_member_id" #input="update(vacancy)"></select2>
working, but
<select2 v-model="vacancy.staff_member_id" #change="update(vacancy)"></select2>
not?
Since normal <input> elements in Vue have a #change handler, it would be nice if my custom select2 input has the same.
Some information on my custom element:
The purpose of this element is to not render all <option> elements but only those needed, because we have many select2 inputs on one page and many options inside a select2 input, causing page load to become slow.
This solution makes it much faster.
Vue.component('select2', {
props: ['options', 'value', 'placeholder', 'config', 'disabled'],
template: '<select><slot></slot></select>',
data: function() {
return {
newValue: null
}
},
mounted: function () {
var vm = this;
$.fn.select2.amd.require([
'select2/data/array',
'select2/utils'
], function (ArrayData, Utils) {
function CustomData ($element, options) {
CustomData.__super__.constructor.call(this, $element, options);
}
Utils.Extend(CustomData, ArrayData);
CustomData.prototype.query = function (params, callback) {
if (params.term && params.term !== '') {
// search for term
var results;
var termLC = params.term.toLowerCase();
var length = termLC.length;
if (length < 3) {
// if only one or two characters, search for words in string that start with it
// the string starts with the term, or the term is used directly after a space
results = _.filter(vm.options, function(option){
return option.text.substr(0,length).toLowerCase() === termLC ||
_.includes(option.text.toLowerCase(), ' '+termLC.substr(0,2));
});
}
if (length > 2 || results.length < 2) {
// if more than two characters, or the previous search give less then 2 results
// look anywhere in the texts
results = _.filter(vm.options, function(option){
return _.includes(option.text.toLowerCase(), termLC);
});
}
callback({results: results});
} else {
callback({results: vm.options}); // no search input -> return all options to scroll through
}
};
var config = {
// dataAdapter for displaying all options when opening the input
// and for filtering when the user starts typing
dataAdapter: CustomData,
// only the selected value, needed for un-opened display
// we are not using all options because that might become slow if we have many select2 inputs
data:_.filter(vm.options, function(option){return option.id === parseInt(vm.value);}),
placeholder:vm.placeholder
};
for (var attr in vm.config) {
config[attr] = vm.config[attr];
}
if (vm.disabled) {
config.disabled = vm.disabled;
}
if (vm.placeholder && vm.placeholder !== '') {
$(vm.$el).append('<option></option>');
}
$(vm.$el)
// init select2
.select2(config)
.val(vm.value)
.trigger('change')
// prevent dropdown to open when clicking the unselect-cross
.on("select2:unselecting", function (e) {
$(this).val('').trigger('change');
e.preventDefault();
})
// emit event on change.
.on('change', function () {
var newValue = $(this).val();
if (newValue !== null) {
Vue.nextTick(function(){
vm.$emit('input', newValue);
});
}
})
});
},
watch: {
value: function (value, value2) {
if (value === null) return;
var isChanged = false;
if (_.isArray(value)) {
if (value.length !== value2.length) {
isChanged = true;
} else {
for (var i=0; i<value.length; i++) {
if (value[i] !== value2[i]) {
isChanged = true;
}
}
}
} else {
if (value !== value2) {
isChanged = true;
}
}
if (isChanged) {
var selectOptions = $(this.$el).find('option');
var selectOptionsIds = _.map(selectOptions, 'value');
if (! _.includes(selectOptionsIds, value)) {
var missingOption = _.find(this.options, {id: value});
var missingText = _.find(this.options, function(opt){
return opt.id === parseInt(value);
}).text;
$(this.$el).append('<option value='+value+'>'+missingText+'</option>');
}
// update value only if there is a real change
// (without checking isSame, we enter a loop)
$(this.$el).val(value).trigger('change');
}
}
},
destroyed: function () {
$(this.$el).off().select2('destroy')
}
The reason is because you are listening to events on a component <select2> and not an actual DOM node. Events on components will refer to the custom events emitted from within, unless you use the .native modifier.
Custom events are different from native DOM events: they do not bubble up the DOM tree, and cannot be captured unless you use the .native modifier. From the docs:
Note that Vue’s event system is separate from the browser’s EventTarget API. Though they work similarly, $on and $emit are not aliases for addEventListener and dispatchEvent.
If you look into the code you posted, you will see this at the end of it:
Vue.nextTick(function(){
vm.$emit('input', newValue);
});
This code emits a custom event input in the VueJS event namespace, and is not a native DOM event. This event will be captured by v-on:input or #input on your <select2> VueJS component. Conversely, since no change event is emitted using vm.$emit, the binding v-on:change will never be fired and hence the non-action you have observed.
Terry pointed out the reason, but actually you can simply pass your update event to the child component as a prop. Check demo below.
Vue.component('select2', {
template: '<select #change="change"><option value="value1">Value 1</option><option value="value2">Value 2</option></select>',
props: [ 'change' ]
})
new Vue({
el: '#app',
methods: {
onChange() {
console.log('on change');
}
}
});
<script src="https://unpkg.com/vue#2.4.2/dist/vue.min.js"></script>
<div id="app">
<div>
<p>custom select</p>
<select2 :change="onChange"></select2>
</div>
<div>
<p>default select</p>
<select #change="onChange">
<option value="value1">Value 1</option>
<option value="value2">Value 2</option>
</select>
</div>
</div>
fiddle