What is affecting on will Vue computed property re-computed or no? - vue.js

I expect that currentSelectionViewContent will be re-computed each time when selectedOptionsIndexes has changed. Really, sometimes it works, sometimes - not.
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
#Component({
template
})
export class SelectField extends Vue {
private onNewOptionSelected(newOption: SelectField.Option, indexInArray: number): void {
console.log("~~~~~~~~~~~~~~~");
console.log(JSON.stringify(this.selectedOptionsIndexes, null, 2));
this.selectedOptionsIndexes[0] = indexInArray;
console.log(JSON.stringify(this.selectedOptionsIndexes, null, 2));
console.log("--------------");
if (isUndefined(newOption.key)) {
this.$emit("change", newOption.relatedEntity);
} else {
this.$emit("change", newOption.key);
}
}
// Vue computed property in "vue-property-decorator" syntax
private get currentSelectionViewContent(): string {
console.log("Recomputing ...");
switch (this.selectedOptionsIndexes.length) {
case 0:
return SelectField.DEFAULT_NOTHING_SELECTED_PLACEHOLDER;
case 1:
return this.selectOptions[this.selectedOptionsIndexes[0]].title;
default:
return SelectField.DEFAULT_MULTIPLE_OPTIONS_SELECTED_LETTERING;
}
}
}
Working case:
Not working case (no re-computing):
I sorry about was not created the repro for this case (the reproducing of component where this problem causes, it's dependencies and also environment) takes too long time. If you can not understand what wrong here without repro, please just teach me what is affecting on will Vue computed property re-computed or no.

Vue has certain behavior around arrays to be aware of. From the docs:
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue
When you modify the length of the
array, e.g. vm.items.length = newLength
To ensure Vue sees your array change, always make a copy of the array and re-assign it, like this:
var updatedIndexes = [...this.selectedOptionsIndexes]; // Copies array
updatedIndexes[0] = indexInArray; // Update the copy
this.selectedOptionsIndexes = updatedIndexes; // Overwrite with copy

Related

From when is #Watch() active?

I'm trying to understand the #Watch() part of the Stencil lifecycle docs: https://stenciljs.com/docs/component-lifecycle
To me, the illustration above does not clearly show from when #Watch() actually starts watching.
There seem to be cases where a #Watch() hook is triggered even before componentWillLoad(), for example when a Stencil component is used in React with React.createRef(). These cases are not always reproducible - meaning that it could be a race condition.
That's why I'd like to know from what particular point in time #Watch() becomes active?
As stated on stencil change #Prop() detection, #Watch decorator of a property triggers when the property value changes, but not when the property is initially set.
To capture the initialization you have to trigger the handler on componentWillLoad passing the property value.
#Watch('name')
onNameChanged(newValue: string, oldValue: string) {
this._name = newValue;
}
componentWillLoad(){
this.onNameChanged(this.name);
}
As can be seen in the graphic of your linked doc page, a #Watch() decorated method is called every time a change in the value of prop or state you watch occurs. That is independent from the willLoad/willRender/render/didRender/didLoad/didUpdate cycle that occurs when the component is attached to the DOM.
#Component({ tag: 'my-comp' })
export class MyComp {
#Prop() input = 'foo';
#State() repeated: string;
#Watch('input')
onInputChange() {
this.repeated = this.input + this.input;
}
componentWillLoad() {
this.onInputChange(); // can manually call the watcher here
}
render() {
return this.repeated;
}
}
<script>
const myComp = document.createElement('my-comp');
// currently `repeated` would be undefined because the component has not yet rendered
myComp.input = 'foobar'; // now `repeated` *should* be `foobarfoobar`
</script>
(saying *should* because I haven't tested this)

How to reset all states when property value is changed from javascript?

I am using stencil framework. In my component I am using different states to trigger different events. I am also updating the property value of component from javascript.
I would like to reset all states value and reload the component with updated property value.
New property value is responsible for many actions like calling api, generating the cache key etc.
Can anyone suggest me the best approach to fulfill my requirement. Currently I am reset all the states in watcher method of property and call the componentWillLoad event but I am facing many issue in this approach.
Sample code
#Prop() symbol!: string;
#Watch('symbol')
symbolChanged(newSymbol: string, prevSymbol: string) {
if (newSymbol && newSymbol !== prevSymbol) {
this.resetStates();
}
}
resetStates() {
//Reset all state values here
this.componentWillLoad();
}
By setting key property on root element of render method would solve my issue like below code snippet.
uniqKeyId = uniqid.get();
#Prop() symbol!: string;
#Watch('symbol')
sysmbolWatcher(newSymbol: string, prevSysmbol: string) {
if (newSymbol != prevSysmbol) {
//update key attribute each switch of exchange
this.uniqKeyId = uniqid.get();
//Set default values based on properties as to consider this as fresh request.
this.setDefaultValues();
}
}
And in render method like below
render() {
return (
<section class="cid-minichart" key={this.uniqKeyId}>
//Render markup
</section>
);
}

Vue Keys do not delete from Object

I'm trying to delete a key from an object in a parent component. A child component emits an event (with an item value) back to the parent method that triggers the delete in the parent's data object.
Parent component:
data() {
return {
savedNews: Object
}
},
methods: {
containsKey(obj, key) {
var result = Object.keys(obj).includes(key)
return result
},
handleSaveNews(item) {
if (!this.containsKey(this.savedNews, item.url)) {
this.savedNews = {
[item.url]: item,
...this.savedNews
}
} else {
console.log(this.containsKey(this.savedNews, item.url))
var res = delete(this.savedNews, item.url)
console.log(res)
console.log(this.containsKey(this.savedNews, item.url))
}
}
}
All of the console.logs in the last else statement return true. It's saying that the delete was successful yet the key is still there. How do I delete this key?
From the docs:
Vue cannot detect property addition or deletion
Use this.$delete:
this.$delete(this.savedNews, item.url)
or this.$set (which also should be used for property changes):
this.$set(this.savedNews, item.url, undefined);
Extra info: The $ is a naming convention Vue uses for its built-in methods that are available on each component instance. There are some plugins which opt to follow this pattern too. You can also use built-ins inside other modules if you import Vue and use Vue.delete, for example. You could add your own methods like Vue.prototype.$mymethod = ....

Dynamically adding an array of objects to Vue (reactivity problem)

I know that in order for an object or array to be reactive in Vue its properties have to be defined on the root data structure.
What's the best way to add an array of objects to a pre-existing variable defined on the root data structure, and make every property of every element in that array reactive?
I have tried looping through the array and adding each to the root data model, ie:
these_terms.forEach(function(term, idx) {
term.selected = false;
Vue.set(vm.game.set,idx,term);
});
However, Vue does still not respond to the "term.selected" property when it is later changed.
Is there a better way of achieving my aim, or do I need to resort to $forceUpdate? (the manual says that in 99% of cases using $forceUpdate, you're doing something wrong, hence this post)
On your parent component, do the following:
Make a data attribute with a empty array starting out
Make a button that calls a method
In that method, push to the empty array.
Example of step 3
methods: {
_addGroup: function() {
let result = {
id: this.wizardGroups.length + 1,
name: '',
};
this.wizardGroups.push(result);
},
If you need to append additional properties afterwards, you can loop through the array of objects and apply Vue.set() as well
Sorry if I understand it wrong but why dont you import the array and bring it into a Vue Data Variable?
import xx from "xxxx.js"
export default {
data() {
return {
y: xx
}
}
}

VueJS reactive binding to module export

I'm new to Vue and I'm trying to bind a component value to a property of an exported object. The initial value is set correctly but it's not reactive. I'm not sure I'm using the right terminology, but the relevant sections are
// Settings.js
export const settings = { showOverlay: true }
// Overlay.vue
<template>
<div v-show="enabled"> Some stuff </div>
</template>
<script>
import { settings } from "../js/Settings.js";
export default {
data() {
return {
enabled: settings.showOverlay
};
}
};
</script>
Now, I know that the exported object (settings) is a read-only view onto the object, because that's how modules work, so probably Vue can't put its hooks into it. The thing is, I want the setting to be "owned" by this Settings service, which is responsible for persisting the values between page loads, but I don't feel like the service should have to be aware that the component wants to watch a value and take care of manually triggering updates on the component when the value changes -- I probably just misunderstand the pattern I'm supposed to use for cases like this.
This is being built with Webpack / babel, if that makes any difference.
I'm feeling a little bit sheepish at the moment. I went down a little rabbit hole based on some syntax I saw in your question and that let to a whole bunch of unnecessary gyrations. The syntax was this:
data() {
return {
enabled: settings.showOverlay
};
}
Which, for some reason, I interpreted as "well sure, whenever enabled changes, settings.showOverlay is going to change because Vue is reactive".
Yeah, no.
In that code, settings.showOverlay is just the initial value for the enabled property. The enabled property will be reactive, but in no way is it going to pass values to the settings object. Basically the data function returns an object with an enabled property that has an initial value of whatever settings.showOverlay is and then that object is turned into a reactive object.
If you want the changes made in Vue to be passed along to your settings object then all you need to do is expose the settings object on Vue's data object.
data() {
return {
settings,
};
}
Now if you have code like
<div v-show="settings.showOverlay"> Some stuff </div>
<button #click="settings.showOverlay= !settings.showOverlay"></button>
settings.showOverlay will not only be reactive in the Vue, but in the settings object. No need for any of the hoops I jumped through below (/facepalm).
FWIW I believe some of the links I mentioned in the comments are referring to the data object itself. The data object needs to be a plain javascript object, not necessarily all the properties on it.
In other words, in
data() {
return something
}
something must be a plain javascript object.
Original Answer
I've done this in a couple ways in my Vue apps. In my first app I wanted to do the same thing, store the settings in an external module that could manage persisting the settings and expose those settings on my Vue. I ended up writing a class that looks like this.
class Settings {
constructor(){
// read settings from persisted solution
}
get(key){
// return "key" from settings
}
set(key){
// set "key" in settings
}
save(){
// save settings to persisted solution
}
}
export default Settings
And then used that in my Vue like this.
import Settings from "./settings"
new Vue({
data:{
someSetting: Settings.get("someSetting")
}
})
And then some point later, trigger set() and save(). That point for me was whenever a route change was triggered, I'd just set all the settings back to the Settings object and then save.
It sounds like what you have is you're exporting an object that has getter/setter properties possibly something like this.
export const settings = {
overlay: stored.showOverlay,
get showOverlay(){
return this.overlay
},
set showOverlay(v){
this.overlay = v
}
}
Where you maybe trigger a save when set is triggered. I like that idea better than the solution I described above. But getting it to work is a little more work. First I tried using a computed.
new Vue({
computed:{
showOverlay: {
get(){ return settings.showOverlay }
set(v) { settings.showOverlay = v }
}
}
})
But that doesn't quite work because it doesn't reflect changes to the Vue. That makes sense because Vue doesn't really know the value changed. Adding a $forceUpdate to the setter doesn't work either, I expect because of the caching nature of computed values. Using a computed in combination with a data property, however, does work.
new Vue({
data(){
return {
showOverlay_internal: settings.showOverlay
}
},
computed:{
showOverlay: {
get(){ return this.showOverlay_internal }
set(v) {
settings.showOverlay = v
this.showOverlayInternal = v
}
}
}
})
That changes both the state of the Vue and triggers the change in the settings object (which in turn can trigger persisting it).
But, damn, that's a lot of work.
It's important to remember sometimes, though, that the objects we use to instantiate Vue are just plain old javascript objects and we can manipulate them. I wondered if I could write some code that creates the data property and the computed value for us. Taking a cue from Vuex, yes we can.
What I ended up with was this.
import {settings, mapSetting} from "./settings"
const definition = {
name:"app"
}
mapSetting(definition, "showOverlay"
export default definition
mapSetting does all the work we did above for us. showOverlay is now a computed property that reacts to changes in Vue and updates our settings object. The only drawback at the moment is that it exposes a showOverlay_internal data property. I'm not sure how much that matters. It could be improved to map multiple properties at a time.
Here is the complete code I wrote that uses localStorage as a persistence medium.
function saveData(s){
localStorage.setItem("settings", JSON.stringify(s))
}
let stored = JSON.parse(localStorage.getItem("settings"))
if (null == stored) {
stored = {}
}
export const settings = {
overlay: stored.showOverlay,
get showOverlay(){
return this.overlay
},
set showOverlay(v){
this.overlay = v
saveData(this)
}
}
function generateDataFn(definition, setting, internalName){
let originalDataFn = definition.data
return function(){
let data = originalDataFn ? originalDataFn() : {}
data[internalName] = settings[setting]
return data
}
}
function generateComputed(internalName, setting){
return {
get(){
return this[internalName]
},
set(v){
settings[setting] = v
this[internalName] = v
}
}
}
export function mapSetting(definition, setting){
let internalName = `${setting}_internal`
definition.data = generateDataFn(definition, setting, internalName)
if (!definition.computed)
definition.computed = {}
definition.computed[setting] = generateComputed(internalName, setting)
}