angular-light icon indicating copy to clipboard operation
angular-light copied to clipboard

router

Open lega911 opened this issue 9 years ago • 88 comments

https://gist.github.com/fend25/3619d6d730039c30a34d

lega911 avatar Jan 25 '16 17:01 lega911

https://jsfiddle.net/fend25/e1LLu72c/ better and preferabe variant now,

guys, feel free to comment)

fend25 avatar Jan 25 '16 18:01 fend25

Haha, thought yesterday how great it would be having some views bound to routes. :D

A good basic router implementation would be this: http://krasimirtsonev.com/blog/article/A-modern-JavaScript-router-in-100-lines-history-api-pushState-hash-url

My approach would use regexes as parameter of a directive:

<div al-router-view="/main/?">
  <a href="/user/1">User 1</a>
</div>
<div al-router-view="/user/:id/?">
  <a href="/main">Back to overview</a>
</div>

This would allow to define the urls directly in the template, no additional code would be needed for basic features. But there should definitivly be an API to do redirects, handle url errors etc.

dev-rke avatar Jan 25 '16 21:01 dev-rke

Oleg's reference implementation (ancient one) http://plnkr.co/edit/QYb53NUnnhIezI5n0JxB?p=preview

fend25 avatar Jan 25 '16 21:01 fend25

Ok, i just build an absolute simple, quick n dirty views implementation: https://jsfiddle.net/r9t3ropc/

Some explanation: the al-router-view directive could be extended to watch the expression. So one could define the url structure in javascript or the url of a view could be changed:

<div al-router-view="myVar.main">Main</div>
<div al-router-view="myVar.user">User</div>
scope.myVar = {
  main: '/main',
  user: '/user'
}

Comments are welcome.

dev-rke avatar Jan 26 '16 00:01 dev-rke

I think it should be separated into two different API's:

  • Router is for router events and state handling
  • View rendering route data into template and managing scope.

The first should have unified interface to change current location and update/rewrite state. Also it should be presented in root scope and should broadcast events. Events could be stopped to prevent page reloading when there is unsaved changes or so.

View should allow to use functions as route value. This functions return async operations to load template and resource specified by route. This is needed to render state dependent routes. By default view listening scope's $routeChanged event. But changes source could be configurable to use exact router instance.

<al-app>
    <al-view al-view.routes = "routes"/>
</al-app>
alight.bootstrap('al-app', {
    routes : {
        // Simple template route
        '/': 'index.html',

        // State dependent route
        '/home': function(route, scope){
            if (scope.user) {
                return {
                    scope: {
                        user: user
                    },
                    template: '/users/home.html'
                }
            } else {
                return '/sign-in.html'
            }
        },
        // Async route
        '/users/:id' : route => {
            var user = users.get(route.id);

            var promise;

            if (! user) {
                promise = loadUser(route.id).then(user => {
                    if (! user) {
                        // User not found, show 404 error page
                        return '/errors/404.html';
                    } else {
                        // User found, use user page template
                        return {
                            scope: {
                                user
                            },
                            template: '/users/item.html'
                        }
                    }
                });
            } else {
                promise = Promise.resolve(user);
            }

            return promise;
        }
    }
});

Also view could has loading view with spinner to indicate intermediate state while resolving promise.

rumkin avatar Feb 15 '16 13:02 rumkin

@rumkin Can I use common header and footer for "/users/*" in your approach. In this example by @fend25 I can do this: https://jsfiddle.net/fend25/e1LLu72c/

Events could be stopped to prevent page reloading when there is unsaved changes or so.

It's @onLoseFocus="showPreventWindow()" in the example.

@onSwitchTo="onSwtichToUser()" could be something like this:

scope.onSwtichToUser = route => {
            var user = users.get(route.id).then

            var promise;

            if (! user) {
                promise = loadUser(route.id).then(user => {
                    if (! user) {
                        // User not found, show 404 error page
                        return '/errors/404.html';
                    } else {
                        // User found, use user page template
                        return {
                            scope: {
                                user
                            },
                            template: '/users/item.html'
                        }
                    }
                });
            } else {
                promise = Promise.resolve(user);
            }

            return promise;
}

What do you think? "Configuring" in html can be easier.

lega911 avatar Feb 15 '16 16:02 lega911

@lega911 Yep, it could be done with several view elements. Like this:

<al-view al-view-routes="routes.head">
<al-view al-view-routes="routes.body">
<al-view al-view-routes="routes.footer">

It's not so elegant as HTML configuration but it simply to manage from code. I think configuring in HTML is the most proper way to do this for MVVM app. But it should be also configurable and controllable from code.

