Angularjs: Can i have one route with multiple views? - angularjs

Consider a certain route let's say myapp\profile
which has two modes (buyer/seller)
What i would like to achieve is:
keep the same route url for both modes
Alternate the view with different HTML files (lets say buyer.html, seller.html), of course each view has it's view model.
Sharing some logic between the two modes.
I would like to have a controller/logic to each mode
What i already considered:
Thought about using ui-router's sub states, but I dont want to change the url.
Thought about creating this 'profile' route and while navigating to it, figure the mode (buyer/seller), and then $state.go to a new state (but again, i would like to keep same route name at the end so it's not ok)
Ideally thought i could navigate to my shared controller and then render the correct view and controller, but this idea kinda messed up me.
Could you share what is a clean way of doing this?

most use cases
Normally, in order to dynamically select a template for a state, you can use a function:
state = {
.....
templateUrl: function($stateParams){
if ($stateParams.isThis === true)
return 'this.html'
else
return 'that.html'
}
}
but...
Unfortunately you can't pass other injectables to the templateUrl function. UI.Router only passes $stateParams. You don't want to alter the URL in anyway so you can't use this.
when you need to inject more than $stateParams
But you can leverage templateProvider instead. With this feature, you can pass a service to your templateProvider function to determine if your user is a buyer or seller. You'll also want to use UI.Router's $templateFactory service to easily pull and cache your template.
state = {
....
templateProvider: function($templateFactory, profileService){
var url = profileService.isBuyer ? 'buyer.html' : 'seller.html';
return $templateFactory.fromUrl(url);
}
}
Here it is working in your plunkr - http://plnkr.co/edit/0gLBJlQrNPUNtkqWNrZm?p=preview
Docs:
https://github.com/angular-ui/ui-router/wiki#templates
http://angular-ui.github.io/ui-router/site/#/api/ui.router.util.$templateFactory

Related

Dynamic parameters with ui-router

I'm developing a search app with angular using ui-router.
Requirements:
The search form has too many fields and all have to be optional.
Users should share with another users the URL that display the page with the result list. (So I need to use querystring)
So I could have urls like
path/to/url/list?p=123&v=876
path/to/url/list?c=yes&a=true&p=123
path/to/url/list?z=yes&c=yes&a=true&p=123
path/to/url/list?z=yes&v=876&a=true&p=123
And endless combinations. I know that I can use $location.search() to get all params in json format. That is great! but the question is How can I define the url state with ui-router? Define explicitly all params in the url is not an option. I have read many post but I didn't find a concrete answers.
If you're getting parameters from $location you don't need to define them in state explicitly.
I think, the best way is to use 'resolve' property of $stateProvider configuration:
$stateProvider.state('mystate', {
// Some code here
resolve: {
queryData: ['$location', ($location) => {
$location.absUrl(); // Contains your full URI
return true;
}]
}
});
It's kind of initialization. After that, ui-router will cut URI, but you will store needed data. This case also works fine, when user passing URI directly in browser address input.
Also you can try to set $urlRouterProvider with $urlMatcher for this purposes, but it will be more difficult.

Updating URL in Angular JS without re-rendering view

I'm building a dashboard system in AngularJS and I'm running into an issue with setting the url via $location.path
In our dashboard, we have a bunch of widgets. Each shows a larger maximized view when you click on it. We are trying to setup deep linking to allow users to link to a dashboard with a widget maximized.
Currently, we have 2 routes that look like /dashboard/:dashboardId and /dashboard/:dashboardId/:maximizedWidgetId
When a user maximizes a widget, we update the url using $location.path, but this is causing the view to re-render. Since we have all of the data, we don't want to reload the whole view, we just want to update the URL. Is there a way to set the url without causing the view to re-render?
HTML5Mode is set to true.
In fact, a view will be rendered everytime you change a url. Thats how $routeProvider works in Angular but you can pass maximizeWidgetId as a querystring which does not re-render a view.
App.config(function($routeProvider) {
$routeProvider.when('/dashboard/:dashboardId', {reloadOnSearch: false});
});
When you click a widget to maximize:
Maximum This Widget
or
$location.search('maximizeWidgetId', 1);
The URL in addressbar would change to http://app.com/dashboard/1?maximizeWidgetId=1
You can even watch when search changes in the URL (from one widget to another)
$scope.$on('$routeUpdate', function(scope, next, current) {
// Minimize the current widget and maximize the new one
});
You can set the reloadOnSearch property of $routeProvider to false.
Possible duplicate question : Can you change a path without reloading the controller in AngularJS?
Regards
For those who need change full path() without controllers reload
Here is plugin: https://github.com/anglibs/angular-location-update
Usage:
$location.update_path('/notes/1');
I realize this is an old question, but since it took me a good day and a half to find the answer, so here goes.
You do not need to convert your path into query strings if you use angular-ui-router.
Currently, due to what may be considered as a bug, setting reloadOnSearch: false on a state will result in being able to change the route without reloading the view. The GitHub user lmessinger was even kind enough to provide a demo of it. You can find the link from his comment linked above.
Basically all you need to do is:
Use ui-router instead of ngRoute
In your states, declare the ones you wish with reloadOnSearch: false
In my app, I have an category listing view, from which you can get to another category using a state like this:
$stateProvider.state('articles.list', {
url: '{categorySlug}',
templateUrl: 'partials/article-list.html',
controller: 'ArticleListCtrl',
reloadOnSearch: false
});
That's it. Hope this helps!
We're using Angular UI Router instead of built-in routes for a similar scenario. It doesn't seem to re-instantiate the controller and re-render the entire view.
How I've implemented it:
(my solution mostly for cases when you need to change whole route, not sub-parts)
I have page with menu (menuPage) and data should not be cleaned on navigation (there is a lot of inputs on each page and user will be very very unhappy if data will disappear accidentally).
turn off $routeProvider
in mainPage controller add two divs with custom directive attribute - each directive contains only 'templateUrl' and 'scope: true'
<div ng-show="tab=='tab_name'" data-tab_name-page></div>
mainPage controller contains lines to simulate routing:
if (!$scope.tab && $location.path()) {
$scope.tab = $location.path().substr(1);
}
$scope.setTab = function(tab) {
$scope.tab = tab;
$location.path('/'+tab);
};
That's all. Little bit ugly to have separate directive for each page, but usage of dynamic templateUrl (as function) in directive provokes re-rendering of page (and loosing data of inputs).
If I understood your question right, you want to,
Maximize the widget when the user is on /dashboard/:dashboardId and he maximizes the widget.
You want the user to have the ability to come back to /dashboard/:dashboardId/:maximizedWidgetId and still see the widget maximized.
You can configure only the first route in the routerConfig and use RouteParams to identify if the maximized widget is passed in the params in the controller of this configured route and maximize the one passed as the param. If the user is maximizing it the first time, share the url to this maximized view with the maximizedWidgetId on the UI.
As long as you use $location(which is just a wrapper over native location object) to update the path it will refresh the view.
I have an idea to use
window.history.replaceState('Object', 'Title', '/new-url');
If you do this and a digest cycle happens it will completely mangle things up. However if you set it back to the correct url that angular expects it's ok. So in theory you could store the correct url that angular expects and reset it just before you know a digest fires.
I've not tested this though.
Below code will let you change url without redirection such as: http://localhost/#/691?foo?bar?blabla
for(var i=0;i<=1000;i++) $routeProvider.when('/'+i, {templateUrl: "tabPages/"+i+".html",reloadOnSearch: false});
But when you change to http://localhost/#/692, you will be redirected.

Angular - different route, same template/controller,different loading method

I want to use routes, but I always want to use same template & controller. I have routes like this:
**a/:albumid**
and
**i/:imageid**
In the first case I want to load an array of images and add them to a list. In the second case I want to load a single image and add it to a list.
So the difference is only in data loading. What is the most efficient way to do this?
Also is it possible to animate ng-show? Something like jQuery's slideDown?
Check out this article, it describes a way to do exactly what you want:
http://www.bennadel.com/blog/2420-Mapping-AngularJS-Routes-Onto-URL-Parameters-And-Client-Side-Events.htm
I've used the technique, it works well.
In a nutshell, something like this for routing:
$routeProvider
.when("/a/:album_id", {
action: "album.list"
}).when("/i/:imgid", {
action: "images.load"
})
Then in your controller you can access $route.current.action and do the appropriate thing. The trick is to create a function in you controller that does all the work (the article calls it render()) and then call that function when $routeChangeSuccess fires:
$scope.$on(
"$routeChangeSuccess",
function( $currentRoute, $previousRoute ){
// Update the rendering.
render();
}
);
I created a super simple directive to handle this that allows routes to be have more like Rails or Codeigniter routes where the controller method is in the route definition. The method name is set in the routeProvider.when options and the directive is set in the template for the route.
See: https://stackoverflow.com/a/22714634/250991

AngularJS Paging with $location.path but no ngView reload

My single page application loads a home page and I want to display a series of ideas. Each of the ideas is displayed in an animated flash container, with animations displayed to cycle between the ideas.
Ideas are loaded using $http:
$scope.flash = new FlashInterface scope:$scope,location:$location
$http.get("/competition.json")
.success (data) ->
$scope.flash._init data
However, to benefit from history navigation and UX I wish to update the address bar to display the correct url for each idea using $location:
$location.path "/i/#{idea.code}"
$scope.$apply()
I am calling $apply here because this event comes from outwith the AngularJS context ie Flash. I would like for the current controller/view to remain and for the view to not reload. This is very bad because reloading the view results in the whole flash object being thrown away and the preloader cycle beginning again.
I've tried listening for $routeChangeStart to do a preventDefault:
$scope.$on "$routeChangeStart", (ev,next,current) ->
ev.preventDefault()
$scope.$on "$routeChangeSuccess", (ev,current) ->
ev.preventDefault()
but to no avail. The whole thing would be hunky dory if I could figure out a way of overriding the view reload when I change the $location.path.
I'm still very much feeling my way around AngularJS so I'd be glad of any pointers on how to structure the app to achieve my goal!
Instead of updating the path, just update query param with a page number.
set your route to ignore query param changes:
....
$routeProvider.when('/foo', {..., reloadOnSearch: false})
....
and in your app update $location with:
...
$location.search('page', pageNumber);
...
From this blog post:
by default all location changes go through the routing process, which
updates the angular view.
There’s a simple way to short-circuit this, however. Angular watches
for a location change (whether it’s accomplished through typing in the
location bar, clicking a link or setting the location through
$location.path()). When it senses this change, it broadcasts an
event, $locationChangeSuccess, and begins the routing process. What
we do is capture the event and reset the route to what it was
previously.
function MyCtrl($route, $scope) {
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function(event) {
$route.current = lastRoute;
});
}
My solution was to use the $routeChangeStart because that gives you the "next" and "last" routes, you can compare them without the need of an extra variable like on $locationChangeSuccess.
The benefit is being able to access the "params" property on both "next" and "last" routes like next.params.yourproperty when you are using the "/property/value" URL style and of course use $location.url or $location.path to change the URL instead of $location.search() that depends on "?property=value" URL style.
In my case I used it not only for that but also to prevent the route to change is the controller did not change:
$scope.$on('$routeChangeStart',function(e,next,last){
if(next.$$route.controller === last.$$route.controller){
e.preventDefault();
$route.current = last.$$route;
//do whatever you want in here!
}
});
Personally I feel like AngularJS should provide a way to control it, right now they assume that whenever you change the browser's location you want to change the route.
You should be loading $location via Dependency Injection and using the following:
$scope.apply(function () {
$location.path("yourPath");
}
Keep in mind that you should not use hashtags(#) while using $location.path. This is for compability for HTML5 mode.
The $locationChangeSuccess event is a bit of a brute force approach, but I found that checking the path allows us to avoid page reloads when the route path template is unchanged, but reloads the page when switching to a different route template:
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function (event) {
if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
$route.current = lastRoute;
}
});
Adding that code to a particular controller makes the reloading more intelligent.
Edit: While this makes it a bit easier, I ultimately didn't like the complexity of the code I was writing to keep friendly looking URL's. In the end, I just switched to a search parameter and angular handles it much better.
I needed to do this but after fussing around trying to get the $locationChange~ events to get it to work I learned that you can actually do this on the route using resolve.
$routeProvider.when(
'/page',
{
templateUrl : 'partial.html',
controller : 'PageCtrl',
resolve : {
load : ['$q', function($q) {
var defer = $q.defer();
if (/*you only changed the idea thingo*/)
//dont reload the view
defer.reject('');
//otherwise, load the view
else
defer.resolve();
return defer.promise;
}]
}
}
);
With AngularJS V1.7.1, $route adds support for the reloadOnUrl configuration option.
If route /foo/:id has reloadOnUrl = false set, then moving from /foo/id1 to /foo/id2 only broadcasts a $routeUpdate event, and does not reload the view and re-instantiate the controller.

multiple matching routes

I've got a backbone.js application that defines two controllers, and the controllers both define route patterns which match the location.hash. I'm having trouble getting both of them to fire - e.g.
ManagerController = Backbone.Controller.extend({
routes: {
":name": "doStuff"
},
doStuff : function(name) {
console.log("doStuff called...");
}
});
Component1Controller = Backbone.Controller.extend({
routes: {
"xyz123": "doMoreStuff"
},
doMoreStuff : function() {
console.log("doMoreStuff called...");
}
});
so if the url is "http://mysite.com/#xyz123", then I am seeing 'doStuff()' called, or if I comment out that route, then 'doMoreStuff()' is called. But not both.
I'm using this architecture because my page is highly component oriented, and each component defines its own Controller. A 'component manager' also defines a Controller which does some house keeping on all routes.
Should I be able to configure two controllers that both respond to the same route? Cheers,
Colin
Short answer: No, you can't do that. One Controller per page.
Long answer: When you instantiate a new Controller, it adds its routes to the History singleton. The History singleton is monitoring the hash component of the URL, and when the hash changes, it scans the routes for the first expression that matches its needs. It then fires the function associated with that route (that function has been bound to the controller in which it was declared). It will only fire once, and if there is a conflict the order in which it fires is formally indeterminate. (In practice it's probably deterministic.)
Philosophical answer: The controller is a "view" object which affects the presentation of the whole page based on the hash component of the URL. Its purpose is to provide bookmark-capable URLs that the user can reach in the future, so that when he goes to a URL he can start from a pre-selected view among many. From your description, it sounds like you're manipulating this publicly exposed, manually addressable item to manipulate different parts of your viewport, while leaving others alone. That's not how it works.
One of the nice things about Backbone is that if you pass it a route that's already a regular expression, it will use it as-is. So if you're trying to use the controller to create a bookmarkable description of the layout (component 1 in the upper right hand corner in display mode "A", component 2 in the upper left corner in display mode "B", etc) I can suggest a number of alternatives-- allocate each one a namespace in the hash part of the URL, and create routes that ignore the rest, i.e.
routes: {
new RegExp('^([^\/]*)/.*$'): 'doComponent1stuff',
new RegExp('^[^\/]*/([^\/]*)\/.*$': 'doComponent2stuff',
}
See how the first uses only items after the first slash, the second after the second slash, etc. You can encode your magic entirely how you want.
I suggest, though, that if you're going to be doing something with the look and feel of the components, and you want that to be reasonably persistent, that you look into the views getting and setting their cookies from some local store; if they're small enough, cookies will be enough.
I have a very similar issue. At present, backbone stops after the first matching route. I have a dirty workaround where I am overriding the loadUrl method of Backbone History. Here I am iterating through all of the registered routes and triggering callback for all of the matching routes .
_.extend(Backbone.History.prototype, {
loadUrl : function() {
var fragment = this.fragment = this.getFragment();
var matched = false;
_.each(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
matched = true;
}
});
return matched;
}
})
Philosophically, I am fine with having single controller per page. However, in a component based view framework, it will be nice to have multiple views per route rendering different parts of a view state.
Comments are welcome.
I've used namespacing to deal with a similar problem. Each module comes with it's own module controller, but is restricted to handle routes that start with /moduleName/ this way modules can be developed independently.
I haven't fully tested this yet, if you take a look at the Backbone.js source, you can see this at line 1449:
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragment) {
fragment = this.fragment = this.getFragment(fragment);
return _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
});
}
The any method will stop as soon as it matches a handler route (with the "return true"), just comment the return and the short-circuit will never happend, and all the handlers will be tested. Tested this with a marionette app with two modules, each one having it's own router and controller, listening same routes anb both fired up.
I think this is the simplest way of resolving it
routes: {
'': 'userGrid',
'users': 'userGrid',
}

Resources