Writing callbacks for a wizard? - javascript

I'm building a custom wizard in knockout that dynamically loads knockout components during each "step" of the wizard. I've managed to get all of this working without much hassle, and it seems to work pretty well.
However, I'm wanting to implement some callbacks within the wizard when certain events happen. For example, before and after navigation.
Currently, one of my navigation functions looks like this:
this.goNext = function () {
if (!this.canGoNext()) return;
this.currentStep(this.pages()[this.currentIndex() + 1]);
this.loadPage();
}
I would like to build 2 callback functions called beforePageChange and onPageChange.
My general assumption is that beforePageChange would pass in a couple parameters, notably the current page and the next page. However, I also want it to be able to be observed from any other class utilization the wizard.
For example, on my parent page I would have something like:
this.wizard = Site.loadWizard(arguments);
this.wizard.beforePageChange(function(options) {
if (!options.currentPage.complete) return false;
// do stuff
return true;
});
In turn the wizard would execute its navigation commands and trigger the appropriate callbacks.
I feel like there's something I'm just fundamentally missing here.
Edit
My current version works as follows:
In the wizard:
this.canChangePage = ko.obserable(true);
this.beforePageChange = function (options) {
};
this.beforePageChangeHandler = function (options) {
this.beforePageChange(options);
// do stuff
return true;
};
this.onPageChange = function (options) {
};
this.onPageChangeHandler = function (options) {
this.onPageChange(options);
//do stuff
return true;
}
On the page implementing the wizard:
this.wizard = Site.loadComponent(params, function () {
this.wizard.beforePageChange = function (options) {
this.canChangePage(false);
};
}.bind(this));
I'm not sure if there's a better way to implement this, or if this is the best solution.

The solution Tomalak described in their comment (I think):
Since you already have access to the wizard instance, you can subscribe to its currentStep observable. To get notifications before it changes, you pass a third parameter: "beforeChange". (The second is the this context).
var Wizard = function() {
this.currentStep = ko.observable(0);
}
Wizard.prototype.next = function() {
this.currentStep(this.currentStep() + 1);
}
Wizard.prototype.prev = function() {
this.currentStep(this.currentStep() - 1);
}
var Page = function() {
this.wizard = new Wizard();
this.wizard.currentStep.subscribe(function(oldStep) {
console.log("Page work before step change from step", oldStep);
}, this, "beforeChange");
this.wizard.currentStep.subscribe(function(newStep) {
console.log("Page work after step change to", newStep);
});
}
ko.applyBindings(new Page());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="with:wizard">
<button data-bind="click: prev">back</button>
<span data-bind="text: currentStep"></span>
<button data-bind="click: next">next</button>
</div>

Related

Durandal JS knockout Deferred updates

I am having an issue when enabling deferred updates in the knockout library. I have implemented Jquery datatables as a component, when navigating to a view that has this component i can see the following methods being called in order.
Getview>Activate>Attach
everything works as expected
But if i press f5 and refresh the page rather than navigating to it from another page it breaks and the following methods are called
Getview>Activate>Attach>Getview>Activate>Attach>Detach>Detach (not sure why its called twice in the end)
and it breaks, no table shows on the UI at all as it does not render from what i can tell, i think it has something to do with durandal transitions and there being a difference between navigating to a page and refreshing a page kinda grasping at straws tho.
This is a minimal class that replicates the problem for me, note i dont have an HTML file for this component i want to use the getView method to pass in some dynamic HTML from JQueryDT
I created a quick sample project with the bare minimum needed to replicate the problem.
https://bitbucket.org/dchosking1988/deferred-update-example
If you pull that and run it you will see that the "hello world" will disappear when you refresh the page but it wont if you navigate between tabs.
the general steps i used to replicate the issue are
1) download sample project
2) add test component (see repo above for the sample file)
3) enable deferred updates
4) disable view caching
4) try compose new instance of the component
Edits to give more info
*This is not a JQuery Datatable problem, it is replicated with the following
So you dont have to download the gitRepo, this is the code i can replicate the problem with in the sample project following the above steps.
define([],
function () {
var test = function () {
var self = this;
var defaultViewHtml = '<div> <h1>Hello World</h1></div>';
var currentView = null;
self.getView = function () {
console.log('GetView');
if (!currentView) {
currentView = $(defaultViewHtml)[0];
}
return currentView;
};
self.activate = function (activateOptions) {
console.log('Activate');
};
self.attached = function (view, parent, settings) {
console.log('Attatched');
};
self.detached = function (view, parent) {
console.log('Detatched');
};
};
return test;
});
Then Add this HTML to the index.html, also dont forget to create an instance of the class in the index.js
<div class="whiteRow">
<div class="container">
<div class="row">
<div class="col-md-12">
<div data-bind="compose: { model: test }"></div>
</div>
</div>
</div>
</div>
This is occurred because it call code twice and second called the currentView stay empty in test.js, I commented the stretch where you set the currentView and code work.
self.getView = function () {
console.log('GetView');
//if (!currentView) {
// currentView = $(defaultViewHtml)[0];
//}
return currentView;
};
-
<div class="whiteRow">
<div class="container">
<div class="row">
<div class="col-md-12">
<div data-bind="compose: { model: test }"></div>
</div>
</div>
</div>
</div>
define([],
function () {
var test = function () {
var self = this;
var defaultViewHtml = '<div> <h1>Hello World</h1></div>';
var currentView = null;
self.getView = function () {
console.log('GetView');
return currentView;
};
self.activate = function (activateOptions) {
console.log('Activate');
};
self.attached = function (view, parent, settings) {
console.log('Attatched');
};
self.detached = function (view, parent) {
console.log('Detatched');
};
};
return test;
});
currentView stay empty in test.js,

