问题
Suppose I've got following elements on the same page:
- Filters panel (something similar to http://www.imdb.com/search/name)
- Items based on filter options
I want to implement following logic:
- 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
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?) ?
回答1:
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":
- http://plnkr.co/edit/IMZZTE?p=preview
- http://run.plnkr.co/fUddH83P2orHUsWj/#/
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!
回答2:
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.
来源:https://stackoverflow.com/questions/27345683/whats-the-correct-way-to-update-model-and-url