I have a very complex Backbone application with many views/Models and collections.
at times when user clicks a button I need to update multiple collections and update certain views.
In order to manage the collections I've handled most events in my router. The sequence of events is as follows:
user clicks a link/button and view triggers an event:
this.model.trigger('someEvent');
the model listening to the event, updates itself if necessary and notifies the router through my eventbus
eventbus.trigger('globalEVent');
My router is listening to global events, fetches collections/models from a cached storage and updates the necessary collections and models.
This has worked really well so far but
I have just too many events and my router code is becoming hard to manage. My question is there a way to handle events outside of the router and still access methods inside the router? is my approach correct or is there a more elegant solution I haven't considered?
Update:
Here's how I do it now:
in Router I call this method in my initialize():
registerModules : function() {
var self = this;
Backbone.trigger(Events.RegisterModule, function(moduleRoutes) {
_.each(moduleRoutes, function(moduleRoute) {
self.routeDetails[moduleRoute.name] = {
buildPageUrl: moduleRoute.buildPageUrl,
fragment : moduleRoute.fragment
}
self.route(moduleRoute.fragment, moduleRoute.name, moduleRoute.action);
});
});
},
Then I have regular self-exec blocks for each module/page which self registers (simplified version):
(function() {
var module = {
loadAndDisplay: function() {},
saveAndContinue: function(model) {
Backbone.trigger(Events.ChangePage, model.get('nextPage'));
},
registerEvents: function() {},
_init: function() {
module.registerEvents();
var self = this,
routes = [{
fragment: Constants.FRAGMENT,
name: Constants.PAGE_NAME,
buildPageUrl: function() {
return Constants.FRAGMENT;
},
action: module.loadAndDisplay
}];
Backbone.on(Events.RegisterModule, function(registerCallback) {
registerCallback.call(registerCallback, routes);
});
}
};
module._init();
})();
Of course your Router script should load before your module code. It works great for my needs. With this design I have separated router / modules completely and they have no knowledge of each other either. Each will handle it's own events/data etc. Shared logic goes in router.
You should split you router functionality into different classes. In our Marionette based application we using a RegionManager to handle all the view related stuff, like change views in different areas, open overlays etc and a StateMachine. The router itself just trigger different events, like state:change or region:change where the manager classes listen to.
Doing it tis way, you can have a different manager classes that handle a special aspect of your app. Let the router to what he is build for: listen on location change events and notify the app about it. The router should not have other logic then this.
Related
We're not using AngularJs as a SPA but embedded module to manage some behavior and shared data, so we're not actually utilising something like angular router. How should I initialize components only after a shared data service finished an asynchronous request? AngularJS was used with Typescript
Angular Module
import ... from '...'
import ... from '...'
...
angular.module('app-1', [])
.service('data-service', DataService)
.component('zeroDateButton', new ZeroDateButtonComponent())
.component('zeroPanel', new ZeroPanelComponent())
.component('zeroChart', new ZeroChartComponent())
ASP.NET Page hosting Angular module
BI.aspx
<asp:Content ID="standardContent" ContentPlaceHolderID="MainContent" runat="server">
...
<zero-date-button></zero-date-button>
<zero-date-button></zero-date-button>
<zero-panel name="panel-1"></zero-panel>
<zero-panel name="panel-2"></zero-panel>
<zero-panel name="panel-3"></zero-panel>
<zero-chart></zero-chart>
...
<script src="Scripts/Components/component.app-1.js) "></script> //compiled angular module js file
</asp:Content>
Page URL: https://www.example.com/BI/Zero
DataService.ts
public tryGetData() {
return $http.get(url).then((res: any) => {
this.panels = res.panels;
});
}
ZeroPanelComponent.ts
...
public $onInit(): void {
this.panels = this.dataService.panels;
this._render();
...
Most of the logics for this module relies on the data from the three components, so I want to fetch and store them all together in the data service, from which each component access the data they need from this service, and let the service figure out the logics and tell each of them by broadcasting the events.
Upon the components initialization(in $onInit method), it should display things using data retrieved from data service. The problem is component initialization is not awaiting data service to finish data fetching, so the component can't get the data they need and render nothing.
Trial with $routeProvider
I've seen seen lot's of people advising $routeProvider with appModule.config(), however it was not working. I'm not sure if this solution will work considering the way we use Angular, but I'm still posting the code snippet.
angular.module('app-1', ['ngRoute'])
.config(($routeProvider) => {
$routeProvider
.when('/BI/Zero', {
template: '<zero-panel class="flex-basis-half marginBottom" panel-name="SalesSnapshot", container-id="sales-snapshot"></zero-panel>',
resolve: {
DataService: (DataService) => {
return DataService.tryGetData();
},
},
});
})
.service('zero-data-service', DataService)
...
and I added ng-view directive to one in BI.aspx
There's NO error in browser, <zero-panel> is not rendered and tryGetDate() is not called too. I found someone said the 'path' defined to when() is part of the URL after the # symbol. Could you verify if this is true?
In terms other solution, the most intuitive thing I can think of is broadcasting an event when data service has obtained the data, and components listen to event to fetch the data, instead of fetching during their initialization.
I appreciate if anyone can suggest if $routeProvider would work in my usecase, or suggest any other possible solution to achieve the goal.
According to the below image:
I want to improve components communication method....I think this way is not efficient.
When clicking tabsetComponent to emit event, then parent controller catch this event, changing rootScope variable. Using $watch rootScope variable in tableComponent to trigger http fetch data function...
Could anyone has better and efficient way to communicate sibling component?
The accepted AngularJS method for communication between components is using component attributes for communication.
<div ng-controller="rootCtrl as vm">
<tab-set-component tsc-click="vm.fn($event, data)">
</tab-set-component>
<table-component="vm.tableData">
</table-component>
</div>
For more information on defining component attributes, see AngularJS Comprehensive Directive API -- isolate scope
Best practices
Only use .$broadcast(), .$emit() and .$on() for atomic events
Events that are relevant globally across the entire app (such as a user authenticating or the app closing). If you want events specific to modules, services or widgets you should consider Services, Directive Controllers, or 3rd Party Libs
$scope.$watch() should replace the need for events
Injecting services and calling methods directly is also useful for direct communication
Directives are able to directly communicate with each other through directive-controllers
-- AngularJS Wiki Best Practices
Controller Example
In your html, you use vm.fn that came from root controller right? So your advice is it should call the click method defined root controller, the click method will trigger http request function defined on the rootScope, then get table component datas, then bind the datas on table component attribute.
As example:
angular.module("myApp", []);
angular.module("myApp").controller("rootCtrl", function($http) {
var vm = this;
vm.tableData = { /* initial data */ };
//click handler
vm.fn = function(event, url) {
$http.get(url).then (function onFulfilled(response) {
vm.tableData = response.data;
}).catch (function onRejected(response) {
console.log(response.status);
});
};
});
The above example avoids cluttering $rootScope. All the business logic and data is contained in the controller.
The controller sets the initial data for the table-component, receives click events from the tab-set-component, makes HTTP requests, handles errors, and updates the data to the table-component.
UPDATE -- Using Expression Binding
Another approach is using expression binding to communicate events:
<header-component view="root.view" on-view-change="root.view = $event.view">
</header-component>
<main-component view="root.view"></main-component>
For more information, see SO: How to pass data between sibling components in angular, not using $scope
With version 1.5.3, AngularJS added the $onChanges life-cycle hook to the $compile service.
app.component("mainComponent", {
template: "<p>{{$ctrl.count}}",
bindings: {view: '<'},
controller: function() {
this.count = 0;
this.$onChanges = function(changesObj) {
if (changesObj.view) {
this.count++;
console.log(changesObj.view.currentValue);
console.log(changesObj.view.previousValue);
console.log(changes)bj.view.isFirstChanged());
};
};
}
});
For more information, see AngularJS Comprehensive Directive API Reference -- Life-cycle hooks
See also SO: AngularJs 1.5 - Component does not support Watchers, what is the work around?
When a model is changed, I update the route (it has a url that contains the application's current state).
When a url is visited (or back is pressed) I update the model from the route.
This creates circular logic problems for me that I can't get my head around. Things are being changed twice for no reason.
Is it normal to base everything on the route, and use that to update the model?
Is it normal to have two models?
What is normal?
Any help or advice would be appreciated. Thanks
I wouldn't advise using Router the way you do. In general, the route action should not change model state. In general, HTTP GET operations should not have side-effects.
Routers should be used for navigation between different pages of a single-page application. Model changes should be triggered directly from the view code that handles user input. Let's say you have a model User, and view UserView, the view could work something like this:
var UserView = Backbone.View.extend({
events: {
"click #save", "save"
},
initialize: function(options) {
this.model = options.model;
},
render: function() {
//your render code here
},
save: function() {
var fields = {
name: this.$("#name").val();
email: this.$("#email").val();
};
this.model.save(fields , {
//after save go back to users page, or whatever
success: function() { window.location.hash = "/users"; },
error: this.displayError
});
}
});
Backbone isn't really an MVC framework, so the Router shouldn't be treated as a pure controller. And even if you did, changing state in a route action would be equivalent to changing state in a MVC controller GET endpoint - bad, bad idea.
If you want to adhere to a pure MVC pattern, you should implement your own controller layer, or look at another layer besides Backbone.
Ok, I think this is something simple, however I am being to stupid to see it. Here is my code in backbone using the backbone boilerplate method
require([
"app",
// Libs
"jquery",
"backbone",
// Modules
"modules/example"
],
function(app, $, Backbone, Example) {
// Defining the application router, you can attach sub routers here.
var Router = Backbone.Router.extend({
routes: {
"": "index",
"item" : 'item'
},
index: function()
{
console.info('Index Function');
var tutorial = new Example.Views.Tutorial();
// Attach the tutorial to the DOM
tutorial.$el.appendTo("#main");
// Render the tutorial.
tutorial.render();
},
item: function()
{
console.info('Item View');
}
});
// Treat the jQuery ready function as the entry point to the application.
// Inside this function, kick-off all initialization, everything up to this
// point should be definitions.
$(function() {
// Define your master router on the application namespace and trigger all
// navigation from this instance.
app.router = new Router();
// Trigger the initial route and enable HTML5 History API support
Backbone.history.start({ pushState: true, root: '/reel' });
});
// All navigation that is relative should be passed through the navigate
// method, to be processed by the router. If the link has a data-bypass
// attribute, bypass the delegation completely.
$(document).on("click", "a:not([data-bypass])", function(evt) {
// Get the anchor href and protcol
var href = $(this).attr("href");
var protocol = this.protocol + "//";
// Ensure the protocol is not part of URL, meaning its relative.
if (href && href.slice(0, protocol.length) !== protocol &&
href.indexOf("javascript:") !== 0) {
// Stop the default event to ensure the link will not cause a page
// refresh.
evt.preventDefault();
// `Backbone.history.navigate` is sufficient for all Routers and will
// trigger the correct events. The Router's internal `navigate` method
// calls this anyways.
Backbone.history.navigate(href, true);
}
});
});
I am running this of a MAMP server and when i type Localhost:8888/reel , I get the example index page that comes with boilerplate. However when I type Localhost:8888/reel/item or Localhost:8888/reel/#item I either get, page can not be found or directed back to my index page.
My question is what am i doing wrong. Do I need to use htaccess? This doesnt seem right. Is there a way using backbone to sort this. Sorry if this is really simple, just cant get my head around it.
the problem may lie with the pushState flag.
With that on the request goes all the way to the server and it sees the full url and responds to it with whatever it would do ...
does it work if you have a
$(function (){
setTimeout(navMe, 2000);
});
function navMe() {
backbone.navigate("item");
}
that way 2 seconds after load it will navigate to item view and you know that its because of the request going to the server and not to backbone.
I just started to delve into backbone.js and I am not sure what is the best way to use routers.
App.Events = _.extend({}, Backbone.Events);
App.HouseDetailRouter = Backbone.Router.extend({
routes: {
'': 'main',
'details/:id': 'details',
},
initialize: function() {
},
main: function() {
App.Events.trigger('show_main_view');
},
details: function(id) {
model = App.houseCollection.get(id);
App.Events.trigger('show_house', model);
},
});
Should routers fire events like above, and then have views listen to these events?
This is actually not a bad way to use a router as it relegates the router to simply handling routes as opposed to business logic. What you might consider when using a router this way is to have a controller object listen to the router events then update the app's models. When the models change state the views would update.