Knockout/JavaScript Ignore Multiclick

I'm having some problems with users clicking buttons multiple times and I want to suppress/ignore clicks while the first Ajax request does its thing. For example if a user wants add items to their shopping cart, they click the add button. If they click the add button multiple times, it throws a PK violation because its trying to insert duplicate items into a cart.
So there are some possible solutions mentioned here: Prevent a double click on a button with knockout.js
and here: How to prevent a double-click using jQuery?
However, I'm wondering if the approach below is another possible solution. Currently I use a transparent "Saving" div that covers the entire screen to try to prevent click throughs, but still some people manage to get a double click in. I'm assuming because they can click faster than the div can render. To combat this, I'm trying to put a lock on the Ajax call using a global variable.
The Button
<span style="SomeStyles">Add</span>
Knockout executes this script on button click
vmProductsIndex.AddItemToCart = function (item) {
if (!app.ajaxService.inCriticalSection()) {
app.ajaxService.criticalSection(true);
app.ajaxService.ajaxPostJson("#Url.Action("AddItemToCart", "Products")",
ko.mapping.toJSON(item),
function (result) {
ko.mapping.fromJS(result, vmProductsIndex.CartSummary);
item.InCart(true);
item.QuantityOriginal(item.Quantity());
},
function (result) {
$("#error-modal").modal();
},
vmProductsIndex.ModalErrors);
app.ajaxService.criticalSection(false);
}
}
That calls this script
(function (app) {
"use strict";
var criticalSectionInd = false;
app.ajaxService = (function () {
var ajaxPostJson = function (method, jsonIn, callback, errorCallback, errorArray) {
//Add the item to the cart
}
};
var inCriticalSection = function () {
if (criticalSectionInd)
return true;
else
return false;
};
var criticalSection = function (flag) {
criticalSectionInd = flag;
};
// returns the app.ajaxService object with these functions defined
return {
ajaxPostJson: ajaxPostJson,
ajaxGetJson: ajaxGetJson,
setAntiForgeryTokenData: setAntiForgeryTokenData,
inCriticalSection: inCriticalSection,
criticalSection: criticalSection
};
})();
}(app));
The problem is still I can spam click the button and get the primary key violation. I don't know if this approach is just flawed and Knockout isn't quick enough to update the button's visible binding before the first Ajax call finishes or if every time they click the button a new instance of the criticalSectionInd is created and not truely acting as a global variable.
If I'm going about it wrong I'll use the approaches mentioned in the other posts, its just this approach seems simpler to implement without having to refactor all of my buttons to use the jQuery One() feature.
You should set app.ajaxService.criticalSection(false); in the callback methods.
right now you are executing this line of code at the end of your if clause and not inside of the success or error callback, so it gets executed before your ajax call is finished.
vmProductsIndex.AddItemToCart = function (item) {
if (!app.ajaxService.inCriticalSection()) {
app.ajaxService.criticalSection(true);
app.ajaxService.ajaxPostJson("#Url.Action("AddItemToCart", "Products")",
ko.mapping.toJSON(item),
function (result) {
ko.mapping.fromJS(result, vmProductsIndex.CartSummary);
item.InCart(true);
item.QuantityOriginal(item.Quantity());
app.ajaxService.criticalSection(false);
},
function (result) {
$("#error-modal").modal();
app.ajaxService.criticalSection(false);
},
vmProductsIndex.ModalErrors);
}
}
you could use the "disable" binding from knockout to prevent the click binding of the anchor tag to be fired.
here is a little snippet for that. just set a flag to true when your action starts and set it to false again when execution is finished. in the meantime, the disable binding prevents the user from executing the click function.
function viewModel(){
var self = this;
self.disableAnchor = ko.observable(false);
self.randomList = ko.observableArray();
self.loading = ko.observable(false);
self.doWork = function(){
if(self.loading()) return;
self.loading(true);
setTimeout(function(){
self.randomList.push("Item " + (self.randomList().length + 1));
self.loading(false);
}, 1000);
}
}
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
Click me
<br />
<div data-bind="visible: loading">...Loading...</div>
<br />
<div data-bind="foreach: randomList">
<div data-bind="text: $data"></div>
</div>

