在Knockoutjs中异步加载页面

6

我希望页面能够立即加载,然后再加载数据以填充select2框。使用Knockout,最终没有错误,但是在我的select2 select框中看不到任何项目。从服务器同步加载可以工作,但非常缓慢(因为获取app_names)。到目前为止,我做了以下工作:

<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="[&#34;cart&#34;, &#34;catalog&#34;, &#34;common-ui&#34;, &#34;content&#34;, &#34;ContentServices&#34;, &#34;cyc&#34;, &#34;deliverFromStore&#34;, &#34;fbr&#34;, &#34;fbt&#34;, &#34;irg&#34;, &#34;localization&#34;, &#34;mylist-domain-service&#34;, &#34;mylist-service&#34;, &#34;mylist-ui&#34;, &#34;nlpplus-service&#34;, &#34;nlpservices&#34;, &#34;orangegraph&#34;, &#34;passbookService&#34;, &#34;pricing&#34;, &#34;promotion&#34;, &#34;recommendations&#34;, &#34;registry&#34;, &#34;relatedsearch&#34;, &#34;review_service&#34;, &#34;sbotd-svcs&#34;, &#34;SearchNavServices&#34;, &#34;shipping&#34;, &#34;SpecialBuy&#34;, &#34;store-search&#34;, &#34;storefinder&#34;, &#34;typeahead2&#34;, &#34;vectorsearch&#34;, &#34;wayfinder&#34;]">

    <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="[&#34;cart&#34;, &#34;catalog&#34;, &#34;common-ui&#34;, &#34;content&#34;, &#34;ContentServices&#34;, &#34;cyc&#34;, &#34;deliverFromStore&#34;, &#34;fbr&#34;, &#34;fbt&#34;, &#34;irg&#34;, &#34;localization&#34;, &#34;mylist-domain-service&#34;, &#34;mylist-service&#34;, &#34;mylist-ui&#34;, &#34;nlpplus-service&#34;, &#34;nlpservices&#34;, &#34;orangegraph&#34;, &#34;passbookService&#34;, &#34;pricing&#34;, &#34;promotion&#34;, &#34;recommendations&#34;, &#34;registry&#34;, &#34;relatedsearch&#34;, &#34;review_service&#34;, &#34;sbotd-svcs&#34;, &#34;SearchNavServices&#34;, &#34;shipping&#34;, &#34;SpecialBuy&#34;, &#34;store-search&#34;, &#34;storefinder&#34;, &#34;typeahead2&#34;, &#34;vectorsearch&#34;, &#34;wayfinder&#34;]">

    <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:\n\n%dups%\n\nPlease 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:\n\n%dups%\n\nPlease 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>

这个页面加载的灵感来自于等待ajax结果以绑定knockout模型

我想要加载html,我会放置一个旋转的gif表示“正在加载”,发起AJAX调用来获取我的app_names,当app_names到达时,我将它们添加到select2框中并初始化select2。

2个回答

2
这可能有助于您理解在AJAX调用后将项目加载到DOM中。
首先初始化您的视图模型(在我的例子中是var PageModel)。
选择列表是observableArrays。将它们初始化为空,然后进行ajax调用。
ajax调用被延迟执行,并且.then()函数有两个参数,一个是xhr成功,一个是xhr失败。
如果ajax成功,则将结果放入observableArray中。
在html中设置一些条件,以防止KO抛出错误,表明此处没有数据,或者您尝试访问的属性不可用。
最后,利用您可以订阅observables并在值更改时执行某些操作的事实,在这种情况下,它从空数组变为具有数据的数组。当您知道它具有数据时,以任何您认为合适的方式初始化您的select2内容。
使用异步调用数据,您可以做各种有趣的事情。您可以使用click绑定运行函数,将数据加载到其他observable中,以检索项目列表或新图像(即第二个示例)。

