Backbone - How to handle element initialization for child views? - backbone.js

I've been using Backbone.js for a couple of months, and I ran into a dilemma recently when using backbone with one CSS frameworks.
Some of the elements from the CSS framework require some JS initializations, and these elements are part of nested views (child views). Being part of the child views, I immediately assumed that these initializations should be handled inside the child view:
app.ChildView = Backbone.View.extend({
el: ....
template: ...
events: {
...
},
initialize: function() {
...
},
render: function() {
....
this.$elementWithInitialization = this.$('.element');
this.$elementWithInitialization.initialize();
return this;
},
});
Being a child view, this view won't be part of the DOM till its highest ancestor inserts all the child views, so its elements' initializations won't work till the outermost view finishes building the DOM. Illustrating this:
<view1>
<view2>
<view3></view3>
<view3></view3>
...
</view2>
<view2>
<view3></view3>
<view3></view3>
...
</view2>
...
</view1>
So, the elements in view3 can only be initialized till view1 ends inserting all its sub views. I'm just curious about what the appropriate approach for handling these elements' initializations in child views is, considering their rendering depends on their ancestors.
I can always do a selector for all these elements in the outermost view and initialize them, but I think this breaks the modularity of the views and separation of concerns. Thanks for your input.

You can pass a reference to the parent view (outermost view) to the children and have them listen for a ready event from the parent.
//Child
initialize: function(options){
this.listenTo(options.outermostParent, 'ready', this.initCSSComponent);
}
//Outermost parent when stuff is ready and injected to DOM
this.trigger('ready');
If you have a global event emitter object you can use it as well.

Related

Which is more appropriate for Modes? Broadcast vs Binding?

I currently have a parent component and several children component.
<parent-component>
<child-component ng-data="$ctrl.object.fieldone"></child-component>
<child-component ng-data="$ctrl.object.fieldtwo"></child-component>
...
<button ng-click='$ctrl.setEdit();">Edit</button>
</parent-component>
Each child component will either: display the data using a series of divs/spans representing View Mode or display the data using form elements representing Edit Mode.
The parent component has a button which triggers editMode.
What is the best way to inform the child components that the state is Edit Mode? There seem to be a lot of different ways of passing this state across: the parent could Broadcast or the parent's editMode variable could be binded to the children component. Is there even a better way beyond these two.
Assume well over 20-30 child components and that these components will be used in different views.
Thanks,
Theoretically, broadcast should only be used for application wide events. Imagine login/logout notifications, websockets events...etc.
If you start to use broadcast for small events, you will end up with tons of events everywhere in your app and you won't know which one is triggered by what.
If your app is small enough and consists of only a parent component and children where the main event is to toggle an edition mode boolean, then it's fine to use broadcast.
For anything bigger than that you can obviously use bindings instead.
In your particular case it feels a little heavy on the html elements to add an attribute to each of your 30 children.
The fact you use a parent / children hierarchy means you can make use of require. By requiring the parent in each of your child component, you can automatically use its properties. This way you can pass an editMode variable and it's scalable if you need to pass more data in the future.
var parent = {
controller: ParentController,
template:`
<button ng-click="$ctrl.editionMode = !$ctrl.editionMode">Edition</button>
<br><br>
<child></child>
`
}
function ParentController() {
this.editionMode = false;
}
var child = {
require: { parent: '^^parent' },
controller: ChildController,
template: `
<input type="text" ng-model="$ctrl.value" ng-if="$ctrl.parent.editionMode">
<span ng-if="!$ctrl.parent.editionMode">{{$ctrl.value}}</span>
`
}
function ChildController() {
this.value = "edit me";
}
angular.module('myApp', []);
angular.module('myApp')
.component('parent', parent)
.controller('ParentController', ParentController)
.component('child', child)
.controller('ChildController', ChildController);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.9/angular.min.js"></script>
<div ng-app="myApp">
<parent></parent>
</div>
Overview
$broadcast: Dispatches an event name downwards to all child scopes (and their children) notifying the registered $rootScope.Scope listeners......simply it trickle down functions to publish the event. Using $broadcast creates a dependency on $rootScope.then if i choose no to use $rootScope, then my controllers have to understand the $scope hierarchy and be able to $broadcast accordingly. Here is the fun part of it... Try testing that! An event based approach can get really messy to track, maintain and debug. It makes it very asynchronous, with no real easy way to track the flow of an application
binding: Data-binding in Angular apps is the automatic synchronization of data between the model and view components. Angularjs has one-way and two way data-binding (See Reference) This quickly leads me to the talking about scope since when binding you have to know the scope hierarchies and view you are able to access, which is fairly easy....