Ember.js - I want an Action event (on="") to trigger when there is a transition to a new Route

I want an Action event (on="") to trigger when there is a transition to a new Route.
I've seen the list of Action event handlers and closest I could find is attaching the action to the largest HTML element on the page and triggering it with 'Mousemove". This is a terribly flawed away of going about what I want to do.
So just to draw it out.
<div {{action 'displayEitherHtml1or2'}} class="largestDOMelement">
{{#if showHtml1}}
// html 1 inside
{{/if}}
{{#if showHtml2}}
// html 2 inside
{{/if}}
</div>
'/objects' is a list of objects and clicking one leads to 'object/somenumber'. The action should automatically trigger when I enter the 'object/somenumber' page.
UPDATE: I've taken the contents from the previous update and dumped them into my DocRoute, but nothing it being triggered when I transition to 'doc' through {{#link-to 'doc' this.docID}} {{docTitle}}{{/link-to}}
VpcYeoman.DocRoute = Ember.Route.extend(VpcYeoman.Authenticated,{
toggleLetterSwitch: false,
togglePermitSwitch: false,
activate: function () {
var docTemplateID = this.get('docTemplateID');
if ( docTemplateID == 2) {
this.set('toggleLetterSwitch', true);
this.set('togglePermitSwitch', false);
console.log('docTemplateID equals 2');
} else {
this.set('toggleLetterSwitch', false);
this.set('togglePermitSwitch', true);
}
}
});
UPDATE DOS: setDocID is set in the DocsController to 1. Here's the whole thing.
VpcYeoman.DocsController = Ember.ArrayController.extend({
tempDocId: 1,
actions: {
addDoc: function (params) {
var docTitle = this.get('docTitle');
var docTemplateID = 1;
var docTemplateID = this.get('tempDocId');
console.log(this.get('tempDocId'));
var store = this.store;
var current_object = this;
var doc = current_object.store.createRecord('doc', {
docTitle:docTitle,
docTemplateID:docTemplateID
});
doc.save();
return true;
},
setDocId: function (param) {
this.set('tempDocId', param);
console.log(this.get('tempDocId'))
},
}
});
As #fanta commented, it seems like you're looking for the activate hook within your Route. This gets called when you enter the route where you define it. If you want to call it on every transition, you might consider defining a base route for your application and extending that instead of Em.Route:
App.BaseRoute = Em.Route.extend(
activate: function () {
// Do your thing
}
);
App.YourRoutes = App.BaseRoute.extend()
It's possible that there's a more appropriate place/time to do this, but without knowing quite what your action does, this is probably the best guess.
ETA: Looking at your edit, you won't want all your routes to extend App.BaseRoute the way I did it above; you should probably just include that activate hook explicitly in the routes which need it.

Create a generic class to bind knockout object with pages

I am bit new to knockout and jquery mobile, There was a question which is already answered, I need to optimize the PageStateManager class to use generic bindings, currently PageStateManager can only use for one binding,I would really appreciate if someone can guide me to create a generic class to manage page states with knockout bindings Heere is the working code,http://jsfiddle.net/Hpyca/14/
PageStateManager = (function () {
var viewModel = {
selectedHospital: ko.observable()
};
var changePage = function (url, viewModel) {
console.log(">>>>>>>>" + viewModel.id());
$.mobile.changePage(url, {viewModel: viewModel});
};
var initPage = function(page, newViewModel) {
viewModel.selectedHospital(newViewModel);
};
var onPageChange = function (e, info) {
initPage(info.toPage, info.options.viewModel);
};
$(document).bind("pagechange", onPageChange);
ko.applyBindings(viewModel, document.getElementById('detailsView'));
return {
changePage: changePage,
initPage: initPage
};
})();
Html
<div data-role="page" data-theme="a" id="dashBoardPage" data-viewModel="dashBoardViewModel">
<button type="button" data-bind="click: goToList">DashBoard!</button>
</div>
New dashboard model
var dashBoardViewModel = function() {
var self = this;
self.userName = ko.observable('Welcome! ' + "UserName");
self.appOnline = ko.observable(true);
self.goToList = function(){
//I would like to use PageStateManager here
// PageStateManager.changePage($("#firstPage"),viewModel);
ko.applyBindings(viewModel,document.getElementById("firstPage"));//If I click Dashbord button multiple times it throws and multiple bind exception
$.mobile.changePage($("#firstPage"));
}
}
ko.applyBindings(dashBoardViewModel,document.getElementById("dashBoardPage"));
update url : http://jsfiddle.net/Hpyca/14/
Thank you in advance
I would probably go for creating a NavigationService which only handles changing the page and let knockout and my view models handle the state of the pages.
An simple example of such a NavigationService could be:
function NavigationService(){
var self = this;
self.navigateTo = function(pageId){
$.mobile.changePage($('#' + pageId));
};
}
You could then, in your view models just call it when you want it to navigate to a new page. One example would be upon selection of a hospital (which could be done either via a selection function or by manually subscribing to changes to the selectedHospital observable):
self.selectHospital = function(hospital){
self.selectedHospital(hospital);
navigationService.navigateTo('detailsView');
};
Other than the call to the navigationService to navigate, it's just ordinary knockout to keep track of which viewmodel should be bound where. A lot easier than having jquery mobile keeping track of which viewmodel goes where, if you ask me.
I have updated your jsfiddle to show a sample of how this could be done, making as few changes as possible to the HTML code. You can find the updated fiddle at http://jsfiddle.net/Hpyca/15/

Dynamically change the handler of a click binding in knockout.js

I got the following binding working like a charm :
<button class="flatButton buttonPin" data-bind="click:EnterPinMode">Add pin</button>
In my viewmodel I define the Handler like this :
self.EnterPinMode = function(data,event)
{
//Doing several things here
//....
}
Now, let's say I want to change the behavior of that button after the first click on it...how could I do it ? I already managed quite easily to change the button text :
self.EnterPinMode = function(data,event)
{
//Doing several things here
//....
var curButton = $(event.target);
curButton.text("Cancel");
}
But what about changing the button behaviour ? If I had set this handler through jQuery, that wouldn't be an issue, but is there a way to "replace" the click binding on that control so that now it will call ExitPinMode handler, for example.
I've got some doubts on this being possible given the fact that knockout works only with declarative binding (at least without plugin...), but I thought it was worth asking.
Please note that I will actually need some kind of 3 ways toggle, I just simplify it here to a "normal" toggle for the sake of the example.
I think using a hasBeenClicked flag that's private to the view model is fine, and probably the best solution for this.
If you really want to swap out the handler, that should be easy enough, though, with something like this:
function enterPinMode() {
//Doing several things here
//....
var curButton = $(event.target);
curButton.text("Cancel");
//set click handler to a step 2 function
self.pinAction = exitPinMode;
}
function exitPinMode() {
//....
}
self.pinAction = enterPinMode;
Maybe one of the simplest solution is to add a boolean like hasBeenClicked set to false at the begining and then set it to true.
Example:
self.hasBeenClicked = false;
self.EnterPinMode = function(data,event)
{
if (!self.hasBeenClicked )
{
var curButton = $(event.target);
curButton.text("Cancel");
self.hasBeenClicked = true;
}
else
{
//behaviour an a second click
}
}
Hope it helps !
You can try this
var vm = function () {
var self = this;
var nextState = 'firstState';
var states = {
firstState: function () {
nextState = 'secondState';
//Do stuff
},
secondState: function () {
nextState = 'thirdState';
//Do stuff
},
thirdState: function () {
nextState = 'firstState';
//Do stuff
}
}
self.EnterPinMode = function () {
states[nextState].call();
}
}
What you should try to remember first about MVVM is that you are designing an object to represent your view. If your view will have different states, There is nothing wrong with having your viewmodel know about these states and knowing what to do in what state. Stick with MVVM. You wont be disappointed.

Categories

Resources