In a (single-page) application implemented on top of React and React Router, we are observing a strange pattern when using the router.transitionTo method.
Apologies for the extra verbose context, but don't want to miss something pertinent.
Let's assume that we have the following Router initialization, which is called from the single "physical" page of the application:
define([], function () {
var _Root = React.createClass({
render : function () {
return React.createElement(React.addons.CSSTransitionGroup, {/**/},
React.createElement(Router.RouteHandler, {/**/}));
}
});
var _NotFoundScreen = React.createClass({/**/});
var _NoDefaultScreen = React.createClass({/**/});
var routes = (
React.createElement(Router.Route, {
handler : _Root,
path : "/SampleApplication/"
},
React.createElement(Router.Route, {
path : "Transfer",
handler : Transfer
}),
React.createElement(Router.Route, {
path : "Home",
handler : Home
}),
React.createElement(Router.DefaultRoute, {
handler : Home || _NoDefaultScreen
}),
React.createElement(Router.NotFoundRoute, {
handler : _NotFoundScreen
})));
return {
init : function () {
var router = Router.create({
routes : routes,
location : Router.HistoryLocation
});
router.run(function (rootClass, state) {
if (state.routes.length === 0) {
window.location = state.path;
}
React.render(React.createElement(rootClass, state), document.getElementById("reactContainer"));
});
}
}
});
And then we have the "Transfer" page with something along the lines of:
define([], function () {
var View = (function (_super) {
/*(...)*/
function View() {
/*(...)*/
}
View.prototype.render = function () {
/*(...)*/
return React.DOM.div(null, React.createElement(Button.Button, {
enabled : true,
onClick : function () {
router.transitionTo("/SampleApplication/Home");
},
style : "btn",
visible : true
}, "B1"), React.DOM.br(), "form:", React.createElement(Form.Form, {
style : "form",
visible : true
}, React.createElement(Button.Button, {
enabled : true,
onClick : function () {
router.transitionTo("/SampleApplication/Home");
},
style : "btn btn-primary",
visible : true
}, "B2")));
};
return View;
})(/*(...)*/);
return View;
});
So, we have 2 buttons on the Transfer page - B1 and B2. B2 is wrapped by a "form" element. Both buttons are capable of "navigating" to the home page, when clicked.
Navigation through B1 works as expected.
Navigation through B2 reveals some peculiar behavior.
browser url: host/SampleApplication/Transfer
we click B2
we "navigate" to host/SampleApplication/Home and see the page content for a fraction of a second
fraction of a second later, browser url changes to host/SampleApplication/Home?
we get a white screen (as if we're loading/accessing the application for the first time and it is initializing)
we get the rendered page # host/SampleApplication/Home?
I have been trying to find the issue for a while now and no amount of debugging seems to be producing any results.
The execution flows for the navigation from both B1 and B2 are identical (down to the point where the React Router calls location.push(path)).
Furthermore, this "only" happens with Chrome (desktop and mobile), Opera (mobile) and Android stock browser, while for Firefox (desktop and mobile) both B1 and B2 are able to navigate from one page to another without any extra reloads nor "leaving" the single physical page of the application.
I have been unable to find any pertinent information about similar behavior patterns that could explain what could be going on.
If anyone could provide some insight on what could be happening here, it would be most appreciated.
With best regards,
SYG
Ok, the source of the problem was identified as being the default type of button element - submit (link to the HTML recommendation).
Since the button was being rendered within a form element, the "onClick" of the button was implicitly submitting the form, causing a page reload to get queued. The "onClick" of the button executes router.transitionTo, effectively navigating to the target page and the "queued" page reload from the form submit gets executed immediately afterwards, causing the application to re-initialize.
The behavior was "fixed" by changing the type of the button element to button.
Related
I'm currently facing an issue on Oro Platform v.4.1.10.
I have a specific form page where I'm performing an ajax reload on a specific field change.
The thing is that everything is working well except that the CSS and JS are not applied to my ajax section when reloaded.
When I first load the page, everything is OK :
When the section is reload using Ajax :
An OroDateTimeType field is used in the reloaded section, and according to my issue, the datepicker doesn't init on it.
Some details about the way my Ajax call is performed :
define(function (require) {
'use strict';
let SinisterAjaxRepairman,
BaseView = require('oroui/js/app/views/base/view');
SinisterAjaxRepairman = BaseView.extend({
autoRender: true,
/**
* Initializes SinisterAjaxRepairman component
*
* #param {Object} options
*/
initialize: function (options) {
// assign options to component object
this.$elem = options._sourceElement;
delete options._sourceElement;
SinisterAjaxRepairman.__super__.initialize.call(this, options);
this.options = options;
},
/**
* Renders the view - add event listeners here
*/
render: function () {
$(document).ready(function() {
let sectionTrigger = $('input.repair-section-trigger');
let sectionTargetWrapper = $('.repair-section-content');
sectionTrigger.on('click', function(e) {
$.ajax({
url: sectionTrigger.data('update-url'),
data: {
plannedRepair: sectionTrigger.is(':checked') ? 1 : 0,
id: sectionTrigger.data('sinister-id') ? sectionTrigger.data('sinister-id') : 0
},
success: function (html) {
if (!html) {
sectionTargetWrapper.html('').addClass('d-none');
return;
}
// Replace the current field and show
sectionTargetWrapper
.html(html)
.removeClass('d-none')
}
});
});
});
return SinisterAjaxRepairman.__super__.render.call(this);
},
/**
* Disposes the view - remove event listeners here
*/
dispose: function () {
if (this.disposed) {
// the view is already removed
return;
}
SinisterAjaxRepairman.__super__.dispose.call(this);
}
});
return SinisterAjaxRepairman;
});
The loaded template just contains the form row to update in the related section :
{{ form_row(form.repairman) }}
{{ form_row(form.reparationDate) }}
I think that my issue is related to the page load events used by Oro to trigger the page-component events and update their contents but I'm stuck at this point, I don't find how to trigger programmatically this update on my Ajax success code, in order to have the same rendering of the fields on an initial Page load and an Ajax reload of the section.
Thank you for your help 🙂
The final fix, that I found thanks to Andrey answer, was to update the JS file like this, with the addition of content:remove and content:changed events on ajax response (success section) :
success: function (html) {
if (!html) {
sectionTargetWrapper
.trigger('content:remove')
.html('')
.trigger('content:changed')
.addClass('d-none');
return;
}
// Replace the current field and show
sectionTargetWrapper
.trigger('content:remove')
.html(html)
.trigger('content:changed')
.removeClass('d-none')
}
Hope it could help ! 🙂
I have created a custom select directive and within this directive when you tap into the options it should use $ionicposition to locate the selected option based on the HTML element ID, and then scroll to it using $ionicscroll delegate.
This is the function which locates the option, and then scrolls to it:
private scrollToOption(selectedCode: activeh.objects.ICode): void {
this.$timeout(() => {
let item: HTMLElement = document.getElementById("code-" + selectedCode.CodeID);
if(item){
var itemPosition = this.$ionicPosition.offset(angular.element(item));
this.$ionicScrollDelegate.$getByHandle('modalContent').scrollTo(0, itemPosition.top + 40, false);
}
}, 200);
}
This is where the scrollTo function is called:
private createModal(): void {
this.$ionicModal.fromTemplateUrl('app/shared/directives/selectoption/selectoption.modal.html', {
scope: this.$scope,
hardwareBackButtonClose: false
}).then((modal) => {
this.selectModal = modal;
this.selectModal.show();
if (this.selectedVal !== undefined) {
this.scrollToOption(this.selectedVal);
}
});
}
So like mentioned in the title, this code works perfectly but only the first time that the modal is opened. After the modal has been closed and opened again the $ionicposition.offset is returning values of only 0.
I found a (probably partial) solution to this, wherein instead of using .hide() to hide the modal I use .remove() to completely remove it forcing a complete rebuild.
This mimics the behaviour where it worked the first time as now every time the modal is opened is the 'first' time.
The following code sets up my tree (the s:property tags are struts2 stuff):
$(function () {
$("#networkTree").jstree({
"json_data" : {
"ajax" : {
"url" : "<s:property value='networkTreeDataUrl'/>"
}
},
"plugins" : [ "themes", "json_data", "ui" ],
"themes" : {
"theme" : "default",
"dots" : true,
"icons" : false
},
"core" : {
"html_titles" : true
}
}).bind("select_node.jstree", function (event, data) {
window.location.href = "<s:property value='companyDetailsUrl'/>" + "?companyId=" + data.rslt.obj.attr("id");
})
});
When the user left clicks a tree item the window URL changes depending on the companyDetailsUrl. So far correct but I'd like the browser (chrome) to open the link in a new tab when I click the middle mouse button as usual. It seems any mouse click selects the tree node and this triggers the bound event which replaces the window.location. What's the best way to prevent this?
I'd go for the which-property of the eventhandler.
This provides an easy way to distinguish buttons, according the jQuery-documentation:
event.which also normalizes button presses (mousedown and mouseupevents), reporting 1 for left button, 2 for middle, and 3 for right.
//rest of the code omitted
.bind("select_node.jstree", function (event, data) {
if(event.which == 1) {
//open link in current window
window.location.href = YOURURL;
} else if (event.which == 3) {
//open link in new window
window.open(YOURURL, '_blank');
}
})
Note that you have to replace YOURURL (obviously). I omitted it for readability.
Further note that this will most likely open a new window, instead of a new tab. For further reading on why it opens a new window and how you can open a new tab I recommend reading this question.
We have a single Backbone view comprised of a sidebar and several sub-views. For simplicity, we've decided to have the sidebar and sub-views governed by a single render function. However, the click .edit event seems to be firing multiple times after clicking on one of the sidebar items. For example, if I start out on "general" and click .edit, then hello fires once. If I then click .profile on the sidebar and click .edit again, hello fires twice. Any ideas?
View
events: {
"click .general": "general",
"click .profile": "profile",
"click .edit": "hello",
},
general: function() {
app.router.navigate("/account/general", {trigger: true});
},
profile: function() {
app.router.navigate("/account/profile", {trigger: true});
},
render: function(section) {
$(this.el).html(getHTML("#account-template", {}));
this.$("#sidebar").html(getHTML("#account-sidebar-template", {}));
this.$("#sidebar div").removeClass("active");
switch (this.options.section) {
case "profile":
this.$("#sidebar .profile").addClass("active");
this.$("#content").html(getHTML("#account-profile-template"));
break;
default:
this.$("#sidebar .general").addClass("active");
this.$("#content").html(getHTML("#account-general-template"));
}
},
hello: function() {
console.log("Hello world.");
},
Router
account: function(section) {
if (section) {
var section = section.toLowerCase();
}
app.view = new AccountView({model: app.user, section: section});
},
Solution
My solution was to change the router to this:
account: function(section) {
if (section) {
var section = section.toLowerCase();
}
if (app.view) {
app.view.undelegateEvents();
}
app.view = new AccountView({model: app.user, section: section});
},
This works for now, but will this create a memory leak?
I had exactly the same problem when I first started using backbone. Like Peter says, the problem is that you have more than one instance of the View being created and listening for the event. To solve this, I created this solution in my last backbone project:
/* Router view functions */
showContact:function () {
require([
'views/contact'
], $.proxy(function (ContactView) {
this.setCurrentView(ContactView).render();
}, this));
},
showBlog:function () {
require([
'views/blog'
], $.proxy(function (BlogView) {
this.setCurrentView(BlogView).render();
}, this));
},
/* Utility functions */
setCurrentView:function (view) {
if (view != this._currentView) {
if (this._currentView != null && this._currentView.remove != null) {
this._currentView.remove();
}
this._currentView = new view();
}
return this._currentView;
}
As you can see, it's always removing the last view and creating a new one, which then renders. I also add a require statement in the router because I don't want to have to load all views in the router until they are actually needed. Good luck.
Sounds like you are attaching multiple view instances to the same DOM element and they are all responding to the events. Are you making a new view each time you navigate without removing the previous view?
I have a dynamic view, that renders different templates inside the same element (about 12), based on router params. Now, the container in which the view renders, is defined inside the view.render() like so "el: '#some-container'". Naturally, i have to remove the view if it exists, before creating a new or the same one, to prevent zombies and s#!t. Just as a reminder, calling view.remove(), actually removes '#some-container' from the DOM, meaning the view has no place to render in, except for the first time. Now, there are dozens of methods to prevent this from happening. Just thought i should share in case anyone needs to save a few hours of research.
I'm creating a slideshow using Backbone.js. My slideshow view is finished, each slide is a model and all the models are inside a collection. Now I want to apply a little hashbang magic to my slideshow :-)
This is my code structure
application.js
models/slideshow/slide.js
collections/slideshow/slides.js
views/slideshow.js
In application.js I create my router:
var App = {};
App.Modules = {
Views: {},
Models: {},
Collections: {}
};
App.slideshow = undefined; // Use this to maintain state between calls.
App.router = (function() {
var Router = Backbone.Router.extend({
routes: {
'slideshow/:id/:page': 'slideshow'
},
slideshow: function(id, page) {
// Whenever this route handler triggers, I want to either:
// 1) Instantiate the slideshow, or:
// 2) Change the page on an already instantiated slideshow
if (App.slideshow && App.slideshow.options.id === id) {
App.slideshow.goToPage(page);
} else {
App.slideshow = new App.Modules.Views.Slideshow({
id: id,
page: page
});
}
}
});
return new Router;
})();
// Using jQuery's document ready handler.
$(function() {
Backbone.history.start({
root: '/'
});
});
This works as I expect. My slideshow works as an overlay so no matter what page it's instantiated on, it will just show itself on top of the existing document.
My first question is how do I close the slideshow (App.slideshow.close()); when the user hits the browser back button or navigates to another hashbang, which doesn't follow the /slideshow/:id/:page syntax?
My last question has to do with the 'navigate' method in Routers. In my slideshow view, I make sure to update the hash fragment whenever the page changes. This is what I do in my view:
pageChange: function(page) {
App.router.navigate('slideshow/' + this.options.id + '/' + page, false);
}
This makes sure the fragment gets updated so that the user at any point can copy the URL and it will open on the same page. The problem is that my 'slideshow' method in my instantiated router triggers even though I pass false in the second 'navigate' parameter (triggerRoute). Why is this?
So, I think I've figured it out. Please let me know if there are cleaner ways to do this.
After reading http://msdn.microsoft.com/en-us/scriptjunkie/hh377172 I saw you can do this in Backbone.js:
var router = Backbone.Router.extend({
routes: {
'*other': 'defaultRoute'
},
defaultRoute: function() {
if (App.slideshow) App.slideshow.close();
}
};
This makes sure everything that doesn't match /slideshow/:id/:page will close the slideshow if it's been instantiated.
With regard to 'navigate' apparently it's because I did App.vent = _.extend({}, Backbone.events); Apparently, I have to do:
App.vent = {};
_.extend(App.vent, Backbone.events);