Run routing as an event - backbone.js

I have a frontend javascript application built with require.js and backbone.js. Most parts of the application use the standard/recommended way of building application, including routing by using the Backbone Router object.
Now I want to add some more visual changes in one part of the application. Instead of clicking a link and the router render another view, I want some visual changes before that happens. Like GUI-effects happening when clicking the link, then when that effect is complete, the new view should render like before.
I guess one possible way to do this is by hooking a click event to the given link, cancel normal propagation (canceling the route catching in the backbone object), perform the visual stuff, and then manually call the router or render the view directly. Then I would need to have access to the router object from the view (to call the action method that normally catch the click), or I would need to render the view from within the click event added to the link, causing the render code to be duplicated (in the event function and in the view).
Is there a good and tidy way to do something like this, without making ugly spaghetti-code?

You can use the following code to catch all clicks on every link, and then do what you want :
$(document).on('click', 'a:not([data-bypass])', function (evt) {
var href = $(this).attr('href');
var protocol = this.protocol + '//';
if (href.slice(protocol.length) !== protocol) {
evt.preventDefault();
var rootLength = Backbone.history.root.length - ((Backbone.history.root.substring(Backbone.history.root.length - 1) === '/') ? 1 : 0);
// Here before calling the history.navigate that trigger your router
// routes, do your visual effects
Backbone.history.navigate(href.slice(rootLength), {
trigger: true
});
}
});

Related

Test fails because AngularJS has not initialized in time

I'm trying out TestCafe for an AngularJS (v1.6) application.
I have a button then when clicked, opens a modal (from UI bootstrap). This works fine when I try myself in Chrome.
<button class="btn" ng-click="open()">Open</button>
Our application requires user authentication, and the login page is not Angular-based. That phase of my test works fine.
However, when the actual test runs, it "clicks" the button but nothing happens.
I suspect, but can't prove, that it's clicked before AngularJS has properly initialized on the page.
With some research, I found the testcafe-angular-selectors project and a waitForAngular method but that appears to apply only to Angular2+.
import { Role, Selector } from 'testcafe';
const regularAccUser = Role('http://127.0.0.1:8080', async t => {
await t
.typeText('[name=username]', 'abc')
.typeText('[name=password]', '123')
.click('.btn-primary');
});
fixture`Characters Modal`;
test('modal title', async t => {
await t
.useRole(regularAccUser)
.navigateTo('http://127.0.0.1:8080/fake/page')
.click('.btn')
.expect(Selector('.modal-title').innerText).eql('Insert Symbol');
});
Adding .wait(1000) before the click solves the issue. It's not waiting for Angular to load. I'd rather not have waits in every test - is there some other technique I can use?
You can use TestCafe assertions as a mechanism to wait until an element is ready before acting on it.
A typical waiting mechanism would be:
const button = Selector('button.btn')
.with({visibilityCheck: true});
await t
.expect(button.exists) // wait until component is mounted in DOM
.ok({timeout: 10000}) // wait enough time
.hover(button) // move TestCafe cursor over the component
.expect(button.hasAttribute('disabled'))
.notOk({timeout: 10000}) // wait until the button is enabled
.click(button); // now we are sure the button is there and is clickable
This article may also help you in managing all those waiting mechanisms.
As you correctly mentioned, the waitForAngular method is intended for Angular only, not for AngularJS.
I recommend you create your own waitForAngularJS function and call it on the beforeEach hook and after the role was initialized.
In the simplest case, it can be implemented as follows:
function waitForAngularJS (t) {
await t.wait(1000);
}
fixture `App tests`
.page('page with angularjs')
.beforeEach(async t => {
await waitForAngularJS(t);
});
However, the use of the wait method is not a solid solution. I recommend you find a way to detect if AngularJS is loaded on a page on the client side. If it is possible, you can implement the waitForAngularJS method using the TestCafe ClientFunctions mechanism.
This post can be useful as well: How to check if angular is loaded correctly

React-Router v4 - Prevent Transition With Function

I was able to prevent navigation as per the v4 docs, but I'm trying to hook up a function so that I can use a modal instead of an alert.
Function:
abandonForm = (route) => {
this.props.showModal('confirm');
console.log('leaving..');
}
In my page:
<NavigationPrompt when={true} message={(location) => this.abandonForm('confirm')} />
this.props.showModal('confirm') activates the modal successfully, but behind the modal the page still transitions - how can I prevent transition until a button in the modal is clicked?
Browsers only allow navigation cancellation by means of the alert box that you've mentioned. This restriction is motivated by phishing/scamming sites that try to use javascript gimmicks to create user experiences that convincingly mimic something that a browser or the OS would do (whom the user trusts). Even the format of the text shown in the alert box is crafted so that it's obvious that it originates from the site.
Of course, as long as the current URL stays within your app, you have control over it using react-router's history. For example you can do the following on navigation:
allow the navigation without confirmation
immediately navigate back to the previous location, but now with a modal on top
navigate away for real this time when the user clicks on a button in the modal.
The disadvantage of this approach (leaving out the sheer complexity of it) is that the user will not get a confirmation dialog if they try to navigate to a different site entirely.
Use:
this.unBlock = this.props.history.block((location, navigateToSelectedRoute) => {
// save navigateToSelectedRoute eg this.navigateToSelectedRoute =
// navigateToSelectedRoute;
// use this.navigateToSelectedRoute() afterwards to navigate to link
// show custom modal using setState
});
and when unblocking is done then call this.unBlock() to remove the listener.
Documentation here for history api

