Sychronous animations in knockout binding - javascript

I want to apply animations when certain properties change with my knockout models (specifically a movement). I need these animations to be synchronous, if there is more than one going on things will get very confusing for the user.
I would like to use a knockout custom binding to do this, as it should make my code easier to understand, but if I do that I can't provide a callback function to the jquery animation. I know that I can't have true synchronous behavior due to javascript limitations, but I can't figure out how to fake it.
The behavior I want:
http://jsfiddle.net/3fLvpxLc/2/
$("#e1").animate({left: 50}, "slow", function() {
// more animations
}
The version with synchronization problems:
http://jsfiddle.net/hrwsd1z3/1/
ko.bindingHandlers.position = {
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var value = valueAccessor();
var valueUnwrapped = ko.unwrap(value);
$(element).animate({left: valueUnwrapped}, "slow");
}
}

jQuery queues are your friend. With them you can serialize asynchronous animations.
Usually they are used implicitly for all animation effects you do on one element, i.e. tied to the animated element itself:
$("element").show("slow").animate({left: 25});
But you can use them explicitly, too. queue adds an animation to the queue, the next callback dequeues the next animation (you can conveniently pass it as the complete handler). That way you can tie the animations to a different element than the animated one:
$("#container").queue(function (next) {
$("element").show("slow", next);
}
$("#container").queue(function (next) {
$("element").animate({left: 25}, next);
}
With that knowledge the task becomes simple:
ko.bindingHandlers.syncPosition = {
init: function(element, valueAccessor) {
var newPosition = ko.toJS(valueAccessor());
// set element to its initial position
$(element).css(newPosition);
},
update: function(element, valueAccessor) {
var newPosition = ko.toJS(valueAccessor());
// queue position update as animation to a common element, e.g. the body
$(document.body).queue(function( next ) {
$(element).animate(newPosition, "slow", next);
});
}
};
function Item(id, top, left) {
this.id = ko.observable(id);
this.position = {
top: ko.observable(top),
left: ko.observable(left)
};
}
function VM(params) {
var self = this;
self.elements = ko.observableArray([
new Item("e1"),
new Item("e2"),
new Item("e3")
]);
}
var vm = new VM();
ko.applyBindings(vm);
vm.elements()[0].position.left(50);
vm.elements()[1].position.left(75);
vm.elements()[2].position.left(25);
vm.elements()[1].position.left(125);
vm.elements()[2].position.top(10);
vm.elements()[1].position.top(20);
vm.elements()[0].position.top(30);
vm.elements()[0].position.left(0);
vm.elements()[1].position.left(0);
vm.elements()[2].position.left(0);
div.container {
position: relative;
}
div.container > div {
width: 20px;
height: 20px;
position: absolute;
}
#e1 {
background-color: blue;
}
#e2 {
background-color: red;
}
#e3 {
background-color: green;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="container" data-bind="foreach: elements">
<div data-bind="syncPosition: position, attr: {id: id}"></div>
</div>

Related

keyup event resets input field

