Skip to content
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

Typed and optional parameters #1032

Merged
merged 25 commits into from
Apr 19, 2014
Merged

Typed and optional parameters #1032

merged 25 commits into from
Apr 19, 2014

Conversation

nateabele
Copy link
Contributor

I have overhauled the URL-matching system

Here's a run-down of the new goodies and various other relevant notes:

  • [BC-BREAK]: params is no longer mutually exclusive with url, and instead of being an array, it is now an object where the keys are parameter names and the values are the parameter's configuration
  • Optional parameters / default values: adding params: { foo: { value: null } } makes the foo parameter optional in a URL — changing null to, for example, "bar", will populate $stateParams.foo with "bar" when no value is supplied (see the tests for extended examples)
  • [BC-BREAK]: $urlMatcherFactoryProvider.caseInsensitiveMatch() is now $urlMatcherFactoryProvider.caseInsensitive(), and the second parameter of UrlMatcher's constructor is an object hash rather than a boolean
  • Strict mode (and consequently, non-strict mode): users now have the option of disabling 'strict mode', which allows UrlMatchers to match URLs with or without a trailing slash — see $urlMatcherFactoryProvider.strictMode()
  • Deferred location change interception for $urlRouter — this isn't directly related, but I snuck it in anyway: users now have the ability to delay $urlRouter from binding to $locationChangeSuccess, allowing the injection of custom behavior prior to syncing the state machine to the current URL.. see the docs for more info
  • Custom parameter types: the ability to define custom type objects that help eliminate boilerplate in controllers and resolves by encapsulating common patterns for serializing and deserializing URL parameters; includes built-in types for several JS primitives; see the docs for more info & examples
  • Lots of cleanup, tests, and docs

[BREAK] This is a breaking change: state parameters are no longer automatically coerced to strings, and unspecified parameter values are now set to undefined rather than null.
Implements optional parameters and default parameter values. [BC-BREAK]: the `params` option in state configurations must now be an object keyed by parameter name.
$urlRouter's listener can now be made to take lower priority than custom listeners by calling `$urlRouterProvider.deferIntercept()` to detach the listener, and `$urlRouter.listen()` to reattach it.
Implements strict (and non-strict) matching, for configuring whether UrlMatchers should treat URLs with and without trailing slashes identically.
urlMatcherFactoryProvider.isMatcher() now validates the entire UrlMatcher interface.
When defining parameter configurations, `{ param: "default" }` is now equivalent to `{ param: { value: "default" }}`.
Adds documentation for the Type object, and misc. cleanup and improvement of UrlMatcher and $urlMatcherFactory docs.
Correctly detects injectable functions annotated as arrays when registering new type definitions.
@nateabele
Copy link
Contributor Author

This PR is a call for comments before I move onto the next steps, which are dynamic/observable parameters, moving the rest of template/uiView handling to the $view service, and refactoring $state's internals.

cc: @legomind, @stereokai, @jshado1, @timcharper, @kevinrenskers, @timkindberg, @ProLoser

@SimplGy
Copy link

SimplGy commented Apr 17, 2014

This is awesome.

Params optional by default? 👍
Params as an object, not in a route/regex? 👍
Match with/without trailing slash? 👍

@nateabele
Copy link
Contributor Author

Params optional by default?

@SimpleAsCouldBe To be clear, params are not optional by default, they're only made that way when assigned a default value.

@SimplGy
Copy link

SimplGy commented Apr 18, 2014

Hmmm. Why not always optional, and leave it up to app code to manage? Breaking change reasons?

I guess as long as I can default it to undefined, I'm all good. What's with the extra layer of value? Are there other properties of params I'm likely to specify? I see maybe some type encoding in here. A shorthand syntax might be nice though.

params: { openWindow: { value: 3 } }
params: { openWindow: 3 } // same same?

@nateabele
Copy link
Contributor Author

Hmmm. Why not always optional, and leave it up to app code to manage?

I don't understand what this means. It's up to app code to manage either way, but if all parameters are optional, and I have to check to make sure something is not null or undefined every time I want to use it, that sounds like a massive pain in the ass.

I guess as long as I can default it to undefined, I'm all good.

See bullet point 2.