Preventing page navigation inside a Backbone-driven SPA

The justification
In my BB app, I allow rapid input from users which gets queued & sent off periodically in the background to the server. The problem I currently have is if a user leaves the page they effectively discard any pending changes sitting in the queue.
So basically what I want to do is inform the user before they leave to give them the opportunity to wait for the changes to be saved rather than just exiting & discarding.
The nitty gritty
So for the general cases where the user refreshes or attempts to navigate to an external URL we can handle the onbeforeunload event. Where it becomes slightly tricky is when we are in the context of an SPA whereby switching between pages does not cause a page refresh.
My immediate thought was to use a global click event handler for all anchors and validate whether or not I want to allow the click, which would work for in-site link navigation. However, where this falls over is navigating via the browsers Back/Forward buttons.
I also had a look at Backbone.routefilter, which at first glance appeared to do exactly what I needed. However, using the simple case as described in the docs, the route was still being executed.
The question
How do we intercept navigation for all scenarios within a Backbone SPA?
Direct link navigation
Use a global event handler to capture all click events
$(document).on('click', 'a[href^="/"]', function (e) {
var href = $(e.currentTarget).attr('href');
e.preventDefault();
if (doSomeValidation()) {
router.navigate(href, { trigger: true });
}
});
Page refreshing / external URL navigation
Handle the onbeforeunload event on the window
$(window).on('beforeunload', function (e) {
if (!doSomeValidation()) {
return 'Leaving now will may result in data loss';
}
});
Browser back/forward button navigation
Behind the scenes Backbone.Router uses the Backbone.history which ultimately leverages the HTML5 pushstate API. Depending on what options you pass to Backbone.history.start, and what your browser is capable of, the API will hook into either the onhashchange event or the onpopstate event.
Delving into the source for Backbone.history.start it becomes apparent that regardless of whether you are using push state or not, the same event handler is used i.e. checkUrl.
if (this._hasPushState) {
addEventListener('popstate', this.checkUrl, false);
} else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
Therefore, we can override this method & perform our validation in there
var originalCheckUrl = Backbone.history.checkUrl;
Backbone.history.checkUrl = function (e) {
if (doSomeValidation()) {
return originalCheckUrl.call(this, e);
} else {
// re-push the current page into the history (at this stage it's been popped)
window.history.pushState({}, document.title, Backbone.history.fragment);
// cancel the original event
return false;
}
};

Handing cold navigation to internal URL in Backbone?

I'm wondering how people handle the following case in Backbone: usually when a user navigates to the root of your app, a certain set of data is loaded from the backend, processed right away and then displayed in the DOM.
There are links in the app that will navigate you to different sub-sections of it. The router catches the navigation and replaces the current page with whatever page you navigated to, all based on the same data that's already been fetched.
The problem is that the user could bookmark that internal/secondary URL and navigate to it "cold", as in before the data has had a chance to be fetched, without going through the root URL. Is there an idiomatic/conventional way of handling that situation (in the router, I'm assuming)?
One way is, in the various router path-handling functions, to always call a method that will check if there's sufficient data to complete the operation, and if not, fetch it and then proceed?
Backbone won't hit the initial route in your router before you call Backbone.history.start, so you can delay it until you've done the necessary setup. I typically define a start method on my application's main router. Looks something like:
var AppRouter = Backbone.Router.extend({
start: function() {
//init code here
something.fetch({success: function() {
//only call history start after the necessary initial setup is done
Backbone.history.start();
}});
}
});
And then start the application using that method:
window.app = new AppRouter();
window.app.start();
It's good to remember that there is nothing confining you to build your application using only the predefined pieces provided by Backbone. If your startup code is heavy, it may not belong to the router. In such case you should define a helper function to encapsulate the startup logic, and leave the router out of it altogether:
//startup.js
function startup(onComplete) {
//do initialization stuff...
onComplete();
});
//main.js
startup(function() {
Backbone.history.start();
});

Converting existing web app to use hashtag URIs using Backbone.js

I'm attempting to use Backbone and it's Router to turn an app into an ajax app, however it currently uses several different methods (helpers) of generating links. Unfortunately, this means manually changing each and every link to use a hashtag is out of the question.
What would be the best method of ensuring every link, form post, redirect, etc. gets parsed as a hashtag URL that can be caught by Backbone's Router? Or, even better, is it possible for the Router to accept "true URL's" from a request? Example: a request to /app/mail/inbox.php is caught by a rule in the Router, and is turned into #/mail/inbox after firing the appropriate method to handle the request.
What would be the best method of ensuring every link, form post, redirect, etc. gets parsed as a hashtag URL that can be caught by Backbone's Router?
I don't think that Backbone.Router is supposed to handle, say, form posts. It's supposed to give your application view state—bookmark-friendly and refreshable URLs [1].
If you want to ‘ajaxify’ forms, then you probably should add a handler for form's submit event and do something like $.ajax() there, preventing the default action.
Regarding plain old links, History.pushState() support has been added to Backbone recently. It means that you can define your routes as /app/*, and don't need to replace old href attributes. However, you'll still need to catch link click events to prevent default action.
For example:
var handle_link_click = function(e) {
path = $(e.target).attr('href');
app.main_router.navigate(path, true); // This.
e.preventDefault();
};
$('a:internal').click(handle_link_click);
Router's navigate() method will do history.pushState() if it's available, falling back to old hashchange. And true as a second argument means that it will fire corresponding handler action.
[1] See also this presentation about Backbone

Resources