I want to load a page immediately, then load data to fill in select2 boxes afterward. Using Knockout, I get no errors finally, but see no items in my select2 select
boxes. Loading synchronously from server works, but very slow (because of getting app_names
). I have so far:
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin suite</title>
<!-- Load javascript libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.css" rel="stylesheet">
<!-- best interactive input box -->
<link href="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css" rel="stylesheet" />
<script type="text/javascript" src="//cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
<script type="text/javascript" src="//cdn.jsdelivr.net/bootstrap.daterangepicker/2/daterangepicker.js"></script>
<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/bootstrap.daterangepicker/2/daterangepicker.css" />
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.1/knockout-min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css" rel="stylesheet">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js"></script>
<!-- semantic -->
<!-- <link href="https://cdnjs.com/libraries/semantic-ui" rel="stylesheet"/> -->
<style>
.center {
float: none;
margin-left: auto;
margin-right: auto;
}
#centered {
width: 50%;
margin: 0 auto;
margin-top: 100
}
#middleman-datepicker {
cursor: pointer;
}
.column { float: left; padding: 5px 10px; }
.row { overflow: hidden; }
label {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
}
input[type=radio],
input[type=checkbox] {
-webkit-box-flex: 0;
-webkit-flex: none;
-ms-flex: none;
flex: none;
margin-right: 10px;
}
.btn-primary,
.btn-primary:active,
.btn-primary:visited,
.btn-primary:focus {
background-color: #f49e42;
border-color: #8064A2;
}
.btn-primary:focus {
background-color: #f49542;
}
.btn-primary:hover {
background-color: #f48c42;
}
</style>
<meta id="my-data"
data-app-names="["cart", "catalog", "common-ui", "content", "ContentServices", "cyc", "deliverFromStore", "fbr", "fbt", "irg", "localization", "mylist-domain-service", "mylist-service", "mylist-ui", "nlpplus-service", "nlpservices", "orangegraph", "passbookService", "pricing", "promotion", "recommendations", "registry", "relatedsearch", "review_service", "sbotd-svcs", "SearchNavServices", "shipping", "SpecialBuy", "store-search", "storefinder", "typeahead2", "vectorsearch", "wayfinder"]">
<style>
.deactivate-services-box,
.delete-services-box {
width: 400px;
}
.clear-button {
margin-left: 10px;
}
</style>
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle Navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">SLO admin suite</a>
</div>
<a data-toggle="dropdown" class="dropdown-toggle" href="#">
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
Navigate
<span class="caret"></span>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li class="dropdown-header"><a href="/">Middleman backfill</a></li>
<li class="dropdown-header"><a href="/healthchecks">Healthcheck statuses</a></li>
<li class="dropdown-header"><a href="/delete-services">Delete/deactivate services</a></li>
</ul></li>
</ul>
</div></a>
</div>
</nav>
<script type="text/javascript">
</script>
<div style="padding-top: 90px; float: left;" class="container">
<div >
<div class="">
<!-- https://select2.github.io/examples.html -->
<meta id="my-data"
data-app-names="["cart", "catalog", "common-ui", "content", "ContentServices", "cyc", "deliverFromStore", "fbr", "fbt", "irg", "localization", "mylist-domain-service", "mylist-service", "mylist-ui", "nlpplus-service", "nlpservices", "orangegraph", "passbookService", "pricing", "promotion", "recommendations", "registry", "relatedsearch", "review_service", "sbotd-svcs", "SearchNavServices", "shipping", "SpecialBuy", "store-search", "storefinder", "typeahead2", "vectorsearch", "wayfinder"]">
<style>
.deactivate-services-box,
.delete-services-box {
width: 400px;
}
.clear-button {
margin-left: 10px;
}
</style>
<body>
<div id="centered">
<span data-bind="visible: currently_running_ajax">
<h4>Pretending to run deactivation/deletion for 3 secs...</h4>
<p><img src="/static/img/loader.gif"/></p>
</span>
<div id="ajax-return-error-message" style="position:fixed; top:10%; right:45%; color: red; z-index: 999; display: none;"></div>
<h2>Deactivate services</h2>
<select class="deactivate-services-box" multiple="multiple" data-bind="foreach: app_names">
<option data-bind="value: $data, text: $data"></option>
</select>
<button id="deactivate-clear-all-button" class="clear-button">Clear all</button>
<h2>Permanently delete services</h2>
<select class="delete-services-box" multiple="multiple" data-bind="foreach: app_names">
<option data-bind="value: $data, text: $data"></option>
</select>
<button id="delete-clear-all-button" class="clear-button">Clear all</button>
<br><br>
<p id="empty-set-error-message" style="color: red; display: none;">Please make a selection</p>
<button id="submit-button" data-bind="click: submit_deactivation_and_or_deletion" class="btn-primary btn-lg" style="margin-left: 20px; ">Submit</button>
<button id="submit-button" data-bind="click: submit_fails_demo" class="btn-info btn-lg" style="margin-left: 20px; ">Submit will fail</button>
</div>
<script type="text/javascript">
var app_names = [];
console.log("app names 1");
// knockout
function DeleteServicesViewModel(){
var self = this;
self.app_names = app_names;
console.log("app names 2");
self.currently_running_ajax = ko.observable(false);
// var djangoData = $('#my-data').data();
// self.app_names = djangoData.appNames;
self.find_any_duplicates = function(list_one, list_two){
var duplicates = [];
for (i = 0; i < list_one.length; i++){
var item = list_one[i];
if (_.contains(list_two, item)){
duplicates.push(item);
}
}
return duplicates;
}
self.display_error_message = function(error){
setTimeout(
function() {
$("#ajax-return-error-message").text(error)
$("#ajax-return-error-message").slideDown(500, function(){
setTimeout(function(){
$("#ajax-return-error-message").slideUp(500);
}, 2600);
});
}, 300
);
}
self.submit_deactivation_and_or_deletion = function(){
var deactivate_values = $deactivate_services_box.val();
var deletion_values = $delete_services_box.val();
// alert(deactivate_values);
if (deactivate_values.length == 0 && deletion_values.length == 0){
$("#empty-set-error-message").slideDown(500, function(){
setTimeout(function(){
$("#empty-set-error-message").slideUp(500);
}, 1700);
});
return;
}
var duplicates = self.find_any_duplicates(deactivate_values, deletion_values);
if (duplicates.length){
alert("We cannot both delete and deactivate the same item. You have the following duplicates:nn%dups%nnPlease remove duplicates".replace("%dups%", duplicates));
return;
}
console.log('duplicates: ', duplicates);
self.currently_running_ajax(true);
$.ajax({
url: "/run-deactivation-and-deletion",
method: "POST",
headers: {
"Content-Type": "application/json"
},
data: ko.toJSON(
{ deactivate_list: deactivate_values, deletion_list: deletion_values }
),
success: function(data) {
console.log("worked");
$deactivate_services_box.val(null).trigger("change");
$delete_services_box.val(null).trigger("change");
},
error: function(xhr, textStatus, error) {
console.log("failed");
console.log(error);
self.currently_running_ajax(false);
self.display_error_message(error);
},
complete: function(){
self.currently_running_ajax(false);
}
});
}
// TODO: delete after demo
self.submit_fails_demo = function(){
var deactivate_values = $deactivate_services_box.val();
var deletion_values = $delete_services_box.val();
// alert(deactivate_values);
if (deactivate_values.length == 0 && deletion_values.length == 0){
$("#empty-set-error-message").slideDown(500, function(){
setTimeout(function(){
$("#empty-set-error-message").slideUp(500);
}, 1700);
});
return;
}
var duplicates = self.find_any_duplicates(deactivate_values, deletion_values);
if (duplicates.length){
alert("We cannot both delete and deactivate the same item. You have the following duplicates:nn%dups%nnPlease remove duplicates".replace("%dups%", duplicates));
return;
}
console.log('duplicates: ', duplicates);
self.currently_running_ajax(true);
$.ajax({
url: "/run-deactivation-and-deletion-fails-demo",
method: "POST",
headers: {
"Content-Type": "application/json"
},
data: ko.toJSON(
{ deactivate_list: deactivate_values, deletion_list: deletion_values }
),
// designed to fail for demo
error: function(xhr, textStatus, error) {
console.log("failed");
console.log(error);
self.currently_running_ajax(false);
self.display_error_message(error);
},
complete: function(){
self.currently_running_ajax(false);
}
});
}
}
$.getJSON("/app-names", function(data){
var app_names_json_string = data.app_names;
var app_names = JSON.parse(data.app_names);
console.log("app names 3");
ko.applyBindings(new DeleteServicesViewModel(app_names) );
var $deactivate_services_box = $(".deactivate-services-box");
var $delete_services_box = $(".delete-services-box");
$deactivate_services_box.select2();
$delete_services_box.select2();
$("#deactivate-clear-all-button").on("click", function () { $deactivate_services_box.val(null).trigger("change"); });
$("#delete-clear-all-button").on("click", function () { $delete_services_box.val(null).trigger("change"); });
});
// ko.applyBindings(new DeleteServicesViewModel() );
</script>
</body>
</div><br>
</div>
</div>
</body>
</html>
The inspiration to help the page load at all was found in wait for ajax result to bind knockout model
I want to load html, I’ll do the spinning gif that says “loading”, make an AJAX call to get my app_names
, and when app_names
arrive I add them to select2 boxes and initialize select2.
2
Answers
This might help you wrap your head around loading items into the DOM after an AJAX call.
Initialize your viewmodel (var PageModel in my instance) first.
The select lists are observableArrays. Initialize them empty, do your ajax call.
The ajax call is deferred, and the .then() function has two params, an xhr success and xhr fail.
If ajax is successful place the results into the observableArray.
Setup some conditionals in your html to prevent KO from throwing errors saying that there is no data here, or the properties you are trying to access aren’t available.
Finally piggy-back off the fact that you can subscribe to observables and do something when the value changes, in this case it goes from an empty array to an array with data. When you know it has data, initialize your select2 stuff however you see fit.
You can do all kinds of neat stuff using async calls to data. You could use a click binding to run a function that loads data into some other observable to retrieve a list of items or a new image (IE. the second example).
here is a working fiddle for databinding the jquery select2 from an ajax call.
http://jsfiddle.net/LkqTU/33425/
not sure if there is a better way to do it but I put the data in the update portion of the custom binding instead of the init since it is being loaded by ajax and may not be their at first.