Vue js : _this.$emit is not a function - vuejs2

I have created a Vue component call imageUpload and pass property as v-model
<image-upload v-model="form.image"></image-upload>
and within imgeUpload component
I have this code
<input type="file" accept="images/*" class="file-input" #change="upload">
upload:(e)=>{
const files = e.target.files;
if(files && files.length > 0){
console.log(files[0])
this.$emit('input',files[0])
}
}
and I received
Uncaught TypeError: _this.$emit is not a function
Thanks

Do not define your method with a fat arrow. Use:
upload: function(e){
const files = e.target.files;
if(files && files.length > 0){
console.log(files[0])
this.$emit('input',files[0])
}
}
When you define your method with a fat arrow, you capture the lexical scope, which means this will be pointing to the containing scope (often window, or undefined), and not Vue.

This error surfaces if $emit is not on the current context/reference of this, perhaps when you're in the then or catch methods of a promise. In that case, capture a reference to this outside of the promise to then use so the call to $emit is successful.
<script type="text/javascript">
var Actions = Vue.component('action-history-component', {
template: '#action-history-component',
props: ['accrual'],
methods: {
deleteAction: function(accrualActionId) {
var self = this;
axios.post('/graphql',
{
query:
"mutation($accrualId: ID!, $accrualActionId: String!) { deleteAccrualAction(accrualId: $accrualId, accrualActionId: $accrualActionId) { accrualId accrualRate name startingDate lastModified hourlyRate isHeart isArchived minHours maxHours rows { rowId currentAccrual accrualDate hoursUsed actions { actionDate amount note dateCreated } } actions {accrualActionId accrualAction actionDate amount note dateCreated }} }",
variables: {
accrualId: this.accrual.accrualId,
accrualActionId: accrualActionId
}
}).then(function(res) {
if (res.data.errors) {
console.log(res);
alert('errors');
} else {
self.$emit('accrualUpdated', res.data.data.deleteAccrualAction);
}
}).catch(function(err) {
console.log(err);
});
}
}
});

