Ancestry with App.addRegions - backbone.js

If you add a region to a layout view using the regions hash like so:
regions: {
compositeRegion: '#ui.compositeViewDiv',
modalRegion:{
regionClass:modal,
selector:'.modalRegion'
}
}
The views that you show in those regions can call triggerMethod and trigger an event on their parent view.
If instead I build my regions (for instance, in a composite view or even an item view) like so:
App.addRegions({
compositeRegion: '#ui.compositeViewDiv',
modalRegion:{
regionClass:modal,
selector:'.modalRegion'
}
Nothing happens when you call triggerMethod. It does not seem to have the _parent attribute defined on the region.
If I manually set _parent to any view, it will trigger a method on the next highest up layout view.
For example, I have a Layout view "A" with a composite view "B", and that composite view "B" has an item view "C". If I create a region using App.addRegions code above in the item view "C", I can then set the _parent and show a view like so:
App.modalRegion._parent = this;
App.modalRegion.show(new someView());
The Layout view "A" childEvent is triggered, not the item view "C", nor the Composite view "B".
If I manually call
this.triggerMethod('sameeventname');
on the item view "C", it WILL trigger the composite view "B"'s childEvent.
Any ideas? I need to be able to use my modal region (takes any marionette view and turns it into a jQuery-UI modal) from anywhere. It works great when you use it with the layout view region hash. But since there is no region hash in a composite or item view, it works but will not communicate with its parent view.

I would recommend having a look at James Kyle's Marionette Wires, in particular the way services are used to display global components such as modal and flashes in his example.
The basic architecture is an application, with a LayoutView containing a region for each component. This is rendered on initialize and the component is passed the region from the app.
For rendering a view in a modal something like the following should work:
//layout-view.js
import { LayoutView } from 'backbone.marionette';
export const BaseLayout = LayoutView.extend({
regions: {
modal: '.modal',
...
}
});
//app.js
import { Application } from 'backbone.marionette';
import { BaseLayout } from './layout-view';
export const App = Application.extend({
initialize(options = {}) {
this.layout = new Layout({
el: options.el
});
this.layout.render();
}
});
//modal/service.js
import { Service } from 'backbone.service';
const ModalService = Service.extend({
setup(options = {}) {
this.container = options.container;
},
requests: {
show: 'show',
hide: 'hide'
},
show(view) {
this.container.show(view);
},
hide() {
this.container.empty()
}
});
export const modalService = new ModalService();
//main.js
import { App } from './app';
import { modalService } from './modal/service';
const app = new App({
el: '.root-element'
});
modalService.setup({
container: app.layout.modal
});
.... init other components
//someComponent/index.js
import { modalService } from '../modal/service';
...
showView() {
const view = new View();
modalService.request('show', view)
.then(doSomething)
.catch(err => {
console.error(err);
});
},
hideView() {
modalService.request('hide')
.then(doSomething);
}

Related

ng-click won't trigger in ui-grid columnDef template

I am trying to trigger a custom function inside of my columnDef cell template as follows:
export class GridComponent {
constructor() {}
this.gridOptions = {
// grid options defined here
columnDef.cellTemplate = '<bss-cell cellval="{{row.entity[col.name]}}"></bss-cell>';
}
private cellClicked () {
// need to get the click event here but can't
console.log('got here');
}
}
This column definition holds the data in my grid. Bss Cell is a custom angular component I made which looks like this:
//BSS CELL CONTROLLER CODE FOR THE COLUMN DEF CELL TEMPLATE
import * as angular from 'angular';
import '../../../../modules/bss/bss.component';
export class TreeCellComponent {
constructor() {
}
private cellClicked () {
// click event does come here if I call $ctrl.cellClicked() from my template but this isn't connected to the grid
}
}
angular.module('app.modules.uigridtemplates.bss-cell', ['app.modules.bss'])
.component('bssCell', {
bindings: {
cellval: '#'
},
controller: TreeCellComponent,
template: require('./bss-cell.html')
});
// BSS CELL DIRECTIVE TEMPLATE
<div ng-click="grid.AppScope.cellClicked()" align="right" class="ui-grid-cell-contents-data-grid" title="
{{$ctrl.cellval}}">
<span>{{$ctrl.cellval}}</span>
</div>
How can I get that click to take place so that it runs to the "cellClicked" function inside of my GridComponent, which would then allow me to affect the grid the way I would like.
this.cellTemplates = ['uiGridTreeCellTemplate.html', 'uiGridTreeColumnHeaderTemplate.html'];
this.gridOptions = {
appScopeProvider: this
}
Inside your gridOptions add this:
appScopeProvider: {
cellClicked: this.cellClicked
}
In this way in you grid.appScope you will find the cellClicked function available.
If you have selectable rows, which perform an action when you select one of your rows, I suggest to pass the $event to your grid.appScope.cellClicked() function, then in your appScopeProvider stop the click event, something like this:
ng-click="grid.appScope.cellClicked($event)"
and then
appScopeProvider: {
cellClicked: (event) => {
this.cellClicked();
event.preventDefault();
event.stopPropagation();
}
}

Binding Angular2 components inside of a Jquery plugin template

I'm working on using a kendo inside of an angular 2 project.
Getting the widget set up correctly is no problem:
ngOnInit() {
let options = inputsToOptionObject(KendoUIScheduler, this);
options.dataBound = this.bound;
this.scheduler = $(this.element.nativeElement)
.kendoScheduler(options)
.data('kendoScheduler');
}
When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.
Example:
public views:kendo.ui.SchedulerView[] = [{
type: 'month',
title: 'test',
dayTemplate: (x:any) => {
let date = x.date.getDate();
let count = this.data[date];
return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
}
}];
The monthly-scheduler-day class:
#Component({
selector: 'monthly-scheduler-day',
template: `
<div>{{date}}</div>
<div class="badge" (click)=dayClick($event)>Available</div>
`
})
export class MonthlySchedulerDayComponent implements OnInit{
#Input() date: number;
#Input() count: number;
constructor() {
console.log('constructed');
}
ngOnInit(){
console.log('created');
}
dayClick(event){
console.log('clicked a day');
}
}
Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.
I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223
I whipped this service up to handle binding my components:
import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '#angular/core';
declare var $:JQueryStatic;
#Injectable()
export class JQueryBinder {
constructor(
private resolver: ComponentResolver,
private injector: Injector
){}
public bindAll(
componentType: any,
contextParser:(html:string)=>{},
componentInitializer:(c: ComponentRef<any>, context: {})=>void):
void
{
let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
return a instanceof ComponentMetadata
}).selector;
this.resolver.resolveComponent(componentType).then((factory)=> {
$(selector).each((i,e) => {
let context = contextParser($(e).html());
let c = factory.create(this.injector, null, e);
componentInitializer(c, context);
c.changeDetectorRef.detectChanges();
c.onDestroy(()=>{
c.changeDetectorRef.detach();
})
});
});
}
}
Params:
componentType: The component class you want to bind. It uses reflection to pull the selector it needs
contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
componentInitializer - callback that initializes the created component with the context you parsed
Example usage:
let parser = (html: string) => {
return {
date: parseInt(html)
};
};
let initer = (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
let d = context.date;
c.instance.count = this.data[d];
c.instance.date = d;
}
this.binder.bindAll(GridCellComponent, parser, initer );
Well your solution works fine until the component needs to change its state and rerender some stuff.
Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side)
the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.
So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:
let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
componentRef = componentFactory.create(this.injector, null, selectorOrNode);
componentRef.changeDetectorRef.detectChanges();
After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:
let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
properties = enumerateProperties(injector.get(NgZone))
.filter(p => p instanceof EventEmitter);
let subscriptions = Observable.merge(...properties)
.subscribe(_ => changeDetectorRef.detectChanges());
Of course don't forget to unsubscribe on destroy:
componentRef.onDestroy(_ => {
subscriptions.forEach(x => x.unsubscribe());
componentRef.changeDetectorRef.detach();
});
UPD after stackoverflowing once more
Forget all the words above. It works but just follow this answer