var PageModel = function(r) {
  var self = this;

  this.Select1 = ko.observableArray([]);
  self.Select1.subscribe(function (val) {
    if (val) {
      // Run function to initialize Select2 box
      // $('#some-select2-thingy').select2 stuff or whatever
      console.log('This select has data now');
    }
  });
  this.Loading1 = ko.observable(false);
  this.Errors = ko.observableArray([]);
  ajaxCall('https://api.punkapi.com/v2/beers', 'GET', self.Select1, self.Loading1, self.Errors);

  this.Image = ko.observable();
  this.Loading2 = ko.observable(false);
  this.LoadImage = function() {
    ajaxCall('https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=american+psycho', 'GET', self.Image, self.Loading2, self.Errors)
  }
  this.ClearImage = function () {
    self.Image(null);
  }
};

window.model = new PageModel();
ko.applyBindings(model);

function ajaxCall(url, method, selectObj, loadingObj, errorObj) {
  return $.when($.ajax({
    url: url,
    method: method,
    beforeSend: function() {
      loadingObj(true);
    }
  })).then(function(response) {
    selectObj(response);
    loadingObj(false);
  }, function(error) {
    errorObj.push(error);
  });
};
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<div class="container-fluid" style="margin-top: 30px;">
  <div class="row" style="margin-bottom: 30px;">
    <div class="col-sm-6 col-sm-push-3">
      <p class="alert alert-info" data-bind="visible: Loading1()">Loading...</p>
      <!-- ko if: Select1().length && !Loading1() -->
      <select id="some-select2-thingy" class="form-control" data-bind="options: Select1, optionsText: 'name', optionsValue: 'id', optionsCaption: '- Select a Beer -'"></select>
      <!-- /ko -->
    </div>
  </div>
  <div class="row">
    <div class="col-sm-6 col-sm-push-3">
      <div class="text-center" style="margin-bottom: 10px;">
        <button class="btn btn-info" data-bind="click: LoadImage, text: Image() ? 'Get A Different Image' : 'Get An Image'">Get An Image</button>
        <button class="btn btn-danger" data-bind="click: ClearImage, visible: Image()">Clear Image</button>
      </div>
      <p class="alert alert-info" data-bind="visible: Loading2()">Loading...</p>
      <!-- ko if: Image() && !Loading2() -->
      <!-- ko with: Image -->
      <div class="text-center">
        <img data-bind="attr: {'src': data.fixed_height_downsampled_url}">
      </div>
      <!-- /ko -->
      <!-- /ko -->
    </div>
  </div>
</div>


Brewdog看起来很酷。是个很好的例子。你们两个都是对的,我不知道该选谁。 - codyc4321

2
这里有一个用于从ajax调用中绑定jquery select2的工作fiddle。 http://jsfiddle.net/LkqTU/33425/ 不确定是否有更好的方法,但我将数据放在自定义绑定的更新部分而不是init中,因为它是通过ajax加载的,可能一开始不存在。
ko.bindingHandlers.select2 = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
         ko.bindingHandlers.value.init(element,valueAccessor, allBindings);
          $(element).select2({
          })
    },
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var data = allBindings.get('select2Data');
         var dataUnwrapped = ko.toJS(data);
          $(element).select2({
               data: dataUnwrapped
          })
       var value = valueAccessor();
       ko.bindingHandlers.value.update(element,valueAccessor);
    }
};

1
作为一个替代方案,您可以将select2控件包装在ko if: createSelect2条件中,其中 createSelect2 期望是返回布尔值的observable。将其默认设置为false,并且一旦从ajax调用返回promise,就将其设置为true。这将在您确保拥有必要的数据之后构建控件,并且仍然能够像通常一样init控件。 - gkb
@gkb 我之前尝试过类似的方法,将select2包装在“.done()”中。但我不知道哪里出了问题,我相信我只是实现得不对,因为我正在学习异步js相关的内容。 - codyc4321
这非常漂亮,似乎适用于任何select2元素,所以我选择它。 - codyc4321

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接