Disclaimer: This is my first attempt at building an MVVM app I have also not worked with vue.js before, so it could well be that my issue is a result of a more fundamental problem.
In my view I have two types of blocks with checkboxes:
Type 1: block/checkboxes
Type 2: block/headers/checkboxes
The underlying object is structured like this:
{
"someTopLevelSetting": "someValue",
"blocks": [
{
"name": "someBlockName",
"categryLevel": "false",
"variables": [
{
"name": "someVarName",
"value": "someVarValue",
"selected": false,
"disabled": false
}
]
},
{
"name": "someOtherBlockName",
"categryLevel": "true",
"variables": [
{
"name": "someVarName",
"value": "someVarValue",
"categories": [
{
"name": "SomeCatName",
"value": "someCatValue",
"selected": false,
"disabled": false
}
]
}
]
}
]
}
My objectives
Selecting checkboxes:
User clicks on checkbox, checkbox is selected (selected=true)
A method is fired to check if any other checkboxes need to be disabled (disabled=true). (If this method has indeed disabled anything, it also calls itself again, because other items could be in turn dependent on the disabled item)
Another method updates some other things, like icons etc
Clearing checkboxes
A user can click on a "clear" button, which unchecks all checkboxes in a list (selected=false). This action should also trigger the methods that optionally disables checkboxes and updates icons etc.
My current method (which doesn't seem quite right)
The selected attribute of the data-model is bound to the checked
state of the checkbox element via the v-model directive.
The disabled attribute (from the model) is bound to the element's class and disabled attribute. This state is set by the aforementioned method.
To initialize the methods that disable checkboxes and change some icons, I am using a v-on="change: checkboxChange(this)" directive.
I think I need to do this part differently
The clearList method is called via v-on="click: clearList(this)"
The problems with my current setup is that the change event is not firing when the checkboxes are cleared programatically (i.e. not by user interaction).
What I would like instead
To me the most logical thing to do would be to use this.$watch and keep track of changes in the model, instead of listening for DOM events.
Once there is a change I would then need to identify which exact item changed, and act on that. I have tried to create a $watch function that observes the blocks array. This seems to pick up on the changes fine, but it is returning the full object, as opposed to the individual attribute that has changed. Also this object lacks some convenient helper attributes, like $parent.
I can think of some hacky ways to make the app work (like manually firing change events in my clearList method, etc.) but my use case seems pretty standard, so I expect there is probably a much more elegant way to handle this.
You could use the 'watch' method.. for example if your data is:
data: {
block: {
checkbox: {
active:false
},
someotherprop: {
changeme: 0
}
}
}
You could do something like this:
data: {...},
watch: {
'block.checkbox.active': function() {
// checkbox active state has changed
this.block.someotherprop.changeme = 5;
}
}
If you want to watch the object as a whole with all its properties, and not only just one property, you can do this instead:
data() {
return {
object: {
prop1: "a",
prop2: "b",
}
}
},
watch: {
object: {
handler(newVal, oldVal) {
// do something with the object
},
deep: true,
},
},
notice handler and deep: true
If you only want to watch prop1 you can do:
watch: {
'object.prop1' : function(newVal, oldVal) {
// do something here
}
}
Other solution not mentioned here:
Use the deep option.
watch:{
block: {
handler: function () {console.log("changed") },
deep: true
}
}
Since nobody replied and I have solved/ worked around the issue by now, I thought it migth be useful to post my solution. Please note that I am not sure my solution is how these types of things should be tackled, it works though.
Instead of using this event listener v-on="change: checkboxChange(this)" I am now using a custom directive which listens to both the selected and disabled model attribute, like this: v-on-filter-change="selected, disabled".
The directive looks like this:
directives: {
'on-filter-change': function(newVal, oldVal) {
// When the input elements are first rendered, the on-filter-change directive is called as well,
// but I only want stuff to happen when a user does someting, so I return when there is no valid old value
if (typeof oldVal === 'undefined') {
return false;
}
// Do stuff here
// this.vm is a handy attribute that contains some vue instance information as well as the current object
// this.expression is another useful attribute with which you can assess which event has taken place
}
},
The if clause seems a bit hacky, but I couldn't find another way. At least it all works.
Perhaps this will be useful to someone in the future.
Related
I'm defining my state with an object, initialized with some nested objects to an empty string and an empty array, as such:
state: {
displayedFarmer: {
name: "",
arrivalDates: []
// some more fields...
},
// more vuex stuff
}
I would expect that if I console.log the displayed farmer, arrivalDates would appear. Here's what I did to track it in my component:
computed: {
...mapState([ 'displayedFarmer' ]),
// more code
},
watch: {
displayedFarmer: {
handler() {
console.log("displayedFarmer", this.displayedFarmer);
},
deep: true,
immediate: true
}
}
The first log line appearing shows the displayedFarmer object, with the arrivalDates and name missing:
displayedFarmer
Object { … }
(basically only the prototype and the __ob__ objects appear when I expand it in the console)
That behavior is unclear to me, and has forced me to use a small and harmless hack to initialize the fields the first time they are being accessed.
For this question, what I want to know is:
Why can't I see the objects I initialized in my state when I access them via the component?
How can I do this differently, so that when I first access the object, all the nested items are initialized?
watch with immediate: true fires before components hook:created. This is probably the reason you don't have access to the initialized object.
I don't know what you are trying to achieve, can you expand your question?
The first thing that came to my mind is to try to call method (which does whatever you want) on a hook:mouted. Then call the same method in watcher, but with immediate: false (which is a default btw)
I have a watcher setup on an array and I have deep watch enabled on it, however the handler function does not trigger when the array changes, applications is defined in the object returned in data. Here's the code:
watch: {
applications: {
handler: function(val, oldVal) {
console.log('app changed');
},
deep: true,
},
page(newPage) {
console.log('Newpage', newPage);
},
},
Vue cannot detect some changes to an array such as when you directly set an item within the index:
e.g. arr[indexOfItem] = newValue
Here are some alternative ways to detect changes in an array:
Vue.set(arr, indexOfItem, newValue)
or
arr.splice(indexOfItem, 1, newValue)
You can find better understanding of Array Change Detection here
If you reset your array with arr[ index ] = 'some value', Vue doesn't track to this variable. It would better to use Vue array’s mutation method. These methods used to track array change detection by Vue.
It is worked for me.
Trying to use vue watch methods but it doesn't seem to trigger for some objects even with deep:true.
In my component, I recieve an array as a prop that are the fields to create
the following forms.
I can build the forms and dynamicly bind them to an object called crudModelCreate and everything works fine (i see in vue dev tools and even submiting the form works according to plan)
But I have a problem trying to watch the changes in that dynamic object.
<md-input v-for="(field, rowIndex) in fields" :key="field.id" v-model="crudModelCreate[field.name]" maxlength="250"></md-input>
...
data() {
return {
state: 1, // This gets changed somewhere in the middle and changes fine
crudModelCreate: {},
}
},
...
watch: {
'state': {
handler: function(val, oldVal) {
this.$emit("changedState", this.state);
// this works fine
},
},
'crudModelCreate': {
handler: function(val, oldVal) {
console.log("beep1")
this.$emit("updatedCreate", this.crudModelCreate);
// This doesn't work
},
deep: true,
immediate: true
},
}
From the docs
Due to the limitations of modern JavaScript (and the abandonment of Object.observe), Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.
Please take a look to Reactivity in Depth https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
In certain circumstances it is possible to force a refresh by adding a key property to the child component containing a json string of the object being passed to it in v-model.
<Component v-model="deepObject" :key="JSON.stringify(deepObject)" />
I have a widget model which has a shallow parent-child relationship. A given widget may be a "root" widget and not have any parent, or it may be a child widget which has a parent.
The ember data model looks like this:
export default DS.Model.extend({
name: DS.attr('string'),
parentWidget: DS.belongsTo('widget', { async: true, inverse: null }),
isSubWidget: DS.attr('boolean')
})
I'm trying to add a "displayName" property that will show the name for root widgets, or "parent name - child name" for child widgets
displayName: Ember.computed('name', 'parentWidget.name', 'isSubLob', function() {
if this.get('isSubWidget') {
return "#{this.get('parentWidget.name')} - #{#get('name')}"
}
else {
return "#{this.get('name')}"
}
})
This is not working, however. The child lob's displayName always comes as
undefined - WidgetName
The json is being returned like so:
{
"widgets": [
{
"id": 2,
"name": "Widget Name",
"is_sub_widget": true,
"parent_widget_id": 1
},
...
}
For the record, all the records are being returne by the json at the same time.
I feel like Ember should be asyncronously resolving the parent widget and the string should be updated as well, however it doesn't seem to be working. Any idea what I'm doing wrong here?
I would say you have two issues:
You're not declaring an inverse to your parentWidget relationship, which means that Ember Data is guessing the inverse (and likely guessing wrong). You should change that declaration to look like this, just to be sure:
parentWidget: DS.belongsTo('widget', { async: true, inverse: null }),
I doubt that will fix your issue, but it's good practice.
You're not waiting for your promise to resolve before trying to use the name. You've specified the parentWidget relationship as being asynchronous, which means that #get('parentWidget') will not return a model. It's going to return a promise that will eventually resolve to your model. Normally this would be fine as the computed property would just recompute when the promise resolves, except that you're not watching the proper key.
/* PS: Assuming that your comma was misplaced on this line */
displayName: Ember.computed('name', 'parentWidget', function() {
^^^^^^^^^^^^
As seen, you're only watching the parentWidget property. So if the name property on the parentWidget every updates, you won't be notified. Change that line to this and you should be good to go:
displayName: Ember.computed('name', 'parentWidget.name', function() {
Just keep in mind that the first few times through, parentWidget.name will still be undefined. It won't be the value you want until the promise resolves, which means the computed property could run several times before it does resolve.
I have a collection that I'm listening to all model change events on. In one specific model property, I need to set an additional value on the model, which causes the event to be fired again. I'm trying to pass in custom options so I can ignore the second call. I don't want to use { silent: true } because I need the UI to update still.
The problem is my custom options are making it through. I have found the reason is because the change event is being fired within the change event on the current model, so it just continues it in the backbone code, and my custom options aren't being passed in.
This is what I'm doing. This is rough code for example purposes and not exact.
var View = Backbone.View.extend({
initialize: function(){
this.collection = new Backbone.Collection();
this.listenTo(this.collection, "change", this._modelChange);
},
_modelChange: function(model, options)
{
if(!_.has(model.changed, "IsSelected")){
model.set({ Selected: true }, { modelChanging: true });
}
// Do some other things...
}
});
What's happening here is that I need to subscribe to any change event that happens and do something when it does. There are 10+ properties so I don't want to list them all. If the property that changed isn't IsSelected, the I need to set it to true. This will cause the change event to fire again, and the _modelChange method will be called again. This is where I'm expecting my { modelChanging: true } option to be available, but it isn't. I've found that this is happening because it's setting the value of the model during a change event, so backbone will fire the event itself, and backbone doesn't know about my options that I passed in.
This is one solution I've come up with, but I really don't like it.
setTimeout(function(){
model.set({ Selected: true }, { modelChanging: true });
}, 0);
This will run it asynchronously so it will no longer be a part of the same event. I really don't like this and it feels like a major hack.
How can I accomplish this without a setTimeout?