diff --git a/lib/js_routes.rb b/lib/js_routes.rb index 322f1ff8..be0da6d1 100644 --- a/lib/js_routes.rb +++ b/lib/js_routes.rb @@ -153,15 +153,14 @@ def any_match?(route, parent_route, matchers) def build_js(route, parent_route) name = [parent_route.try(:name), route.name].compact parent_spec = parent_route.try(:path).try(:spec) - required_parts = route.required_parts.clone - optional_parts = route.optional_parts.clone + required_parts, optional_parts = route.required_parts.clone, route.optional_parts.clone optional_parts.push(required_parts.delete :format) if required_parts.include?(:format) - route_name = "#{name.join('_')}_path" - route_name = route_name.camelize(:lower) if true == @options[:camel_case] + route_name = generate_route_name(name) url_link = generate_url_link(name, route_name, required_parts) _ = <<-JS.strip! // #{name.join('.')} => #{parent_spec}#{route.path.spec} #{route_name}: function(#{build_params(required_parts)}) { + if (!#{LAST_OPTIONS_KEY}){ #{LAST_OPTIONS_KEY} = {}; } return Utils.build_path(#{json(required_parts)}, #{json(optional_parts)}, #{json(serialize(route.path.spec, parent_spec))}, arguments); }#{",\n" + url_link if url_link.length > 0} JS @@ -170,16 +169,18 @@ def build_js(route, parent_route) def generate_url_link(name, route_name, required_parts) return "" unless @options[:url_links] raise "invalid URL format in url_links (ex: http[s]://example.com)" if @options[:url_links].match(URI::regexp(%w(http https))).nil? - url_route_name = "#{name.join('_')}_url" - url_route_name = url_route_name.camelize(:lower) if true == @options[:camel_case] _ = <<-JS.strip! - #{url_route_name}: function(#{build_params(required_parts)}) { - if (!#{LAST_OPTIONS_KEY}){ #{LAST_OPTIONS_KEY} = {}; } + #{generate_route_name(name, true)}: function(#{build_params(required_parts)}) { return "" + #{@options[:url_links].inspect} + this.#{route_name}(#{build_params(required_parts)}); } JS end + def generate_route_name(name, is_url = false) + route_name = "#{name.join('_')}_#{is_url ? "url" : "path"}" + @options[:camel_case] ? route_name.camelize(:lower) : route_name + end + def json(string) self.class.json(string) end @@ -188,7 +189,7 @@ def build_params required_parts params = required_parts.map do |name| # prepending each parameter name with underscore # to prevent conflict with JS reserved words - "_" + name.to_s + "_#{name}" end << LAST_OPTIONS_KEY params.join(", ") end diff --git a/lib/routes.js b/lib/routes.js index 9a3ecfd1..70fce6f8 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -23,14 +23,14 @@ } if (window.jQuery) { result = window.jQuery.param(obj); - return (!result ? "" : "?" + result); + return (!result ? "" : result); } s = []; for (key in obj) { if (!__hasProp.call(obj, key)) continue; prop = obj[key]; if (prop != null) { - if (prop instanceof Array) { + if (this.getObjectType(prop) === "array") { for (i = _i = 0, _len = prop.length; _i < _len; i = ++_i) { val = prop[i]; s.push("" + key + (encodeURIComponent("[]")) + "=" + (encodeURIComponent(val.toString()))); @@ -43,7 +43,7 @@ if (!s.length) { return ""; } - return "?" + (s.join("&")); + return s.join("&"); }, clean_path: function(path) { var last_index; @@ -70,14 +70,14 @@ anchor = ""; if (options.hasOwnProperty("anchor")) { anchor = "#" + options.anchor; - options.anchor = null; + delete options.anchor; } return anchor; }, extract_options: function(number_of_params, args) { var ret_value; ret_value = {}; - if (args.length > number_of_params && typeof args[args.length - 1] === "object") { + if (args.length > number_of_params && this.getObjectType(args[args.length - 1]) === "object") { ret_value = args.pop(); } return ret_value; @@ -91,9 +91,9 @@ return ""; } property = object; - if (typeof object === "object") { + if (this.getObjectType(object) === "object") { property = object.to_param || object.id || object; - if (typeof property === "function") { + if (this.getObjectType(property) === "function") { property = property.call(object); } } @@ -101,7 +101,7 @@ }, clone: function(obj) { var attr, copy, key; - if (null === obj || "object" !== typeof obj) { + if ((obj == null) || "object" !== this.getObjectType(obj)) { return obj; } copy = obj.constructor(); @@ -122,7 +122,7 @@ return result; }, build_path: function(required_parameters, optional_parts, route, args) { - var opts, parameters, result; + var opts, parameters, result, url, url_params; args = Array.prototype.slice.call(args); opts = this.extract_options(required_parameters.length, args); if (args.length > required_parameters.length) { @@ -130,16 +130,24 @@ } parameters = this.prepare_parameters(required_parameters, args, opts); this.set_default_url_options(optional_parts, parameters); - result = "" + (Utils.get_prefix()) + (this.visit(route, parameters)); - return Utils.clean_path("" + result + (Utils.extract_anchor(parameters))) + Utils.serialize(parameters); + result = "" + (this.get_prefix()) + (this.visit(route, parameters)); + url = Utils.clean_path("" + result + (this.extract_anchor(parameters))); + if ((url_params = this.serialize(parameters)).length) { + url += "?" + url_params; + } + return url; }, visit: function(route, parameters, optional) { var left, left_part, right, right_part, type, value; + if (optional == null) { + optional = false; + } type = route[0], left = route[1], right = route[2]; switch (type) { case NodeTypes.GROUP: - case NodeTypes.STAR: return this.visit(left, parameters, true); + case NodeTypes.STAR: + return this.visit_globbing(left, parameters, true); case NodeTypes.LITERAL: case NodeTypes.SLASH: case NodeTypes.DOT: @@ -154,7 +162,7 @@ case NodeTypes.SYMBOL: value = parameters[left]; if (value != null) { - parameters[left] = null; + delete parameters[left]; return this.path_identifier(value); } if (optional) { @@ -167,6 +175,23 @@ throw new Error("Unknown Rails node type"); } }, + visit_globbing: function(route, parameters, optional) { + var left, right, type, value; + type = route[0], left = route[1], right = route[2]; + value = parameters[left]; + if (value == null) { + return this.visit(route, parameters, optional); + } + parameters[left] = (function() { + switch (this.getObjectType(value)) { + case "array": + return value.join("/"); + default: + return value; + } + }).call(this); + return this.visit(route, parameters, optional); + }, get_prefix: function() { var prefix; prefix = defaults.prefix; @@ -175,6 +200,28 @@ } return prefix; }, + _classToTypeCache: null, + _classToType: function() { + var name, _i, _len, _ref; + if (this._classToTypeCache != null) { + return this._classToTypeCache; + } + this._classToTypeCache = {}; + _ref = "Boolean Number String Function Array Date RegExp Undefined Null".split(" "); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + name = _ref[_i]; + this._classToTypeCache["[object " + name + "]"] = name.toLowerCase(); + } + return this._classToTypeCache; + }, + getObjectType: function(obj) { + var strType; + if (window.jQuery && (window.jQuery.type != null)) { + return window.jQuery.type(obj); + } + strType = Object.prototype.toString.call(obj); + return this._classToType()[strType] || "object"; + }, namespace: function(root, namespaceString) { var current, parts; parts = (namespaceString ? namespaceString.split(".") : []); diff --git a/lib/routes.js.coffee b/lib/routes.js.coffee index 6f03e107..c08f9c53 100644 --- a/lib/routes.js.coffee +++ b/lib/routes.js.coffee @@ -6,21 +6,21 @@ defaults = NodeTypes = NODE_TYPES Utils = + serialize: (obj) -> return "" unless obj if window.jQuery result = window.jQuery.param(obj) - return (if not result then "" else "?#{result}") + return (if not result then "" else result) s = [] - for own key, prop of obj - if prop? - if prop instanceof Array - for val, i in prop - s.push "#{key}#{encodeURIComponent("[]")}=#{encodeURIComponent(val.toString())}" - else - s.push "#{key}=#{encodeURIComponent(prop.toString())}" + for own key, prop of obj when prop? + if @getObjectType(prop) is "array" + for val, i in prop + s.push "#{key}#{encodeURIComponent("[]")}=#{encodeURIComponent(val.toString())}" + else + s.push "#{key}=#{encodeURIComponent(prop.toString())}" return "" unless s.length - "?#{s.join("&")}" + s.join("&") clean_path: (path) -> path = path.split("://") @@ -37,12 +37,12 @@ Utils = anchor = "" if options.hasOwnProperty("anchor") anchor = "##{options.anchor}" - options.anchor = null + delete options.anchor anchor extract_options: (number_of_params, args) -> ret_value = {} - if args.length > number_of_params and typeof (args[args.length - 1]) is "object" + if args.length > number_of_params and @getObjectType(args[args.length - 1]) is "object" ret_value = args.pop() ret_value @@ -51,13 +51,13 @@ Utils = # null, undefined, false or '' return "" unless object property = object - if typeof (object) is "object" + if @getObjectType(object) is "object" property = object.to_param or object.id or object - property = property.call(object) if typeof (property) is "function" + property = property.call(object) if @getObjectType(property) is "function" property.toString() clone: (obj) -> - return obj if null is obj or "object" isnt typeof obj + return obj if !obj? or "object" isnt @getObjectType(obj) copy = obj.constructor() copy[key] = attr for own key, attr of obj copy @@ -73,8 +73,10 @@ Utils = throw new Error("Too many parameters provided for path") if args.length > required_parameters.length parameters = @prepare_parameters(required_parameters, args, opts) @set_default_url_options optional_parts, parameters - result = "#{Utils.get_prefix()}#{@visit(route, parameters)}" - Utils.clean_path("#{result}#{Utils.extract_anchor(parameters)}") + Utils.serialize(parameters) + result = "#{@get_prefix()}#{@visit(route, parameters)}" + url = Utils.clean_path("#{result}#{@extract_anchor(parameters)}") + url += "?#{url_params}" if (url_params = @serialize(parameters)).length + url # # This function is JavaScript impelementation of the # Journey::Visitors::Formatter that builds route by given parameters @@ -86,11 +88,13 @@ Utils = # If set to `true`, this method will not throw when encountering # a missing parameter (used in recursive calls). # - visit: (route, parameters, optional) -> + visit: (route, parameters, optional = false) -> [type, left, right] = route switch type - when NodeTypes.GROUP, NodeTypes.STAR + when NodeTypes.GROUP @visit left, parameters, true + when NodeTypes.STAR + @visit_globbing left, parameters, true when NodeTypes.LITERAL, NodeTypes.SLASH, NodeTypes.DOT left when NodeTypes.CAT @@ -101,7 +105,7 @@ Utils = when NodeTypes.SYMBOL value = parameters[left] if value? - parameters[left] = null + delete parameters[left] return @path_identifier(value) if optional "" # missing parameter @@ -115,11 +119,65 @@ Utils = else throw new Error("Unknown Rails node type") + # + # This method convert value for globbing in right value for rails route + # + visit_globbing: (route, parameters, optional) -> + [type, left, right] = route + value = parameters[left] + return @visit(route, parameters, optional) unless value? + parameters[left] = switch @getObjectType(value) + when "array" + value.join("/") + else + value + @visit route, parameters, optional + + # + # This method check and return prefix from options + # get_prefix: -> prefix = defaults.prefix prefix = (if prefix.match("/$") then prefix else "#{prefix}/") if prefix isnt "" prefix + # + # This is helper method to define object type. + # The typeof operator is probably the biggest design flaw of JavaScript, simply because it's basically completely broken. + # + # Value Class Type + # ------------------------------------- + # "foo" String string + # new String("foo") String object + # 1.2 Number number + # new Number(1.2) Number object + # true Boolean boolean + # new Boolean(true) Boolean object + # new Date() Date object + # new Error() Error object + # [1,2,3] Array object + # new Array(1, 2, 3) Array object + # new Function("") Function function + # /abc/g RegExp object + # new RegExp("meow") RegExp object + # {} Object object + # new Object() Object object + # + # What is why I use Object.prototype.toString() to know better type of variable. Or use jQuery.type, if it available. + # _classToTypeCache used for perfomance cache of types map (underscore at the beginning mean private method - of course it doesn't realy private). + # + _classToTypeCache: null + _classToType: -> + return @_classToTypeCache if @_classToTypeCache? + @_classToTypeCache = {} + for name in "Boolean Number String Function Array Date RegExp Undefined Null".split(" ") + @_classToTypeCache["[object " + name + "]"] = name.toLowerCase() + @_classToTypeCache + getObjectType: (obj) -> + return window.jQuery.type(obj) if window.jQuery and window.jQuery.type? + strType = Object::toString.call(obj) + @_classToType()[strType] or "object" + namespace: (root, namespaceString) -> parts = (if namespaceString then namespaceString.split(".") else []) return unless parts.length @@ -129,4 +187,4 @@ Utils = Utils.namespace window, "NAMESPACE" window.NAMESPACE = ROUTES -window.NAMESPACE.options = defaults \ No newline at end of file +window.NAMESPACE.options = defaults diff --git a/spec/js_routes/rails_routes_compatibility_spec.rb b/spec/js_routes/rails_routes_compatibility_spec.rb index ec5b860b..dbad75b8 100644 --- a/spec/js_routes/rails_routes_compatibility_spec.rb +++ b/spec/js_routes/rails_routes_compatibility_spec.rb @@ -76,14 +76,28 @@ evaljs("Routes.book_path('thrillers', 1)").should == routes.book_path('thrillers', 1) end - it "should support routes globbing as hash" do - pending - evaljs("Routes.book_path(1, {section: 'thrillers'})").should == routes.book_path(1, :section => 'thrillers') + it "should support routes globbing as array" do + evaljs("Routes.book_path(['thrillers'], 1)").should == routes.book_path(['thrillers'], 1) + end + + it "should bee support routes globbing as array" do + evaljs("Routes.book_path([1, 2, 3], 1)").should == routes.book_path([1, 2, 3], 1) end it "should bee support routes globbing as hash" do - pending - evaljs("Routes.book_path(1)").should == routes.book_path(1) + evaljs("Routes.book_path('a_test/b_test/c_test', 1)").should == routes.book_path('a_test/b_test/c_test', 1) + end + + it "should support routes globbing as array with optional params" do + evaljs("Routes.book_path([1, 2, 3, 5], 1, {c: '1'})").should == routes.book_path([1, 2, 3, 5], 1, { :c => "1" }) + end + + it "should support routes globbing in book_title route as array" do + evaljs("Routes.book_title_path('john', ['thrillers', 'comedian'])").should == routes.book_title_path('john', ['thrillers', 'comedian']) + end + + it "should support routes globbing in book_title route as array with optional params" do + evaljs("Routes.book_title_path('john', ['thrillers', 'comedian'], {some_key: 'some_value'})").should == routes.book_title_path('john', ['thrillers', 'comedian'], {:some_key => 'some_value'}) end end @@ -98,6 +112,7 @@ it "should support serialization of objects" do evaljs("window.jQuery.param(#{_value.to_json})").should == _value.to_param evaljs("Routes.inboxes_path(#{_value.to_json})").should == routes.inboxes_path(_value) + evaljs("Routes.inbox_path(1, #{_value.to_json})").should == routes.inbox_path(1, _value) end end context "when parameters is a hash" do @@ -202,5 +217,25 @@ "Routes.inbox_message_path({id:1, to_param: 'my'}, {id:2}, {custom: true, format: 'json'})" ).should == routes.inbox_message_path(inbox, 2, :custom => true, :format => "json") end + + context "when globbing" do + it "should prefer to_param property over id property" do + evaljs("Routes.book_path({id: 1, to_param: 'my'}, 1)").should == routes.book_path(inbox, 1) + end + + it "should call to_param if it is a function" do + evaljs("Routes.book_path({id: 1, to_param: function(){ return 'my';}}, 1)").should == routes.book_path(inbox, 1) + end + + it "should call id if it is a function" do + evaljs("Routes.book_path({id: function() { return 'technical';}}, 1)").should == routes.book_path('technical', 1) + end + + it "should support options argument" do + evaljs( + "Routes.book_path({id:1, to_param: 'my'}, {id:2}, {custom: true, format: 'json'})" + ).should == routes.book_path(inbox, 2, :custom => true, :format => "json") + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 190608c1..f609a126 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -68,6 +68,7 @@ def draw_routes match "/other_optional/(:optional_id)" => "foo#foo", :as => :foo match 'books/*section/:title' => 'books#show', :as => :book + match 'books/:title/*section' => 'books#show', :as => :book_title mount BlogEngine::Engine => "/blog", :as => :blog_app