What's with the extra layer of value? Are there other properties of params I'm likely to specify?

Yes.

A shorthand syntax might be nice though.

See 5b72430.

@timkindberg
Copy link
Contributor

Friggin Awesome 🍻 I will review in more detail this weekend.

@stereokai
Copy link

Outstanding work Nate.

Hmmm. Why not always optional, and leave it up to app code to manage? Breaking change reasons?

This breaks a lot of common sense and brings back questions from the original discussion, which are solved in this PR, just for example: How do you tackle slashes when there are multiple optional parameters in a given url, and a value is only passed to the latter?

@nateabele Great work on this one. I could find only one place in the specs, where you handle null values. In my implementation - all optional parameters are null by default - but it seemed in your tests they are populated with consecutive numbers (1, 2, 3, 4, etc).

The whole purpose in oppams for our project was that we could have URLs like these:

/userprofile/photos/ --> the resolved would pick up user id from the session
/userprofile/6afb36/photos/ --> the resolved would pull this user's photos from the server

That was my guideline when working on my solutions - have you considered anything like this? Cheers!

@JakobJingleheimer
Copy link
Contributor

@crisbeto in your route you specify :id, but you refer to it as param in your params object. Change params.param to params.id

@nateabele
Copy link
Contributor Author

@crisbeto I'm not sure why you would expect a default parameter value to redirect to a different URL...?

@crisbeto
Copy link

crisbeto commented Sep 7, 2014

@nateabele I was expecting it to "fill" out the missing place in the URL, but I suppose that wasn't the intended functionality in the first place and could would be worked around.

@ghost
Copy link

ghost commented Sep 8, 2014

OK, I got an interesting case. I'm trying to forward an old URL to the new one. My state looks something like:
.state('topics', { url: '/topics', abstract: true })
.state('topics.show', {
url: '/:id?start&end',
params: {
id: {},
start: { value: null },
end: { value: null}
},
...
});

So the URL is: /topics/:id?start&end
Where the query string are the optional params.
The previous url was: /shows/:id?start&end
So I want to use $urlRouterProvider to forward the previous URL to the new one, so I did:

$urlRouterProvider.when('/shows/:id', '/topics/:id')

I left out start and end since they were optional, and since they were a querystring, I was hoping they would just be copied over if there. Sadly when I go to the previous URL with the optional params, it removes them... but I'm assuming if I set the optional params in the .when() ... then if they are not there, it won't catch it.

Any ideas?

@nateabele
Copy link
Contributor Author

Not sure, never tried that. Maybe look at the docs for $urlRouterProvider.when() and see if you can figure it out. If you can't get it to work, maybe try creating a plunkr and submitting it with a new issue.

@ghost
Copy link

ghost commented Sep 9, 2014