Drag and drop Angular 1.x directive/component and parent scope

Let's say my grandparent component/directive's scope variable is declared like this: $scope.g = { // methods here };
In each of my nested component's/directive's controllers, I'm referring to that variable like $scope.$parent.$parent.g and then calling some function off of g such as $scope.fromNgClick = function() { $scope.$parent.$parent.g.run(); };. That works great (though I would like a better way of referring to ancestors such as an alias name? instead of a $parent chain).
When I natively drag a component from it's grandparent component into another grandparent sibling component (got that?), the same $scope.fromNgClick = function() { $scope.$parent.$parent.g.run(); }; still points to the original scope, not the new one like I need it to. So clicking the same ng-clickable element still triggers the run() method on the previous grandparent's scope.
That all makes sense; but, is there a way to get the scope to point to the new dropped locations grandparent scope instead?
Thanks!
EDIT
The markup would be something like the following where <g-directive> is treated as a grandparent because it uses transclude on its template, ultimately wrapping the child directives:
<g-directive>
<child-directive></child-directive>
<another-child-directive></another-child-directive>
<yet-another-child-directive></yet-another-child-directive>
</g-directive>
<g-directive>
<c-child-directive></c-child-directive>
<c-another-child-directive></c-another-child-directive>
<c-yet-another-child-directive></c-yet-another-child-directive>
</g-directive>
That's the reason for the $scope.$parent.$parent.g on the child directives/components.
A child component can be dragged and then dropped into another <g-directive> but it's controller still points to its original grandparent (original <g-directive>'s controller scope variable). I want it to point to the new grandparent is was dropped into, essentially resetting it's scope to the newly placed scope.
<g-directive some-attr=1>
<child-directive></child-directive some-attr=1>
<another-child-directive></another-child-directive some-attr=1>
<yet-another-child-directive></yet-another-child-directive some-attr=1>
</g-directive>
<g-directive some-attr=2>
<child-directive></child-directive some-attr=2>
<another-child-directive></another-child-directive some-attr=2>
<yet-another-child-directive></yet-another-child-directive some-attr=2>
</g-directive>
Each can have different listener for broadcast and emit.
if the directives are repeated through ng-repeat then the $index can be that attribute.
Hope this helps.

BACKBONE/JS Best Practice: Is it good or bad to initialise a model inside view?

