lega911 / angular-light

Alight is a library for building interactive MVVM web interfaces/applications. (project is deprecated)
MIT License
274 stars 43 forks source link

router #137

Open lega911 opened 8 years ago

lega911 commented 8 years ago

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

fend25 commented 8 years ago

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

guys, feel free to comment)

dev-rke commented 8 years ago

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.

fend25 commented 8 years ago

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

dev-rke commented 8 years ago

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.

rumkin commented 8 years ago

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

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.

lega911 commented 8 years ago

@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.

rumkin commented 8 years ago

@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 commented 8 years ago

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

lega911 commented 8 years ago

@ 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

rumkin commented 8 years ago

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
lega911 commented 8 years ago

Maybe, but custom events can be more flexible.

dev-rke commented 8 years ago

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?

lega911 commented 8 years ago

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 commented 8 years ago

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>
rumkin commented 8 years ago

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

lega911 commented 8 years ago

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

rumkin commented 8 years ago

@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.

dev-rke commented 8 years ago

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 "/".

lega911 commented 8 years ago

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).

dev-rke commented 8 years ago

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?

lega911 commented 8 years ago

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 commented 8 years ago

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

dev-rke commented 8 years ago

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

I like the approach. :-)

lega911 commented 8 years ago

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

with no regex yet.

rumkin commented 8 years ago

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

lega911 commented 8 years ago

Here an regex router with params and tail capturing:

I took it for my example

lega911 commented 8 years ago

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

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

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

        <div al-route="*">
          404 - Not found
        </div>
dev-rke commented 8 years ago

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)
lega911 commented 8 years ago

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"

rumkin commented 8 years ago

@dev-rke It could be done manually with hooks. And sometimes it could cause type mismatch.

I prefer to assign route params to scope.$route.

dev-rke commented 8 years ago

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

No, it should not. When you provide for each route a own scope, e.g.

<div>
  <header />
  <div al-route="/profile/:profileID">
    {{$route.profileID}}
  </div>
  <div al-route="/user/:id/view">
    {{$route.id}}
  </div>
  <div al-route="/user/details/booking/:id">
    {{$route.id}}
  </div>
  <footer />
</div>

Then i see no problems. :-)

lega911 commented 8 years ago

No, it should not. When you provide for each route a own scope, e.g.

Ok, I don't see normal cases for collision, so $route should be ok. But I don't want al-route makes a new scope, you can use al-ctrl if you want a new scope, also in this case you don't need @route-to because your controller be called when route is active.

lega911 commented 8 years ago

$route is ready http://plnkr.co/edit/pQBycumCKE1DJb3oVaPE?p=preview

lega911 commented 8 years ago

http://plnkr.co/edit/pQBycumCKE1DJb3oVaPE?p=preview embedded: http://run.plnkr.co/plunks/pQBycumCKE1DJb3oVaPE/

what works: al-route - define route (can be empty) al-route:default - it's activated on undefined url (for 404) al-route-out="onOut()" - return true if you want prevent url be changed al-on.route-to - event when route is activated, you can use al-ctrl instead of it. scope.$route - contains arguments from url

what else?

lega911 commented 8 years ago

what about "href" ?

<a al-link="/login">Login</a>
<a al-router-link="/login">Login</a>
<a al-router-href="/login">Login</a>

Also we have js API:

service.location = {
    go(link)  // move to link
    subscribe(fn, [flag])  // subscribe on changes url, on 404 page, out-event
    unsubscribe(fn, [flag])
    getCurrentUrl()
    isDefault()  // returns true if it's 404
}
rumkin commented 8 years ago

What about routes that should not be routed and need page reloading? If I wish to serve /blog with another lib or have sub applications?

<ul al-router>
    <li al-route="/blog/**" al-route.reload></li>
</ul>
lega911 commented 8 years ago

need page reloading

you can use usual <a href=""> to reload the page.

If I wish to serve /blog with another lib or have sub applications? al-route="/blog/**"

Your example works, but router should be only one. Your sub applications can use API: location.subscribe()

rumkin commented 8 years ago

you can use usual<a href=""> to reload the page.

ok

what about "href" ?

al-href?

lega911 commented 8 years ago

al-href?

Why not, this name is not busy.

fend25 commented 8 years ago

al-href will collide with :href, at least in mind.

AngularUI uses ui-sref attribute.

I vote for al-router-link. Although al-router-href is ok too.

lega911 commented 8 years ago

vuejs and A2 seems to use name "link", v-link, router-link

dev-rke commented 8 years ago

Hm, your examples do not work anymore, maybe due to the last release?

lega911 commented 8 years ago

It doesn't work in FF because of wrong transpiler, FF doesn't support sync xhr

dev-rke commented 8 years ago

I tried it in Chrome, it works fine. I like the defined behaviour.

What do you think about prefixing normal href attributes using "#" instead of introducing something like al-link? When a href attribute is prefixed with "#" the attribute gets handled by alight. If html5 push states are used, the prefix get removed before applying the url.

lega911 commented 8 years ago

Do you mean?

<a href="#/user/profile">Profile</a>

Interesting, what about buttons and other?

<button href="#/user/profile">Profile</button>
<div href="#/user/profile">Profile</div>
<li href="#/user/profile">Profile</li>
rumkin commented 8 years ago

Shebang is using to navigate across webview. This behaviour will be broken. I think al-href is good enough. It short and clean. It refers to default href which it actually is.

fend25 commented 8 years ago

What do you think about prefixing normal href attributes using "#" instead of introducing something like al-link?

it will bring some strange logic to the service.go processor. But the most important thing is that it will collide and brake specs about hashes in url. And what if I want to use hashes?

I think, special directive is the best way because it won't collide with specs nor with other things in mind. In appearance al-href is very similar to :html. And it hasn't any allusion to router. If nobody wants see al-router-href, maybe al-rhref?

New idea: make new namespace for router: r, or rt or something else?

Interesting, what about buttons and other? <button href="#/user/profile">Profile</button>

Why? it's totally confusing. And all workaround (like twitter bootstrap) is prepared for <li><a></a></li> structure.

dev-rke commented 8 years ago

@lega911

Interesting, what about buttons and other?

href is meant as compatibility mode. So for using buttons normally one would use a link element and style it like a button (compare twitter bootstrap). Also a button can be used with a form, so the form action also should be handled.

I also think it would not be necessary to prefix links using a hash. As we have a global routing object, we can check each click event of a link in the current scope, if the url matches to a defined route. E.g.

<a href="/user">Will show /user route element</a>
<a href="/profile">There is no matching route, so page /profile will load</a>

<div al-route="/user"></div>

There should be no problems and it is easy to implement. Also it would be good for SEO purposes, because one can use prerender.io and all links should be accessible.

@rumkin

Shebang is using to navigate across webview.

Can you please give an example? I did not understand what the problem is. Maybe its obsolete when we check a clicked link against our routes (see above).

@fend25

But the most important thing is that it will collide and brake specs about hashes in url. And what if I want to use hashes?

Why would it break? Using views requires to use hashes or html5 push states, because they are bound to the url. When someone uses a link outside of defined routes, the link would not be handled by alight. The great benefit would be, that someone can define "hard" links (e.g. for SEO purposes) which are handled by javascript, if the client supports javascript and fallback to normal links, if no javascript is used.

lega911 commented 8 years ago

if the client supports javascript and fallback to normal links, if no javascript is used.

What if I need a normal link, but I use javascript.

<a href="/profile">There is no matching route, so page /profile will load</a>

You can do this with al-route:default