Scenarios for 'can-reflect.setValue - Can not set value'?


#1

We are currently migrating a huge and equally old collection of apps from v3 to v4 (they have a lot of v2 remnants, too). All models are already extensions of DefineMap. But now (CanJS 4.3.0) we see a lot of errors like this:

Error: can-reflect.setValue - Can not set value

This appears for setting fields, which have a custom setter method, but I can’t see any reason for the error. Here is a simple and working example where a field is set in a setter of another field and this can be tried by clicking on a button. You can also set a property in a setter for a parent model or map there and this works, too - and why shouldn’t it.

So my question is: what are possible scenarios for an undefined setter here? I promise that we have field/property definitions for the model in question and otherwise we would rather get an object is not extensible error, right? For which kind of property definitions could it happen that the setter is undefined?


#2

So, what’s odd is that setValue is called when setting a “single value” observable’s value. It’s not called when setting a property.

item is undefined?

Could you share your stack trace?


#3

setValue is undefined in the next line. Here is a stack trace

debug.js:21 Error: can-reflect.setValue - Can not set value.
at Object.setValue (get-set.js:179)
at Constructor.<anonymous> (can-define.js:384)
at Constructor.<anonymous> (can-define.js:437)
at Constructor.setter (can-define.js:495)
at Constructor.set (CAggregationWidgetModel.js:412)

The last line refers to a setter in our model, the shortened version is:

'iAggregationIntervalEnd' : {
    'type'  : ourTypeFunction,  // function, which normalizes null and undefined cases
    'value' : function()
    {
        return this.constructor.iEND_DATE_UNTIL_NOW;
    },

    'set' : function(iAggregationIntervalEnd)
    {
        switch (iAggregationIntervalEnd) {
            case this.constructor.iEND_DATE_UNTIL_NOW :
                this.dtFixedEnd = null; // the mess starts here
                break;
            // and so on and so on
           
           default:
        }
   }
}

The definition of this property, which basically refers to a model of a date picker, then looks like this:

'dtFixedEnd' : {

    'type' : ourTypFunction, // function, which normalizes null and undefined cases

    'get' : function()
    {
        var oDatePickerModelEnd = this.oDatePickerModelEnd;
        var oDate = oDatePickerModelEnd.oDate;
        var bUtc  = oDatePickerModelEnd.bUtc;
        return oDatePickerModelEnd.constructor.dtGetDate(oDate, bUtc);
    },

    'set' : function(dtFixedEnd)
    {
        if (dtFixedEnd !== null) {
            this.dtLastFixedEnd = dtFixedEnd;
        }
        this.oDatePickerModelEnd.oDate = dtFixedEnd;
        return dtFixedEnd;
    }
},

#4

Alright I was able to reproduce the problem in the demo: you need to remove the parameter from the virtual getter in the defintion of MyMap.varA.

Now you can see the error immediately, because I’m trying to set the property in ll 87ff.

I don’t know, whether this behavior is a bug or intended.


#5

I’m taking a look …


#6

So the problem is that the following property definition doesn’t really make sense:

	varA: {
		type: 'string',
		default: undefined,
		set: function(sVarA) {
			if (sVarA === null) {
				this.secondVar = 'varA is NULL';
			}
			return sVarA;
		},
		get: function() {
			return 'get';
		}
	}

As there is a get: function() that doesn’t accept the lastSet argument, the set: function(sVarA) is has no impact.

NOTE: For a similar reason the default: undefined has no impact. Also, as there is a get function, the type has no impact.

So in some ways, trying to set varA to null shouldn’t really work. Throwing an error (a nicer one) might actually be the right thing. Ideally, we should be able to detect a setter like the one above and warn immediately.

The reason why this is breaking is that we are not creating a writable observable for the get because it can not be set.


#7

Here’s a more minimal example: https://jsfiddle.net/javascriptmvc/7058chsy/56/


#8

I created an issue around this: https://github.com/canjs/can-define/issues/367


#9

Thanks for the explanation and the issue on Github.

I’m not sure whether I understand it completely. We are using the explicit getter usually for a combination or transformation of other properties (for example from Date object to timestamp), so as really virtual (ha, ha) getter. So we rarely use lastSetValue because we are creating bindings to other properties.

It now seems like this is not the idea of getters in CanJS, right?


#10

It now seems like this is not the idea of getters in CanJS, right?

Having getters interact with the last set value is totally fine. This isn’t about the “idea of getters in CanJS”. It’s more about providing CanJS incompatible logic. It’s more like calling a function with the wrong arguments.

I’ll try to explain a bit better …

When you do a getter in CanJS, it creates one of several observable types:

  • get: function(){} -> new Observation( ... )
  • get: function(lastSet){} -> new SettableObservable( ... )
  • get: function(lastSet, resolve){} -> new AsyncObservable( ... )

These observables have different abilities. Critically, new Observation( ... ) can not be set to a new value. The following throws the error you are seeing:

var obs  = new Observation(...);

canReflect.setValue(obs, "some new value"); // THROWS!

CanJS SHOULD be building an Observation for a get: function(){} that doesn’t take arguments. This is because there’s no way to interact with a value that might be set.

Essentially, a get: function(){} with no arguments and a set are logically incompatible. The setter can have no real effect.