My Backbone.View looks this way:
define(["promise!table_config", "BBModel"], function (config, myModel) {
"use strict";
return Backbone.View.extend({
initialize: function () {
this.model = new myModel({
foo: config
});
...
},
render: function () {
...
}
});
});
Is is a good or bad practice to initialise a model inside a view?
Particularly in this case, in a require.js module where 'config' is a require-promise, motivating me to put put a model inside a view.
While Backbone does have a Model and a View class, it is not a strict MVC framework (eg. it lacks a Controller class). The Backbone documentation page explains as much (emphasis mine):
How does Backbone relate to "traditional" MVC?
Different implementations of the Model-View-Controller pattern tend to disagree
about the definition of a controller. If it helps any, in Backbone,
the View class can also be thought of as a kind of controller,
dispatching events that originate from the UI, with the HTML template
serving as the true view. We call it a View because it represents a
logical chunk of UI, responsible for the contents of a single DOM
element.
Given that, even though (as #Evgenly mentioned in the comments) "its not view responsibility to instance model, its controllers task" ... since the Backbone View is (conceptually) a controller, it absolutely makes sense to create your models inside your views.
But putting that theory aside, here's a more practical answer. I work on a 3+ year-old Backbone app, along with two other developers (and more previously). In that app the vast majority of all models get created inside views (the few remaining ones get created inside routes). Not only has this not been a problem for us, but I can't even imagine any other way of doing it.

How can I execute code after $digest is complete?

I am having a sortable list that gets populated by data in my Angular Controller. This list also contains an extra element containing some controls that can also be dragged
What I want do do, is make sure that the extra element stays in place after the $digest cycle is run, and I made a directive just for that case.
App.directive('ngIgnore', ['$timeout',function($timeout){
return {
link: function(scope, element){
scope.$watch(function(){
// Keep track of the original position of the element...
var el = element[0];
var siblings = Array.prototype.slice.call(el.parentNode.children);
var parent = el.parentNode;
var index = siblings.indexOf(el);
$timeout(function(){
// After the digest is complete, place it to it's previous position if it exists
// Otherwise angular places it to it's original position
var item;
if(index in parent.children)
item = parent.children[index];
if(!!item){
parent.insertBefore(el, item);
}
});
});
}
}
}]);
It worked, but not as I wanted it to... As you can see in this example shuffling the list does not move the ignored element, but the problem is that the $watch function gets executed infinitely since the $timeout triggers another $digest cycle... I tried changing the $timeout with setTimeout but no luck...
Is there any Angulary way to run code right after $digest is executed? Or is my entire logic wrong here?
Another (highly recommended) solution is to stop thinking in DOM manipulations.
Common problem with developers who start writing AngularJS code is tend to do DOM manipulations as the result of some event. Its common in jQuery:
$('#someid').click(function() { this.toggleClass('clicked') })
In AngularJS you should design your View to visualize your Model state (that should be in $scope). So
<div ng-click="clicked = !clicked" ng-class="{clicked: clicked}">I was clicked</div>
Same logic should be applied when designing components. In a HTML code you should put all visual logic - hide some elements using ng-show/ng-if, ng-switch. Add/remove classes using ng-class etc. So you define all possible model states.
Then by just changing model state you will get your view automatically updated reflecting current model state.
Same goes for repeated elements: you can repeat on some collection and then, depending on what element is present, you define how it would look. Keep in mind, that in ng-repeat each element will have own child (so mostly independed) Scope, so you can define some per-element manipulations.
See also this great answer: "Thinking in AngularJS" if I have a jQuery background?
You can try to use Scope.evalAsync Scope.evalAsync:
it will execute after the function that scheduled the evaluation
(preferably before DOM rendering).

Backbone.js: how can I most effectively apply my shared Mustache template to a View's el?

Backbone JS Views are very "helpful" in that they always creates a DOM node for you, with a configurable tag, id and class. This is very nice and accommodating, but I'm finding that it creates an unfortunate situation: the DOM node created by the View is a hidden template.
This became obvious to me on our current project, where we are sharing Mustache templates between the front and back ends. With Backbone, when you want a DOM that looks like this:
<section class="note">
<textarea>
...
</textarea>
<input type="button" class="save-button" value="Save">
<input type="button" class="cancel-button" value="Cancel">
</section>
you end up creating templates that look like this:
<textarea>
{{& content}}
</textarea>
<input type="button" class="save-button" value="Save">
<input type="button" class="cancel-button" value="Cancel">
But now your template is tied to the secret root-node template on your Backbone view, which will need to be duplicated on the server side. So much for DRY encapsulation!
I don't see an immediately obvious way to address this, except by using setElement() with the rendered template at render time, but this brings other problems with it, like having to replace the newly rendered subtree in the DOM after every render().
How have you addressed this issue?
It's an interesting question. I've never had to solve this particular problem before, but I tried out a few options and I think I found one that I like.
First, here's the code:
//A base view which assumes the root element of its
//template (string, DOM node or $) as its el.
var RootlessView = Backbone.View.extend({
//override the view constructor
constructor: function(options) {
//template can be provided by constructor argument or
//as a subclass prototype property
var template = options.template || this.template;
//create a detached DOM node out of the template HTML
var $el = Backbone.$(template).clone()
//set the view's template to the inner html of the element
this.template = $el.html();
//set the element to the template root node and empty it
this.el = $el.empty()[0];
//call the superclass constructor
Backbone.View.prototype.constructor.apply(this, arguments);
}
});
Essentially you define a base view that expects every derived view to have a template property, or to take a template as an argument in the options hash. The template can be a string, a DOM node or a jQuery/Zepto -wrapped DOM node. The view assumes the root node of the template as its el, and redefines the template property as the contents of the root node.
You would use it as a normal view:
var View = RootlessView.extend({
template: templateHtml,
render: function() {
this.$el.html(Mustache.render(this.template, this.model));
return this;
}
});
The el property is available from the get-go, and it's not detached and reattached on re-render. The only exception to the normal Backbone.View behavior is that if you've defined the id, cssClass or tagName properties or arguments, they will be ignored, because the template provides the root element.
This is not extensively tested, but seems to pass most simple test cases. The only drawback that I can think of is that the template html string is stored on every view instance (instead of the prototype) and wastes precious bytes of memory, but that shouldn't be hard to solve either.
Here is a working demo on JSFiddle.
The solution is to include the root node itself (in this case, your section tag) in a shared base template (no children) and then initialise the "el" property of the view upon instantiation:
var existingElement = $('section.note')[0];
new View({el: existingElement})
You can then proceed to attach the form template as normal to the el property.

Resources