Every time I press a key on my keyboard, it sets $(this).val(""); to null and the score variable will be -2.
initialize: function() {
var score = 0;
var wrapper = $('<div>')
.css({
position:'fixed',
top:'0',
left:'0',
width:'100%',
height:'100%'
});
this.wrapper = wrapper;
var self = this;
var text_input = $('<input>')
.addClass('form-control')
.css({
'border-radius':'4px',
position:'absolute',
bottom:'0',
'min-width':'80%',
width:'80%',
'margin-bottom':'10px',
'z-index':'1000'
}).keyup(function() {
var words = self.model.get('words');
for(var i = 0;i < words.length;i++) {
var word = words.at(i);
var typed_string = $(this).val();
var string = word.get('string');
if(string.toLowerCase().indexOf(typed_string.toLowerCase()) === 0) {
word.set({highlight:typed_string.length});
if(typed_string.length === string.length) {
$(this).val("");
score+=10;
$("#dialog").dialog('option', 'title', 'Score : '+score);
}
}
else {
word.set({highlight:0});
$(this).val(""); // problem
score-=2; // problem
$("#dialog").dialog('option', 'title', 'Score : '+score); // problem
}
}
});
$(this.el)
.append(wrapper
.append($('<form>')
.attr({
role:'form'
})
.submit(function() {
return false;
})
.append(text_input)));
text_input.css({left:((wrapper.width() - text_input.width()) / 2) + 'px'});
text_input.focus();
this.listenTo(this.model, 'change', this.render);
},
When I remove the code that causes the problem, it works perfectly every time. inputting the right word and giving the var score score of +10.
How the keyup event works?
The keyup event is triggered every time a key is released.
This means that if the target word is haromy, when typing the h, the event is triggered and the code in the callback is run.
It means that the following will always be false if typing the first letter wrong.
"haromy".toLowerCase().indexOf("f".toLowerCase()) === 0
So the user types a letter, it's not the same first letter, so the field is emptied immediatly by $(this).val("").
Maybe use another event?
If you want to check once the user unfocus the input, the blur event would work.
If you want to make the check when the user clicks a button, use a click event on a new button.
How to stylize a JavaScript application?
Do not set the initial CSS using jQuery's css function. Keep the styling in a CSS file linked in the HTML.
Using the css function only clutters your application logic, makes it difficult to maintain and delay the application of the style to after the JavaScript execution.
How to bind jQuery events with Backbone?
I removed the backbone.js tag from the question as it's irrelevant, but seeing that you're using it and that it could be improved a lot, I'll throw additional information here.
When using Backbone, don't bind events using jQuery directly. Use the Backbone view's events hash.
Your view could look like this:
var View = Backbone.View.extend({
template: '<div class="wrapper"><input class="form-control" /></div>'
events: {
"blur input": "onInputBlur"
},
onInputBlur: function() {
var words = this.model.get('words').each(function(word) {
var typed_string = this.$input.val(),
string = word.get('string');
// Check each word and score here
}, this);
},
initialize: function() {
this.score = 0;
this.listenTo(this.model, 'change', this.render);
},
render: function() {
this.$el.html(this.template);
this.$wrapper = this.$('.wrapper');
this.$input = this.$('input').focus();
return this;
},
});
With styles out, the CSS file would be:
.wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.input {
border-radius: 4px;
position: absolute;
bottom: 0;
min-width: 80%;
width: 80%;
margin-bottom: 10px;
z-index: 1000;
}

How do I properly destroy a javascript timer object in a nested hover event?

I am having difficulty properly clearing a timer function from within a hover event. I have two divs. They are siblings and the next sibling should display on hover of the other. I can't determine where I am messing up, but what I have figured out is that each time I hover over the main-item that it creates an entirely new timer with setTimeout. So the first iteration works fine, a second hover will trigger twice and so forth.
.main-item {
width: 300px;
height: 100px;
background: #000;
}
.sub-item {
display: none;
width:450px;
height: 75px;
background: red;
&.open {
display: block;
}
}
<div>
<div class="main-item">
</div>
<div class="sub-item"></div>
</div>
var timer;
$('.main-item').hover(function() {
var $this = $(this);
var $sub = $this.next();
$sub.addClass('open');
}, function() {
var $this = $(this);
var $sub = $this.next();
$sub.hover(function() {
var $this = $(this);
clearTimeout($this.data('timerId'));
timer = null;
console.log(timer);
}, function() {
var $this = $(this);
timer = setTimeout(function() {
$this.removeClass('open');
alert('this triggered');
}, 2000);
$this.data('timerId', timer);
});
});
The issue isn't the timer, but with binding event handlers.
Each time you mouse off of .main-item you bind a hover handler to .sub-item. You either need to remove the previous handler, or set a boolean to remember that you have binded the hover handler, or use the jquery function one with mouseenter and mouseoff events, there are many ways to solve this problem.
Using $sub.off() to remove previous handler.
https://jsfiddle.net/p2wtonac/2/
Using boolean to bind once.
https://jsfiddle.net/p2wtonac/3/

Prevent background events trigger when clicking on overlay div

I have a Backbone view and I have attached some events to it:
var View1 = Backbone.View.extend({
initialize: function() {
// ...
},
events: {
"touchend .elem": "callback",
// ...
}
render: function() {
// ...
}
});
Now, when I render View1 another view (View2) is rendered first and this has a div that overlays the View1 elements.
That div has these CSS properties:
.overlay {
background-color: rgba(0, 0, 0, 0.22) !important;
z-index: 500;
width: 100%;
height: 100%;
top: 0;
left: 0;
position: fixed;
}
View2 disappears after some seconds.
The problem is that if I click on the overlay before it disappears, the events under the overlay are triggered and so, in my example, the callback function would be called.
I tried to attach a fake event to View2 by doing this:
var View2 = Backbone.View.extend({
initialize: function() {
// ...
},
events: {
// ...
"touchend .overlay": "overlayCallback"
},
overlayCallback: function( event ) {
if ( event ) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
}
return false;
},
render: function() {
// ...
}
});
but it does not solve my problem.
How can I prevent View1 from triggering the events when user clicks on View2 overlay div?
Thanks
Assuming View1 calls View2, You could do it like:
Add your View1 as a parameter to View2s initialize-method
then, use undelegateEvents - method as stated in the
backbonejs-docs in your initialize-method of view2 to disable
the events of view1 temporarily.
on close of View2, re-delegate
your events using delegateEvents - method in View1 or create a
custom method in view1 and call it from view2's reference to view1.