@ is not valid HTML attribute name character. Is this some kind of future DSL?

rumkin avatar Feb 15 '16 17:02 rumkin

I think it's better to configure router with html and implement fully controllable router and view interfaces with focus on usability.

rumkin avatar Feb 15 '16 17:02 rumkin

@ is not valid HTML attribute name character. Is this some kind of future DSL?

Yes, but we can make it like this (with the event directive): al-on:on-lose-focus="showPreventWindow()" or alias @on-lose-focus="showPreventWindow()"

and "on-lose-focus" is an event (new CustomEvent), which you can catch even with jQuery. https://github.com/lega911/angular-light/issues/123

lega911 avatar Feb 15 '16 18:02 lega911

Maybe it's better to avoid on duplication and bind events to target component to avoid mess of events in the future:

al-router:on-reload-requested
al-input:on-lost-focus

rumkin avatar Feb 15 '16 18:02 rumkin

Maybe, but custom events can be more flexible.

lega911 avatar Feb 15 '16 19:02 lega911

May it possible to implement url handling like express.js? Another point is, that using Views which use "templates" would not be useful, because in this case i could redirect the user to a page which shows the content of this "template". Views should be used inline, to easily switch between views by a changing url. I also think it is not useful to bind a view to a scope variable.

My approach uses a global routes object, where regexes are defined. Using al-route or by adding routes to the object, new routes can be defined. Also a custom route can be defined in this object, which will be referenced by a given element selector.

<div id="home"></div>
<div id="players"></div>
<div al-route="/users/:user"></div>
alight.routes = {
  '/home': (scope) ->
    return '#home'
  '/players/:id': (scope, id) ->
    scope.id = id
    return '#players'
}
alight.d.al.route = (scope, exp, element, env) ->
  alight.routes[exp] = ->
    return element

What do you think?

dev-rke avatar Feb 16 '16 00:02 dev-rke

My approach uses a global routes object,

I agree

div id="home"></div>
<div id="players"></div>
<div al-route="/users/:user"></div>```

should it work like al-if with dependency on url?

lega911 avatar Feb 16 '16 05:02 lega911

What about this one?

<div>
  <div al-route:group @switch-to="onSwtichToAuth()">
    <div al-route="/login" al-ctrl="App"/>
    <div al-route="/register" al-ctrl="App"/>
  </div>
  <div al-route:group @switch-to="onSwtichToApp()" @lose-focus="showPreventWindow()">
    <Header/>
    <div al-route="/posts" al-ctrl="Posts"/>
    <div al-route="/feed" al-ctrl="Feed"/>
    <div al-route="/user">
      <div al-ctrl="userList"></div>
      <div al-route="/user/:userId" al-ctrl="userItem" @lose-focus="checkIfUserSaved()">
        <div al-if="!user">Loading...</div>
        <div al-if="user" al-include="/templates/userItem.html"></div>
      </div>
    </div>
    <div al-route="*">
      No page 404
    </div>
    <Footer/>
  </div>
</div>

when url = "/user/:userId", then controller al-ctrl="userItem" is activated, there you can load a user, a template can be loaded by al-include when a user is loaded <div al-if="user" al-include="/templates/userItem.html"></div>, and you can see this before user is loaded <div al-if="!user">Loading...</div>

so, visible html for /user/:userid would be

<div>
  <div al-route:group @switch-to="onSwtichToApp()" @lose-focus="showPreventWindow()">
    <Header/>
    <div al-route="/user">
      <div al-ctrl="userList"></div>
      <div al-route="/user/:userId" al-ctrl="userItem" @lose-focus="checkIfUserSaved()">
        <div al-if="!user">Loading...</div>
        <div al-if="user" al-include="/templates/userItem.html"></div>
      </div>
    </div>
    <Footer/>
  </div>
</div>

visible html for /user would be

<div>
  <div al-route:group @switch-to="onSwtichToApp()" @lose-focus="showPreventWindow()">
    <Header/>
    <div al-route="/user">
      <div al-ctrl="userList"></div>
    </div>
    <Footer/>
  </div>
</div>

visible html for /login would be

<div>
  <div al-route:group @switch-to="onSwtichToAuth()">
    <div al-route="/login" al-ctrl="App"/>
  </div>
</div>

lega911 avatar Feb 16 '16 05:02 lega911

I agree. It looks exciting. And it should to allow to load subroutes with al-include for complex components.

rumkin avatar Feb 16 '16 08:02 rumkin

@rumkin al-include is deprecated, look at http://angular-light.readthedocs.org/en/latest/directive/html.html , it's more flexible :)

lega911 avatar Feb 16 '16 08:02 lega911

@lega911 I will. But you're using al-include in your example.

Let's focus on modular structure, easy of use and flexibility. And thus it should to use al-html to load subroutes.

rumkin avatar Feb 16 '16 09:02 rumkin

should it work like al-if with dependency on url?

Yes, you have a pretty good understanding of what i mean. :-)

al-route:group

I do not understand how this works. I understand that it groups some requests. But how is defined that a specific request should be used? Which request is the first if you are visiting url "/"?

What i also thought about are subroutes:

<div al-route="/user">
  <div al-route="/:id"></div>
  <div al-route="/details"></div>
</div>

Which would be matched when accessing: /user /user/:id /user/details

Maybe it would be great to also handle redirects in the global route object:

alight.routes = {
  '/home': '/'
  '/players/:id': (scope, id) ->
    scope.id = id
    return '#players'
}

which would redirect "/home" to "/".

dev-rke avatar Feb 17 '16 09:02 dev-rke

al-route:group I do not understand how this works.

It's easy, one rule - if there is a route that maches to url, then it should be visible, then all parent DOM should be visible, other route-elements should be hidden. I changed your example in this style:

<div al-route>
  <header />
  <div al-route="/profile"></div>
  <div al-route="/user/:id/view"></div>
  <div al-route="/user/details"></div>
  <footer />
</div>

for url "/user/details" we will have:

<div al-route>
  <header />
  <div al-route="/user/details"></div>
  <footer />
</div>

In your approach we have to copy-past header and footer for "/profile". On the other side, your approach works well for loaded html (al-html).

lega911 avatar Feb 17 '16 10:02 lega911

Hm. This is a bit confusing for me. The grouping attribute is there to render a specific header an a footer, right? The content for a route is inside of

<div al-route="/profile">
  <h1>My user profile</h1>
</div>

So when accessing /profile i will see this:

<header />
<div al-route="/profile">
  <h1>My user profile</h1>
</div>
<footer/>

Am i right?

dev-rke avatar Feb 17 '16 10:02 dev-rke

So when accessing /profile i will see this: Am i right?

Yes. Actually for example above we don't need a "group" route:

<div>
  <header />
  <div al-route="/profile"></div>
  <div al-route="/user/:id/view"></div>
  <div al-route="/user/details"></div>
  <footer />
</div>

In a simple, "group route" just holds all child routes, like al-route="/profile; /user/:id/view; /user/details", but it takes them automatically.

lega911 avatar Feb 17 '16 10:02 lega911

The grouping attribute is there to render a specific header an a footer, right?

Not exactly, it renders all DOM, except routes that don't match to URL

lega911 avatar Feb 17 '16 10:02 lega911

Not exactly, it renders all DOM, except routes that don't match to URL

I like the approach. :-)

dev-rke avatar Feb 17 '16 12:02 dev-rke

Prototype: http://plnkr.co/edit/lZkXWXg5xk1J147nb1jX embeded: http://run.plnkr.co/plunks/lZkXWXg5xk1J147nb1jX/

with no regex yet.

lega911 avatar Feb 17 '16 18:02 lega911

There is an regex router with params and tail capturing: https://jsfiddle.net/rumkin/y6xLu1sm/

rumkin avatar Feb 17 '16 19:02 rumkin

Here an regex router with params and tail capturing:

I took it for my example

lega911 avatar Feb 17 '16 19:02 lega911

You can get arguments using event al-on.route-to

<div al-route="/user/:name/view" @route-to="username=$event.value.name">

lega911 avatar Feb 17 '16 19:02 lega911

One more example: http://plnkr.co/edit/pQBycumCKE1DJb3oVaPE?p=preview

  • you can use the same routes a few times in different places (al-route="/user/details" in the example)
  • default route
        <div al-route="*">
          404 - Not found
        </div>

lega911 avatar Feb 17 '16 21:02 lega911

You can get arguments using event al-on.route-to

Why not providing these variables automatically in a scope variable? E.g.

scope.$route.name

Parsing from URL shouldn't be so difficult:

"/user/:id/name/:name/view/:action".match(/:\w+/g)

dev-rke avatar Feb 18 '16 09:02 dev-rke

Why not providing these variables automatically in a scope variable?

It can give you collisions for routes "/users/:name", "/users/root" they both will be visible on url="/users/root"

lega911 avatar Feb 18 '16 12:02 lega911