AngularJS route parameters with any characters
Asked Answered
D

4

8

I am new to AngularJS, so forgive me if this is obvious, but I am looking for someone who can answer this tricky question. I am implementing an application, and need to pass some parameters to a particular view to display details about a book. Basically I would like to be able to use the following routing expressions:

bookApp.config(['$routeProvider', function($routeProvider) {
    $routeProvider.
        when('/catalog', {
            templateUrl: 'cataloglist.htm',
            controller: 'catCtrl'
        }).
        when('/book/:title/:chapter', {
            template: 'chapterdetail.htm',
            controller: 'chapterCtrl'
        }).
        otherwise({
            template: 'oops ... do not understand that route',
        });
    }]);

The expression /book/:title/:chapter should allow me to pass in the name of the title of a book. I am expecting to be able to pass ANY title of any book in there. To make sure that things are properly delimited, I am going to URL encode the title value, so that in the encoded value there will be no slashes and the value will be clearly delimited by the slash characters. This is the normal way to construct URLs that contain values.

The problem is that there exist book titles that contain the slash character (e.g. The 3/5 solution) This is URL encoded as The+3%2F5+Solution. So can construct a URL like this:

 /app/#/book/The+3%2F5+Solution/The%20Beginning

However, my experience seems to show that the entire value is URL decoded BEFORE it is broken into parameters! This means that any data value with a slash in it, will be misinterpreted as two values, and the pattern matching of the route parameters is broken, and only the first half of the value is passed in. Furthermore, the chapter might have a slash in the name as well.

If I was making a REST service, I would URL encode the value, and the URL will be parsed into pieces BEFORE each piece is decoded. For example, I can use query parameters in a URL like this:

app.jsp?title=The+3%2F5+Solution&chapter=The%20Beginning

and this will work correctly. Using URL encoding, I can pass ANY string value in the title. I would have expected route parameters to do the same thing ... but I already mentioned I am NEW to AngularJS.

To decode the %2F into a slash BEFORE determining the pieces seems like a very serious bug. Apparently, you simply can't pass values with a slash in them as route parameters. Am I missing something here? What is the solution to allow me to safely pass a book name with ANY possible character in it (along with a chapter title with ANY character in it), as a route parameter?