How to avoid setting the on click bind event every time the function is ran

So I made this overlay function, and I've also made it close on click on the overlay its self. Problem is I bind the click event every time I run the function ($overlay.click(function() {...}), and I think this is bad for performance. Any ideas?
function fluidOverlayShow(action, currentElement) {
var $overlay = $('#fluid-overlay');
if (action == 'open') {
$overlay.click(function() {
emgFluidOverlayShow('close', currentElement);
});
$(currentElement).addClass('fluid-bring-front');
$overlay.addClass('fluid-anim-overlay');
$overlay.data('statuson', true);
} else if (action == 'close') {
$overlay.removeClass('fluid-anim-overlay');
$overlay.data('statuson', false);
$('.fluid-header').find('.fluid-bring-front').removeClass('fluid-bring-front');
}
}
$('#overlay_test').mouseover(function() {
fluidOverlayShow('open', '#overlay_test');
});
$('#overlay_test').mouseout(function() {
fluidOverlayShow('close');
});
#fluid-overlay {
display: none;
opacity: 0.3;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 1000;
}
#overlay_test {
position: relative;
}
#fluid-overlay.fluid-anim-overlay {
display: block;
-webkit-animation: fade-in-overlay 0.2s 1;
-moz-animation: fade-in-overlay 0.2s 1;
animation: fade-in-overlay 0.2s 1;
}
.fluid-bring-front {
z-index: 1100;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Overlay test
<div id="fluid-overlay"></div>
If you absolutely have to keep the click event in the function then use .on() and .off() and then define .off("click") before you define on("click") like this:
$overlay.off("click").on("click", function() {
emgFluidOverlayShow('close', currentElement);
});
This will remove the event binding before it adds it.
You can even namespace the click event like this so it only removes that instance of click (in case other events get added elsewhere):
$overlay.off("click.fluidOverlayClose").on("click.fluidOverlayClose", function() {
emgFluidOverlayShow('close', currentElement);
});
Or...
...do as Guruprasad Rao suggests and move it out of the function (which is a much better way of handling it).
An alternative implementation uses jQuery's .on() method combined with event delgation. What his means is you allow the click event to bubble up through the DOM to a parent element that will always be there that will capture and process the event rather than having to rebind it to the dynamic element everytime it's created.
This would look something like
$("body").on("click","#fluid-overlay",function() {
//code goes here
});

Intercept display change on hover?

I have some elements that, upon hover, toggle the display (none/block) of a child ul. Unfortunately, I can't transition a fade via CSS with display. Using opacity won't work either since the hover area expands onto the opacity: 0 ul, and a combo of both (transition opacity, but still toggle display) doesn't seem to work.
Is there a way to intercept the display change via Javascript, and have it fade between block and none? Are there alternate suggestions (I tried a height: 0/auto toggle too, didn't work right)? I'd prefer an intercept method than a pure JS method, in case JS is disabled or something.
If I understand you correctly. Assuming something like: <div class="nav-container"><ul></ul></div>.
You can listen for hover on the parent, since it contains the child:
var parent = document.getElementsByClassName('nav-container')[0];
var connect = function (element, event, callback) {/* I think you */};
var disconnect = function (handle) { /* can find */};
var addClass = function (element, className) { /* these elsewhere. */};
var removeClass = function (element, className) { /* ... */};
var hoverHandle = connect(parent, 'hover', function (event) {
addClass(parent, 'hovered');
if (blurHandle) {
disconnect(blurHandle);
}
});
var blurHandle = = connect(parent, 'blur', function (event) {
removeClass(parent, 'hovered');
if (hoverHandle) {
disconnect(hoverHandle);
}
});
Then in the CSS:
.nav-container > ul {
display: none;
/* do fade out */
}
.nav-container.hovered > ul {
display: block;
/* do fade in */
}
If you're using jQuery 1.7, then this'll become:
var navContainers = $('.nav-container');
navContainers.on('hover', function () {
$(this).toggleClass('hovered');
});
navContainers.on('blur', function () {
$(this).toggleClass('hovered');
});

Categories