BackboneJS - same el for many views

I am using same el for more than 1 view like below. I'm not facing any problem till now. Is this good approach or should i do any changes?
<div id="app">
<div id="app-header"></div>
<div id="app-container"></div>
<div id="app-footer">
</div>
App View:
{
el: "#app",
v1: new View1(),
v2: new View2(),
render: function () {
if (cond1) {
this.v1.render();
} else if (cond2) {
this.v2.render();
}}
}
View 1:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
View 2:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
By reading your question, I do not really see what advantages you could possibly have using this approach rather than having the different div elements being the root el for your views 1, 2, 3 and using
this.$el.html(template)
in the render method.
Your approach could work for a small application, but I think it will become really hard to maintain as the application grows.
EDIT
I still do not really get your point, you could only initialize everything only once in both cases.
Here is a working Fiddle.
By the way I am changing the content by listening to the click event but this is to simplify the example. It should be done by the router.
I do use a mixin to handle such situation, I call it stated view. For a view with all other options I will send a parameter called 'state', render will in-turn call renderState first time and there after every time I set a 'state' renderState will update the view state. Here is my mixin code looks like.
var setupStateEvents = function (context) {
var stateConfigs = context.getOption('states');
if (!stateConfigs) {
return;
}
var state;
var statedView;
var cleanUpState = function () {
if (statedView) {
statedView.remove();
}
};
var renderState = function (StateView) {
statedView = util.createView({
View: StateView,
model: context.model,
parentEl: context.$('.state-view'),
parentView:context
});
};
context.setState = function (toState) {
if (typeof toState === 'string') {
if (state === toState) {
return;
}
state = toState;
var StateView = stateConfigs[toState];
if (StateView) {
cleanUpState();
renderState(StateView);
} else {
throw new Error('Invalid State');
}
} else {
throw new Error('state should be a string');
}
};
context.getState = function () {
return state;
};
context.removeReferences(function(){
stateConfigs = null;
state=null;
statedView=null;
context=null;
})
};
full code can be seen here
https://github.com/ravihamsa/baseapp/blob/master/js/base/view.js
hope this helps
Backbone Rule:
When you create an instance of a view, it'll bind all events to el if
it was assigned, else view creates and assigns an empty div as el for that view and bind
all events to that view.
In my case, if i assign #app-container to view 1 and view 2 as el and when i initialize both views like below in App View, all events bind to the same container (i.e #app-container)
this.v1 = new App.View1();
this.v2 = new App.View2();
Will it lead to any memory leaks / Zombies?
No way. No way. Because ultimately you are having only one instance for each view. So this won't cause any memory leaks.
Where does it become problematic?
When your app grows, it is very common to use same id for a tag in both views. For example, you may have button with an id btn-save in both view's template. So when you bind btn-save in both views and when you click button in any one the view, it will trigger both views save method.
See this jsFiddle. This'll explain this case.
Can i use same el for both view?
It is up to you. If you avoid binding events based on same id or class name in both views, you won't have any problem. But you can avoid using same id but it's so complex to avoid same class names in both views.
So for me, it looks #Daniel Perez answer is more promising. So i'm going to use his approach.