Dailey answered 24/12, 2014 at 7:29 Comment(8)
likely better off just creating slugs for url's like most sites doLoma
If I understand your suggestion, a 'slug' is usually a non-reversable conversion such as used for blog post paths: you throw away all the difficult characters and spaces, and get a cleaned up path which is analogous to the orig value. However, I need to look the passed value up in a database, and while the book title is unique, a simplified version of the title is not necessarily unique, which might prevent me from being able to display information about a book. I simply want to pass a DB value exactly. Maybe it simply can't be done.Dailey
use an ID instead or create the slug when the title is added to db and use the slug as an index. The url doesn't have to contain the best identifier field for the databaseLoma
Well, yes, the whole theory behind a relational DB is that you use the actual key values in the DB to query the data. However, if route parameters are arbitrarily limited to exclude certain characters it means there are some DB values you can not search for. Forcing one to add another column with a "record number" just to get around the inability to pass a value as a parameter to the search screen seems like a rather drastic limitation. I can do that, but I would rather keep the DB designed the way it originally was.Dailey
you are totally mixing up a url with db fields. Why would creating slugs make a title less likely to be unique if it was looked up by slug? The url can be made to say /foo-man-chu but the ajax request uses its dewey(sic) decimal system number. remember that you have all the book data available in an object in your scopeLoma
You have to consider the algorithm that creates the slug. For example, the value "foo (man) chu" and "foo/man chu?" would be encoded into the same slug. Upon receiving the slug, you have no idea which was meant. In other words, the value passed ceases to be a value that can be used to uniquely identify the item you are to display. Again -- I can create a new column in the database, but it still seems like a big disadvantage that route parameters can not contain ALL string values.Dailey
you create it once and store it...game over. You are over complicating it. Since book titles may often collide (slug or no slug) you probably want some sort of absolutely unique identifier anyway. I have sites that the last part of the p[ath is completely meaningless in the app...I put human readable entities in them though as slugs for presentationLoma
There are lots of other other applications where you want to be able to pass values without them being corrupted. Consider a mail list application where you have a DB of email messages, and you have a display mode where you want all the messages with a particular subject. You really want to pass the subject, regardless of what characters are in it. Also, if you are writing a front end application that is mashing up data from multiple other web services, you again don't have the option of adding a column to all the DBs that exist out there.Dailey
D
12

Taking a look at the angular's route.js sourcecode, there's described a possibility to achieve what you are looking for:

path can contain named groups starting with a colon and ending with a star: e.g.:name*. All characters are eagerly stored in $routeParams under the given name when the route matches.

For example, routes like /color/:color/largecode/:largecode*\/edit will match /color/brown/largecode/code/with/slashes/edit and extract:
color: brown
largecode: code/with/slashes.

Notice the backslash at the end of the :largecode*\ param in the example. It's not present in the description of named groups ending with a star, but it's there in the example. I didn't test this and don't know whether that backslash is required, so take into account that it it might/might not be required.

Thus, a solution to your question will look like: /book/:title*/chapter/:chapter*

Notice the added /chapter/ part. It is required in order to differentiate between the two named groups. If you'll just use /book/:title*/:chapter*, everything will fall under the :title* named group. However, if you use /:title*/chapter/:chapter*, angular knows when :title* named group ends: when it encounters /chapter/ in the route. Everything after the /chapter/ will fall under the :chapter* named group

Demoss answered 24/12, 2014 at 8:7 Comment(6)
so long as you are careful with extra / such as in a book title and spaces in url's get ugly depending on browser tooLoma
@Loma I agree that a slug field or id would be more appropriate, but I've just answered the author's question.Demoss
oh absolutely. To be honest digging into the source was helpful. Good thinkingLoma
Nice try. That will work except when the title ends with /chapter and while I know that is exceedingly unlikely, it is still not 100%. I found another approach to use base64 encoding to avoid the problem altogether, and to be able to carry 100% of the values thrown at it. Maybe I am too picky, but I like to write code that can be proved to be 100% correct.Dailey
Good solution. The only downside is that you get ugly routes, which is not wrong in itself, but less aesthetically pleasant.Demoss
About the backslash: it is not required. It is an artifact of the way documentation is embedded in the comments.Plait
D
3

The appears to be that you can not use URL Encoding because of the behavior above. This means that you must use an encoding that will represent the full Unicode character set without ever using a slash or a percent sign or a plus sign nor anything else that might cause the URL decoder to think there is an encoded value there to be decoded.

BASE 64 encoding of the value will do the trick, and the solution seems to be to define the following filters:

    bookApp.filter('btoa', function() {
        return function (str) {
            return window.btoa(encodeURIComponent(escape(str)));
        }
    });

    bookApp.filter('atob', function() {
        return function(str) {
            return unescape(decodeURIComponent(window.atob(str)));
        }
    });

This allows you to write code like this:

<a href="#/book/{{title | atob}}/{{chapter | atob}}">See Details</a>

Then, on the receiving side, you call btoa to get the value back to use for whatever purpose you want. The value The 3/5 solution is then encoded as VGhlJTI1MjAzJTJGNSUyNTIwc29sdXRpb24= which will always be a single parameter in the route parameter pattern recognizer, and is decoded exactly back into the value that was sent.

Full support for languages. A book named 奥の細道 will be encoded as JTI1dTU5NjUlMjV1MzA2RSUyNXU3RDMwJTI1dTkwNTM= -- again avoiding any problem that might corrupt the value.

The extra encodeURIComponent and escape calls are there to trick the JS to convert the unicode string into UTF-8 encoding, which is then transformed by the window.atob function, which would otherwise fail with certain higher order Unicode characters. More information at Mozilla Developer Network

Dailey answered 24/12, 2014 at 8:54 Comment(0)
P
1

Relevant github issue: https://github.com/angular/angular.js/issues/10479 Workaround is to encode param twice:

encodeURIComponent(encodeURIComponent("The 3/5 solution"))
Parvis answered 5/12, 2018 at 14:5 Comment(0)
O
0

when('/catalog/:book', { templateUrl: 'cataloglist.htm', controller: 'catCtrl' })

if your routeparameter book is encrypted it may contain '/' (backslash character), inorder to convert it proper executable in browser you must use encodeURIComponent which converts '/' to '%2F'.

$window.location = '/catalog/'+encodeURIComponent(Solution/The%20Beginning);

Orgasm answered 13/12, 2017 at 3:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.