Skip to content

Instantly share code, notes, and snippets.

@Josh68
Last active November 12, 2015 06:29
Show Gist options
  • Save Josh68/869d83ef4e46c8c49db4 to your computer and use it in GitHub Desktop.
Save Josh68/869d83ef4e46c8c49db4 to your computer and use it in GitHub Desktop.
FAQs viewmodel from myModa mobile application
/*jshint smarttabs:true, maxerr:1000, strict:false*/
/*Sample FAQ code*/
var loadViewModel = function(){
//------------------------------------------------------------------------------
//
// MenuItems : Object
// Data object for available menu items.
//
//------------------------------------------------------------------------------
var MenuItems = function(data) {
var self = this;
var items = data ? data.split(",") : [];
console.log(items.indexOf("account") >= 0);
self.hasAccount = ko.observable(items.indexOf("account") >= 0);
self.hasBenefits = ko.observable(items.indexOf("benefits") >= 0);
self.hasClaims = ko.observable(items.indexOf("claims") >= 0);
self.hasEOBs = ko.observable(items.indexOf("eobs") >= 0);
self.hasFindCare = ko.observable(items.indexOf("findcare") >= 0);
self.hasIDCard = ko.observable(items.indexOf("idcard") >= 0);
self.hasODSWell = ko.observable(items.indexOf("odswell") >= 0);
};
//------------------------------------------------------------------------------
//
// ViewModel : Object
// The main view model controlling this section.
//
//------------------------------------------------------------------------------
var ViewModel = function(){
var self = this;
self.currentUser = null;
self.currentCategory = "";
/**
Observables
**/
self.menuItems = ko.observable(new MenuItems());
/*For SSO to full site*/
self.fullSiteRootUrl = ko.computed(function(){
var host = window.location.host,
firstDelimiter = host.indexOf("."),
fourthLevelDomain = host.substring(0,firstDelimiter),
domainMinusFourthLevel = host.substring(firstDelimiter);
if (fourthLevelDomain === "mobile") {
host = "www" + domainMinusFourthLevel;
}
return window.location.protocol + "//" + host;
}, this);
self.fullSiteLink = ko.computed(function(){
return self.fullSiteRootUrl() + "/" + GLOBALS.fullSiteAppName + "/?site_preference=normal";
});
self.userName = ko.observable("");
self.userPassword = ko.observable("");
// Title in the navigation bar
self.navTitle = ko.observable("Frequently asked questions");
// Title in the back button
self.backTitle = ko.observable("Back");
self.historyIsValid = ko.computed(function(){
var force = self.navTitle();
var validPrevious = getParameterByNameFromHash("previous") && getParameterByNameFromHash("previous") != "undefined";
// var validReferrer = getParameterByNameFromHash("referrer") === document.referrer;
// return validPrevious && validReferrer;
return validPrevious;
}, this);
self.setBackTitle = function(fallback) {
self.backTitle(self.historyIsValid() ? getParameterByNameFromHash("previous") : fallback ? fallback : "Back");
};
// Query string to add to outbound URL
self.historyString = ko.computed({
read: function(){
if(document.referrer && document.referrer !== null && typeof document.referrer !== 'undefined'){
return "?previous=" + encodeURIComponent(self.navTitle()) + "&" + "referrer=" + encodeURIComponent(document.referrer);
} else {
return "?previous=" + encodeURIComponent(self.navTitle());
}
},
deferEvaluation: true,
owner: this
});
/**
FindCare link in footer
**/
self.memberKey = ko.observable("");
self.subscriberKey = ko.observable("");
self.groupKey = ko.observable("");
self.dispType = ko.observable("");
self.findCareLink = ko.computed(function(){
return GLOBALS.domain + GLOBALS.findCarePath + "?memberKey=" + self.memberKey() + "&subscriberKey=" + self.subscriberKey() +
"&groupKey=" + self.groupKey() + "&asOfDate=" + GLOBALS.linkDate + "&dispType=" + self.dispType();
});
/**
Browser detection
**/
self.isMobile = ko.observable(false);
self.detectBrowser = function(){
var ua = navigator.userAgent;
self.checker = {
android: (/Android/).test(ua),
blackberry: (/BlackBerry/).test(ua),
ios: (/(iPhone|iPod)/).test(ua),
iphone: (/iPhone/).test(ua)
};
// If this isn't a phone, don't link phone numbers. //not working
/*if (!self.checker.android || !self.checker.blackberry || !self.checker.iphone) {
$(".phone-number").find("a").each(function(){var me = $(this); me.replaceWith(me.text());});
}*/
// If this isn't a phone, don't link phone numbers.
self.isMobile(self.checker.android || self.checker.blackberry || self.checker.iphone);
// If this is an android, hide the back button
/*
if(self.checker.android){
$('#navbar a').addClass('ui-hidden');
}
*/
};
/**
App Installation
**/
self.isInstalled = function(){
return (("standalone" in window.navigator) && window.navigator.standalone);
};
/** Exception Handling
Three scenarios covered: 401 (session expired), 404 (service missing), and general errors
Params contains:
- kill : throw an error and stop execution
- back : go back to a previous page
- TODO: silent : don't say or do anything (do we need something like this?)
**/
self.handleException = function(jqXHR,params){
// 401 - Special error, our session has just expired
// 401 - Special error, our session has just expired
if(jqXHR.status === 401 || jqXHR.status === 500) { //eating unauthorized as session expired, if this ever occurs
// Take us back to login - does not work with Sammy
window.location.href = GLOBALS.domain + "/" + GLOBALS.mobileAppName;
}
// Ignore an abort, since those can happen often
else if(jqXHR.status != 0) {
if(jqXHR.status === 404){
$("#dialog-header").html("Service unavailable");
$("#dialog-message").html("Unfortunately, this service is currently unavailable. Please try again soon.");
}
else {
$("#dialog-header").html("Error");
try{
var json = JSON.parse(jqXHR.responseText);
$("#dialog-message").html(json.error);
}
catch(e){
$("#dialog-message").html(jqXHR.responseText);
}
}
$("#dialog-action .ui-btn-text").text("OK");
$("#dialog-action").unbind();
$("#dialog-action").bind('mouseup touchup',function(event){
$("#dialog-popup").popup("close");
event.preventDefault();
if(params.back){
window.history.go(-2);
}
});
$("#dialog-popup").popup("open");
if(params.kill){
$.mobile.hidePageLoadingMsg();
throw "Error";
}
}
};
/**
* isOfficeHours : BOOL
* Is Moda Health Custormer Service currently open?
*/
self.isOfficeHours = ko.observable(false);
self.show = function(element, effect, speed){
// $(element).show(effect || "slide", {direction: "right"}, speed || "slow");
var elementClass = element.slice(1) + "-page";
$(".page").hide().parents("body").removeClass(function(i, c){
var classes = c.split(' '),
remove = [];
$.each(classes, function(){
if(this.match(/-page/)){
remove.push(this);
}
});
return remove.join(' ');
});
$("body").addClass(elementClass).scrollTop(0);
if($(element).hasClass("hasnav")){
$("#navbar").show();
}
else {
$("#navbar").hide();
}
$(element).show(0, function(){
var category = element.slice(1);
if ($(element).is("[data-bind='attr:{id:faqType}']")){ //callback to re-show opened FAQ on back
showCollapsible(category); // global function to show a previously shown collapsible, stored in a cookie
}
$(this).trigger('pageshown');
});
};
/**
* isLoggedIn : BOOL
* Is a myModa member currently logged in?
*/
self.loginUpdate = ko.observable();
self.isLoggedIn = ko.computed(function(){
self.loginUpdate(); // triggers reevaluation
if(!self.dataModel){
return false;
}else{
console.log("Logged in = " + (self.dataModel.isExpired() ? false : true));
return self.dataModel.isExpired() ? false : true;
}
}, this);
//--------------------------------------
// FAQs
//--------------------------------------
var Faq = function(question, answer) {
this.question = question;
this.answer = answer;
};
self.faqTitle = ko.observable("");
self.isFromAllFaqs = ko.computed(function(){
var myWatcher = self.navTitle();
if (getParameterByNameFromHash("link") === "fromAll") {
return true;
} else {
return false;
}
}, this);
self.faqType = ko.computed(function(){
return self.faqTitle().toLowerCase();
}, this);
self.faqsList = ko.mapping.fromJS([]);
self.faqCategoryList = ko.mapping.fromJS({
"vision":"Yes",
"medical":"Yes",
"dental":"Yes",
"pharmacy":"Yes",
"customerservice":"Yes"
});
self.hasMedical = ko.computed(function(){
console.log("hasMedical: " + (self.faqCategoryList.medical() === "Yes"));
return (self.faqCategoryList.medical() === "Yes");
}, this);
self.hasDental = ko.computed(function(){
console.log("hasDental: " + (self.faqCategoryList.dental() === "Yes"));
return (self.faqCategoryList.dental() === "Yes");
}, this);
self.hasVision = ko.computed(function(){
console.log("hasVision: " + (self.faqCategoryList.vision() === "Yes"));
return (self.faqCategoryList.vision() === "Yes");
}, this);
self.hasPharmacy = ko.computed(function(){
console.log("hasDental: " + (self.faqCategoryList.pharmacy() === "Yes"));
return (self.faqCategoryList.pharmacy() === "Yes");
}, this);
self.getFaqList = function(callback) {
$.ajax({
type: "GET",
contentType: "application/json",
url: GLOBALS.domain + "/" + GLOBALS.appName + "/" + GLOBALS.servicePathName + "/contactUsLinks",
dataType: "json",
data: "",
success: function(data, textStatus, jqXHR){
ko.mapping.fromJS(data, self.faqCategoryList);
callback();
},
error: function(jqXHR, textStatus, errorThrown){
self.handleException(jqXHR,{kill:true, back:true});
return false;
}
});
};
self.getFaqData = function(serviceSubpath, callback) { //called with the subPath for each type of FAQ (set) and a callback that handles utility tasks (setting titles, showing things, etc)
console.log("AJAX getFaqData");
$.ajax({
type: "GET",
contentType: "application/json",
url: GLOBALS.domain + "/" + GLOBALS.appName + "/" + GLOBALS.servicePathName + "/faq" + serviceSubpath,
dataType: "json",
data: "",
success: function(data, textStatus, jqXHR){
var jsonString = JSON.stringify(data.faq);
var subscriberID = self.dataModel.getField({ table:'current_member', field:'subscriberId', encrypt:true });
var memberKey = self.dataModel.getField({ table:'current_member', field:'memberKey', encrypt:true });
var pathArray = window.location.pathname.split("/");
pathArray = pathArray.slice(0, pathArray.length - 2);
var parentPath = pathArray.join("/");
jsonString = jsonString.replace(/href='..\//g, "href='" + window.location.protocol + "//" + window.location.host + parentPath + "/");
jsonString = jsonString.replace(/%%category%%/g, self.currentCategory);
jsonString = jsonString.replace(/%%memberKey%%/g, memberKey);
jsonString = jsonString.replace(/%%subscriberID%%/g, subscriberID);
jsonString = jsonString.replace(/%%historyString%%/g, self.historyString());
jsonString = jsonString.replace(/%%fullSiteLink%%/g, self.fullSiteLink());
// Fill up the FAQ array
ko.mapping.fromJS(JSON.parse(jsonString), self.faqsList);
callback(); //the utility tasks callback (see bottom of viewModel)
},
error: function(jqXHR, textStatus, errorThrown){
self.handleException(jqXHR,{kill:false, back:true});
return false;
}
});
};
self.prepopulate = function(){
if(!self.dataModel.isExpired()){
$("[name=SubscriberId]").each(function(){
$(this).val(self.dataModel.getField({table:'current_member',field:'subscriberId',encrypt:true}));
});
$("[name=phone_number]").each(function(){
$(this).val(self.dataModel.getField({table:'current_contact',field:'telephone',encrypt:true}));
});
$("[name=eMail_address]").each(function(){
$(this).val(self.dataModel.getField({table:'current_contact',field:'email',encrypt:true}));
});
}
};
self.router = Sammy("body", function(){
this.disable_push_state = true;
this.use('GoogleAnalytics');
this.after(function(){ //trigger a custom event available outside the viewmodel when any route is complete
$(document).trigger('routeComplete');
});
//--------------------------------------
// The around filter.
//--------------------------------------
function navEvent(callback){
/*
* body section class
*/
var firstSlash = self.router.getLocation().slice(1).indexOf('/') + 1,
partialPath = self.router.getLocation().slice(1).substring(firstSlash),
secondSlash = self.router.getLocation().slice(1).substring(firstSlash).indexOf('/'),
firstPathString = partialPath.substring(0, secondSlash);
sectionClass = firstPathString + '-section';
$("body").removeClass(function(i, c){
var classes = c.split(' '),
remove = [];
$.each(classes, function(){
if(this.match(/-section/)){
remove.push(this);
}
});
return remove.join(' ');
});
if(firstPathString !== '#'){
$('body').addClass(sectionClass);
}
var state = this.path.split("#/")[1];
var pageID = '#' + state;
if (state === "") {
$("#home-footer").removeClass("secondary");
} else {
$("#home-footer").addClass("secondary");
}
$("#dialog").dialog("close");
// close flyout menu if it's been left visible
$('#expandable').trigger('close');
/*SSO for myModa full site*/
$('#menu-full-site, .full-site-link').unbind().off();
$('body').on('vmousedown', '#menu-full-site, .full-site-link', function(event){
event.preventDefault();
event.stopImmediatePropagation();
if(self.isLoggedIn()){
GLOBALS.externalLink = false;
self.userName(self.dataModel.getField({table:"current_user",field:"login",encrypt:true}));
self.userPassword(self.dataModel.getField({table:"current_user",field:"password",encrypt:true}));
document.fullSiteForm.submit();
if($('#expandable').is(':visible')){
$('#expandable').trigger('close');
}
} else {
window.location.href = self.fullSiteLink();
}
});
if(String(self.dataModel.isExpired()) === "false"){ //if logged in, ping myModa full site to keep session
var keepFullSiteSessionAlive = (function() { //Thanks, Matt
var now = new Date().getTime();
$('img.session-keeper').remove();
//hit the server and return a 1x1 transparent gif
//append a timestamp query string so each request is unique,
//i.e., so we always hit the server, not a cached gif
$('<img class="ui-hidden session-keeper" src="' + GLOBALS.fullSiteDomain + '/' + GLOBALS.appName + '/images/spacer.gif?time=' +
now + '" height="0" width="0" border="0">').appendTo('#indexPage');
}());
}
// Everything is noauth on this page, so I just trimmed the logic
console.log("Setting state to: " + state);
callback();
}
this.around(navEvent);
//--------------------------------------
// 404
//--------------------------------------
this.notFound = function notFound(verb, path)
{
if (path === "/faq/" || path === "/" + GLOBALS.mobileAppName + "/faq/" || path === "/" + GLOBALS.appName + "/faq/") {
//history.back();
self.router.setLocation("#/" + self.historyString());
}
console.log("Uh oh. Sammy 404: " + verb + " " + path);
return;
};
//--------------------------------------
// Naked URL
//--------------------------------------
this.get("#", function(context){
context.redirect("#/all");
});
this.get("#/", function(context){
var directory = this.path.split("#/")[0].split("/");
directory = directory[directory.length-2];
if (directory === "faq") {
context.redirect("#/all");
} else {
self.router.unload();
window.location.reload(true);
}
});
this.get("#/all", function(){
self.getFaqList(function(){
self.show("#all");
document.title = "FAQ | All FAQs | myModa Mobile";
self.navTitle("Frequently asked questions");
self.setBackTitle("Back");
});
});
this.get("#/category/:name", function(context) { /*BEWARE - if you have path params (eg, :name, here) and that param is anything besides
what you have as a fixed, post-hash subpath ("all," above, is an actual div id), it will drop down into this function and get caught,
IF AND ONLY IF the subpath has a single level (eg, "#/:name"). This will create an infinite loop situation, where you can't get home.
For this reason, make your pages with params have multiple levels (eg, "#/page/:name"), with the first level being a fixed div id.*/
var subpath = this.params['name'],
newSubpath = subpath;
if (subpath === "vision") {
newSubpath = "medical";
}
if (subpath === "mymoda") {
newSubpath = "general";
}
self.currentCategory = subpath === "mymoda" ? "general" : subpath;
switch (subpath) {
case "medical" :
self.faqTitle("Medical");
break;
case "dental" :
self.faqTitle("Dental");
break;
case "pharmacy" :
self.faqTitle("Pharmacy");
break;
case "vision" :
self.faqTitle("Medical");
break;
case "mymoda" :
self.faqTitle("myModa");
break;
case "claims" :
self.faqTitle("Claims");
break;
default :
self.currentCategory = "";
context.redirect("#/");
return;
}
self.getFaqData("/" + newSubpath, function(){ //vision does not return FAQs, instead using what's in "Medical"
$("div[data-role='collapsible-set']").trigger('create'); //Necessary to recreate the jQM accordion after KO magic, also necessary to use a template to insert the collapsibles into the set (see html)
self.show("#" + (newSubpath === "general" ? "mymoda" : newSubpath)); //vision does not return FAQs, instead using what's in "Medical"
document.title = "FAQ | " + toTitleCase(self.faqTitle()) + " FAQs | myModa Mobile";
self.navTitle(self.faqTitle() + " FAQs");
self.setBackTitle("Back");
});
$('#allFaqs').off();
$(document).on('vmouseup', '#allFaqs', function(e){
self.router.setLocation("#/all" + self.historyString());
//window.location = '#/all' + self.historyString();
});
});
});
// Configure JQuery UI objects
self.configureUI = function(){
self.detectBrowser();
self.loginUpdate(true);
};
// Run once initializations
self.init = function(){
try {
self.menuItems(new MenuItems(self.dataModel.getField({table: "current_user", field: "menu_items", encrypt: false})));
} catch (error) {
// Relax.
}
self.router.run("#/");
self.configureUI();
};
// Instantiate database and start app
self.dataModel = DataModel.getInstance();
self.dataModel.initDatabase({success: function(){
self.init();
}});
}; // End ViewModel
ko.applyBindings(new ViewModel());
};
var DataModel = {
getInstance: function(){
return true;
}
initDatabase: function(){
$.get('https://www.modahealth.com/mymoda/rest/contactUs', function(data){
console.log(data.officeHour);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment