-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Design #1
Comments
First of all, I'm happy to try coding with you guys a better routing system :) I suggest that each participant sees the 2 other solutions (Like @ProLoser said) I think that the first step, before implementation, is to determine what we want or need. In other words we have to set the specifications of this project. So this is what i think is important :
From what I can see of other projects, here are few notices :
Here are some pros that my solution offers (by this, I do not mean that it's perfect) :
Well, i'm excited about your point of view ;-) Vincent |
@vincentdieltiens Cool, I didn't know about your proposal. I'd like to add a summary and some additional comments about both @bennadel and @vincentdieltiens approaches: (@hkdobrev proposal lacks "selective" reloading, which I think is critical for any multiview system).
Good work Ben & Vicent :) |
@lrlopez Thanks. My solution is rather new. There is also a topic about this : https://groups.google.com/forum/#!msg/angular/ayG1hCUOfX0/eVxb7fNqRgQJ I need to read more in-deph the solution @bennadel . I will try to do this quickly :) @lrlopez : you are right about my solution. When i developped it, i didn't find good names (I often lacking of vocabulary in English). I didn't have proposed my solution to the angularjs team because :
So, you are right, I have to merge my solution with the enhancements/fixes that are applied to the angularjs Routing system. There is also one Pro of my solution : it works with the syntax of the angularJS routing system. So no breaking. |
I didn't understand the @bennadel solution at first. I was getting confused between the routes and action paths, the action paths in no way resemble the routes. Creating a second layer of abstraction people must learn and fundamentally understand. My vote is @vincentdieltiens approach (overall) with some modifications. An ideal world I aim to use the core routing system. However I realize this may not be enough. On @vincentdieltiens's solution:
My main concern is intuitive usage |
I also like @jeme suggestion of using sub-routes. It would be fairly trivial to add recursive appending: .when('/article', {
controller: 'ArticleList',
templateUrl: 'articles/index.html',
subroutes: {
'/:articleId[/:tab]' : { // -> '/article/:articleId[/:tab]' (the :tab token is optional, the :articleId is mandatory)
controller: 'ArticleDetails',
templateUrl: 'articles/details.html'
},
'/categories' : { // -> '/article/categories'
controller: 'ArticleCategoryList',
templateUrl: 'articles/categories/index.html',
subroutes: {
'/:categoryId' : { // -> '/article/categories/:categoryId'
controller: 'ArticleCategoryDetails',
templateUrl: 'articles/categories/details.html',
}
}
}
}
}) Actually after writing that ^^ I find that syntax fucking smexy. AND super intuitive! |
Few key hurdles trying to allow each view partial to remain as independent as I could; and, to allow each view partial to respond only to relevant changes. We have need for three different, distinct layouts and several layers of nesting. |
For the two solutions proposed by @ProLoser :
How do you deal with
That's why, in my solution, I store "super" controllers (controllers that are used for several routes), in global variables that can be accessed in different modules. And It's not a good way (i prefer to avoid global variables). It will be great to find a solution with the proposition of @jeme where we can "append" subroutes. $routeProvider.when('/article', {
controller: 'ArticleList',
templateUrl: 'articles/index.html',
subroutes: {
...
}
});
$routeProvider.addSubroutes('/article', '/article/:id/coments', {
controller: 'ArticleCommentList',
templateUrl: 'articles/comment/index.html',
subroutes: {
...
}
}); Maybe an other idea ? |
@bennadel, welcome! I also like @jeme proposal of subroutes. They successfully degrade to the AngularJS traditional routing if subroutes are not used. This means that is backward compatible so allows older code and examples to continue working even if the new routing model gets pulled into the main codebase. They are also cleaner to understand and to implement. So, 👍 About @ProLoser idea of optional parameters, may be now is the right time to propose enhancements to the route specification. Some weeks ago I had a look into the different proposals, and one of the most flexible was @xealot's angular/angular.js#1634, which uses the werkzeug routing syntax. That new syntax would allow to specify the typed parameters (integer only, path which includes slashes, etc). I.e. "/client/int:bar" or "/search/path:where/string:what". The new implementation also remembers the generated regexp for each route in order to reduce the overhead, so it should be even faster than the original code. We could use @xealot work as a base and expand the syntax to include optional params (which I find extremely useful) or, if is interesting enough, we could fully implement werkzeug route spec (i.e. minlength, maxlength, maps, etc.) Of course there are other alternatives that are worth a look (first section). What do you think? |
I think the case of .when('/customers/:customerId', {
controller: 'CustomerCtrl',
templateUrl: 'customers/index.html',
subroutes: {
'/contacts' : {
controller: 'CustomerContactsDetailCtrl',
templateUrl: 'contacts/details.html'
},
'/prices' : {
controller: 'CustomerPricesDetailCtrl',
templateUrl: 'prices/details.html'
}
}
}) What I miss is another parameter that specifies which subview should the template of each subroute render in. Without it, you are implying that each view can one have one subview inside, and that may not be ok on some cases. You'd need to specify in which view you'd like to render each template of every subroute (the main route could always be rendered on the top one, ngView). This would also be backward compatible with the current routing implementation. .when('/customers', {
controller: 'CustomerManagementCtrl',
templateUrl: 'customers/index.html',
subroutes: {
'/manage' : {
controller: 'CustomerListCtrl',
templateUrl: 'customers/list.html',
subview: 'CustomerListView'
},
'/:customerId' : {
controller: 'CustomerDetailCtrl',
templateUrl: 'customers/detail.html',
subview: 'CustomerDetailView'
}
}
}) Also, dynamically adding subroutes would be nice, but I'm concerned that it could break the relation between one route and its relevant subviews. Why? Because if the browser reloads the page, it could lead to a different set of routing rules if you're not careful enough. |
I had understood the idea. But that's does not allow to register subroutes in several modules and that's a pity. |
Appreciated that people like the idea of sub-routes, basically I ended up defining the "interface" as i wanted it before I started implementing the solution. Thats where I often start anyways... If things are to complex or obscure they pose to big a barrier in my experience, and then they get no traction... I might question a little if this is something we can add to Angular-UI alone, currently I have core changes to route.js. This might be a change that ultimately won't be difficult to get into the Angular stream though as it is isolated and should be backwards compatible... Currently I have had more of a prototyping approach to it, so haven't worked much with testing etc... Anyways... Within the router I have changed the parseRoute to the following (which in long term should be the only needed change to route):
You can also see my fork here: https://github.com/jeme/angular.js Still getting into the way they structure their code, so fighting allot with what closures there is etc. |
@jeme : with your syntax, how will you determine :
|
@vincentdieltiens currently it is left to the controller since I don't have a directive implemented yet that supports sub-routing, but the idea is that you go down the ladder until you hit something that is changed, from there you execute the views... (that also means that I as of now don't have sub controllers, what @ProLoser typed was an extented version of what I had) This will require that we know the actual route-chain post route update, that is not added to the above yet as i have to do things very incrementally right now, simply because I am not that familiar with the original source, this also boils down to a requirement that ones the routing part is done it should be fully backwards compatible... One of the things I would like to provide however is a flexibility where you might choose to execute an entire route if it's parameters has changed, or in the controller react to that change... As often when parameters change, we wan't to use the same template, yet populate it with different data, so re-executing that route in that case also seems "un-needed" to me... Instead the controller could just react to the route, load the data and update the scope... I have yet to find a design for that though, where it is optional behavior. |
I don't get it. By executing the route you mean executing the controllers of that route ? A controller get a route and his parameters, then it get the data via resources (or an other way) and populate the view and maybe do other thing according to the routing parameters. If the parameters change, it must be re-executed. But that's my point of view, maybe you can convince me that i'm wrong ;-) @lrlopez, @ProLoser and @bennadel : what do you thing about it ? |
Sorry guys, getting into this conversation later than I would have hoped. I haven't had a chance to review the code samples yet, but just a few thoughts. As you what @vincentdieltiens was saying:
I think this is one of the ways in which traditionally server-side and client-side frameworks start to split paths. On a server-side framework, in which the entire response is re-built for every request, it is the job of the framework to determine which Controller should load data... and when. On a client-side application, however, I think there is far too much persisted UI for this same model to hold true. Now that the UI structures are far more complex and now that we are refreshing only portions of a page, I think the "when do I refresh" must fall to the responsibility of the Controller. After all, a Controller may choose to load a sub-view that isn't tied to a specific route, but responds to route changes in general. As a really contrite example, imagine a view that simply counted the number of pages a user had visited. That View would be tied to route changes, but not in a way that the framework would ever know about. |
@vincentdieltiens you might have misunderstood me a little... I certainly agree that what you mention is the default and most common behavior and that it would serve 95%. Yet for unforeseen cases, I was thinking of providing a way to hook into the framework and "override" the behavior for a specific route, rather than leaving people in a situation where they had to create a whole new directive similar to the one we might provide, since that again would become a barrier to some... It could be as simple as an event/call back that gets called if registered that returns true or false. The constraint however is that it needs to be close to the controller in my mind, so it's a optional "hook" you attach to either in the controller or the parent controller. (properly the controller it self, that should be ok) And also, this won't the the routing providers responsibility, that should ALWAYS raise the $routeChangeStart and $routeChangeSuccess on any of the before mentioned cases, this will be something that could be added to the new view directive. So it doesn't pollute in the core. |
Also, there is definitely a strong possibility that a single view could depend on two different route parameters. I have that in my app in which time-stamps are part of the URL: /updates/:projectID/:since ... where projectID is an ID, and :since is a UTC milliseconds value. If the URL changed and only the :since value changed, you couldn't simply reload the entire Controller as that would require the project to be reloaded as well. Instead, what you'd want is the controller to say, "The ID is the same, so use the currently loaded project; but, the Since has changed, to reload the updates." |
@lrlopez, this is definitely an interesting idea:
I had thought of trying to add some sort of validation, using RegEx (I am biased in that I love regex) that would be provided as part of the route definition. Something like: .when(
"/projects/:id/:tab",
{
action: "splash.home"
},
{
id: /^\d+$/i,
foo: /(info|contacts|audit)/i,
tab: /(\w+)/i
}
) In this case, the last hash would contain a key-value pair in which the key was the route param and the value was the RegExp instance that would be run (ie. .test( .. )) against the matching parameters. In the end, I decided not to go with this since I didn't see a huge win in "guarding" my app. After all, the only time a URL would be bad would be if someone was purposefully messing with the routes. And, in that case, either a view wouldn't be rendered (due to no matching ngSwitchWhen case); or, the server would throw some 4xx error since the requested parameters for data were not valid. I had a number of "validation" parts built into my original framework; but, I ended up stripping most of them out. As long as people use the app in the way it was designed, route-based validation shouldn't be needed. And, for people who abuse the app, I see no need to jump through hoops to help them get good views. |
On something that @lrlopez said about template reuse:
I have seen this thought on server-side programming as well - people are always looking for a way to reuse views with different controllers. In my experience, this typically only works in low-design applications. I work with a lot of designers who like to curate and customize every view to its exact use-case. At first, you think you think you can reuse a view in two different parts of your application. But, over time, your UI team / product manager / users want more and more customizations in a given view. Ultimately, ... and again, this is just my experience ... shared views tend to be broken up over time because they start to have too many conditionals. That said, you can always have shared views in an application. There's nothing to say that the view: /views/project/detail.htm ... can't load, as part of its UI: /views/shared/project-widget.htm .... that maybe shows up in a number of places. |
@bennadel I think the werkzeug syntax would allow for a regex converter, sort of (if implemented to specs):
Or something like that at least... |
@jeme : I had understood the idea. I have a feeling that something could not work with this behavior but i can't put a word on it. I will sleep on it, maybe tomorow will be better :D About typed parameters, I think too that it's an interresting idea but as @bennadel explained it, it's maybe an "overhead" that is not always useful. But it can be an optional feature of the routing system. The way @bennadel would writing it, is like Symfony (in php) do. It's easy to understand and easy to use and can be optional. |
@vincentdieltiens you're approach is incredibly clever. I think your understanding of how templates are compiled and linked is much stronger than mine. I don't believe I would have ever come up with a way to manually breakdown and re-link templates and keep all the $scope chain stuff in tact. Very impressive! That said, I think that our two approaches actually have a good deal in common. Your execute() ( and the related persist() method) seem to be doing what my: if ( renderContext.isChangeRelevant() ) { ... } ... and if ( requestContext.haveParamsChanged( [ "categoryID", "petID" ] ) ) { ... } ... are doing. The major difference is that you put your configuration in the route declarations and I put my configuration in the Controllers themselves. Internally, my "Request Context" , stores the "action" and current / previous route params, so it can be queried as to what has changed. |
@vincentdieltiens , I was thinking about your approach and looking at my sample app, specifically this UI: URL: http://bennadel.github.com/AngularJS-Routing/#/pets/dogs/5 This UI has the "Pet Detail" UI. And, it has the nested, "Related Pets" UI (see red boxed). Both of the UI values depend on the change in route:
If I were to change the :petID in the URL, both of these UIs have to update; but, one it nested inside of the other. Would you define both of those in the route provider? Or, would you have the "detail" view be tied to the route handler; and the "related pets" widget simply fall back to using $routeChangeSuccess? |
@bennadel Thanks Yes we have handled the same problems (param changing) but in a different way. I don't know what's the best solution : defining this in the configuration of the route (like my solution) or in the controllers (like your solution). And more you handle things in controller, greater the controller will be... When I'm writing a controller my focus is :
When I'm writing a controller my focus is not :
In this way, when I read a controller all i need to know is which data it is retreiving and which actions exists.
I tend to define it in the via the route provider because of the reason above. Maybe it's the role of an other layer than controller and route provider. |
God damned it's getting near impossible to follow this thread. PLEASE START POSTING TL;DR Notes:
I believe that this design covers every requirement you can throw at it EXCEPT defining routes in multiple places. |
In an attempt to reach a consensus I started this set of specifications: https://github.com/angular-ui/router/wiki Please add your votes or expand specifications. Do NOT discuss bullets or add complex explanations. Keep bullets concise. |
@ProLoser sorry to be long winded :) I'll try to be more bullet-pointy. @vincentdieltiens I'd be curious in trying to re-write my sample app using your approach. Hopefully I can try that tomorrow night. |
@ProLoser sorry too About my vote :
|
Like this? https://github.com/timkindberg/router/blob/master/router_api_playground.html. Should I do a pull request? I named the file router_api_playground.html because I feel like we need to establish the API first, making it as user friendly as possible, and then try to make it work. We can then change things as needed during implementation if absolutely necessary. |
@timkindberg I've had a look at your API -- I like the ability to define nested states "fluidly" like that, however I think an API like that should be sugar on top of a documented simple "here's a state with these properties" expressed as a plain old object API. I think it's important to
For these reasons I think it's advantageous if the $stateProvider is somewhat flexible in what exactly a state object looks like, and does not strictly require them to be configured via the high-level configuration "builder"-style interface. I agree the ability to have the state provide data to the controller is crucial, but I think the existing route.resolve mechanism already covers this, as the functions referenced by route.resolve are injectable and can therefore reference the current $stateParams. I've done some work on my prototype and split out the URL routing from the actual state transition mechanics, and to treat state objects as outlined above, as well as removing the requirement of hierarchical state names, and allowing URLs of sub-states to be either relative (the default) or absolute (by prefixing them with '^', in which case the sub-state URL will still need to provide all the parameters required by the parent state, but can do so with a different syntax). I'm just working on putting the pieces back together so the sample works again. |
I like your plan. Having a more generic back end that is flexible and adding a sugar API on top. I am not an angular expert. I'm partaking in this discussion because as I compare frameworks, it's coming down between angular and ember. Routing in ember just seems much more powerful while remaining simple to use. So I may miss the mark a bit with my API ideas but I am useful for serving as the "layman" perspective in this discussion. That's why I place so much emphasis on the API, because that is what will sell this to the simple folk like me. That said, I know you have a deeper knowledge of what needs to happen technically. I like your bullet points 2 & 3, but wonder why 1 is so necessary. I feel that setting multiple views per state is more important. If we must use an object literal instead of function objects then I guess the object could have a Regardless I look forward to your work. The last version was very good. |
@timkindberg I've just pushed the updated working implementation. The named-view part is still missing, but other than that the internals feel much tidier now than in the first attempt. It seems being backwards-compatible with $route will be reasonably easy as well. I definitely agree that a good and easy to understand API is important though! Just trying to wrap my head around how multiple named views should work conceptually, and how to handle cases like
Another area that needs some thought is what happens during a state transition? A transition involves resolving templates and controller dependencies asynchronously, and if this step fails a reasonable expectation would be for the UI state to remain unchanged and an error event to be broadcast. However this means that during that time, $stateParams, $state.current etc all still reflect the previous state. At the moment I'm working around this by overriding $stateParams for $injector.invoke() when resolving dependencies, but I'm not sure if this is the most intuitive approach. Another related question is what happens if another transition is initiated while one is already in progress -- should the first one be abandoned, or the second attempt refused? |
Reviewing your code in depth right now. I think you are doing a good job. Let me put on my reviewers hat on if I may.
Wow, again your code looks very well written. So on to multiple named views:
|
@timkindberg: Thanks for your feedback! I wanted to be able to specify 'parent' via a direct object reference, but I agree putting automatic derivation of the parent via hierarchical names sounds good -- it seems to me this would be used in 95% of the cases. I'll put that back in for the case where 'parent' is undefined (so it can be specified as null to override automatic derivation) run() is part of the module API, I've just used this in the sample to publish some stuff in the scope rather than using an application-level controller. I think an abstract state is very similar to an abstract class in an OO language -- so I'm finding it quite intuitive. In the sample 'contacts' is abstract and simply defines the sidebar, but leaving the definition of the right-hand part of the UI to the child states. The contacts state can't be activated without also activating one of it's children, e.g. contacts.list (which you call the "index" state, I'm currently simply using the empty URL relative to the parent state. Note that this only works because the URL of the abstract parent state is not itself added to the routing rules -- otherwise the rules for the parent and the default child would conflict). I suppose it would be possible to derive the fact that 'contacts' is abstract from the fact that it has a child state with an identical URL, but I think that would make things less obvious -- I think as it stands an abstract state is rather easy to understand: It's a state that can only become active indirectly via one of its sub-states, and it's URL is not added to the router. Note that it also prevents you from doing $state.transitionTo('contacts'); it's not purely tied to URL. Apart from the use case in 'contacts' for common UI, I think having common services / data can also be a good use case for abstract states (via the 'resolve' property). The locals that are made available to controllers inherit down the state hierarchy, so the values resolved for the (maybe abstract) parent state are also available to sub-state controllers. I've got the automatic nesting level working now, but still pondering the exact semantics of multipel named views -- you say "States can only set views at their own level", I guess that would be a reasonable restriction and would avoid some of the pathological cases. The states now have optional onEnter / onExit callbacks, so those could maybe be used to do custom 'weird' stuff, by setting a scope variable that's picked up by an ng:include for example. In terms of JSON syntax I was thinking (within the state object): views: {
main: {
template: ...,
controller: ...,
resolve: ...,
},
sidebar: {
template: ...,
controller: ...,
resolve: ...,
}
} Having a 'views' property would mean that the implicit default view properties (template, controller) in the state object itself may not be used (but you can still have a view named ""), however 'resolve' can be meaningfully used both at the state and view level to have shared and/or controller specific data. It feels like this is getting pretty close to a full working solution! |
This IS starting to be a perfect solution in my opinion! No else seems to be weighing in, I wonder if we're missing anything. Also just want to applaud you again for doing the actual coding. It's clear that you know what you are doing. |
I've just pushed an updated implementation and sample (also updated the hosted sample at http://filedrop.plukmobile.net/angular-states/sample/index.html) It supports hierarchical names now as well as multi-view. Multiple named viewsWhat finally made this "click" for me and seems to have resolved all the implementation problems is that view names (by default) need to be taken not as absolute names, but as names relative to the parent state / template. In the sample, the root template (index.html) has an unnamed ng-state-view tag. The absolute name of this view therefore becomes ' The contacts.html template loaded by the contacts state now contains another unnamed ng-state-view directive. The resolved name of this view is ' The contacts.detail state defines views that reference these two directives in turn: The unnamed view resolves to the absolute view name ' The contacts.detail state contains a third view definition for 'hint@'. Because this view name is already absolute (it contains an '@'), it is left untouched and references the ng-state-view="hint" directive in the root template. The contacts.detail.item state then overrides this view with a different template. So putting it all together, the rules as currently implemented boil down to this
With these rules, both the very common use-case of simply nesting unnamed views that I started with, as well as more complex cases like the global 'hint@' view in the sample work as expected. One key insight for me into when thinking about schemes for figuring out which view goes into which directive is this: When a view directive is encountered, it needs to be able to figure out it's absolute name without being able to look at any nested templates that will be loaded into it eventually. This means that while a child state can override a view defined by a parent state (e.g. the "hint@" view should now display xyz rather than what the parent state said), it doesn't make sense to want to override where in the DOM a view goes (e.g. view "hint@" should now go here rather than where the parent template had it defined). The other aspect that seems a little restrictive at first is that when a state references a directive, e.g. 'menu' in the sample, it intuitively makes sense to want to not just blindly qualify this with the name of the parent state (i.e. menu@contacts), but instead walk up the chain of parent states until we find one that does provider a place to put that view. The reason this won't work is that states only provider view placeholders indirectly, via the ng-state-view directives inside the templates that get loaded (and these can even be provided by functions / providers dynamically based on parameters or other runtime state). I think it's important to be able to look at the definition of a state and be able to understand 'statically' what it's view definitions mean, without having to trace through all the templates that could possible loaded by the parent states. One extension that I think might be OK is to allow an ng-state-view directive to specify an absolute name, but I'm struggling to think of a use case for this. Next Steps'abstract'I hear what you're saying regarding 'abstract' not being a well-understood concept. It's also not a term that's generally used in the context of Hierarchical State Machines. I'll think some more about the idea of designating a 'default' child state (which is a common concept with HSMs), however
resolve / $injectorWhen trying to demonstrate the use of 'resolve' I noticed that even though I'm setting up a nice inheritance chain of resolved dependencies (with the intention that child state controllers have access to dependencies resolved by parent states) the $injector service explicitly excludes inherited properties from injection (this behaviour isn't documented though). So either
It would be good to get some feedback on what others think would be reasonable here. Having resolved dependencies inherit down the state hierarchy seems very neat, but if it's really needed in practice is debatable. Maybe somebody has used the corresponding 'resolve' feature in $routeProvider and could comment on this? move into angular-ui repo, docs, testsAnd other tidy-up. |
Wow lots to take in. I'll try to review tonight. I've been very excited waiting for this update. |
For anyone coming into this now the latest code is here: https://github.com/pluk/angular-extras. Anyone on this mailing list should review the current state of Karsten's implementation and provide feedback/make pull request as needed. |
Are we going to be relocating the project?
|
Karsten needs to move his code into a fork or custom branch of this repo. I think he/we should also add lots more comments in the code for easier review. |
Man I don't have time to write up a thorough review (wish I did) but I did review for the last hour or so and I just want to say that I LOVE IT!! I think you've done an exceptional job here. I feel like you've taken everyone's concerns into consideration and covered all the major requirements (at least from my requirements). I agree with all of your insights and statements about the restrictions you've set. They make sense to me and actually don't feel very restrictive at all. My opinion on your next steps:
|
I just realized one thing I don't like about the new views API is I'd prefer that each view object represent the view, but right now the view name is not part of the view object, its the key. It'll be easier to explain with code: Currently it is:
But I kind of like how vincent's approach worked with arrays of view objects, I'll just show you:
The reason I like the more is the view objects could be created elsewhere and it would be clear as part of the object which view it was going to plug into. What do you think? |
Hm I guess that makes sense too. It would be easy enough to support either an array with explicitly named views or an object with views named by key. Tim Kindberg [email protected] wrote:
|
I'd like to close this issue to urge people to start logging new issues (one issue per singular topic/bug/idea) |
Specifications and Votes
https://github.com/angular-ui/router/wiki
Resources
https://github.com/bennadel/AngularJS-Routing
https://github.com/vincentdieltiens/Angular-Multi-View
angular/angular.js#1198
https://github.com/angular/angular.js/wiki/Routing-Design-Discussion
The text was updated successfully, but these errors were encountered: