I have a datepicker that sets a date on an input. I am binding it with stickit. The problem is that since the datepicker (and not a keystroke) changes the value of the input, the stickit binding doesn't observe the change. If I enter the date manually, there is no problem.
bindings: {
'input[name=RecordDate]': {
observe: 'recdate',
onSet: 'dosome'
}
},
I would recommend adding a global handler to handle all of the datepickers across your project. The following handler will match any bound element with the class: 'jquery-datepicker':
Backbone.Stickit.addHandler({
selector: '.jquery-datepicker',
initialize: function($el, model, options) {
$el.datepicker({
onChangeMonthYear: function() {
model.set(options.observe, $el.val());
}
});
}
});
Here is an example fiddle:
http://jsfiddle.net/px6UP/29/
More about handlers.
Let me know if that works for you. I plan on documenting handlers better with a cookbook or examples in the near future...
Related
In Angular 1, change detection was by dirty checking the $scope hierarchy. We would implicitly or explicitly create watchers in our templates, controllers or components.
In Angular 2 we no longer have $scope, but we do override setInterval, setTimeout, et al. I can see how Angular might use this to trigger a $digest, but how does Angular determine what has changed, especially given that Object.observe never made it into browsers?
Example
Here is a simple example. An object defined in a service is updated in a setInterval. The DOM is recompiled each interval.
How is Angular able to tell that the AppComponent is watching the service, and that the value of an attribute of the service has changed?
var InjectedService = function() {
var val = {a:1}
setInterval(() => val.a++, 1000);
return val;
}
var AppComponent = ng.core
.Component({
selector: "app",
template:
`
{{service.a}}
`
})
.Class({
constructor: function(service) {
this.service = service;
}
})
AppComponent.parameters = [ new ng.core.Inject( InjectedService ) ];
document.addEventListener('DOMContentLoaded', function() {
ng.platform.browser.bootstrap(AppComponent, [InjectedService])
});
Angular creates a change detector object (see ChangeDetectorRef) per component, which tracks the last value of each template binding, such as {{service.a}}. By default, after every asynchronous browser event (such as a response from a server, or a click event, or a timeout event), Angular change detection executes and dirty checks every binding using those change detector objects.
If a change is detected, the change is propagated. E.g.,
If an input property value changed, the new value is propagated to the component's input property.
If a {{}} binding value changed, the new value is propagated to DOM property textContent.
If the value of x changes in a style, attribute, or class binding – i.e., [style.x] or [attr.x] or [class.x] – the new value is propagated to the DOM to update the style, HTML attribute, or class.
Angular uses Zone.js to create its own zone (NgZone), which monkey-patches all asynchronous events (browser DOM events, timeouts, AJAX/XHR). This is how change detection is able to automatically run after each asynchronous event. I.e., after each asynchronous event handler (function) finishes, Angular change detection will execute.
I have a lot more detail and reference links in this answer: What is the Angular2 equivalent to an AngularJS $watch?
Zone.js
Changes happen as a reaction to something, so in this respect they are asynchronous. They are caused by asynchronous actions, and in the browser world those are Events. To intercept those events angular uses zone.js, which patches JavaScript call stack (I beleive, someone correct me if I'm wrong) and exposes hooks that can be used to take other actions.
function angular() {...}
zone.run(angular);
If you imagine this angular function is the entire Angular, this would be how it is run in zone. By doing so Events can be intercepted and if they are triggered we can assume changes happen, and listen/watch for them.
ApplicationRef
In reality ApplicationRef creates the zone:
/**
* Create an Angular zone.
*/
export function createNgZone(): NgZone {
return new NgZone({enableLongStackTrace: assertionsEnabled()});
}
and class NgZone is created with few event emitters:
this._onTurnStartEvents = new EventEmitter(false);
this._onTurnDoneEvents = new EventEmitter(false);
this._onEventDoneEvents = new EventEmitter(false);
this._onErrorEvents = new EventEmitter(false);
that it exposes to the outside world via getters:
get onTurnStart(): /* Subject */ any { return this._onTurnStartEvents; }
get onTurnDone() { return this._onTurnDoneEvents; }
get onEventDone() { return this._onEventDoneEvents; }
get onError() { return this._onErrorEvents; }
When ApplicationRef is created it subscribes to the zone's events, specifically onTurnDone():
this.zone.onTurnDone
.subscribe(() => this.zone.run(() => this.tick());
Changes
When events are triggered tick() function is run which loops through every component:
this._changeDetectorRefs.forEach((detector) => detector.detectChanges());
and detects changes based on components' ChangeDetectionStrategy. Those changes are collected as an array of SimpleChange objects:
addChange(changes: {[key: string]: any}, oldValue: any, newValue: any): {[key: string]: any} {
if (isBlank(changes)) {
changes = {};
}
changes[this._currentBinding().name] = ChangeDetectionUtil.simpleChange(oldValue, newValue);
return changes;
}
witch is available for us through onChanges interface:
export interface OnChanges {
ngOnChanges(changes: {[key: string]: SimpleChange});
}
I have view with states represented by class (ex. state: .inserting-element) on the view's element. I have run into problem when binding events, the following won't work:
events: {
'mouseenter .inserting-element .list-item': function...
}
because the selector expects that .inserting-element is defined on some descendant of this.el (which is one possible solution - have an extra wrapper to represent the state). I have posted my current solution as answer but I wonder if there is some pure 'selector' way?
The (current) solution is to test the state inside handler:
events: {
'mouseenter .list-item': function (event) {
if ( this.$el.is('.inserting-element') ) {
//...
}
}
}
In my app, I am getting the show method issue. something is wrongly going on with my app. But i couldn't able to find that.
in case if i do like this my view is properly rendering ( but this is wrong approach ):
regions:{
header:'header',
content:'section',
footer:'footer'
},
initialize:function(){
console.log('initialized by layout')
},
renderRegions:function(options){
this.formData = _.defaults(options || {}, requireViews);
if(this.formData.headerView){ //this is true.
this.headerView();
this.renderHeaderView();
}
},
headerView:function(){
this.appHeaderView = new AppHeaderView({model:this.model});
return this.appHeaderView;
},
renderHeaderView:function(){
$(this.header.el).html(this.appHeaderView.render().el) //working fine
//but this is not workig: this.header.show(this.appHeaderView)..why not working?
}
why i use the "this.header.show" - nothing is appending to header.
Any one highlight me what is wrong i do here?
I have simplified my total process, and added in Jsfiddle here is the link:
Live Demo Here
You have a few issues in your code:
A view's model needs to be instantiated, not just the model class.
There is absolutely no reason for you to overwrite the ItemView's render method with what you had.
Your order of operations is wrong. show can only be called after the Layout is in the DOM already.
Here's the main thing that fixes your issue:
var Controller = Backbone.Marionette.Controller.extend({
initialize:function(){
this.layout = new Layout;
$('#wrapper').html(this.layout.render().el);
this.layout.renderRegions();
}
});
Updated FIDDLE
Another option (and something I find myself doing often) is to render the regions using the onShow method of the layout.
I am using Marionette's CollectionView to render a list of items with ItemViews. Whenever a new item is added, I want to run a short fade-in animation. But not when the collection is rendered initially (or the collection is reset).
Before using Marionette, I handled the reset and add events slightly differently, but I can not figure out how to do this here. I looked at the source code and it seems that addItemView is responsible for adding the child view and both addChildView (called when add is triggered on the collection) and render (for reset events) call this method.
Maybe I am missing something obvious.
This is one way of doing it:
Include these functions in your CompositeView declaration:
onBeforeRender: function(){
this.onBeforeItemAdded = function(){};
},
onRender: function(){
this.onBeforeItemAdded = myAnimation;
}
This is similar to the solution I present in my book on Marionette (https://leanpub.com/marionette-gentle-introduction/)
How it works: Marionette triggers the "before:render" before it renders the entire collection, so you can set the the onBeforeItemAdded function to do nothing. Once the collection has been rendered, set that function to animate the new item view.
Since each time the collection view adds an item view it also triggers the "before:item:added", you can define an onBeforeItemAdded function that will automatically be called when that event is triggered. This matching happens thanks to triggerMethod.
This solution should solve your problem, without your needing to add flags on the model.
David Sulc answer is pretty hacky, fadeIn should be defined within item it self, not within parent view.
Another thing is that onBeforeItemAdded() is not mentioned in documentation, so it could be for internal use and may change over time.
What I suggest is to add following to parent view, note flag parentRendered:
itemViewOptions: function() {
return {
collection: this.collection,
parentRendered: this.rendered
};
},
onRender: function() {
this.rendered = true;
}
and use that flag in onShow function inside item view:
onShow: function() {
// show visual effect on newly added items
if (this.options.parentRendered) {
this.$el.css('opacity', 0).slideDown(200).animate(
{ opacity: 1 },
{ queue: false, duration: 400 }
);
}
else {
this.$el.show();
}
}
I think that your best choice is to bind your event after the CollectionView has been rendered.
myCollectionView.on( "render", function() {
this.on( "after:item:added", executeMyAnimation );
});
If I have a View in backbone.js and it has an event in the events list:
events: {
'click #somebutton': 'clicked'
},
clicked: function () {
console.log('clicked');
}
How can I then disable/enable that event? So for instance if its clicked then
the event is removed (the button remains on screen but is greyed out etc). When some other part of the view is updated or whatever the event
enabled. Sure I can use jquery but I want to know if this functionality is available in backbone.
Thanks for any answers
Paul
You can always use delegateEvents() and undelegateEvents() to redo your event binding between the DOM and your Backbone View. That said, I usually just keep the event handler and add a conditional in the handler.
// .disabled class (CSS) grays out the button
clicked: function(event) {
var buttonEl = $(event.currentTarget);
if (buttonEl.hasClass('disabled')) {
// Do nothing
} else {
// Do something AND...
buttonEl.addClass('disabled');
}
}
Then you can have your other view or code simply removeClass('disabled') when you want to restore functionality.
UPDATE - disabled property
See comments, but a simpler, much better solution is to use the disabled property disabled="disabled" of buttons.
Use delegateEvents and undelegateEvents for binding and unbinding events. Check for reference: delegateEvents