I have a list of employee list on EmployeesComponent and there is "Education Overview" and "Salary Overview" buttons for each records. When I click one of the overview button it goes to the OverviewComponent first and then load the correponding component (salary or education) into this OverviewComponent. There is also a "Back" button on each of these salary and education components. The structure is as shown on the following image:
components
The problem is that: When I come back to the EmployeesComponent, I need to reload the paging params e.g. the last page number before navigating to the overview pages. For this I use localStorage and check the saved value on each page load of the EmployeesComponent.
searchParams: any;
ngOnInit() {
let searchParams = JSON.parse(localStorage.getItem('routeParams'))?.searchParameters;
if(searchParams){
this.searchParams = searchParams;
window.localStorage.removeItem('routeParams'); // remove routeParams from localStorage
// load list using this.searchParams
}
But I save the page params on the OverviewComponent so that use a single place for salary and education pages. I think it is not a good approach and it may cause the localStorage items to be mixed as they use the same key (for some reason I need to use the same key sometimes).
So, should I set the paging parameters just before navigating to the overview page in the EmployeesComponent? And then check them on loading EmployeesComponent? What is a proper way for this scenario?
You can use the query-params in routing.
So now when you redirect from employess component to overViewComponent, then based on click i.e., Education Overview or Salary Overview just send the query params with the url.
Then now when you get back to employess component, just use the query params value you get in overView component and you can get the information you want back in employess component.
Q- what is the most proper place for adding and removing paging items to local storage
A- Most proper place for adding and removing localstorage item is where it get's change.
In your case, just set localstorage in overView component where you are getting params ( this.activateRoute.params() ) inside this function. And remove the localstorage on ngOnInit function of employee component.
Related
I have a Link where I want to pass certain params in the URL but I don't want the browser to display the params.
I'm using Link's as for this:
<Link href={`/link?foo=bar`} as ={`/link`}>
<a>Link</a>
</Link>
But when I click this link and I try to access the params via router, I can't access foo=bar:
const router = useRouter()
console.log(router.query)
Returns
{
slug: ["link"],
}
And not
{
slug: ["link"],
foo: "bar",
}
So how can I access the URL params in href when using as for Link?
TL;DR You can't use as like that.
This is an incorrect usage of href and as. It would be cool if we could hide state from the end users to keep our URLs nice, clean, and compact, but obviously if you do that, you'll actually lose the state when copy/pasting the URL. That's why you can't hide query parameters in anyway (except for excluding them).
Here's the docs on href and as (dynamic routes, has little to do with hiding query params):
https://nextjs.org/docs/tag/v9.5.2/api-reference/next/link#dynamic-routes
And to further bring up my point, imagine if we could hide state, and we redirect to this URL:
https://example.com/stateful/
Presumably there would be some behind-the-scenes browser action that persists the state.
Now we copy/paste the URL:
https://example.com/stateful/
Oops! We don't have the state anymore because the browser has no previous state to keep track of! That's why you use query parameters, because they keep the state in the URL itself.
In my angular app, I need a component to pass data to another component which do not have parent child relationship.I have an Orders table in one component with each row representing an order. When user clicks on any specific row, I need navigation to OrderDetails component& pass the order object representing the clicked row along with it
validation.component.html
<tr *ngFor="let order of allOrders" (click)="onNavToOrderDetails(order)">
<td>{{order.id}}</td>
</tr>
validation.component.ts
onNavToOrderDetails(order) {
this.router.navigate(['orderdetails'], { state: {data: order} });
}
orderdetails.component.ts
order;
ngOnInit(): void {
this.order=history.state.data;
console.log(this.order);
}
orderdetails.component.html
<p>{{order.id}}</p>
orderdetails.component.html displays order id when navigated from validation.component.html but refreshing the orderdetails page cause order id to disappear. I understand on the page refresh history.state.data becomes undefined but how to get around this issue? Since the app is a SPA, storing the data from the validationcomponent to a service and using that service in the orderdetailscomponent won't work either.
Page refresh means reloading the entire angular app ,and order object stored in the serivce by the validation componentwill also disappear. How to solve this issue? I want previously stored data in a serivce to stay unaffected and display it again on page reload?
There are 3 ways to handle it:
Use sessionStorage (don't go for localStorage) , but then make sure to maintain sessionStorage data as per the scenarios
ngOnInit(): void {
if(history.state.data){
this.order=history.state.data;
sessionStorage.setItem('order_page_info', JSON.stringyfy(this.order));
}else{
this.order = JSON.parse(sessionStorage.getItem('order_page_info'))
}
}
Use Cache (not recommended)
Rather than passing entire data, pass the id as router data and make server call to fetch details for the id. This would maintain id in url and so you can fetch details on refresh by calling the server
try putting ngOnChanges
ngOnChanges detects changes from other component specially from api call of services
ngOnChanges(){
this.order=history.state.data
}
I'm building an App with reactjs and in some point I have a SearchForm, then I click on one of the items and in that view I have a back button.
What I want is to populate the search form with the previous data. I already achieved it with localStorage from javascript. And saving the state on componentWillUnmount, the problem is this data is always loaded even if I don't arrive from the go back.
Is there a way to add a kind of state to the goBack to tell it?
So, the best way here is to not use localStorage, and instead use history state.
When the user updates the form, you can do this (optionally debounce it for better performance):
history.replaceState(this.props.location.pathname, {
formData: JSON.stringify(formData),
});
Then when the component loads, you can access that state like this:
const {location} = this.props;
const routeState = location.state && location.state.formData;
if (routeState) {
this.setState({
data: JSON.parse(routeState),
});
}
If they refresh the page, or go to another page and go back, or go back and then go forward, they'll have that state. If they open it in a new tab, they won't have the state. This is usually the flow you want.
I’m working on a MVC 5 (asp) application. One of the requirements I get is to have multiple navigation paths to point at the same destination page. The problem is going back to a previous page after a post according to the navigation history of the user.
Let’s consider a basic scenario of three webpages
Customers/ShowAll -> Show a list of all customers
Customers /Search -> Show a list of customers according to a search (name, country …)
Customers /Update/1134 -> Show the update page for a specific customer (i.e. customer_id=1134)
So the navigation path goes like this
ShowAll -> Update
or
Search -> Update
If the user navigates to “Customers /Search” then “Customers /Update/1134”, updates the customer information and saves the data, I want the server to redirect to the page “Customers /Search” since it’s the path the user uses.
This is a very basic case but it can be more complex like going back many pages and always return a previously visited page (or a default one if no pages match the history).
What I’ve done so far
I have created a prototype that keeps track of the navigation history of a user on the server side. It uses session storage on the client side to give a unique id for the current browser tab. On each page unload it adds the browser tab id to the cookies. Then on the server side there’s a dictionary (in the session) with the tab id (extracted from the cookies) as the key and a list of visited URLs as the value. The current URL is added to the list of URLs. I found this solution to be working but it has some flaws.
If JavaScript is disabled this solution won’t work (this is not a very big deal since I can require all the users to turn it on (it’s an intranet for a small company))
If a tab is duplicated the resulting two tabs will have the same id. This is due to the implementation of session storage (at least on Chrome). So the history on the server can get corrupted if the user uses both tabs.
I store the dictionary of history in a session variable so if the session timed out, the history is lost. I thought about keeping the history in the database but I feel it’s a little of overheat for the database.
A last thing is that the dictionary of URLs is limited to the last 30 pages visited since I want to limit the server memory. It’s not important to my question but I feel to mention it since I’m sure some of you may see the problem of keep all pages from all users for all tabs in history.
I also thought of a similar solution using cookies to transmit the last 30 pages visited on each request and have the server parse this history when it needs it. Only the pages from the application domain will be kept. This will resolve the problem of persistence after a session times out but it introduce a little more processing to the server since the history will be parsed in about each request.
I want to know if there a better solution to redirect the user according to navigation history. Maybe there’s a build-in functionality in MVC 5 that I don’t know.
Thanks for any advice.
Regards.
Using session state (as you have discovered) is not a very good solution to this problem because:
It times out, in which case the data is lost.
If the user doesn't navigate to the page the way you expect (for example, coming directly to a page via Google SERP), then it doesn't work.
The only way to make it work 100% of the time is to put all of the navigation identifier information into the URL so the system can determine how to build the navigation links.
There is no built-in functionality for this in MVC 5, but you could use MvcSiteMapProvider to solve your issue. It contains HTML helpers for Menu and SiteMapPath, which acts like a breadcrumb trail.
#Html.MvcSiteMap().Menu()
#Html.MvcSiteMap().SiteMapPath()
It works on a different principle - it loads a single shared hierarchy of nodes (a site map) into memory. Then each request that comes in matches one of the nodes and uses the map to determine how to build the links in the HTML helpers. There is no session state used at all.
The trick to getting it to work in your scenario is to make the Customers/Update/1134 page available on 2 different URLs. Then you can configure 2 different node hierarchies and it will know which one to match based on the routing information.
For example, you could add an additional route value that indicates that the page you are navigating from is the search page.
#Html.ActionLink("Customer 1134", "Update", "Customers", new { source = "Search" }, null)
By default, this will build a URL like Customers/Update/1134?source=Search. You can make it look prettier by adjusting your route configuration.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "SearchSource",
url: "Search/{controller}/{action}/{id}",
defaults: new { source = "Search", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "ShowAllSource",
url: "ShowAll/{controller}/{action}/{id}",
defaults: new { source = "ShowAll", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Now with the same ActionLink shown above, you will get the URL /Search/Customers/Update/1134. That's better. Note that when you put the ActionLink on your ShowAll page, it should be like this instead:
#Html.ActionLink("Customer 1134", "Update", "Customers", new { source = "ShowAll" }, null)
Then when you set up the node configuration in MvcSiteMapProvider, you need to make 2 different parent nodes, like this.
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<!-- Additional nodes here -->
<mvcSiteMapNode title="Search" controller="Customers" action="Search">
<mvcSiteMapNode title="Update Customer" controller="Customers" action="Update" source="Search" preservedRouteParameters="id"/>
</mvcSiteMapNode>
<mvcSiteMapNode title="Show All Customers" controller="Customers" action="ShowAll">
<mvcSiteMapNode title="Update Customer" controller="Customers" action="Update" source="ShowAll" preservedRouteParameters="id"/>
</mvcSiteMapNode>
<!-- Additional nodes here -->
</mvcSiteMapNode>
You will then get a complete navigation solution:
/Customers/ShowAll | Home > Show All Customers
/ShowAll/Customers/Update/1134 | Home > Show All Customers > Update Customer
/Customers/Search | Home > Search
/Search/Customers/Update/1134 | Home > Search > Update Customer
Of course, this is just an example. You can make the URLs and navigation links look any way you want.
Redirecting Back
Finally, there is redirecting back to the location the user came from. That's easy because MvcSiteMapProvider keeps track of the parent node.
[HttpPost]
public ActionResult Update(CustomerModel model)
{
// Update customer here...
var currentNode = this.GetCurrentSiteMapNode();
if (currentNode != null)
{
var parentNode = currentNode.ParentNode;
if (parentNode != null)
{
return Redirect(parentNode.Url);
}
}
return View(model);
}
You may wish to store some additional information (sort order, search term, etc) from the original parent page, in which case you will need to pass those parameters through the Update page and back to the parent page somehow.
One way is to use session and then use some logical default behavior if they are missing when you get to the redirect page.
Another (easier) approach is to add them as parameters (query string or route values) to the URL of the Customer Update page so they will automatically be built into the return URL. Since you have 2 different routes, you would just need to add the information to the appropriate route. You just need to ensure both the routes of the Search page and the Customer Update page include them so the parent URL is built appropriately.
Full Disclosure: I am a major contributor of the MvcSiteMapProvider project.
See also:
https://github.com/maartenba/mvcsitemapprovider/wiki/Multiple-Navigation-Paths-to-a-Single-Page
http://www.shiningtreasures.com/post/2013/08/07/MvcSiteMapProvider-40-a-test-drive
http://www.shiningtreasures.com/post/2013/09/02/how-to-make-mvcsitemapprovider-remember-a-user-position
In our shop the user selects a product (/shop/products) and then gets redirected to the first customization page (/shop/details). He does some customization and clicks "next". In our controller (productDetailsController) we create a new object (Order) with the selected properties and redirect to the next page (shop/individualization?orderId=2) where that order is further customized. Whenever the user now uses the browser back button we want to make that Order available to the previous page via parameter. So we need to change the url that the back button is directing to (/shop/details?orderId=2 instead of /shop/details).
In short:
/shop/products -nextButton- /shop/details -nextButton- shop/individualization?orderId=2
shop/individualization?orderId=2 -BROWSER-BACK- /shop/details?orderId=2
If I just use $location.replace() inside the controller it will back-button from shop/individualization?orderId=2 to the product selection /shop/products.
If I do two $location.path() inside one digest cycle it will just ignore the first one:
// inside the order creation promise...
var search = {orderId: createdOrder.id};
$location.path("/shop/details").search(search);
$location.path("/shop/individualization").search(search);
I can't use replace() when navigating from /shop/products to /shop/details because using the back button from there still needs to navigate to /shop/products.
Any suggestions?
Thanks!
Outlining a possible solution:
a service keeps track of the order (could be just the orderId, depends on your exact use case)
shop/individualization (or the action of shop/details that navigates to shop/individualization) sets the orderId in the service
both shop/individualization and shop/details define reloadOnSearch: false
both shop/individualization and shop/details bind the URL search parameter to the service; the controller logic could be:
app.controller("XXX", function($scope, orderService, $location) {
// initialization
var orderId = $location.search("orderId");
if( orderId ) {
orderService.setOrderId(orderId); // setOrderId() could handle loading the order too
}
else {
orderId = orderService.getOrderId();
// reloadOnSearch is false, so this doesn't trigger a navigation
if( orderId ) $location.search("orderId",orderId);
}
// ...rest of controller logic
});
Keep in mind:
What you are doing makes shop/details bookmarkable, so that the user can return at any time to see an old order. The system should be prepared to handle this case, e.g. reload the order from the server. If bookmarking is not desired, things are simplified: just use the service and drop the search param altogether.
You may also want to remove the order object from the orderService at some time.