subscribed events in durandaljs are not unsubscribed - durandal

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');

Related

How to access vue component data from socket.io listener function?

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
})

VueJS $set not making new property in array of objects reactive

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.

Why my vue js data member is not getting updated?

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.

Angular5- setInterval whiten the page

I am adding notification to my project. The website should shown a number of notification numbers that the user have got without refreshing page. To do so i have used setInterval function inside ngOnInit but when i used it inside ngOnInit the page goes white and shows nothing but the timer is still running.
Here is how i have implement the code.
ngOnInit
ngOnInit() {
this.subscription = this.loginInfoService.getLoginChangeEmitter()
.subscribe(item => this.loginSuccess(item));
this.subscription = this.loginInfoService.getSiteNameEmitter()
.subscribe(item => this.getSiteName(item));
this.subscription = this.loginInfoService.getSiteDescriptionEmitter()
.subscribe(item => this.getSiteDescription(item));
if (innerWidth < 766) {
this.notificationBell = true;
} else {
this.notificationBell = false;
}
//if i add this line page goes white
setInterval(() => {
this.getAllNotification();
}, 3000);
}
Code to get all notification
getAllNotification() {
this.unSeenNotification = [];
this.notificationService.getAllNotifications().subscribe(
result => {
this.notificationModel = result;
this.showLoading = false;
result.forEach(result => {
if (!result.isSeen) {
this.unSeenNotification.push(result);
}
})
this.notificationCount = this.unSeenNotification.length;
},
error => {
this.alertService.error(error, "Error!");
});
}
You can below window interval:
export class FoobarComponent implements OnInit {
static intervalId: Number;
ngOnInit(): void {
this.startInterval();
}
startInterval() void {
if(FoobarComponent.intervalId) { // always undefined
window.clearInterval(this.intervalId);
}
FoobarComponent.intervalId = window.setInterval(() => {
console.log('Hi'); }, 1000);
}
}

Testing backbone model extend from another model with mocha

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';
}
}
}
});