I'm trying to pass a directory location and the file path as part of the hashmaps in backbone routes. This is the url with hashmaps:
localhost/index.html#directory-http://localhost/foobar-html/foo.html
and this is what my route that maps the above url:
routes: {
'directory-*directoryPath-*filePath': 'render'
},
render: function (directoryPath, filePath) {
// I get the entire url in directoryPath variable
// http://localhost/foobar-html/foo.html
// and filePath is empty.
}
What would be the right way to map such type of hash URL? Thanks!
From the fine manual:
Routes can contain parameter parts, :param, which match a single URL component between slashes; and splat parts *splat, which can match any number of URL components.
Your problem is that a splat eats up everything so having two splats in one route is pointless; you can't use a parameter part, :x, because that stops at a slash.
There are a few things you can do.
You could URI encode the slashes in link and use parameter parts. The URL would look like this:
#directory-http:%2f%2flocalhost%2ffoobar-html%2ffoo.html
and the router would be like this:
routes: {
'directory-:directoryPath-:filePath': 'render'
},
render: function(d, f) {
d = decodeURIComponent(d);
f = decodeURIComponent(f);
//...
}
Demo: http://jsfiddle.net/ambiguous/xBnaN/
You could add your route as a regex using route, that would give you more freedom in how you construct the pattern. For example, a fragment like this:
#directory-http://localhost/foobar-html/foo.html
could be handled with a router like this:
initialize: function() {
this.route(/directory-(.*?)-(.*)/, 'render');
},
render: function(d, f) {
//...
}
Demo: http://jsfiddle.net/ambiguous/r8MBb/
The second option will run into problems with you inevitably get a - inside your directoryPath or filePath; you could URI encode embedded -s to get them through the first option though.
Related
I've a link like this:
<a ui-sref="someState(Param:'مثال')"> A localized param </a>
when compiling the Angular-ui-router, generates a href like this :
A localized param
how can I avoid this?
what i have tried:
creating a new type using $urlMatcherFactoryProvider
$urlMatcherFactoryProvider.type('decoded', {
encode: function (item) {
return decodeURIComponent(item) // i put this to decode personally
},
decode: function (item) {
return decodeURIComponent(item);
},
is: function (item) {
return true;
}
});
this actually happens inside 'angular' not 'ngRoute' , angular forces encoding all urls using encodeURICompenent,
so you need to change encodeUriQuery inside angular.js so it would pass arabic characters without encoding
function encodeUriQuery(val, pctEncodeSpaces) {
var r = /[\u0600-\u06ff]|[\u0750-\u077f]|[\ufb50-\ufc3f]|[\ufe70-\ufefc]/;
if(r.test(val)){
return val.replace(/\s/g,'-');
}else{
return encodeURIComponent(val).
replace(/%40/gi, '#').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%3B/gi, ';').
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
}
}
if you're using minified version or don't want to bother looking into code here is a monkey patch you can copy and paste this anywhere in your code
window.encode = window.encodeURIComponent;
window.encodeURIComponent = function(val){return /[\u0600-\u06ff]|[\u0750-\u077f]|[\ufb50-\ufc3f]|[\ufe70-\ufefc]/.test(val) ? val.replace(/\s/g,'-') : window.encode(val)};
notice that part replace(/\s/g,'-') it replaces whitespaces with a dash because in angular it causes some issue to have whitespace in the url
the solution it's very simple actually
you can use javaScript decodeURIComponent() function on $scope.item.title for me it's worked like charm .
in controller use it like
$state.go("single-page", { contentType: "portfolio", date: "139411", title: decodeURIComponent("اولین-نمونه-کار-تست") });
in inspect you see something like :
its better for google seo ، google understood farsi better that way
but when you click on the link in url place you see this
So this is my first Backbone project and I'm wondering if I'm doing things in the best way. My app basically has two states, one of them displays a search box and the other displays a search box with a table under it. My router has routes for searching and for the initial landing page with just the search view. When the user types in the query the router navigates to the search route and the table view is added to the page. This is my router:
app.Router = Backbone.Router.extend({
routes: {
'': 'index',
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search: function(address, age, rad){
app.statusView || (app.statusView = new app.StatusView());
app.searchView || (app.searchView = new app.SearchView());
app.trigger('status:loading');
app.Practices.fetch({
reset: false,
success: function() {
app.searchView.setElement($('#search-box')).render();
var searchQuery = new app.SearchQueryModel({age: age, coords: address.split(","), radius: rad});
if (!app.tableView){
app.tableView = new app.TableView({model: searchQuery});
} else {
app.tableView.model = searchQuery;
app.tableView.refresh();
};
}
});
app.trigger('status:clear');
},
index: function() {
app.statusView = new app.StatusView();
app.searchView = new app.SearchView();
app.footerView = new app.FooterView();
app.searchView.setElement($('#search-box')).render();
}
});
As you can see my views are instantiated in the index route and then the same views are used when you search, unless the user is going directly to the search page in which case the views are instantiated there. I'd be surprised if this wasn't very sub-optimal because it seems clumsy to be checking if the view already exists in the search route. Is there a better way of doing things?
Lets say its not bad, but there is one better approach.
As for now you router is in charge of hook-up URL with app astatus and also for view and model control. The second may be detached from Router, so you will need Controller abstraction, but Backbone does not provide Controller "from the box".
But this is not the problem, you can use plugin or take a look at Controller realization in Marionette.js
The main idea here is to split responsibilities between app part correctly:
1) Router - keeps routes and hook up URL with controller action
2) Controller - manage views and models (create, delete, fetch and so on)
3) View - listen to model and DOM events and render data
4) Model - provide actual data and work with data.
First of all welcome to Backbone. It is a lovely framework which can allow you to make things as beautiful or ugly as you'ld like. Your question is about where view instantiation should be, in terms of good practices. Of course it seems sort of wrong to do it there as it violates the Law of Demeter by handling both url routing and view instantiation.
But the views have to be run from somewhere right? If not the router then where?
So I have two responses:
If your app is simple and you just want to play with backbone then you're probably going to be fine. A lot of people let single page app frameworks complicate otherwise simple apps. I'm not trying to be lazy, but where you have it now is the natural beginner's choice in Backbone. If this is your case then stop here.
If you want to use the full power of backbone to custom make a framework then read on.
So my setup is designed to be able to start a new project using some boilerplate functions and create only a few classes which are specific to the new app. Route handling and all of that kind of thing seems low-level enough to me that it should be just part of some configuration that I don't want to look at often. The upshot is that my router looks like this:
define([
'autorouter'
], function(AutoRouter){
var AppRouter = AutoRouter.extend({
autoRoutes: {
":page" : "routeDirect",
":page/:object" : "routeDirect",
":page/:object/:action" : "routeDirect",
"": "routeDirect"
}
});
return AppRouter;
});
Then for each new project I have a file where I keep the non-default routes, for instance:
define(function(require){
return {
"schedule" : require('screens/schedule')
, "logout" : require('screens/logout')
, "login" : require('screens/login')
, "create" : require('screens/create')
, "upload" : require('screens/upload')
, "select" : require('screens/selection')
, "inventory" : require('screens/inventory')
, "describe" : require('screens/description')
}
});
I put each screen into it's own file (using requirejs for the multi-file dependency management). The extra variables get passed through to the screen.
Each screen is the brain for a particular user experience and is responsible for loading views and maybe handling some events while that screen is active.
If that seems like an interesting setup then here is how I did it:
For the router itself I use a boilerplate class which I borrowed from Derick Bailey with some slight modifications:
define([
'jquery', 'underscore', 'backbone'],
function($, _, Backbone) {
var AutoRouter = Backbone.Router.extend({
constructor: function(options){
Backbone.Router.prototype.constructor.call(this, options);
var that = this;
that.app = options.app;
if (this.autoRoutes){
that.processAutoRoutes(options.app, that.autoRoutes);
}
},
processAutoRoutes: function(app, autoRoutes){
var method, methodName;
var route, routesLength;
var routes = [];
var router = this;
for(route in autoRoutes){
routes.unshift([route, autoRoutes[route]]);
}
routesLength = routes.length;
for (var i = 0; i < routesLength; i++){
route = routes[i][0];
methodName = routes[i][1];
method = app[methodName];
router.route(route, methodName, method);
}
}
});
return AutoRouter;
});
I never have to look at it, but I do need to pass it an app instance. For example:
this.appRouter = new AppRouter({app : this});
Finally my route direction function:
define(function(require){
var pathParser = function(path){
return Array.prototype.slice.call(path);
}
var pathApply = function(path, routes, context){
var pathArray = pathParser(path);
var primary = pathArray[0];
if (routes.hasOwnProperty(primary)){
routes[primary].apply(context, pathArray.slice(1));
} else {
routes["default"].apply(context, pathArray.slice(1));
}
}
return function(path){
//NOTE PLEASE that this references AutoRouter
//Which has an app property
var oApp = this.app;
var pathRoutes = _.extend(require('urls'), {
"default" : require('screens/default')
});
pathApply(arguments, pathRoutes, oApp);
};
});
So, did I make things better? Well if you're doing something very simple with just a screen or two, then you certainly don't want to build this sort of setup from scratch. But if you're like me, and you want to be able to quickly produce new projects then having some boilerplate like the two classes above allows for one JSON object to tell the app which routes I should send to which screens. Then I can have all of the logic in the appropriate places, allowing separation of concerns. Which is why I think Backbone is so pleasant.
My understanding of your problem is that you are triggering a route each time you are hitting search.
If this is how you are doing it, then use view events hash (used to capture and handle events that happen in a view) for search.Don't use routes. Define an events hash in the view and have a callback to handle the search.
var myAppEventBus = _.extend({},Backbone.Events);
var myAppController = {
function : search(options) {
// create an instance of the collection and do a fetch call passing the
// search parameters to it.
var searchResultsCollection = new SearchResultsCollection();
// pass search criteria, the success and error callbacks to the fetch
// method.
var that = this;
searchResultsCollection.fetch(
{
data:that.options,
success : function() {
// Pass the fetched collection object in the trigger call so that
// it can be
// received at the event handler call back
var options = {
"searchResultsCollection" : that.searchResultsCollection;
};
myAppEventBus.trigger("search_event_triggered",options);
},
error : function() {
// do the error handling here.
}
}
);
}
};
// Application Router.
var MyAppRouter = Backbone.Router.extend({
routes : {
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search : function(searchParams) {
// Fetch the query parameters and pass it to the view.
var routeSearchExists = false;
var searchOptions = {};
var options = {};
if(searchParams) {
routeSearchExists = true;
// If search params exist split and set them accordingly in
// the searchOptions object.
options.searchOptions = searchOptions;
}
// Create and render the search view. Pass the searchOptions
var searchView = new SearchView(options);
searchView.render();
// Create and render an instance of the search results view.
var searchResultsView = new SearchResultsView();
searchResultsView.render();
// If there are search parameters from the route, then do a search.
if(routeSearchExists) {
searchView.search();
}
}
});
// The main view that contains the search component and a container(eg: div)
// for the search results.
var SearchView = Backbone.View.extend({
el : "#root_container",
searchOptions : null,
initialize : function(options) {
// Intialize data required for rendering the view here.
// When the user searches for data thru routes, it comes down in the
// options hash which can then be passed on to the controller.
if(options.searchOptions) {
this.searchOptions = options.searchOptions;
}
},
events : {
"search #search_lnk":"initSearch"
},
initSearch : function(event) {
event.preventDefault();
var searchOptions = {};
// Fetch the search fields from the form and build the search options.
myAppController.search(searchOptions);
},
search : function() {
if(this.searchOptions) {
myAppController.search(searchOptions);
}
}
});
// The view to display the search results.
var SearchResultsView = Backbone.View.extend({
searchResultsCollection : null;
initialize : function(options) {
// Handling the triggered search event.
myAppEventBus.on("search_event_triggered",this.render,this);
},
render : function(options) {
//search results collection is passed as a property in options object.
if(options.searchResultsCollection)
//Render your view.
else
// Do it the default way of rendering.
}
});
SearchView is the root view that contains the search component and a container like div to hold the search results.
SearchResultsView displays the result of a search.
When search option is clicked, the event callback (initSearch) gets the entered search data.
The search method on myAppController object is invoked and the search query is passed.
An instance of the search collection is created and fetch is invoked passing it the search query and also the success and error callback.
On success, a custom backbone event is triggered along with the fetched collection.
The callback(render method in SearchResultsView) for this event is invoked.
The callback renders the results of the search.
When loading in the router an instance for both the views can be created(the results view will be empty) and attached to the dom.
If you wish to search by multiple query strings at the url then I would suggest you to use the following route.
search?*queryString.
In the route callback make a call to a utility function the splits the querystring and returns you a search object and pass on the search string to the view.
in my view I've got a scope object which contains the params of my query.
What is the easiest way to generate a href attribute using it in a link in my view?
$scope.search_params // => { q: "test", order: "asc", filter: "price" }
// in my view I want to generate a link like this.
...
thanks.
You could use the ng-href directive :
<a ng-href="/search?q={{ search_params.q }}&order={{ search_params.order }}&filter={{ search_params.filter }}">...</a>
If you want to customise your URL (when for example parameters are missing), you should create a custom function in your controller and refer it in the view.
Something like this :
$scope.myHref = function () {
var res = '?';
if (search_param.q) {
res = res + 'q=' + search_param.q;
}
...
}
<a ng-href="/search{{ myHref() }}">...</a>
I think that the first solution is much cleaner, and you should check the given URL after to check if it's null.
More info from the docs.
Similar to Django's {{ url }}, is there a method or way to reverse a particular route by passing it a name and variables.
// example Router
var router = Backbone.Router.extend({
routes: {
'!/user/:user_id': 'editUserAction',
'!/': 'homeAction'
},
editUserAction(user_id) {
// edit user view
},
homeAction() {
// home view
}
});
Some method like
router.reverse('editUserAction', '5');
Would return the hash: !/user/5
router.reverse('homeAction');
Would return the hash: !/
A discussion about reverse routing. https://github.com/documentcloud/backbone/issues/951
a simple hack
var personRoutes = {
list: "/persons",
show: "/persons/:id",
edit: "/persons/:id/edit"
}
var getRoute = function(obj, route, routeDefinitions){
return routeDefinitions[route].replace(":id", obj.id);
}
var person = new Model({id: 1});
var route = getRoute(person, "edit", personRoutes); // => "/persons/1/edit"
Unfortunately no, there isn't anything like this built in to backbone. I've wanted something similar and there has been discussion of this on the list once or twice - maybe even a pull request (don't remember for sure at the moment). But it has not yet been done.
The best that I've come up with is to write my own methods that produce the correct route string:
function userEditPath(userId){
return "/user/" + userId + "/edit";
}
Using Backbone, is it possible for me to get the name of the current route? I know how to bind to route change events, but I'd like to be able to determine the current route at other times, in between changes.
If you have instantiated a Router in your application, the following line returns the current fragment:
Backbone.history.getFragment();
From the Backbone.js documentation:
"
[...]
History serves as a global router (per frame) to handle hashchange events or pushState, match the appropriate route, and trigger callbacks. You shouldn't ever have to create one of these yourself — you should use the reference to Backbone.history that will be created for you automatically if you make use of Routers with routes.
[...]"
If you need the name of the function bound to that fragment, you can make something like this inside the scope of your Router:
alert( this.routes[Backbone.history.getFragment()] );
Or like this from outside your router:
alert( myRouter.routes[Backbone.history.getFragment()] );
Robert's answer is interesting, but sadly it will only work if the hash is exactly as defined in the route. If you for example have a route for user(/:uid) it won't be matched if the Backbone.history.fragment is either "user" or "user/1" (both which are the two most obvious use cases for such route). In other words, it'll only find the appropriate callback name if the hash is exactly "user(/:uid)" (highly unlikely).
Since i needed this functionality i extended the Backbone.Router with a current-function that reuses some of the code the History and Router object use to match the current fragment against the defined Routes for triggering the appropriate callback.
For my use case, it takes the optional parameter route, which if set to anything truthful will return the corresponding function name defined for the route. Otherwise it'll return the current hash-fragment from Backbone.History.fragment.
You can add the code to your existing Extend where you initialize and setup the Backbone router.
var Router = new Backbone.Router.extend({
// Pretty basic stuff
routes : {
"home" : "home",
"user(:/uid)" : "user",
"test" : "completelyDifferent"
},
home : function() {
// Home route
},
user : function(uid) {
// User route
},
// Gets the current route callback function name
// or current hash fragment
current : function(route){
if(route && Backbone.History.started) {
var Router = this,
// Get current fragment from Backbone.History
fragment = Backbone.history.fragment,
// Get current object of routes and convert to array-pairs
routes = _.pairs(Router.routes);
// Loop through array pairs and return
// array on first truthful match.
var matched = _.find(routes, function(handler) {
var route = handler[0];
// Convert the route to RegExp using the
// Backbone Router's internal convert
// function (if it already isn't a RegExp)
route = _.isRegExp(route) ? route : Router._routeToRegExp(route);
// Test the regexp against the current fragment
return route.test(fragment);
});
// Returns callback name or false if
// no matches are found
return matched ? matched[1] : false;
} else {
// Just return current hash fragment in History
return Backbone.history.fragment
}
}
});
// Example uses:
// Location: /home
// console.log(Router.current()) // Outputs 'home'
// Location: /user/1
// console.log(Router.current(true)) // Outputs 'user'
// Location: /user/2
// console.log(Router.current()) // Outputs 'user/2'
// Location: /test
// console.log(Router.current(true)) // Outputs 'completelyDifferent'
I'm sure some improvements could be made, but this is a good way to get you started. Also, it's easy to create this functionality without extending the Route-object. I did this because it was the most convenient way for my set-up.
I haven't tested this fully yet, so please let me know if anything goes south.
UPDATE 04/25/2013
I did some changes to the function, so instead of returning either the hash or route callback name, i return an object with fragment, params and route so you can access all the data from the current route, much like you would from the route-event.
You can see the changes below:
current : function() {
var Router = this,
fragment = Backbone.history.fragment,
routes = _.pairs(Router.routes),
route = null, params = null, matched;
matched = _.find(routes, function(handler) {
route = _.isRegExp(handler[0]) ? handler[0] : Router._routeToRegExp(handler[0]);
return route.test(fragment);
});
if(matched) {
// NEW: Extracts the params using the internal
// function _extractParameters
params = Router._extractParameters(route, fragment);
route = matched[1];
}
return {
route : route,
fragment : fragment,
params : params
};
}
See previous code for further comments and explanations, they look mostly the same.
To get the calling route (or url) from the called route handler, you can get it by checking
Backbone.history.location.href ... the full url
Backbone.history.location.search ... query string starting from ?
I got here in the search of this answer so I guess I should leave what I have found.
If you use the root setting for the Router, you can also include it to get the 'real' fragment.
(Backbone.history.options.root || "") + "/" + Backbone.history.fragment
Here's a tad more verbose (or, depending on your taste, more readable) version of Simon's answer:
current: function () {
var fragment = Backbone.history.fragment,
routes = _.pairs(this.routes),
route,
name,
found;
found = _.find(routes, function (namedRoute) {
route = namedRoute[0];
name = namedRoute[1];
if (!_.isRegExp(route)) {
route = this._routeToRegExp(route);
}
return route.test(fragment);
}, this);
if (found) {
return {
name: name,
params: this._extractParameters(route, fragment),
fragment: fragment
};
}
}
If you look at the source for the Router, you'll see that when the router triggers the event saying that something changes, it passes the name with it as "route:name".
http://documentcloud.github.com/backbone/docs/backbone.html#section-84
You can always hook the "route" event on the router and store it to get the current route.
router = new Backbone.Router.extend({
routes: { "stuff/:id" : "stuff" },
stuff: function(id) {}
});
router.on('route', function(route) {
this.current = route;
});
Now if you navigate to /stuff/1, router.current will be "stuff"