When I run my tests (I am using Mocha, Chai and Sinon) for a backbone model which extends from another backbone model, I get an error:
TypeError: 'undefined' is not an object (evaluating 'Model._entity')
I thought that by instantiating Model._entity that this error would go away, but it doesn't. Should I be using a stub instead?
I've searched the web for an answer but all the examples are for models that extend directly from Backbone and not another model.
What do I need to do to avoid this error?
My test code:
describe("Model.address", function () {
beforeEach(function () {
// build up a fake server
this.server = sinon.fakeServer.create();
this.server.autoRespond = true;
this.parentModel = new Model._entity();
this.Model_address = new Model.Address();
});
afterEach(function () {
// delete fake server
this.server.restore();
});
describe("Defaults", function () {
// test defaults
it("has default values", function () {
// create empty model
var model = this.Model_address;
// expect model to be truthy
expect(model).to.be.ok;
});
});
});
Model._entity:
Model._entity = Backbone.Model.extend({
initialize: function(options) {
//If options passed with created and/or modified values, set the corresponding model attributes
if (options && options.created && typeof options.created === 'string') {
this.set('created', new Date(options.created));
}
if (options && options.modified && typeof options.modified === 'string') {
this.set('modified', new Date(options.modified));
}
},
defaults: function() {
return {
created: new Date(),
modified: new Date()
};
}
});
Model.Address:
Model.Address = Model._entity.extend({
invalidField: false,
defaults: function () {
var superDefs; //defaults 'inherited' from parent model
superDefs = Model._entity.prototype.defaults.apply(this, arguments);
return _.defaults(superDefs, {
firstLine: '',
secondLine: '',
postalCode: '', //TODO: move down
houseNumber: '', //TODO: move up
city: '',
country: 'uk'
});
},
urlRoot: function () {
return '/checkout/address';
},
validate: function (attributes, options) {
var attr;
for (attr in attributes) {
if (attributes[attr] === '') {
this.invalidField = true;
return 'Blank field';
}
}
}
});
Related
I have this code on my server,
socket.on('join-room', (roomId, passcode, nickname) => {
room = rooms.find((e) => e.id == roomId)
if (!room) {
socket.emit('join-room-error', 'Room not found!',)
return
}
if (room.users.has(nickname)) {
socket.emit('join-room-error', 'Nickname has been used!',)
return
}
if (!room.passcode) {
JoinRoomWithNickname(room, nickname)
return
}
if (room.passcode != passcode) {
socket.emit('join-room-error', 'Wrong passcode!')
return
}
JoinRoomWithNickname(room, nickname)
})
and I want to listen to the error from socket.io. So in the created function I add some listener but it's seem in the listener function I can't access the vue component data
created() {
this.$socket.removeAllListeners()
this.$socket.on('join-room-success', function() {
store.state.roomId = this.id
router.push('/room')
})
this.$socket.on('join-room-error', function(err) {
this.errorLog = err
// I can't access this.errorLog from there
})
},
data: function() {
return {
code: '',
nickname: '',
wasWrongPasscode: false,
isEnteringPasscode: true,
errorLog: null
}
},
You should use arrow functions when you want to pass the global context (this) to the function being invoked.
this.$socket.on('join-room-error', (err) => {
this.errorLog = err
})
In my VueJS 2 component below, I can add the imgdata property to each question in the area.questions array. It works - I can see from the console.log that there are questions where imgdata has a value. But despite using $set it still isn't reactive, and the imgdata isn't there in the view! How can I make this reactive?
var componentOptions = {
props: ['area'],
data: function() {
return {
qIndex: 0,
};
},
mounted: function() {
var that = this;
that.init();
},
methods: {
init: function() {
var that = this;
if (that.area.questions.length > 0) {
that.area.questions.forEach(function(q) {
Util.HTTP('GET', '/api/v1/photos/' + q.id + '/qimage').then(function(response) {
var thisIndex = (that.area.questions.findIndex(entry => entry.id === q.id));
var thisQuestion = (that.area.questions.find(entry => entry.id === q.id));
thisQuestion.imgdata = response.data;
that.$set(that.area.questions, thisIndex, thisQuestion);
})
});
}
console.log("area.questions", that.area.questions);
},
Since area is a prop, you should not be attempting to make changes to it within this component.
The general idea is to emit an event for the parent component to listen to in order to update the data passed in.
For example
export default {
name: "ImageLoader",
props: {
area: Object
},
data: () => ({ qIndex: 0 }), // are you actually using this?
mounted () {
this.init()
},
methods: {
async init () {
const questions = await Promise.all(this.area.questions.map(async q => {
const res = await Util.HTTP("GET", `/api/v1/photos/${encodeURIComponent(q.id)}/qimage`)
return {
...q,
imgdata: res.data
}
}))
this.$emit("loaded", questions)
}
}
}
And in the parent
<image-loader :area="area" #loaded="updateAreaQuestions"/>
export default {
data: () => ({
area: {
questions: [/* questions go here */]
}
}),
methods: {
updateAreaQuestions(questions) {
this.area.questions = questions
}
}
}
Here that variable has a value of this but it's bound under the scope of function. So, you can create reactive property in data as below :
data: function() {
return {
qIndex: 0,
questions: []
};
}
Props can't be reactive so use :
that.$set(this.questions, thisIndex, thisQuestion);
And assign your API output to directly questions using this.questions.
Data part
data () {
return {
containsAd: true
}
},
Method that manipulates the data member containsAd
updated () {
let _this = this
window.googletag.pubads().addEventListener('slotRenderEnded', function (event) {
if (event.slot.getSlotElementId() === 'div-gpt-ad-nativead1') {
_this.containsAd = !event.isEmpty // this is false
console.log('Ad Exists? ', _this.containsAd)
}
})
},
Just to check if the value has changed or not.
check () {
let _this = this
setTimeout(function () {
console.log('Current Value', _this.containsAd)
}, 5000)
}
Resulting Output
I think doing the event listener in the mounted hook will sort your issue.
data() {
return {
containsAd: true
};
},
mounted() {
window.googletag.pubads().addEventListener('slotRenderEnded', event => {
if (event.slot.getSlotElementId() === 'div-gpt-ad-nativead1') {
this.containsAd = ! event.isEmpty // this is false
console.log('Ad Exists?', this.containsAd);
}
});
}
Also using es6 shorthand function will avoid you having to set _this.
I am trying to hold user info with defaultUser as default state after fetching. But If user state changes with UPDATEUSERSTATE, defaultUser also changes. I could not understand that behaivour
Firstly fetching the data from restApi
Updating user state on MainComponent
If User changes textinput on ModalView, updating the user state.
const userReducer = (state = {} ,action) => {
switch (action.type) {
case actionTypes.GETUSERINFOBYUSERNAME_SUCCESS:
return {
...state,
isFetching: action.isFetching,
error: action.error,
user: action.user,
defaultUser:action.user,
open: true
};
case actionTypes.UPDATEUSERSTATE:
return {
...state,
user: action.user
}
default:
console.log("[userReducer]: defalt state");
return state;
}
};
//ACTIONS
export const getUserInfoByUserNameSuccess=(user) => {
return {type: actionTypes.GETUSERINFOBYUSERNAME_SUCCESS, isFetching: true, error: null, user: user}
}
export const updateUserState=(user) => {
return {type: actionTypes.UPDATEUSERSTATE, user:user}
}
//CALLING GETUSERINFO
this.props.onGetUserInfoByUserName(val);
const mapDispatchToProps = dispatch => {
return{
onGetUserInfoByUserName : userName => dispatch(getUserInfoByUserNameFetching(userName))
};
};
//AND CALLING UPDATEUSERSTATE
textChangedHandler = (key,value) => {
let user = this.props.user;
user[key]=value;
this.props.onUpdateUserState(user);
}
const mapDispatchToProps = dispatch => {
return{
onUpdateUserState : (user) => dispatch(updateUserState(user))
};
};
The problem here is that you're directly setting the values of two separate objects to reference the same object (both user and defaultUser are being set to reference object action.user). So if you change the value of one of the objects, the reference changes which changes the second object.
Redux doesn't replace the object with a new one, but rather does a shallow copy of the new values. See the below snippet for an example:
var actionUser = { foo: 1 }
var defaultUser = actionUser
var user = actionUser
user.bar = 2
console.log(actionUser)
// { foo: 1, bar: 2 }
console.log(defaultUser)
// { foo: 1, bar: 2 }
console.log(user)
// { foo: 1, bar: 2 }
To fix this you can use the Object.assign() method to assign the references to a new object. See below snippet:
var actionUser = { foo: 1 }
var defaultUser = Object.assign({}, actionUser)
var user = Object.assign({}, actionUser)
user.bar = 2
console.log(actionUser)
// { foo: 1 }
console.log(defaultUser)
// { foo: 1 }
console.log(user)
// { foo: 1, bar: 2 }
So whenever you assign a new value from your actions to any of your state objects, use Object.assign().
Example (from your code):
case actionTypes.GETUSERINFOBYUSERNAME_SUCCESS:
return {
...state,
user: Object.assign({}, action.user),
defaultUser: Object.assign({}, action.user),
};
case actionTypes.UPDATEUSERSTATE:
return {
...state,
user: Object.assign({}, action.user),
}
This is the flow of the application:
The shell.js loads the schoolyeardialog.js which contains the schoolyearbrowser.js which loads the schoolyearwizard.js via create or edit button.
When I repeat these steps multiple times: click create/edit button then I do multiple
requests in my SchoolyearWizard to this:
$.when(service.editSchoolyear(schoolyearId))
The reason is that the subscribed events are not correctly unsubscribed in my opinion.
I have tried different ways of unsubscribing in the SchoolyearDialog.js file.
Either the events did not fire - when I did the subscription.off(); after the app.on(...)
or It was unsubscribed at the wrong position.
Where should I unsubscribe correctly?
If you guys need a sample repo as visual studio solution I can provide this if it helps or maybe you see clearly and immediately the error?!
I have also thought about unsubscribing from the 2 events create/edit when the SchoolyearDialog module is "unloaded" because then both events could/would be unsubscribed not only the create OR edit subscription as it is now when I either click the add or edit button... how would I do that?
SHELL
define(['plugins/router', 'durandal/app', 'viewmodels/SchoolyearDialog', 'knockout'], function (router, app, schoolyearDialog, ko) {
self.schoolyearIsLoaded = ko.observable(false);
var saveTimeTableSubscription = app.on('savedTimeTable').then(function (options) {
// after coming the 2nd time here
if (!self.schoolyearIsLoaded()) {
router.map([{ route: 'lessonplanner', moduleId: 'viewmodels/lessonplanner', title: 'lesson planner', nav: true },
{ route: 'documentbrowser', moduleId: 'viewmodels/documentbrowser', title: 'document browser', nav: true }])
.buildNavigationModel();
self.schoolyearIsLoaded(true);
}
router.navigate("lessonplanner", true);
});
return {
router: router,
activate: function () {
router.map([{ route: '', moduleId: 'viewmodels/SchoolyearDialog', nav: true, title: 'Schoolyearbrowser' }
]).buildNavigationModel();
return router.activate('SchoolyearDialog');
}
};
});
SchoolyearDialog
define(['durandal/app', 'knockout', 'plugins/router', 'viewmodels/SchoolyearWizard'],
function (app, ko, router, wizard) {
var ctor = function () {
debugger;
var self = this;
self.createSubscribe = ko.observable();
self.editSubscribe = ko.observable();
self.activeScreen = ko.observable('viewmodels/SchoolyearBrowser'); // set the schoolyear browser as default module
var createWizardSubscription = app.on('createWizard').then(function () {
self.createSubscribe().off();
self.createSubscribe(null);
self.activeScreen(new wizard('create'));
}, self);
self.createSubscribe(createWizardSubscription);
var editWizardSubscription = app.on('editWizard').then(function (schoolyearId) {
self.editSubscribe().off();
self.editSubscribe(null);
self.activeScreen(new wizard('edit', schoolyearId));
}, self);
self.editSubscribe(editWizardSubscription);
}
return ctor;
});
SchoolyearBrowser
define(['durandal/app', 'plugins/dialog', 'knockout', 'services/dataservice', 'plugins/router'],
function (app, dialog, ko, dataservice, router) {
var SchoolyearBrowser = function () {
var self = this;
self.schoolyears = ko.observableArray();
$.when(dataservice.getSchoolyears())
.done(function (schoolyearModels) {
self.schoolyears(schoolyearModels);
});
self.create = function () {
app.trigger('createWizard');
}
self.edit = function () {
app.trigger('editWizard', 1);
}
};
return SchoolyearBrowser;
});
SchoolyearWizard
define(['durandal/activator', 'viewmodels/step1', 'viewmodels/step2', 'knockout', 'durandal/app', 'services/dataservice', 'viewmodels/CreateEditSchoolyearViewModel'],
function (activator, Step1, Step2, ko, app, service, CreateEditSchoolyearViewModel) {
var ctor = function (viewMode, schoolyearId) {
debugger;
// depending on the mode I could setup 2 different step modules for create and edit ? and the Wizard has one property called content
if (viewMode === 'edit') {
$.when(service.editSchoolyear(schoolyearId))
.done(function (response) {
debugger;
self.viewModel(new CreateEditSchoolyearViewModel(response));
}).fail(function (error) {
alert(error);
});
}
else if (viewMode === 'create') {
$.when(service.createSchoolyear())
.done(function (response) {
debugger;
self.viewModel(new CreateEditSchoolyearViewModel(response));
}).fail(function (error) {
alert(error);
});
}
var self = this;
var steps = [new Step1(viewMode), new Step2(viewMode)];
var step = ko.observable(0); // Start with first step
self.activeStep = activator.create();
var stepsLength = steps.length;
self.viewModel = ko.observable();
this.hasPrevious = ko.computed(function () {
return step() > 0;
});
self.caption = ko.observable();
this.activeStep(steps[step()]);
this.hasNext = ko.computed(function () {
if ((step() === stepsLength - 1) && self.activeStep().isValid()) {
// save
self.caption('save');
return true;
} else if ((step() < stepsLength - 1) && self.activeStep().isValid()) {
self.caption('next');
return true;
}
});
this.isLastStep = function () {
return step() === stepsLength - 1;
}
this.next = function () {
if (this.isLastStep()) {
var vm = this.activeStep(); //.viewModel;
$.when(service.saveCreateSchoolyear({ schoolyearId: 1 })).done(function () {
app.trigger('savedTimeTable', { isSuccess: true });
}).fail(function (e) {
alert(e);
});
}
else if (step() < stepsLength) {
step(step() + 1);
self.activeStep(steps[step()]);
}
}
this.previous = function () {
if (step() > 0) {
step(step() - 1);
self.activeStep(steps[step()]);
}
}
}
return ctor;
});
This helped me greatly:
activator.deactivate function allows the previous object to execute custom deactivation logic."
SchoolyearDialog.js
self.deactivate = function () {
self.createSubscribe().off();
self.editSubscribe().off();
}
When the schoolyearDialog is deactivated both events are unsubscribed independing wether button create/edit is clicked. This is for me a clean solution :)
I agree with your solution but I'd recommend not to use plain .off() without parameters as this will cause to deregister all the events in the application.
Rather pass the event name as a parameter to your off method:
self.createSubscribe().off('savedTimeTable');