You can write the method in short using upload(e) { instead of upload:(e)=>{ to make this point to the component.
Here is the full example
watch: {
upload(e) {
const files = e.target.files;
if(files && files.length > 0) {
console.log(files[0]);
this.$emit('input',files[0]);
}
}
}

Related

Window object - JSON.stringify replacer

I am attempting to persist a Window object in Local Storage in my Vue project via JSON.stringify with a replacer.
Since Window has a circular structure, I can't use JSON.stringify by itself.
So I've researched the usage of the replacer.
I came across this implementation:
const getCircularReplacer = () => {
console.log("replacer called...");
const seen = new WeakSet();
return (key, value) => {
if (typeof value === window && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
The following replacer implementations didn't work for me....meaning that the getCircularReplacer() method doesn't get called.
const winClean = JSON.stringify(win, getCircularReplacer());
but wrapping the replacer in a function seems to do the trick and it calls the getCircularReplacer method
const winClean = JSON.stringify(win, function () { getCircularReplacer(); });
Another issue is that the winClean returns 'undefined'.
I tested the getCircularReplacer with the following modifications:
if (typeof value === 'object' && value !== null)
and
if (typeof value === window && value !== null) {
and
if (typeof value === Window && value !== null) {
to no avail.
Here's the full code in HellowWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<div>
Google |
Bing |
Yahoo |
BBC
</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
methods: {
openWindow(url, name) {
console.log(url + " | " + name);
const win = window.open(url, name);
console.log("window name: " + win.name);
console.log(win);
const getCircularReplacer = () => {
console.log("replacer called...");
const seen = new WeakSet();
return (key, value) => {
if (typeof value === window && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
const winClean = JSON.stringify(win, function () {
getCircularReplacer();
});
console.log(winClean);
},
},
};
</script>
Could anybody please clarify why my winClean is undefined and how to correctly stringify my window object to avoid the "Converting circular structure to JSON" error.
Thank you!

Using dropzone.js in vue, calling function with image file name

I'm having a hard time getting anything to work with this the way I need it, but I have a working dropzone instance in my Vue project.
I can upload the image and call functions within the dropzone code, however, I need to call a function directly from the form in the html in order to send the 'card' object.
All I need to do is call a function when a file is added through the dropzone form, with the filename.
My code:
<div class="uk-width-3-10">
<form v-on:change="imageChange(card)" method="post" action="{{url('product/parts/upload/store')}}" enctype="multipart/form-data"
class="dropzone" v-bind:id="'dropzone-'+i">
</form>
</div>
...
imageChange(Card){
console.log('working');
},
addCard(){
Vue.nextTick(function () {
new Dropzone("#dropzone-"+cardIndex, {
maxFilesize: 12,
renameFile: function (file) {
var dt = new Date();
var time = dt.getTime();
return time + file.name;
},
acceptedFiles: ".jpeg,.jpg,.png,.gif",
addRemoveLinks: true,
timeout: 50000,
removedfile: function (file) {
console.log(file.upload.filename);
var name = file.upload.filename;
var fileRef;
return (fileRef = file.previewElement) != null ?
fileRef.parentNode.removeChild(file.previewElement) : void 0;
},
init: function() {
this.on("addedfile",
function(file) {
instance.imageZoneNames.push({name: file.upload.filename});
console.log(file);
console.log(instance.imageZoneNames);
});
}
});
});
}
Dropzone has many events, You used removedfile() event! there is another event called addedfile() and executes when a file is added to the dropzone list
imageChange(card) {
console.log(card)
},
addCard() {
Vue.nextTick(() => {
new Dropzone('#dropzone-` + cardIndex, {
addedfile(file) {
this.imageChange(file);
}
}
}
}

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]];
}
}
}

Using conditional class with store getters: not updating class

I have a div with a conditional class that works well when the app is loaded, but it's not updated when the store data change.
The code in my vue component looks like this
<span class="week-day"
v-bind:class="{ complete: isDayComplete(day) }"
v-for="day in daysInWeek"
v-bind:data-day="day"
> </span>
And I have ...mapGetters(['isDayComplete']) in my computed object.
The getter looks like this
isDayComplete(state) {
return (day) => {
console.log(`called isDayComplete(${day})`)
const formattedDay = moment(day, 'DD/MM/YYYY').format('YYYY-MM-DD');
if (state.daysData[formattedDay]) {
if (state.daysData[formattedDay].meals.length > 0) {
console.log(`day ${day} is complete`);
return true;
} else {
console.log(`day ${day} is NOT complete`);
return false;
}
} else {
console.log(`no data for day ${day}`);
return false;
}
}
},
I update my meals data in a mutation
updateMeals(state, meals) {
_.forEach(meals, (meal) => {
state.daysData[meal.day].meals.push(meal);
});
}
And I have an action that commits that mutation
loadMeals({ state, commit }) {
return new Promise((resolve, reject) => {
get.meals.from.api()
.then((response) => {
commit('initDaysData');
commit('updateMeals', response.data.data);
return resolve();
})
.catch(reject);
});
}
So whenever I call loadMeals the class is not updated if one day changes its status (complete/not-complete). If I reload the page, the class is set correctly.
What am I doing wrong? Thanks!
It's a common reactivity problem. You can make deep copy (use JSON.parse(JSON.stringify())) to make data reactive:
updateMeals(state, meals) {
_.forEach(meals, (meal) => {
state.daysData[meal.day].meals.push(meal);
});
state.daysData = JSON.parse(JSON.stringify(state.daysData))
}
#ittus answer was correct. I found another way to achieve this that maybe could
help someone else.
add another mutation on the store
updateCompletedDays(state) {
const newState = [];
_.forEach(state.daysData, (currentDayData, currentDay) => {
if (currentDayData.meals.length > 0) {
newState.push(currentDay);
}
});
state.completedDays = newState;
},
commit this mutation after meals are updated
change isDayComplete getter to
isDayComplete(state) {
const formattedDay = moment(day, 'DD/MM/YYYY').format('YYYY-MM-DD');
return state.completedDays.indexOf(formattedDay) !== -1;
}
Basically when using reactivity going deep into arrays/object will not work, better have arrays of aggregated data (check also Vue.set api)

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