I think I have a solution. Our setup now has all optional params in the query string. So, I should be able to write a relatively simple regex that looks for /shows/:id[?.*] (been lazy on my regex's, so will have to do a quick reference to write the correct one to look for an option '?' followed by anything.

@FoxxMD
Copy link

FoxxMD commented Sep 9, 2014

@bobmash I don't think(know) if you can pass parameters using a regex capture group using $urlRouterProvider, so you may need to write the handler as a function.

Also, because using .* in a regex pattern captures everything after that point in the url if you plan on building the url farther you'll need to use a different pattern to make sure you don't capture the rest of the URL into one parameter.

@JakobJingleheimer
Copy link
Contributor

@bobmash Did you try when('shows/:id?start&end', 'topics/:id?start&end') ? Otherwise, create a handler function, inject $location, and append $location.search() to the string you return.

@amcdnl
Copy link
Contributor

amcdnl commented Sep 10, 2014

I found a new bug around this today. If I have the following routes:

$stateProvider.state('search', {
    url: '/search/{appId}/{reportId}',
    params: { 
        appId: { },
        reportId: { value: null } 
    }
});

$stateProvider.state('search.stats', {
    url: '/stats',
    params: { 
        appId: {},
        reportId: { value: null }
    }
});

$stateProvider.state('search.detail', {
    url: '/detail/{recordId}',
    params: { 
        appId: {},
        reportId: { value: null },
        recordId: {}
    }
});

and then I do:

$state.go('search.detail', {
    appId: app.id,
    reportId: report.id,
    recordId: model.id
});

Result: http://windows.local/app/search/53dbce9dd273a802ec9e4b85//detail/53dbd091d273a802ec9e4ba7

things work fine, but if I do:

this.$state.go('search.detail', {
    appId: this.$.app.id,
    recordId: model.id
});

despite reportId being defined as null, I still get:

Result: http://windows.local/app/search/53dbce9dd273a802ec9e4b85//53dbd091d273a802ec9e4ba7

which resolves to the first route not my 'detail' route. If I set reportId as null it works fine though. Probably need to add some checks for undefined instead of just null.

@nateabele
Copy link
Contributor Author

@amcdnl Please open a new issue with a Plunkr that demonstrates it. Thanks.

@futurechan
Copy link

This is awesome.

@JakobJingleheimer
Copy link
Contributor

Are optional params not allowed on abstract states?

$stateProvider
.state('root', {
    abstract: true,
    url: '/:locale',
    params: {
        locale: [
            '$preferences',
            function setLocaleParam($preference) {
                var locale = $preference.get('locale');
                return locale;
            }
        ]
    }
});

urlMatcherFactory.js#L509 does not even get called with the above (but it does for non-abstract states).

@JakobJingleheimer
Copy link
Contributor

Ah, it seems optional params cannot be set on an abstract parent state: jsfiddle (comment out abstract: true in base state, and it works). Also, the a param declared on an ancestor must be re-stated in the params hash on all of its children (no new value required)—not a blocker, but not ideal (and not even remotely suggested in the documentation).

@futurechan
Copy link

I ran into the same problem as @jshado1 . I'd really like to see support for param inheritance.

@WayneFurmidge
Copy link

@amcdnl was the issue of it retaining parameters when using a go transition ever raised as another bug, as we are seeing the same thing

@amcdnl
Copy link
Contributor

amcdnl commented Oct 14, 2014

@WayneFurmidge Ugh, I forgot :( ... you can log your case

@christopherthielen
Copy link
Contributor

are any of you successfully using Typed params? I think there is a bug that stops typed param registry from working at all.

@intellix
Copy link

intellix commented Nov 7, 2014

I see alot of "see the docs for more info & examples" but am not seeing any of this information in the docs anywhere. In fact, I didn't even know about any of this until I saw this PR.

params - {object=} - An array of parameter names or regular expressions. Only use this within a state if you are not using url. Otherwise you can specify your parameters within the url. When a state is navigated or transitioned to, the $stateParams service will be populated with any parameters that were passed.

I'm not sure where the docs are generated, the ngdocs branch of ui-router says 0.2.8

@amcdnl
Copy link
Contributor

amcdnl commented Nov 21, 2014

After updating to 12 and 13, I'm getting unmatched params on the following route:

$stateProvider.state('record', {
    url: '/record/{appId}/{recordId:[0-9a-fA-F]{10,24}}',
    params: { 
        appId: { value: null },
        recordId: { value: null }
    },
    views:{
        '': {
            controller: 'RecordCtrl',
            templateUrl: 'app/record/record.tpl.html'
        }
    }
});

when I goto a url like:

<a href="/myapp/record/546a3e4dd273c60780e35df3/">

was there an API change in that version that I missed?

@christopherthielen
Copy link
Contributor

@amcdnl you're expecting that url to match {appId: null, recordId: 546a3e4dd273c60780e35df3}? See the discussion in #1501 around parameter squashing. Mark appId as {squash: true} and it should route again.

@christopherthielen
Copy link
Contributor

@intellix I updated the docs for 0.2.12 release

@amcdnl
Copy link
Contributor

amcdnl commented Nov 21, 2014

@christopherthielen I'm expecting appId to be there and recordId to be undefined. Would the same still apply?

@christopherthielen
Copy link
Contributor

@amcdnl Ah, I now see you have a trailing slash on your URL.

I think that pattern should be matching that URL. Open a new issue, please. That looks like a bug, if it's not matching

@amcdnl
Copy link
Contributor

amcdnl commented Nov 21, 2014

@christopherthielen Ah, your right. #1576

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.