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!
Related
I have this watcher, where I am changing value of the activeTable - this value is stored inside Pinia store:
const { tables, freeTables, usedTables, activeTable, selectedTable } = storeToRefs(useTableStore())
watch([tables, urlPath], (newValue, oldValue) =>
{
if(urlPath.value[0] === 'tables' && Number.isInteger(+urlPath.value[1]))
{
let tableIndex = tables.value.findIndex(table =>
+table.id === +urlPath.value[1]
)
if(tableIndex > -1)
{
let table = tables.value[tableIndex]
if(+table.userID === +userData.id || +table.waiterID === +userData.id)
{
mineButton.click();
}
else
{
document.getElementById('other-tables').click()
}
activeTable.value = table
}
}
})
In another component I am watching activeTable
const {showTableOrder, activeTable, selectedTable} = storeToRefs(useTableStore())
watch(
() => activeTable.value,
async (newValue, oldValue) => {
console.log(newValue);
console.log(oldValue);
},
{ deep: true }
)
If you refresh the page, the first watcher is working as expected, and activeTable is set properly, but the second watcher is never activated. What am I doing wrong here?
I have a v-data-table on vue, which gets data and dynamically adds and deltes rows based on the incoming object of arrays, Vue is reactive to adding and deleting but doesn't seem to react to array replace.
My function to add, delete and replace is the setup the following way:
function update_helper(update_obj, dataObject, colObject) {
update_obj.Data.forEach((item) => {
if (typeof item.RowData !== 'undefined'){
let temp_list = updateRow(item, colObject);
temp_list.forEach((row_obj) => {
var found = dataObject.find(Element => Element.RowID === row_obj.RowID);
if (typeof found !== 'undefined'){
//Replace
var found = dataObject.findIndex(Element => Element.RowID === item.RowID);
//console.log(row_obj);
//console.log(dataObject[found]);
dataObject[found] = row_obj;
}
else{
// Add
dataObject.push(row_obj);
}
});
}
else if (typeof item.RowData === 'undefined') {
// Delete
var found = dataObject.findIndex(Element => Element.RowID === item.RowID);
dataObject = dataObject.splice(found, 1);
}
});
}
The function keeps track of the row Id . My replace function dataObject[found] = rowObj works but isn't reactive, i.e the change can only be seen when I switch tabs or refresh the page.
How do I workaround this.
Instead of passing it as argument, you could better have it as a data variable like
data() {
return {
dataObject: [],
}
}
and then define your function inside the methods section like
methods: {
update_helper(update_obj, colObject) {
update_obj.Data.forEach((item) => {
if (typeof item.RowData !== 'undefined'){
let temp_list = updateRow(item, colObject);
temp_list.forEach((row_obj) => {
var found = dataObject.findIndex(Element => Element.RowID === row_obj.RowID);
if (found !== -1){
this.dataObject[found] = row_obj;
}
else{
// Add
this.dataObject.push(row_obj);
}
});
}
else if (typeof item.RowData === 'undefined') {
// Delete
var found = this.dataObject.findIndex(Element => Element.RowID === item.RowID);
dataObject = this.dataObject.splice(found, 1);
}
});
}
}
If possible you can declare the colObject also in the data() section
Note: If you observe the above function body, I would have accessed the dataObject using this operator.
I'm having the following initial structure of vuex state (in VueJS project):
state: {
level1prop: null
}
And then I'm changing it dynamically and I'm mutating it into the following structure:
state: {
level1prop: {
level2prop: {
level3prop: {
"customKey1": { /* this is some object 1 */ },
"customKey2": { /* this is some object 2 */ },
...
}
}
}
}
I will be then adding "customKeyN": { /* this is some object N */ } under the level3prop and what it's important for me is on every change to trigger a watcher, which is watching for a changes into the level1prop from the state.
Initially, in my mutation I was doing this update on the following way:
if (!state.hasOwnProperty("level1prop"))
state["level1prop"] = {};
else if (state["level1prop"] === null || state["level1prop"] === undefined)
state["level1prop"] = {};
if (!state["level1prop"].hasOwnProperty("level2prop"))
state["level1prop"]["level2prop"] = {};
else if (state["level1prop"]["level2prop"] === null || state["level1prop"]["level2prop"] === undefined)
state["level1prop"]["level2prop"] = {};
if (!state["level1prop"]["level2prop"].hasOwnProperty("level3prop"))
state["level1prop"]["level2prop"]["level3prop"] = {};
else if (state["level1prop"]["level2prop"]["level3prop"] === null || state["level1prop"]["level2prop"]["level3prop"] === undefined)
state["level1prop"]["level2prop"]["level3prop"] = {};
let payloadObj = { "customKey1": { /* this is some object 1 */ } };
state["level1prop"]["level2prop"]["level3prop"] = payloadObj;
And this is creating the structure on the way I want, but the watcher of changes is not triggered. Following the advises from here i refactor my code to a few different ways, but none of them is triggering the changes. Here is an example for latest option which I tried:
if (!state.hasOwnProperty("level1prop"))
state = Object.assign(state, { "level1prop" : {} });
else if (state["level1prop"] === null || state["level1prop"] === undefined)
state = Object.assign(state, { "level1prop" : {} });
if (!state["level1prop"].hasOwnProperty("level2prop"))
state["level1prop"] = Object.assign(state["level1prop"], { "level2prop" : {} });
else if (state["level1prop"]["level2prop"] === null || state["level1prop"]["level2prop"] === undefined)
state["level1prop"] = Object.assign(state["level1prop"], { "level2prop" : {} });
if (!state["level1prop"]["level2prop"].hasOwnProperty("level3prop"))
state["level1prop"]["level2prop"] = Object.assign(state["level1prop"]["level2prop"], { "level3prop" : {} });
else if (state["level1prop"]["level2prop"]["level3prop"] === null || state["level1prop"]["level2prop"]["level3prop"] === undefined)
state["level1prop"]["level2prop"] = Object.assign(state["level1prop"]["level2prop"], { "level3prop" : {} });
let payloadObj = { "customKey 1": { /* this is some object 1 */ } };
state["level1prop"]["level2prop"]["level3prop"] = Object.assign(state["level1prop"]["level2prop"]["level3prop"], payloadObj);
Again, this is creating the structure which I need, but watcher is still not triggered. A few other options that I tried, but were not triggering the change were:
...
state["level1prop"]["level2prop"]["level3prop"] = Object.assign({}, state["level1prop"]["level2prop"]["level3prop"], payloadObj);
...
and
...
Object.assign(state["level1prop"]["level2prop"]["level3prop"], payloadObj);
...
Is there any way to be able to trigger the watcher for a changes, in such a complex object state with so many nested levels?
As explained in the docs Object Change Detection Caveats section, you should better use the specifically designed Vue setter Vue.set to later add sub-levels to your state.
Then make sure your watcher specifies the deep option, so that it is correctly triggered when your sub-levels change.
const store = new Vuex.Store({
state: {
level1prop: null,
},
});
const state = store.state;
if (!state["level1prop"])
Vue.set(state, "level1prop", {})
if (!state["level1prop"]["level2prop"])
Vue.set(state["level1prop"], "level2prop", {})
if (!state["level1prop"]["level2prop"]["level3prop"])
Vue.set(state["level1prop"]["level2prop"], "level3prop", {})
let payloadObj = {
"customKey1": {
hello: "world",
},
};
state["level1prop"]["level2prop"]["level3prop"] = payloadObj;
setTimeout(() => {
// Change an already existing key.
state["level1prop"]["level2prop"]["level3prop"].customKey1.hello = "too";
}, 1000);
setTimeout(() => {
// To add or remove keys, make sure to use again Vue.set or Vue.delete.
state["level1prop"]["level2prop"]["level3prop"].customKey1.hello = "too";
Vue.set(state["level1prop"]["level2prop"], "level3propSibling", {
hi: "again",
});
}, 2000);
new Vue({
store: store,
watch: {
"$store.state": {
// Make sure you specify the `deep` option
deep: true,
handler() {
console.log(store.state);
},
},
},
});
<script src="https://unpkg.com/vue#2"></script>
<script src="https://unpkg.com/vuex#3"></script>
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
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]);
}
}
}