What's the correct way to update model and URL

2019-09-06 20:48发布

Suppose I've got following elements on the same page:

  1. Filters panel (something similar to http://www.imdb.com/search/name)
  2. Items based on filter options

I want to implement following logic:

  1. URL should contain applied filter data in path (/appliedOptionA/appliedOptionB)
  2. When user opens site app get filter data from URL, updates filter panel and items panel
  3. When user changes filters app updates URL and refreshes items

First idea: configure ng-router, get filter data as param, convert data to model, construct filters panel, load items, construct items panel. Main problem: how should it work when user changes filter? If I'll update URL it will trigger same controller with different param and repeat the process -> get filter data as param, convert data to model, construct filters panel, items and so on. I don't need it - my model and UI is already up to date.

My question:

what is the correct way to keep model and URL synchronized? Should URL manage model (ng-route) or model changes manage URL (how?) ?

2条回答
劫难
2楼-- · 2019-09-06 21:01

My recommendation would be to use angular-ui-router and use that instead of ng-router since angular-ui-router gives you much more flexibility and is, in my opinion, easier to use.

Given your "Main Problem", it sounds like you want to have an single application controller (let's refer to this as the "appCtrl") handle all changes and make the necessary changes to its model. The main drawback with this approach is it forces you to the maintain the "filteredItems" model within the appCtrl, which is not fun and could lead to unnecessary headaches. An easier way to approach it would be to have another controller deal with those changes to the url, rather than the appCtrl itself. That being said, let me show you how you could accomplish this.

*WARNING: The post is very long! (but hopefully helpful) *

Because of this, I have also created a demo of everything I am about to discuss in case you "just want teh codez":

Creating the application

Since we are creating a brand new application, let's start with creating and configuring the application.

NOTE: Remember to load in the angular-ui-router script into your application and add the dependency.

app.js

var myApp = angular.module('myApp', ["ui.router"]);

Now that we have the app, let's add it to the index.html using the ngApp directive.

index.html

<body ng-app="myApp"></body>

With the application in place, let's look at the issues you addressed in your question:

  • URL should contain applied filter data in path (/appliedOptionA/appliedOptionB)
  • When user opens site app get filter data from URL, updates filter panel and items panel
  • When user changes filters app updates URL and refreshes items

What you want to do is create a state. Essentially, a state is activated/deactivated based on the url pattern. Given a particular pattern, you can tell the application how to act (using a controller) and what to look like (using a template). Before continuing, make sure to reference url routing, which will help with url patterns.

We need to:

  • give the state a name ("filteredItems")
  • provide a url pattern
  • provide a controller
  • provide a templateUrl

Let's configure the application, as well as:

  • provide a default url route in the event the url does not exist
  • add a default home state
  • add the filteredItems state

app.js

myApp.config(function($stateProvider, $urlRouterProvider){

// default url - used if url pattern is not recognized
 $urlRouterProvider.otherwise('/'); 

 $stateProvider
    .state('home', { // default state
      url: '/'
    })
    .state('filteredItems', { // give the state a name
        url: "/:appliedOptionA/:appliedOptionB", // provide the url pattern
        controller: 'FilteredItemsCtrl', // controller to use for this state
        templateUrl: "filteredItems.html" // url for the template
    });
});

We have now configured the state's url to take parameters (appliedOptionA and appliedOptionB). Using $stateParams service, we can create a filteredItemsCtrl and have that be responsible for filtering out the data.

But before we do that, we should create a service. When using Angular, AVOID using controllers to maintain data since they can be created/destroyed. Best practice is to use services instead. That being said, let's create an amazing itemService:

itemService.js

angular.module('myApp').factory('itemService', itemService);

function itemService (){
  var items = {
      appliedOptionA: [ 'Item 1', 'Item 2', 'Item 3' ],
      appliedOptionB: [ 'Item 4', 'Item 5', 'Item 6' ]
    };

    function getItems (){
      return items;
    }

    return {
      getItems: getItems
    }
}

Gorgeous right? I am glad you think so too!

Now that we have a service, we can create the FilteredItemsCtrl. Let's also inject our newly created itemService into the FilteredItemsCtrl, so its able to access the data.

NOTE: Remember to inject the $stateParams service as a dependency!

filteredItemsCtrl.js

angular.module('myApp').controller('FilteredItemsCtrl', filteredItemsCtrl);

filteredItemsCtrl.$inject = ['$stateParams', '$scope', 'itemService'];

function filteredItemsCtrl ($stateParams, $scope, itemService){
  var items = itemService.getItems(); // get items from service

    // filter items
    $scope.appliedOptionA = items.appliedOptionA.filter(function (item){
      if ($stateParams.appliedOptionA){
        return item.toLowerCase().indexOf($stateParams.appliedOptionA) > -1 ||
               item.indexOf($stateParams.appliedOptionA) > -1
      }
    });
    $scope.appliedOptionB = items.appliedOptionB.filter(function (item){
      if ($stateParams.appliedOptionB){
        return item.toLowerCase().indexOf($stateParams.appliedOptionB) > -1 ||
               item.indexOf($stateParams.appliedOptionB) > -1
      }
    });
}

When we defined our state's url, we set it up like: url: "/:appliedOptionA/:appliedOptionB". This means the $stateParams will be populated with appliedOptionA, appliedOptionB properties. Also this means every time the URL changes, a new filteredItemsCtrl is created, thus, you do not have to maintain the application's model!

Before we forget, we also need to create a template for this state.

filteredItems.html

<div>
  <div>
     <span>Filtered Option A Items:</span>
    <ul>
      <li ng-repeat="optionItemA in appliedOptionA">
        {{ optionItemA }}
      </li>
    </ul>
  </div>

  <div>
    <span>Filtered Option B Items:</span>
    <ul>
      <li ng-repeat="optionItemB in appliedOptionB">
        {{ optionItemB }}
      </li>
    </ul>
  </div>
</div>

With the state, filteredItemCtrl, and view created, all that is left is to include the ui-view directive, in order for the state's template to properly appear:

index.html

 <body ng-app="myApp"
      ng-controller="MyAppCtrl">
      <div>
        <div>
            Applied Option A: <input ng-model="appliedOptionA" />
          </div>
          <div>
            Applied Option B: <input ng-model="appliedOptionB" />
          </div>

          <button type="button"
                  ng-click="filter()">Search</button>
      </div>

      <!-- where the state template goes -->
      <div ui-view></div>
    </body>

That is all there is to it! Sorry about the long post, but hopefully you found this informative! Please let me know if you have any issues!

查看更多
Animai°情兽
3楼-- · 2019-09-06 21:19

When you want the search keywords to be reflected in the URL (so they can be bookmarked and addressed directly), I would recommend not to force every change of the model (which would be after every keystroke), but wait for a submit of the form. Then you rewrite the URL (via $location.path and/or $location.search) and the ngRoute will kick in.

NgRoute will (re)load/refresh the page as if it were the first time, and that matches the idea of a directly addressable page that includes search keywords. I would not recommend getting the data as soon as you get the submit, because as you stated, ngRoute will refresh the page.

查看更多
登录 后发表回答