Events not firing after extending a custom backbone view

What's the correct way to have both events from the parent and child views be registered properly and fire?
With this approach the praent's events events wipe out the child's. I've also tried to pass in the child's events as part of the options to the parent, and then have the parent extend them before registering but then the parent's events no longer work.
Parent
// this is helpers/authorization/views/authHelper
export class AuthView extends Backbone.View {
constructor(options?) {
this.events = {
'keypress #auth': 'setAuthorizationCodeKeypress',
'click .create': 'setAuthorizationCode'
};
super(options);
}
}
Child
import AV = module("helpers/authorization/views/authHelper")
export class PageHelperView extends AV.AuthView {
constructor(options?) {
this.events = {
'click .configHead': 'toggle'
}
super(options);
}
}
I'd prefer them to share the same element and only require a call to new EHV.EncoderAPIHelperView().render(); to render them.
NOTE: edited with probably better answer
You can declare parent events directly inside the object, by doing that, you won't have to create new constructor. Parent view would look like this:
export class AuthView extends Backbone.View {
events = {
'keypress #auth': 'setAuthorizationCodeKeypress',
'click .create': 'setAuthorizationCode'
}
}
Now you can rewrite child to this:
import AV = module("helpers/authorization/views/authHelper")
export class PageHelperView extends AV.AuthView {
initialize(options?) {
this.events = {
'click .configHead': 'toggle'
}
}
}
_.extend call add missing entries to events and replace entries that share keys. (see more here)
Also, I'm not really great with typescript, so there might be a problem or two with this code.
The complete working solution:
Parent view:
export class AuthView extends Backbone.View {
constructor(options?) {
this.events = {
'keypress #auth': 'setAuthorizationCodeKeypress',
'click .create': 'setAuthorizationCode'
};
super(options);
}
}
Child view:
import AV = module("helpers/authorization/views/authHelper")
export class PageHelperView extends AV.AuthView {
constructor(options?) {
super(options);
}
initialize(options) {
this.events = _.extend(this.events, {
'click .configHead': 'toggle'
});
}
}

