From aeb1137a6db90f8a0d0192ca488397993befe49e Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Fri, 9 Oct 2020 09:55:29 -0400 Subject: [PATCH] Improve internal route and parameter parsing (#330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tokenize all route template parameter segments (e.g. `{post?}`) into descriptive objects: - immediately extract the name of every parameter present in the route template - check what the type of the passed parameters *is* (instead of what it *isn't*) so that we can wrap strings and integers in an array and completely ignore objects - pre-fill default parameters immediately - remove the numericParamIndices flag and conditionals that check for it - remove conditionals that depend on missing parameter key names, since there now shouldn't ever be any * Fix default parameter assignment and merging, formatting * Refactor core parameter matching logic: - use route model binding keys more frequently now that they're always available - use type checks to return early for non-object parameter values - don't modify the parameters object with `delete` during matching - normalize single object parameters properly * Replace UrlBuilder class with simplified getters on main Router class - remove unreachable error thrown if the provided `name` is undefined (called from UrlBuilder which was only instantiated at all if `name` was set) - return early from hydrateUrl if there are no template segments to replace - error as early as possible on non-existent routes - remove check for pre-existing hydrated URL (this never happened) - formatting * Formatting * Add support for parsing query parameters out of current URL, formatting, update tests * Re-implement current route checking with full support for partially matching params - Remove matchUrl method (undocumented, only used internally, and pretty much useless on its own) - Refactor some internal methods to accept a route so they can be used in any context - Match passed params partially against current route - Add option to match passed params exactly when necessary - Remove normalizeParams method and replace with internal _parseParameters() - Formatting and renaming things * Cleanup - extract new substituteBindings helper and replace switch statements - extract new dehydrate helper - add comments and jsdocs - improve error messages - rename and deprecate things, mostly maintaining backwards compatibility - add class properties - rename internal configuration object from 'ziggy' to 'config' * Enable microbundle property mangling to reduce size and restrict internal APIs See /~https://github.com/developit/microbundle#mangling-properties * Formatting, errors, organization, and comments * Remove `exact` flag from `current()` * Add tests for errors, strings, and bindings * :) * Update description and tightenco → tighten * Remove comment * Cleanup * Collapse everything into a simple new Route class * Clean up formatting and comments, rename _locationURL to _dehydrate * Check for Ziggy config on `globalThis` as a fallback * Remove tests for removed/deprecated methods * Parse the current URL once instead of for each route * Make config and queryParams private * Revert "Update description and tightenco → tighten" This reverts commit c22342c0ec88b1a26b87d81e99ac50d8811b9ccd. * Update Changelog * Add and improve comments, rename/reorder methods * Add implicit test coverage for default parameters * Allow fallback route model 'binding' of 'id' to increase backwards compatibility * Formatting --- CHANGELOG.md | 10 + package.json | 11 +- src/Ziggy.php | 2 +- src/js/UrlBuilder.js | 40 -- src/js/route.js | 522 ++++++++++++++--------- tests/Unit/CommandRouteGeneratorTest.php | 2 + tests/Unit/RouteModelBindingTest.php | 2 +- tests/Unit/ZiggyTest.php | 6 +- tests/fixtures/admin.js | 2 +- tests/fixtures/custom-url.js | 2 +- tests/fixtures/ziggy.js | 2 +- tests/js/route.test.js | 254 +++++++---- 12 files changed, 516 insertions(+), 339 deletions(-) delete mode 100644 src/js/UrlBuilder.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ac342eb3..9a61d9c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Breaking changes are marked with ⚠️. - Document the `check()` method ([#294](/~https://github.com/tighten/ziggy/pull/294)) and how to install and use Ziggy via `npm` and over a CDN ([#299](/~https://github.com/tighten/ziggy/pull/299)) - Add support for [custom scoped route model binding](https://laravel.com/docs/7.x/routing#implicit-binding), e.g. `/users/{user}/posts/{post:slug}` ([#307](/~https://github.com/tighten/ziggy/pull/307)) - Add support for [implicit route model binding](https://laravel.com/docs/7.x/routing#implicit-binding) ([#315](/~https://github.com/tighten/ziggy/pull/315)) +- Add support for passing parameters to `current()` to check against the current URL in addition to the route name ([#330](/~https://github.com/tighten/ziggy/pull/330)) **Changed** @@ -26,10 +27,19 @@ Breaking changes are marked with ⚠️. - Use Jest instead of Mocha for JS tests ([#309](/~https://github.com/tighten/ziggy/pull/309)) - Use [microbundle](/~https://github.com/developit/microbundle) instead of Webpack to build and distribute Ziggy ([#312](/~https://github.com/tighten/ziggy/pull/312)) - ⚠️ Default Ziggy's `baseUrl` to the value of the `APP_URL` environment variable instead of `url('/')` ([#334](/~https://github.com/tighten/ziggy/pull/334)) +- ⚠️ Allow getting the route name with `current()` when the current URL has a query string ([#330](/~https://github.com/tighten/ziggy/pull/330)) + +**Deprecated** + +- Deprecate the `with()` and `check()` methods ([#330](/~https://github.com/tighten/ziggy/pull/330)) **Removed** - ⚠️ Remove `Route` Facade macros `Route::only()` and `Route::except()` (previously `Route::whitelist()` and `Route::blacklist()`) ([#306](/~https://github.com/tighten/ziggy/pull/306)) +- ⚠️ Remove the following undocumented public properties and methods from the `Router` class returned by the `route()` function ([#330](/~https://github.com/tighten/ziggy/pull/330)): + - `name`, `absolute`, `ziggy`, `urlBuilder`, `template`, `urlParams`, `queryParams`, and `hydrated` + - `normalizeParams()`, `hydrateUrl()`, `matchUrl()`, `constructQuery()`, `extractParams()`, `parse()`, and `trimParam()` +- ⚠️ Remove the `UrlBuilder` class ([#330](/~https://github.com/tighten/ziggy/pull/330)): **Fixed** diff --git a/package.json b/package.json index 5ddeec9f..42bdebd6 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "ziggy-js", "version": "0.9.1", - "description": "Generates a Blade directive exporting all of your named Laravel routes. Also provides a nice route() helper function in JavaScript.", + "description": "Use your Laravel named routes in JavaScript.", "keywords": [ "laravel", "routes", "ziggy" ], - "homepage": "/~https://github.com/tighten/ziggy#readme", + "homepage": "/~https://github.com/tighten/ziggy", "bugs": "/~https://github.com/tighten/ziggy/issues", "license": "MIT", "authors": [ @@ -18,6 +18,10 @@ { "name": "Jake Bathman", "email": "jake@tighten.co" + }, + { + "name": "Jacob Baker-Kretzmar", + "email": "jacob@tighten.co" } ], "files": [ @@ -40,6 +44,9 @@ "test": "jest", "prepublishOnly": "npm run build" }, + "mangle": { + "regex": "^_" + }, "dependencies": { "qs": "^6.8.0" }, diff --git a/src/Ziggy.php b/src/Ziggy.php index e1bd19fb..26b882a2 100644 --- a/src/Ziggy.php +++ b/src/Ziggy.php @@ -21,7 +21,7 @@ public function __construct(string $group = null, string $url = null) { $this->group = $group; - $this->baseUrl = Str::finish($url ?? config('app.url', url('/')), '/'); + $this->baseUrl = rtrim($url ?? config('app.url', url('/')), '/'); tap(parse_url($this->baseUrl), function ($url) { $this->baseProtocol = $url['scheme'] ?? 'http'; diff --git a/src/js/UrlBuilder.js b/src/js/UrlBuilder.js deleted file mode 100644 index 4e645ecc..00000000 --- a/src/js/UrlBuilder.js +++ /dev/null @@ -1,40 +0,0 @@ -class UrlBuilder { - constructor(name, absolute, ziggyObject) { - - this.name = name; - this.ziggy = ziggyObject; - this.route = this.ziggy.namedRoutes[this.name]; - - if (typeof this.name === 'undefined') { - throw new Error('Ziggy Error: You must provide a route name'); - } else if (typeof this.route === 'undefined') { - throw new Error(`Ziggy Error: route '${this.name}' is not found in the route list`); - } - - this.absolute = typeof absolute === 'undefined' ? true : absolute; - this.domain = this.setDomain(); - this.path = this.route.uri.replace(/^\//, ''); - } - - setDomain() { - if (!this.absolute) - return '/'; - - if (!this.route.domain) - return this.ziggy.baseUrl.replace(/\/?$/, '/'); - - let host = (this.route.domain || this.ziggy.baseDomain).replace(/\/+$/, ''); - - if (this.ziggy.basePort) { - host = `${host}:${this.ziggy.basePort}`; - } - - return this.ziggy.baseProtocol + '://' + host + '/'; - } - - construct() { - return this.domain + this.path - } -} - -export default UrlBuilder; diff --git a/src/js/route.js b/src/js/route.js index c9aca2c4..39ac20c1 100644 --- a/src/js/route.js +++ b/src/js/route.js @@ -1,267 +1,367 @@ -import UrlBuilder from './UrlBuilder'; -import { stringify } from 'qs'; - -class Router extends String { - constructor(name, params, absolute, customZiggy = null) { - super(); - +import { parse, stringify } from 'qs'; + +/** + * A Laravel route. This class represents one route and its configuration and metadata. + */ +class Route { + /** + * @param {String} name - Route name. + * @param {Object} definition - Route definition. + * @param {Object} config - Ziggy configuration. + */ + constructor(name, definition, config) { this.name = name; - this.absolute = absolute; - this.ziggy = customZiggy ? customZiggy : Ziggy; - this.urlBuilder = this.name ? new UrlBuilder(name, absolute, this.ziggy) : null; - this.template = this.urlBuilder ? this.urlBuilder.construct() : ''; - this.urlParams = this.normalizeParams(params); - this.queryParams = {}; - this.hydrated = ''; + this.definition = definition; + this.bindings = definition.bindings ?? {}; + this.config = { absolute: true, ...config }; } - normalizeParams(params) { - if (typeof params === 'undefined') return {}; - - // If you passed in a string or integer, wrap it in an array - params = typeof params !== 'object' ? [params] : params; - - this.numericParamIndices = Array.isArray(params); - return Object.assign({}, params); + /** + * Get a 'template' of the complete URL for this route. + * + * @example + * https://{team}.ziggy.dev/user/{user} + * + * @return {String} Route template. + */ + get template() { + // If we're building just a path there's no origin, otherwise: if this route has a + // domain configured we construct the origin with that, if not we use the app URL + const origin = !this.config.absolute ? '' : this.definition.domain + ? `${this.config.baseProtocol}://${this.definition.domain}${this.config.basePort ? `:${this.config.basePort}` : ''}` + : this.config.baseUrl; + + return `${origin}/${this.definition.uri}`; } - with(params) { - this.urlParams = this.normalizeParams(params); - return this; + /** + * Get an array of objects representing the parameters that this route accepts. + * + * @example + * [{ name: 'team', required: true }, { name: 'user', required: false }] + * + * @return {Array} Parameter segments. + */ + get parameterSegments() { + return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({ + name: segment.replace(/{|\??}/g, ''), + required: !/\?}$/.test(segment), + })) ?? []; } - withQuery(params) { - Object.assign(this.queryParams, params); - return this; + /** + * Get whether this route's template matches the given URL. + * + * @param {String} url - URL to check. + * @return {Boolean} Whether this route matches. + */ + matchesUrl(url) { + if (!this.definition.methods.includes('GET')) return false; + + // Transform the route's template into a regex that will match a hydrated URL, + // by replacing its parameter segments with matchers for parameter values + const pattern = this.template + .replace(/\/{[^}?]*\?}/g, '(\/[^/?]+)?') + .replace(/{[^}]+}/g, '[^/?]+') + .replace(/^\w+:\/\//, ''); + + return new RegExp(`^${pattern}$`).test(url.replace(/\/+$/, '').split('?').shift()); } - hydrateUrl() { - if (this.hydrated) return this.hydrated; - - let hydrated = this.template.replace( - /{([^}]+)}/gi, - (tag, i) => { - let keyName = this.trimParam(tag), - defaultParameter, - tagValue; + /** + * Hydrate and return a complete URL for this route with the given parameters. + * + * @param {Object} params + * @return {String} + */ + compile(params) { + if (!this.parameterSegments.length) return this.template.replace(/\/+$/, ''); + + return this.template.replace(/{([^}?]+)\??}/g, (_, segment) => { + // If the parameter is missing but is not optional, throw an error + if ([null, undefined].includes(params[segment]) && this.parameterSegments.find(({ name }) => name === segment).required) { + throw new Error(`Ziggy error: '${segment}' parameter is required for route '${this.name}'.`) + } - if (this.ziggy.defaultParameters.hasOwnProperty(keyName)) { - defaultParameter = this.ziggy.defaultParameters[keyName]; - } + return encodeURIComponent(params[segment] ?? ''); + }).replace(/\/+$/, ''); + } +} - // If a default parameter exists, and a value wasn't - // provided for it manually, use the default value - if (defaultParameter && !this.urlParams[keyName]) { - delete this.urlParams[keyName]; - return defaultParameter; - } +/** + * A collection of Laravel routes. This class constitutes Ziggy's main API. + */ +class Router extends String { + /** + * @param {String} name - Route name. + * @param {(String|Number|Array|Object)} params - Route parameters. + * @param {Boolean} absolute - Whether to include the URL origin. + * @param {Object} config - Ziggy configuration. + */ + constructor(name, params, absolute = true, config) { + super(); - // We were passed an array, shift the value off the - // object and return that value to the route - if (this.numericParamIndices) { - this.urlParams = Object.values(this.urlParams); + this._config = config ?? Ziggy ?? globalThis?.Ziggy; - tagValue = this.urlParams.shift(); - } else { - tagValue = this.urlParams[keyName]; - delete this.urlParams[keyName]; - } + if (name) { + if (!this._config.namedRoutes[name]) { + throw new Error(`Ziggy error: route '${name}' is not in the route list.`); + } - // The block above is what requires us to assign tagValue below - // instead of returning - if multiple *objects* are passed as - // params, numericParamIndices will be true and each object will - // be assigned above, which means !tagValue will evaluate to - // false, skipping the block below. + this._route = new Route(name, this._config.namedRoutes[name], { ...this._config, absolute }); + this._params = this._parse(params); + } + } - // If a value wasn't provided for this named parameter explicitly, - // but the object that was passed contains an ID, that object - // was probably a model, so we use the ID. + /** + * Get the compiled URL string for the current route and parameters. + * + * @example + * // with 'posts.show' route 'posts/{post}' + * route('posts.show', 1).url(); // 'https://ziggy.dev/posts/1' + * + * @return {String} + */ + url() { + // Get parameters that don't correspond to any route segments to append them to the query + const unhandled = Object.keys(this._params) + .filter((key) => !this._route.parameterSegments.some(({ name }) => name === key)) + .reduce((result, current) => ({ ...result, [current]: this._params[current] }), {}); - let bindingKey = this.ziggy.namedRoutes[this.name]?.bindings?.[keyName]; + return this._route.compile(this._params) + stringify({ ...unhandled, ...this._queryParams }, { + addQueryPrefix: true, + arrayFormat: 'indices', + encodeValuesOnly: true, + skipNulls: true, + }); + } - if (bindingKey && !this.urlParams[keyName] && this.urlParams[bindingKey]) { - tagValue = this.urlParams[bindingKey]; - delete this.urlParams[bindingKey]; - } else if (!tagValue && !this.urlParams[keyName] && this.urlParams['id']) { - tagValue = this.urlParams['id'] - delete this.urlParams['id']; - } + /** + * Get the name of the route matching the current window URL, or, given a route name + * and parameters, check if the current window URL and parameters match that route. + * + * @example + * // at URL https://ziggy.dev/posts/4 with 'posts.show' route 'posts/{post}' + * route().current(); // 'posts.show' + * route().current('posts.index'); // false + * route().current('posts.show'); // true + * route().current('posts.show', { post: 1 }); // false + * route().current('posts.show', { post: 4 }); // true + * + * @param {String} name - Route name to check. + * @param {(String|Number|Array|Object)} params - Route parameters. + * @return {(Boolean|String)} + */ + current(name, params) { + const url = window.location.host + window.location.pathname; + + // Find the first route that matches the current URL + const [current, route] = Object.entries(this._config.namedRoutes).find( + ([_, route]) => new Route(name, route, this._config).matchesUrl(url) + ); - // The value is null or undefined; is this param - // optional or not - if (tagValue == null) { - if (tag.indexOf('?') === -1) { - throw new Error( - "Ziggy Error: '" + - keyName + - "' key is required for route '" + - this.name + - "'" - ); - } else { - return ''; - } - } + // If a name wasn't passed, return the name of the current route + if (!name) return current; - // If an object was passed and has an id, return it - if (tagValue.id) { - return encodeURIComponent(tagValue.id); - } else if (tagValue[bindingKey]) { - return encodeURIComponent(tagValue[bindingKey]) - } + // Test the passed name against the current route, matching some + // basic wildcards, e.g. passing `events.*` matches `events.show` + const match = new RegExp(`^${name.replace('.', '\\.').replace('*', '.*')}$`).test(current); - return encodeURIComponent(tagValue); - } - ); + if (!params) return match; - if (this.urlBuilder != null && this.urlBuilder.path !== '') { - hydrated = hydrated.replace(/\/+$/, ''); - } + params = this._parse(params, new Route(current, route, this._config)); - this.hydrated = hydrated; + // Check that all passed parameters match their values in the current window URL + return Object.entries(this._dehydrate(route)) + .filter(([key]) => params.hasOwnProperty(key)) + // Use weak equality because all values in the current window URL will be strings + .every(([key, value]) => params[key] == value); + } - return this.hydrated; + /** + * Get all parameter values from the current window URL. + * + * @example + * // at URL https://tighten.ziggy.dev/posts/4?lang=en with 'posts.show' route 'posts/{post}' and domain '{team}.ziggy.dev' + * route().params; // { team: 'tighten', post: 4, lang: 'en' } + * + * @return {Object} + */ + get params() { + return this._dehydrate(this._config.namedRoutes[this.current()]); } - matchUrl() { - let windowUrl = - window.location.hostname + - (window.location.port ? ':' + window.location.port : '') + - window.location.pathname; - - // Strip out optional parameters - let optionalTemplate = this.template - .replace(/(\/\{[^\}]*\?\})/g, '/') - .replace(/(\{[^\}]*\})/gi, '[^/?]+') - .replace(/\/?$/, '') - .split('://')[1]; - - let searchTemplate = this.template - .replace(/(\{[^\}]*\})/gi, '[^/?]+') - .split('://')[1]; - let urlWithTrailingSlash = windowUrl.replace(/\/?$/, '/'); - - const regularSearch = new RegExp('^' + searchTemplate + '/$').test( - urlWithTrailingSlash - ); - const optionalSearch = new RegExp('^' + optionalTemplate + '/$').test( - urlWithTrailingSlash - ); + /** + * Check whether the given route exists. + * + * @param {String} name + * @return {Boolean} + */ + has(name) { + return Object.keys(this._config.namedRoutes).includes(name); + } - return regularSearch || optionalSearch; + /** + * Add query parameters to be appended to the compiled URL. + * + * @param {Object} params + * @return {this} + */ + withQuery(params) { + this._queryParams = params; + return this; } - constructQuery() { - if ( - Object.keys(this.queryParams).length === 0 && - Object.keys(this.urlParams).length === 0 + /** + * Parse Laravel-style route parameters of any type into a normalized object. + * + * @example + * // with route parameter names 'event' and 'venue' + * _parse(1); // { event: 1 } + * _parse({ event: 2, venue: 3 }); // { event: 2, venue: 3 } + * _parse(['Taylor', 'Matt']); // { event: 'Taylor', venue: 'Matt' } + * _parse([4, { uuid: 56789, name: 'Grand Canyon' }]); // { event: 4, venue: 56789 } + * + * @param {(String|Number|Array|Object)} params - Route parameters. + * @param {Route} route - Route instance. + * @return {Object} Normalized complete route parameters. + */ + _parse(params = {}, route = this._route) { + // If `params` is a string or integer, wrap it in an array + params = ['string', 'number'].includes(typeof params) ? [params] : params; + + // Separate segments with and without defaults, and fill in the default values + const segments = route.parameterSegments.filter(({ name }) => !this._config.defaultParameters[name]); + + if (Array.isArray(params)) { + // If the parameters are an array they have to be in order, so we can transform them into + // an object by keying them with the template segment names in the order they appear + params = params.reduce((result, current, i) => ({ ...result, [segments[i].name]: current }), {}); + } else if ( + segments.length === 1 + && !params[segments[0].name] + && (params.hasOwnProperty(Object.values(route.bindings)[0]) || params.hasOwnProperty('id')) ) { - return ''; + // If there is only one template segment and `params` is an object, that object is + // ambiguous—it could contain the parameter key and value, or it could be an object + // representing just the value (e.g. a model); we can inspect it to find out, and + // if it's just the parameter value, we can wrap it in an object with its key + params = { [segments[0].name]: params }; } - let remainingParams = Object.assign(this.urlParams, this.queryParams); - - return stringify(remainingParams, { - encodeValuesOnly: true, - skipNulls: true, - addQueryPrefix: true, - arrayFormat: 'indices' - }); + return { + ...this._defaults(route), + ...this._substituteBindings(params, route.bindings), + }; } - current(name = null) { - let routeNames = Object.keys(this.ziggy.namedRoutes); - - let currentRoute = routeNames.filter(name => { - if (this.ziggy.namedRoutes[name].methods.indexOf('GET') === -1) { - return false; - } - - return new Router( - name, - undefined, - undefined, - this.ziggy - ).matchUrl(); - })[0]; - - if (name) { - const pattern = new RegExp( - '^' + name.replace('.', '\\.').replace('*', '.*') + '$', - 'i' - ); - return pattern.test(currentRoute); - } - - return currentRoute; + /** + * Populate default parameters for the given route. + * + * @example + * // with default parameters { locale: 'en', country: 'US' } and 'posts.show' route '{locale}/posts/{post}' + * defaults(...); // { locale: 'en' } + * + * @param {Route} route + * @return {Object} Default route parameters. + */ + _defaults(route) { + return route.parameterSegments.filter(({ name }) => this._config.defaultParameters[name]) + .reduce((result, { name }, i) => ({ ...result, [name]: this._config.defaultParameters[name] }), {}); } - check(name) { - let routeNames = Object.keys(this.ziggy.namedRoutes); + /** + * Substitute Laravel route model bindings in the given parameters. + * + * @example + * _substituteBindings({ post: { id: 4, slug: 'hello-world', title: 'Hello, world!' } }, { post: 'slug' }); // { post: 'hello-world' } + * + * @param {Object} params - Route parameters. + * @param {Object} bindings - Route model bindings. + * @return {Object} Normalized route parameters. + */ + _substituteBindings(params, bindings = {}) { + return Object.entries(params).reduce((result, [key, value]) => { + // If the value isn't an object there's nothing to substitute, so we return it as-is + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return { ...result, [key]: value }; + } - return routeNames.includes(name); - } + if (!value.hasOwnProperty(bindings[key])) { + if (value.hasOwnProperty('id')) { + // As a fallback, we still accept an 'id' key not explicitly registered as a binding + bindings[key] = 'id'; + } else { + throw new Error(`Ziggy error: object passed as '${key}' parameter is missing route model binding key '${bindings[key]}'.`) + } + } - extractParams(uri, template, delimiter) { - const uriParts = uri.split(delimiter); - const templateParts = template.split(delimiter); - - return templateParts.reduce( - (params, param, i) => - param.indexOf('{') === 0 && - param.indexOf('}') !== -1 && - uriParts[i] - ? Object.assign(params, { - [this.trimParam(param)]: uriParts[i] - }) - : params, - {} - ); + return { ...result, [key]: value[bindings[key]] }; + }, {}); } - get params() { - const namedRoute = this.ziggy.namedRoutes[this.current()]; - + /** + * Get all parameters and their values from the current window URL, based on the given route definition. + * + * @example + * // at URL https://tighten.ziggy.dev/events/8/venues/chicago?zoom=true + * _dehydrate({ domain: '{team}.ziggy.dev', uri: 'events/{event}/venues/{venue?}' }); // { team: 'tighten', event: 8, venue: 'chicago', zoom: true } + * + * @param {Object} route - Route definition. + * @return {Object} Parameters. + */ + _dehydrate(route) { let pathname = window.location.pathname - .replace(this.ziggy.baseUrl.split('://')[1].split('/')[1], '') + // If this Laravel app is in a subdirectory, trim the subdirectory from the path + .replace(this._config.baseUrl.replace(/^\w*:\/\/[^/]+/, ''), '') .replace(/^\/+/, ''); - return Object.assign( - this.extractParams( - window.location.hostname, - namedRoute.domain || '', - '.' - ), - this.extractParams( - pathname, - namedRoute.uri, - '/' - ) - ); - } + // Given part of a valid 'hydrated' URL containing all its parameter values, + // a route template, and a delimiter, extract the parameters as an object + // E.g. dehydrate('events/{event}/{venue}', 'events/2/chicago', '/'); // { event: 2, venue: 'chicago' } + const dehydrate = (hydrated, template = '', delimiter) => { + const [values, segments] = [hydrated, template].map(s => s.split(delimiter)); + + return segments.reduce((result, current, i) => { + // Only include template segments that are route parameters + // AND have a value present in the passed hydrated string + return /^{[^}?]+\??}$/.test(current) && values[i] + ? { ...result, [current.replace(/^{|\??}$/g, '')]: values[i] } + : result; + }, {}); + } - parse() { - this.return = this.hydrateUrl() + this.constructQuery(); + return { + ...dehydrate(window.location.host, route.domain, '.'), // Domain parameters + ...dehydrate(pathname, route.uri, '/'), // Path parameters + ...parse(window.location.search?.replace(/^\?/, '')), // Query parameters + }; } - url() { - this.parse(); - return this.return; + toString() { + return this.url(); } - toString() { + valueOf() { return this.url(); } - trimParam(param) { - return param.replace(/{|}|\?/g, ''); + /** + * @deprecated since v1.0, pass parameters as the second argument to `route()` instead. + */ + with(params) { + this._params = this._parse(params); + return this; } - valueOf() { - return this.url(); + /** + * @deprecated since v1.0, use `has()` instead. + */ + check(name) { + return this.has(name); } } -export default function route(name, params, absolute, customZiggy) { - return new Router(name, params, absolute, customZiggy); +export default function route(name, params, absolute, config) { + return new Router(name, params, absolute, config); } diff --git a/tests/Unit/CommandRouteGeneratorTest.php b/tests/Unit/CommandRouteGeneratorTest.php index d79a884f..82a1d5da 100644 --- a/tests/Unit/CommandRouteGeneratorTest.php +++ b/tests/Unit/CommandRouteGeneratorTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\URL; use Tests\TestCase; class CommandRouteGeneratorTest extends TestCase @@ -55,6 +56,7 @@ public function can_generate_file_with_custom_url() $router = app('router'); $router->get('posts/{post}/comments', $this->noop())->name('postComments.index'); $router->getRoutes()->refreshNameLookups(); + URL::defaults(['locale' => 'en']); Artisan::call('ziggy:generate', ['--url' => 'http://example.org']); diff --git a/tests/Unit/RouteModelBindingTest.php b/tests/Unit/RouteModelBindingTest.php index 7e7e39aa..b388a7b2 100644 --- a/tests/Unit/RouteModelBindingTest.php +++ b/tests/Unit/RouteModelBindingTest.php @@ -170,7 +170,7 @@ public function can_include_bindings_in_json() $this->markTestSkipped('Requires Laravel >=7'); } - $json = '{"baseUrl":"http:\/\/ziggy.dev\/","baseProtocol":"http","baseDomain":"ziggy.dev","basePort":null,"defaultParameters":[],"namedRoutes":{"users":{"uri":"users\/{user}","methods":["GET","HEAD"],"bindings":{"user":"uuid"}},"tags":{"uri":"tags\/{tag}","methods":["GET","HEAD"],"bindings":{"tag":"id"}},"tokens":{"uri":"tokens\/{token}","methods":["GET","HEAD"]},"users.numbers":{"uri":"users\/{user}\/{number}","methods":["GET","HEAD"],"bindings":{"user":"uuid"}},"posts":{"uri":"blog\/{category}\/{post}","methods":["GET","HEAD"],"bindings":{"category":"id","post":"slug"}},"posts.tags":{"uri":"blog\/{category}\/{post}\/{tag}","methods":["GET","HEAD"],"bindings":{"category":"id","post":"slug","tag":"slug"}}}}'; + $json = '{"baseUrl":"http:\/\/ziggy.dev","baseProtocol":"http","baseDomain":"ziggy.dev","basePort":null,"defaultParameters":[],"namedRoutes":{"users":{"uri":"users\/{user}","methods":["GET","HEAD"],"bindings":{"user":"uuid"}},"tags":{"uri":"tags\/{tag}","methods":["GET","HEAD"],"bindings":{"tag":"id"}},"tokens":{"uri":"tokens\/{token}","methods":["GET","HEAD"]},"users.numbers":{"uri":"users\/{user}\/{number}","methods":["GET","HEAD"],"bindings":{"user":"uuid"}},"posts":{"uri":"blog\/{category}\/{post}","methods":["GET","HEAD"],"bindings":{"category":"id","post":"slug"}},"posts.tags":{"uri":"blog\/{category}\/{post}\/{tag}","methods":["GET","HEAD"],"bindings":{"category":"id","post":"slug","tag":"slug"}}}}'; $this->assertSame($json, (new Ziggy)->toJson()); } diff --git a/tests/Unit/ZiggyTest.php b/tests/Unit/ZiggyTest.php index 707b185e..6cd96736 100644 --- a/tests/Unit/ZiggyTest.php +++ b/tests/Unit/ZiggyTest.php @@ -391,7 +391,7 @@ public function route_payload_can_array_itself() $ziggy = new Ziggy; $expected = [ - 'baseUrl' => 'http://ziggy.dev/', + 'baseUrl' => 'http://ziggy.dev', 'baseProtocol' => 'http', 'baseDomain' => 'ziggy.dev', 'basePort' => null, @@ -438,7 +438,7 @@ public function route_payload_can_json_itself() ]]); $expected = [ - 'baseUrl' => 'http://ziggy.dev/', + 'baseUrl' => 'http://ziggy.dev', 'baseProtocol' => 'http', 'baseDomain' => 'ziggy.dev', 'basePort' => null, @@ -453,7 +453,7 @@ public function route_payload_can_json_itself() $this->addPostCommentsRouteWithBindings($expected['namedRoutes']); - $json = '{"baseUrl":"http:\/\/ziggy.dev\/","baseProtocol":"http","baseDomain":"ziggy.dev","basePort":null,"defaultParameters":[],"namedRoutes":{"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"]}}}'; + $json = '{"baseUrl":"http:\/\/ziggy.dev","baseProtocol":"http","baseDomain":"ziggy.dev","basePort":null,"defaultParameters":[],"namedRoutes":{"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"]}}}'; if ($this->laravelVersion(7)) { $json = str_replace( diff --git a/tests/fixtures/admin.js b/tests/fixtures/admin.js index afbdb61b..3bdf27f8 100644 --- a/tests/fixtures/admin.js +++ b/tests/fixtures/admin.js @@ -1,4 +1,4 @@ -var Ziggy = {"baseUrl":"http:\/\/ziggy.dev\/","baseProtocol":"http","baseDomain":"ziggy.dev","basePort":null,"defaultParameters":[],"namedRoutes":{"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]}}}; +var Ziggy = {"baseUrl":"http:\/\/ziggy.dev","baseProtocol":"http","baseDomain":"ziggy.dev","basePort":null,"defaultParameters":[],"namedRoutes":{"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]}}}; if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') { for (var name in window.Ziggy.namedRoutes) { diff --git a/tests/fixtures/custom-url.js b/tests/fixtures/custom-url.js index e26e80a8..7e57ae77 100644 --- a/tests/fixtures/custom-url.js +++ b/tests/fixtures/custom-url.js @@ -1,4 +1,4 @@ -var Ziggy = {"baseUrl":"http:\/\/example.org\/","baseProtocol":"http","baseDomain":"example.org","basePort":null,"defaultParameters":[],"namedRoutes":{"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"]}}}; +var Ziggy = {"baseUrl":"http:\/\/example.org","baseProtocol":"http","baseDomain":"example.org","basePort":null,"defaultParameters":{"locale":"en"},"namedRoutes":{"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"]}}}; if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') { for (var name in window.Ziggy.namedRoutes) { diff --git a/tests/fixtures/ziggy.js b/tests/fixtures/ziggy.js index d4c443b0..e9baec84 100644 --- a/tests/fixtures/ziggy.js +++ b/tests/fixtures/ziggy.js @@ -1,4 +1,4 @@ -var Ziggy = {"baseUrl":"http:\/\/ziggy.dev\/","baseProtocol":"http","baseDomain":"ziggy.dev","basePort":null,"defaultParameters":[],"namedRoutes":{"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"]}}}; +var Ziggy = {"baseUrl":"http:\/\/ziggy.dev","baseProtocol":"http","baseDomain":"ziggy.dev","basePort":null,"defaultParameters":[],"namedRoutes":{"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"]}}}; if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') { for (var name in window.Ziggy.namedRoutes) { diff --git a/tests/js/route.test.js b/tests/js/route.test.js index f6fe15a8..f2174b93 100644 --- a/tests/js/route.test.js +++ b/tests/js/route.test.js @@ -3,7 +3,7 @@ import route from '../../src/js/route.js'; const defaultWindow = { location: { - hostname: 'ziggy.dev', + host: 'ziggy.dev', }, }; @@ -25,15 +25,22 @@ const defaultZiggy = { 'posts.show': { uri: 'posts/{post}', methods: ['GET', 'HEAD'], + bindings: { + post: 'id', + }, }, 'posts.update': { uri: 'posts/{post}', methods: ['PUT'], + bindings: { + post: 'id', + }, }, 'postComments.show': { uri: 'posts/{post}/comments/{comment}', methods: ['GET', 'HEAD'], bindings: { + post: 'id', comment: 'uuid', }, }, @@ -45,17 +52,36 @@ const defaultZiggy = { uri: '{locale}/posts/{id}', methods: ['GET', 'HEAD'], }, + 'translatePosts.update': { + uri: '{locale}/posts/{post}', + methods: ['PUT', 'PATCH'], + }, 'events.venues.index': { uri: 'events/{event}/venues', methods: ['GET', 'HEAD'], + bindings: { + event: 'id', + }, }, 'events.venues.show': { uri: 'events/{event}/venues/{venue}', methods: ['GET', 'HEAD'], + bindings: { + event: 'id', + venue: 'id', + }, + }, + 'events.venues.update': { + uri: 'events/{event}/venues/{venue}', + methods: ['PUT', 'PATCH'], }, 'translateEvents.venues.show': { uri: '{locale}/events/{event}/venues/{venue}', methods: ['GET', 'HEAD'], + bindings: { + event: 'id', + venue: 'id', + }, }, 'conversations.show': { uri: 'subscribers/{subscriber}/conversations/{type}/{conversation_id?}', @@ -163,11 +189,11 @@ describe('route()', () => { }); test('can error if a required parameter is not provided', () => { - assert.throws(() => route('posts.show').url(), /'post' key is required/); + assert.throws(() => route('posts.show').url(), /'post' parameter is required/); }); test('can error if a required parameter is not provided to a route with default parameters', () => { - assert.throws(() => route('translatePosts.show').url(), /'id' key is required/); + assert.throws(() => route('translatePosts.show').url(), /'id' parameter is required/); }); test('can error if a required parameter with a default has no default value', () => { @@ -175,24 +201,31 @@ describe('route()', () => { assert.throws( () => route('translatePosts.index').url(), - /'locale' key is required/ + /'locale' parameter is required/ ); }); test('can generate a URL using an integer', () => { // route with required parameters equal(route('posts.show', 1), 'https://ziggy.dev/posts/1'); - // route with optional parameters + // route with default parameters equal(route('translatePosts.show', 1), 'https://ziggy.dev/en/posts/1'); }); + test('can generate a URL using a string', () => { + // route with required parameters + equal(route('posts.show', 'my-first-post'), 'https://ziggy.dev/posts/my-first-post'); + // route with default parameters + equal(route('translatePosts.show', 'my-first-post'), 'https://ziggy.dev/en/posts/my-first-post'); + }); + test('can generate a URL using an object', () => { // routes with required parameters equal(route('posts.show', { id: 1 }), 'https://ziggy.dev/posts/1'); equal(route('events.venues.show', { event: 1, venue: 2 }), 'https://ziggy.dev/events/1/venues/2'); // route with optional parameters equal(route('optionalId', { type: 'model', id: 1 }), 'https://ziggy.dev/optionalId/model/1'); - // route with both required and optional parameters + // route with both required and default parameters equal(route('translateEvents.venues.show', { event: 1, venue: 2 }), 'https://ziggy.dev/en/events/1/venues/2'); }); @@ -200,15 +233,16 @@ describe('route()', () => { // routes with required parameters equal(route('posts.show', [1]), 'https://ziggy.dev/posts/1'); equal(route('events.venues.show', [1, 2]), 'https://ziggy.dev/events/1/venues/2'); - // route with optional parameters + equal(route('events.venues.show', [1, 'coliseum']), 'https://ziggy.dev/events/1/venues/coliseum'); + // route with default parameters equal(route('translatePosts.show', [1]), 'https://ziggy.dev/en/posts/1'); - // route with both required and optional parameters + // route with both required and default parameters equal(route('translateEvents.venues.show', [1, 2]), 'https://ziggy.dev/en/events/1/venues/2'); }); test('can generate a URL using an array of objects', () => { - let event = { id: 1, name: 'World Series' }; - let venue = { id: 2, name: 'Rogers Centre' }; + const event = { id: 1, name: 'World Series' }; + const venue = { id: 2, name: 'Rogers Centre' }; // route with required parameters equal(route('events.venues.show', [event, venue]), 'https://ziggy.dev/events/1/venues/2'); @@ -217,7 +251,7 @@ describe('route()', () => { }); test('can generate a URL using an array of integers and objects', () => { - let venue = { id: 2, name: 'Rogers Centre' }; + const venue = { id: 2, name: 'Rogers Centre' }; // route with required parameters equal(route('events.venues.show', [1, venue]), 'https://ziggy.dev/events/1/venues/2'); @@ -240,10 +274,36 @@ describe('route()', () => { ]), 'https://ziggy.dev/posts/1/comments/12345' ); + equal( + route('postComments.show', [1, { uuid: 'correct-horse-etc-etc' }]), + 'https://ziggy.dev/posts/1/comments/correct-horse-etc-etc' + ); + }); + + test("can fall back to an 'id' key if an object is passed for a parameter with no registered bindings", () => { + equal(route('translatePosts.update', { id: 14 }), 'https://ziggy.dev/en/posts/14'); + equal(route('translatePosts.update', [{ id: 14 }]), 'https://ziggy.dev/en/posts/14'); + equal(route('events.venues.update', [{ id: 10 }, { id: 1 }]), 'https://ziggy.dev/events/10/venues/1'); + }); + + test('can generate a URL for an app installed in a subfolder', () => { + global.Ziggy.baseUrl = 'https://ziggy.dev/subfolder'; + + equal( + route('postComments.show', [1, { uuid: 'correct-horse-etc-etc' }]), + 'https://ziggy.dev/subfolder/posts/1/comments/correct-horse-etc-etc' + ); + }); + + test('can error if a route model binding key is missing', () => { + assert.throws( + () => route('postComments.show', [1, { count: 20 }]).url(), + /Ziggy error: object passed as 'comment' parameter is missing route model binding key 'uuid'\./ + ); }); test('can return base URL if path is "/"', () => { - equal(route('home'), 'https://ziggy.dev/'); + equal(route('home'), 'https://ziggy.dev'); }); // @todo duplicate @@ -254,7 +314,7 @@ describe('route()', () => { }); test('can error if a route name doesn’t exist', () => { - assert.throws(() => route('unknown-route').url(), /route 'unknown-route' is not found in the route list/); + assert.throws(() => route('unknown-route').url(), /Ziggy error: route 'unknown-route' is not in the route list\./); }); test('can append values as a query string with .withQuery', () => { @@ -292,7 +352,7 @@ describe('route()', () => { }); test('can generate a URL with a port', () => { - global.Ziggy.baseUrl = 'https://ziggy.dev:81/'; + global.Ziggy.baseUrl = 'https://ziggy.dev:81'; global.Ziggy.baseDomain = 'ziggy.dev'; global.Ziggy.basePort = 81; @@ -303,13 +363,13 @@ describe('route()', () => { }); test('can handle trailing path segments in the base URL', () => { - global.Ziggy.baseUrl = 'https://test.thing/ab/cd/'; + global.Ziggy.baseUrl = 'https://test.thing/ab/cd'; equal(route('events.venues.index', 1), 'https://test.thing/ab/cd/events/1/venues'); }); test('can URL-encode named parameters', () => { - global.Ziggy.baseUrl = 'https://test.thing/ab/cd/'; + global.Ziggy.baseUrl = 'https://test.thing/ab/cd'; equal( route('events.venues.index', { event: 'Fun&Games' }), @@ -339,8 +399,8 @@ describe('route()', () => { }); test('can accept a custom Ziggy configuration object', () => { - const customZiggy = { - baseUrl: 'http://notYourAverage.dev/', + const config = { + baseUrl: 'http://notYourAverage.dev', baseProtocol: 'http', baseDomain: 'notYourAverage.dev', basePort: false, @@ -354,55 +414,42 @@ describe('route()', () => { }; equal( - route('tightenDev.packages.index', { dev: 1 }, true, customZiggy), + route('tightenDev.packages.index', { dev: 1 }, true, config), 'http://notYourAverage.dev/tightenDev/1/packages' ); }); - test('can remove braces and question marks from route parameter definitions', () => { - equal(route().trimParam('optional'), 'optional'); - equal(route().trimParam('{id}'), 'id'); - equal(route().trimParam('{id?}'), 'id'); - equal(route().trimParam('{slug?}'), 'slug'); - }); - - test('can extract named parameters from a URL using a template and delimiter', () => { - deepEqual(route().extractParams('', '', '/'), {}); - deepEqual(route().extractParams('posts', 'posts', '/'), {}); + test('can extract parameters for an app installed in a subfolder', () => { + global.Ziggy.baseUrl = 'https://ziggy.dev/subfolder'; - deepEqual(route().extractParams('users/1', 'users/{id}', '/'), { id: '1' }); - deepEqual( - route().extractParams('events/1/venues/2', 'events/{event}/venues/{venue}', '/'), - { event: '1', venue: '2' } - ); - deepEqual( - route().extractParams('optional/123', 'optional/{id}/{slug?}', '/'), - { id: '123' } - ); - deepEqual( - route().extractParams('optional/123/news', 'optional/{id}/{slug?}', '/'), - { id: '123', slug: 'news' } - ); + global.window.location.href = 'https://ziggy.dev/subfolder/ph/en/products/4'; + global.window.location.host = 'ziggy.dev'; + global.window.location.pathname = '/subfolder/ph/en/products/4'; - deepEqual( - route().extractParams('tighten.myapp.dev', '{team}.myapp.dev', '.'), - { team: 'tighten' } - ); + deepEqual(route().params, { country: 'ph', language: 'en', id: '4' }); }); - test('can generate URL for an app installed in a subfolder', () => { - global.Ziggy.baseUrl = 'https://ziggy.dev/subfolder/'; + test('can extract parameters for an app installed in nested subfolders', () => { + global.Ziggy.baseUrl = 'https://ziggy.dev/nested/subfolder'; - global.window.location.href = 'https://ziggy.dev/subfolder/ph/en/products/4'; - global.window.location.hostname = 'ziggy.dev'; - global.window.location.pathname = '/subfolder/ph/en/products/4'; + global.window.location.href = 'https://ziggy.dev/nested/subfolder/ph/en/products/4'; + global.window.location.host = 'ziggy.dev'; + global.window.location.pathname = '/nested/subfolder/ph/en/products/4'; deepEqual(route().params, { country: 'ph', language: 'en', id: '4' }); }); + test('can extract domain parameters from the current URL', () => { + global.window.location.href = 'https://tighten.ziggy.dev/users/1'; + global.window.location.host = 'tighten.ziggy.dev'; + global.window.location.pathname = '/users/1'; + + deepEqual(route().params, { team: 'tighten', id: '1' }); + }); + test('can extract named parameters from the current URL', () => { global.window.location.href = 'https://ziggy.dev/posts/1'; - global.window.location.hostname = 'ziggy.dev'; + global.window.location.host = 'ziggy.dev'; global.window.location.pathname = '/posts/1'; deepEqual(route().params, { post: '1' }); @@ -413,23 +460,35 @@ describe('route()', () => { deepEqual(route().params, { event: '1', venue: '2' }); }); - test('can extract domain parameters from the current URL', () => { - global.window.location.href = 'https://tighten.ziggy.dev/users/1'; - global.window.location.hostname = 'tighten.ziggy.dev'; - global.window.location.pathname = '/users/1'; + test('can extract query parameters from the current URL', () => { + global.window.location.href = 'https://ziggy.dev/posts/1?guest[name]=Taylor'; + global.window.location.host = 'ziggy.dev'; + global.window.location.pathname = '/posts/1'; + global.window.location.search = '?guest[name]=Taylor'; - deepEqual(route().params, { team: 'tighten', id: '1' }); + deepEqual(route().params, { post: '1', guest: { name: 'Taylor' } }); + + global.window.location.href = 'https://ziggy.dev/events/1/venues/2?id=5&vip=true'; + global.window.location.pathname = '/events/1/venues/2'; + global.window.location.search = '?id=5&vip=true'; + + deepEqual(route().params, { event: '1', venue: '2', id: '5', vip: 'true' }); }); }); -describe('check', () => { +describe('has()', () => { test('can check if given named route exists', () => { + assert(route().has('posts.show')); + assert(!route().has('non.existing.route')); + }); + + test('can check if given named route exists with .check()', () => { assert(route().check('posts.show')); assert(!route().check('non.existing.route')); }); }); -describe('current', () => { +describe('current()', () => { test('can get the current route name', () => { global.window.location.pathname = '/events/1/venues/2'; @@ -449,12 +508,18 @@ describe('current', () => { equal(route().current(), 'events.venues.index'); }); + test('can ignore query string when getting current route name', () => { + global.window.location.pathname = '/events/1/venues?foo=2'; + + equal(route().current(), 'events.venues.index'); + }); + test('can get the current route name with a custom Ziggy object', () => { global.Ziggy = undefined; global.window.location.pathname = '/events/'; - const customZiggy = { - baseUrl: 'https://ziggy.dev/', + const config = { + baseUrl: 'https://ziggy.dev', baseProtocol: 'https', baseDomain: 'ziggy.dev', basePort: false, @@ -466,7 +531,7 @@ describe('current', () => { }, }; - equal(route(undefined, undefined, undefined, customZiggy).current(), 'events.index'); + equal(route(undefined, undefined, undefined, config).current(), 'events.index'); }); test('can check the current route name against a pattern', () => { @@ -498,22 +563,62 @@ describe('current', () => { assert(route().current('optional')); }); - test('can check the current route name on a route with empty optional parameters', () => { + test('can check the current route name on a route with trailing empty optional parameters', () => { global.window.location.pathname = '/optional/1'; assert(route().current('optional')); }); - test.todo('can check the current route name and parameters'); - // test.todo('can check the current route name and parameters', () => { - // global.window.location.pathname = '/events/1/venues/2'; + test('can check the current route name on a route with optional parameters in the middle of the URI', () => { + global.Ziggy.baseUrl = 'https://ziggy.dev/subfolder'; - // assert(route().current('events.venues.show', { event: 1, venue: 2 })); - // assert(!route().current('events.venues.show', { event: 4, venue: 2 })); - // assert(!route().current('events.venues.show', { event: 1, venue: 6 })); - // }); + // Missing the optional 'language' parameter (e.g. subfolder/ph/en/products...) + global.window.location.href = 'https://ziggy.dev/subfolder/ph/products/4'; + global.window.location.host = 'ziggy.dev'; + global.window.location.pathname = '/subfolder/ph/products/4'; + + assert(route().current('products.show')); + }); - test('can ignore routes that dont allow GET requests', () => { + test('can check the current route with parameters', () => { + global.window.location.pathname = '/events/1/venues/2'; + + assert(route().current('events.venues.show', { event: 1, venue: 2 })); + assert(route().current('events.venues.show', [1, 2])); + assert(route().current('events.venues.show', [1, { id: 2, name: 'Grand Canyon' }])); + assert(route().current('events.venues.show', { event: 1 })); + assert(route().current('events.venues.show', { venue: 2 })); + assert(route().current('events.venues.show', [1])); + assert(route().current('events.venues.show', {})); + assert(route().current('events.venues.show', null)); + + assert(!route().current('events.venues.show', { event: 4, venue: 2 })); + assert(!route().current('events.venues.show', { event: null })); + assert(!route().current('events.venues.show', [1, 6])); + assert(!route().current('events.venues.show', [{ id: 1 }, { id: 4, name: 'Great Pyramids' }])); + assert(!route().current('events.venues.show', { event: 4 })); + assert(!route().current('events.venues.show', { venue: 4 })); + assert(!route().current('events.venues.show', [5])); + }); + + test('can check the current route with query parameters', () => { + global.window.location.pathname = '/events/1/venues/2'; + global.window.location.search = '?user=Jacob&id=9'; + + assert(route().current('events.venues.show', { event: 1, venue: 2, user: 'Jacob' })); + assert(route().current('events.venues.show', { + event: { id: 1, name: 'Party' }, + venue: 2, + id: 9, + })); + assert(route().current('events.venues.show', { user: 'Jacob', venue: { id: 2 } })); + + assert(!route().current('events.venues.show', { user: 'Matt', venue: { id: 9 } })); + assert(!route().current('events.venues.show', { event: 5, id: 9, user: 'Jacob' })); + assert(!route().current('events.venues.show', { id: 12, user: 'Matt' })); + }); + + test('can ignore routes that don’t allow GET requests', () => { global.window.location.pathname = '/posts/1'; assert(!route().current('posts.update')); @@ -524,11 +629,4 @@ describe('current', () => { equal(route().current(), 'events.venues.index'); }); - - test.todo('can ignore query parameters'); - // test('can ignore query parameters', () => { - // global.window.location.pathname = '/events/1/venues?foo=2'; - - // equal(route().current(), 'events.venues.index'); - // }); });