Backbone Boilerplate Layout Manager

Can someone help explain / provide an example on how to use the LayoutManager within the Backbone Bolierplate?
Within app.js I can see a useLayout function that extends the main app object. Within here it appears to be setting a base layout element:
// Helper for using layouts.
useLayout: function(name, options) {
// Enable variable arity by allowing the first argument to be the options
// object and omitting the name argument.
if (_.isObject(name)) {
options = name;
}
// Ensure options is an object.
options = options || {};
// If a name property was specified use that as the template.
if (_.isString(name)) {
options.template = name;
}
// Create a new Layout with options.
var layout = new Backbone.Layout(_.extend({
el: "#main"
}, options));
// Cache the refererence.
return this.layout = layout;
}
Is that correct? If so, do I somehow the use the 'UseLayout' function with the applications Router? ...to add different UI elements/nested views to the main view?
Thanks.
I will usually have an "app" object that stores all my settings needed throughout the application. This object then extends some useful functions like the one you listed above. For example:
var app = {
// The root path to run the application.
root: "/",
anotherGlobalValue: "something",
apiUrl: "http://some.url"
};
// Mix Backbone.Events, modules, and layout management into the app object.
return _.extend(app, {
// Create a custom object with a nested Views object.
module: function(additionalProps) {
return _.extend({ Views: {} }, additionalProps);
},
// Helper for using layouts.
useLayout: function(options) {
// Create a new Layout with options.
var layout = new Backbone.Layout(_.extend({
el: "#main"
}, options));
return this.layout = layout;
},
// Helper for using form layouts.
anotherUsefulFunction: function(options) {
// Something useful
}
}, Backbone.Events);
});
Now in my router I would do something like:
app.useLayout({ template: "layout/home" })
.setViews({
".promotional-items": new Promotions.Views.PromotionNavigation(),
".featured-container": new Media.Views.FeaturedSlider({
vehicles: app.vehicles,
collection: featuredCollection
})
}).render().then(function() {
//Do something once the layout has rendered.
});
I have just taken a sample from one of my applications, but I am sure you can get the idea. My main layout is basically just a layout template file which holds the elements so the views can be injected into their respective holders.
You would use it as if you're using a regular Backbone View. Instead of building the View directly, you can use this to create a new instance. The code you posted is a wrapper object on top of the Backbone Layout Manager extension with el: #main set as the default View element which is overridable.
var layout = new useLayout({ template: "#viewElement", ... });

Resources