jQuery animation with knockout beforeAdd and afterRemove - javascript

I wan't to animate tabs divs. When closing, the width is animated to 0px, and when opening, it is animated from 0px to the size of it's inner content.
https://jsfiddle.net/rrfogrku/5/
but the expected behaviour not work, closed div are not removed from DOM after the fadeOut function is called, and when I add a new tab, there is no animation.
Html:
<div class="main">
<div class="tab-list">
<!-- ko foreach: {data: editors, afterAdd: fadeIn, beforeRemove: fadeOut} -->
<div class="tab" data-bind="
css: { active: isSelected },
click: $parent.select" >
<span data-bind="text: title"></span>
<button data-bind="click: $parent.close">x</button>
</div>
<!-- /ko -->
<div class="plus">
<button data-bind="click: add">+</button>
</div>
</div>
</div>
Javascript
function MainViewModel() {
var self = this;
var storage = localStorage;
self.editors = ko.observableArray([new EditorViewModel("default.txt", "some content")]);
self.close = function(item){
self.editors.remove(item);
if(self.editors().length == 0){
self.add();
}
else if(!self.editors().some(val=>val.isSelected())){
self.select(self.editors()[0]);
}
}
self.select = function(item){
for(var i in self.editors()){
self.editors()[i].isSelected(false);
}
item.isSelected(true);
}
self.add = function(){
var editor = new EditorViewModel("sans-titre.js", "");
self.editors.push(editor);
self.select(editor);
}
self.fadeIn = function(el){
var $el = $(el);
var w = $el.width();
$el.css({ width: '0px' });
$el.animate({width : w });
}
self.fadeOut = function(el){
$(el).animate({ width: '0px'});
}
if(self.editors().length >0){
self.select(self.editors()[0]);
}
};
function EditorViewModel(title, initialContent){
var self = this;
self.title = ko.observable(title);
self.content = ko.observable(initialContent);
self.isSelected = ko.observable(false);
}
var viewModel = new MainViewModel();
ko.applyBindings(viewModel);

In the example in the knockout docs, you can see that you need to check for the nodeType of the added element, since knockout can include multiple elements from the template and we're only interested in the <div>:
if (el.nodeType === 1) { /* do work */ }
This page explains that, when using beforeRemove:
Knockout cannot know how soon it is allowed to physically remove the DOM nodes (who knows how long your animation will take?), so it is up to you to remove them
So for your fade out animation, you'll have to add the code that updates the DOM:
$(el).animate({
width: '0px'
}, 500, () => $(el).remove());
// ^^^^^^^^^^^^^^^^^^^^---- remove element after animation is done
Here's the two changes in an updated fiddle:
https://jsfiddle.net/1Lwfqpxk/
Free bonus tip: add white-space: nowrap; to your .tab css to prevent the reflow during the width animation.

Related

multiple 'md-tab' have 'md-active' class at the same time when I switch tabs frequently with angular md-selected index

When I am switching md-tabs frequently, Md-tabs are switching correctly but multiple md-tab-item elements have 'md-active' class at the same time, So I can not see the tab's content which is active because it is overlapped by its right tab's content.
According to me, in angular-material, when we select a tab, angular first deselect the previous tab(hide previously displayed content on the page) and displays selected tabs content. While doing this process, angular misses removing 'md-active' class of previously active tab.
Here is the fiddle to reproduce the behavior. This is random behavior and comes while concurrent clicks.
Kindly wait for 1 minute after click on 'switch tabs' button
jsFiddle
angular.module('firstApplication', ['ngMaterial']).controller('tabController', tabController);
function tabController ($scope) {
$scope.boards = Array.from(Array(100).keys());
$scope.data = {
selectedIndex: 0,
secondLocked: true,
secondLabel: "2",
bottom: false
};
$scope.next = function() {
$scope.data.selectedIndex = Math.min($scope.data.selectedIndex + 1, 2) ;
};
$scope.previous = function() {
$scope.data.selectedIndex = Math.max($scope.data.selectedIndex - 1, 0);
};
$scope.switch = function() {
$scope.activeTabsCounter = $("md-tab-item.md-active").length;
}
/********* Here i am changing tabs ****8**/
/******** This is strict behaviour where I get the problem but randomly *****/
$scope.switchTabs = function(){
clearInterval(interval);
var i = $scope.boards.length - 1;
var interval = setInterval(function() {
if($scope.activeTabsCounter == 1) {
if(i >= 0)
$(".md-accent md-tab-item")[i--].click();
$scope.switchTabs();
} else {
clearInterval(interval);
}
}, 100);
/********* /Here i am changing tabs ****8**/
}
}
<link href="https://rawgit.com/angular/bower-material/master/angular-material.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.js"></script>
<script src="https://rawgit.com/angular/bower-material/master/angular-material.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular-animate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular-aria.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<div ng-app="firstApplication">
<div id="tabContainer" ng-controller="tabController as ctrl" ng-cloak>
<div> <br><br>
<button ng-click="switchTabs()">Click here to switch tabs</button>
<br><br>
Current active Tabs: {{activeTabsCounter}}
<br>
</div>
<md-content class="md-padding">
<md-tabs class="md-accent" md-selected="data.selectedIndex" md-align-tabs="{{data.bottom ? 'bottom' : 'top'}}">
<md-tab md-on-select="switch()" id="tab{{board}}" ng-repeat="board in boards">
<canvas id="canvas-{{board}}-1" width="1600" height="900"></canvas>
<canvas id="canvas-{{board}}-2" width="1600" height="900"></canvas>
<md-tab-label>{{board}}</md-tab-label>
<md-tab-body>Item #{{board}} <br/>selectedIndex = {{board}};</md-tab-body>
</md-tab>
</md-tabs>
</md-content>
</div>
</div>
I am switching tabs in 100 ms but in our application, we are getting this issue in longer time interval also. We are emitting board switch event using socket.
As I understand, your idea is change the tabs programmatically every X seconds so...
Firstly, use angularJs $interval instead of setInterval() (that's because setInterval() doesn't care about angular digest).
Secondly, you have to update the selectedIndex instead of calling click() event...
Finally, remove the active tabs check (I suppose that you added for testing)
I've updated your jsFiddle
CONTROLLER:
function tabController($scope, $interval) {
$scope.boards = Array.from(Array(100).keys());
$scope.data = {
selectedIndex: 0
};
$scope.switchTabs = function() {
var size = $scope.boards.length - 1;
var interval = $interval(function() {
if(size >= 0){
$scope.data.selectedIndex = size--;
}else{
$interval.cancel(interval);
}
}, 750);
}
}
HTML:
<md-tabs class="md-accent" md-selected="data.selectedIndex">
<md-tab ng-repeat="board in boards">
<md-tab-label>{{board}}</md-tab-label>
<md-tab-body>
Item #{{board}}<br/>
selectedIndex = {{board}};
</md-tab-body>
</md-tab>
</md-tabs>
As the OP said in a comment, this was solved by manually removing the md-active class from all the tabs before selecting the new tab (through changing the data.selectedIndex in the code in the question).
For those of us who are not using jQuery in our Angular, the JavaScript looks something like this:
function fixMdTabsActive(tabsId, currentIndex)
{
var tabs = angular.element(document.getElementById(tabsId));
var elements = tabs.find('md-tab-item');
for (var i = 0; i < elements.length; ++i)
{
if (i != currentIndex)
{
angular.element(elements[i]).removeClass('md-active');
}
}
elements = tabs .find('md-tab-content');
for (var i = 0; i < elements.length; ++i)
{
if (i != currentIndex)
{
angular.element(elements[i]).removeClass('md-active');
}
}
}
I partially solved this problem in my code by adding a md-active="$index == data.selectedIndex" to my md-tab definition. In theory, this shouldn't be needed, but when I constructed my tabs, I would set the data.selectedIndex to the last tab, and the first and last would be active. After adding this line, it mostly worked. I did have one race condition that forced me to remove the md-active class as documented in the OP's comment and my other (community) answer.

How to make the position of an element Directive relative to an ng-repeated item?

I have a ng-repeat list of tag objects from an array. When you hover over any tag, a <tags-hover> element related to that tag displays additional tag information.
With the way I have my markup and code setup, the <tags-hover> does display the correct information for each tag hovered. However, the position of the <tags-hover> that shows up is always the last one in the ng-repeat list.
The ideal situation would be that the <tags-hover> for each tag hovered, would appear directly below that tag, and not always the last one.
UPDATE I'm using a scopeFactory to save and store scopes for my controllers and directives, looks like I may need a way to index the scope of the tagHover I need.
< I'm hovering over the first tag here.
My markup in the tagsPanel:
<ul>
<li ng-repeat="(k, m) in tags | filter:filterTags | orderBy:predicate:reverse"
ng-class="{'selected': m.selected}"
ng-mouseover="hoverTag(m)"
ng-mouseleave="leaveTag()"
ng-click="selectTag(m)" class="tag-li" style="transition-delay: {{$index * 60}}ms">
<div class="tag"
ng-class="{'positive': m.direction == 'positive',
'negative': m.direction == 'negative',
'' : m.direction == 'stagnant'}">
{{m.term}}
</div>
<!-- below is that popover div with tag details -->
<tags-hover></tags-hover>
</li>
</ul>
tagsHover HTML markup:
<div class="tags-hover-container" ng-show="tagsHoverDisplay">
...
tagsHover Styling:
.tags-hover-container {
float: left;
position: relative;
width: 250px;
height: auto;
background: $gray_bg;
border: 1px solid $gray2;
z-index: 10000;
#include rounded(3px);
#include clearfix;
&:before {
position: absolute;
top: -10px;
left: 26px;
z-index: 9999;
#include triangle(up, 10px, $gray_bg);
}
&:after {
position: absolute;
top: -11px;
left: 25px;
z-index: 9998;
#include triangle(up, 11px, $gray2);
}
}
tagsPanel Controller:
vs.hoverTag = function(tagObj) {
tagsHover = ScopeFactory.getScope('tagsHover');
tagsHover.breakTimeout = true;
TagDetailsFactory.saveTagDetails(vs.ticker, tagObj);
};
vs.leaveTag = function() {
tagsHover.breakTimeout = false;
tagsHover = ScopeFactory.getScope('tagsHover');
$timeout(tagsHover.leavingTag, 500);
};
The inbetween service tagsDetailsFactory:
var saveTagDetails = function(ticker, tag) {
ApiFactory.getTagData(ticker, tag.term_id).then(function(data) {
// API code and data calculation stuffs...
tag.tweet_percentage = increase;
details.direction = tag.direction;
//etc etc ...
// I get the scope of the tagsHover Directive and then call hoveringTag()
tagsHover = ScopeFactory.getScope('tagsHover');
tagsHover.hoveringTag();
});
};
The code in my tagHover Directive:
var vs = $scope;
vs.breakTimeout = false,
vs.tagDetails = {},
vs.tagsHoverDisplay = false;
ScopeFactory.saveScope('tagsHover', vs);
vs.hoveringTag = function() {
vs.tagDetails = {};
vs.tagDetails = TagDetailsFactory.getTagDetails();
vs.tagsHoverDisplay = true;
};
vs.leavingTag = function() {
if (vs.breakTimeout) {} else {
vs.tagsHoverDisplay = false;
}
};
How would you approaching fixing the that displays is the one relative to the tag that was hovered? And not the last one in the ng-repeat?
Note here is a screenshot of the HTML from chrome dev tools, even when I hover over other tags, the tags-hover in the last li gets all the updating, I see the tweets count change only in that last one:
As already shown in the comments, the most probably reason that only one <tags-hover> is visible is the ScopeFactory.
My solution is to keep the decision if a <tags-hover> is visible in the model and avoid accessing scopes of other elements.
Code example:
http://plnkr.co/edit/aHUh1AX7hMyafzaCZnPI?p=preview
.directive('tagDetails', function() {
return {
restrict: "E",
link: function($scope, el, attrs) {
console.debug($scope, attrs);
},
scope:{
tag:'=ngModel'
},
template: '<div ng-show="tag.showDetails">{{tag.details}}</div>'
};
})
I implemented the directive in this way, that its visibility depends on the showDetails property which belongs to the model.
Now all I need is to change this property, e.x. in the controller
$scope.showTagDetails = function(t) {
t.showDetails = true;
}
Corresponding html:
<li ng-repeat="t in tags">
<div ng-mouseover="showTagDetails(t)">{{t.name}}</div>
<tag-details ng-model="t"></tag-details>
</li>
The showDetails doesn't have to be initialized with false: It will simply evaluate to false if the property is not there.
The only disadvantage is that you need to add this property to your model data and it could eventually overwrite something else.
I that case you would need to wrap your model in another object.

Animate a div down and up using Jquery on click

Need help in animating a div up on click and down via the same click link. Here is my Javascript below:
$(function() {
// global functions
var dash = $('#Dashboard');
var dashBtn = $('#dashClick');
var state = dash.css({
"top":600
});
var clicked = dashBtn.click(function(e) {
e.preventDefault();
if(clicked) {
dash.animate({"top":0});
}
if(state > 0 && clicked) {
dash.animate({"top":600});
}
});
//make it height of document
dash.height($(document).height());
});
and my HtML below showing the references to the javascript with the ID's:
<a id="dashClick" href="#">Dashboard</a>
<div id="Dashboard">
<h2 class="dashTitle">Project Information</h2>
<div class="dashInnerAdd">
<p>
Project name: CSS3 Effects N' Stuff
My github is: https://github.com/Amechi101/css3effects
</p>
</div>
</div>
</main> <!-- end grid main-->
</div>
<!--end wrap -->
Among other things (see the code for all changes):
If you want to use top, you probably are wanting it to behave like it does with absolute positioning. To do that, you need a container with relative positioning around the #Dashboard. Also, your javascript animate needs px values. "top":600 should really be top:"600px".
html:
<a id="dashClick" href="#">Dashboard</a>
<div class="container">
<div id="Dashboard">
<h2 class="dashTitle">Project Information</h2>
<div class="dashInnerAdd">
<p>
Project name: CSS3 Effects N' Stuff
My github is: https://github.com/Amechi101/css3effects
</p>
</div>
</div>
</div>
js:
$(function() {
// global functions
var dash = $('#Dashboard');
var dashBtn = $('#dashClick');
var clicked = false;
dashBtn.click(function(e) {
e.preventDefault();
if(clicked === true) {
clicked = false;
dash.animate({top:"0px"});
} else {
clicked = true;
dash.animate({top:"600px"});
}
});
//make it height of document
dash.height($(document).height());
});
and some css:
.container {
position: relative;
}
#Dashboard {
position: absolute;
top: 0;
}
fiddle: http://jsfiddle.net/SQK78/2/
If you don't need the absolute positioning, you can just change top to marginTop, and you can get rid of the container wrapper as well as all of the css in that fiddle.
Your main javascript only executes once. You need to track state from within the click handler. Try this:
$(function() {
// global functions
var dash = $('#Dashboard');
var dashBtn = $('#dashClick');
var state = true;
dashBtn.click(function(e) {
state = !state;
e.preventDefault();
if(state) {
dash.animate({"top":20});
}
if(!state) {
dash.animate({"top":600});
}
});
//make it height of document
dash.height($(document).height());
});

Refresh child in dynamically created nested accordion

I am trying to append items to a nested accordion dynamically when the user clicks a button. I am using the following code to create a nested accordion:
$(".accordion").accordion({
collapsible: true,
autoHeight: false,
animated: 'swing',
heightStyle: "content",
changestart: function(event, ui) {
child.accordion("activate", false);
}
});
var child = $(".child-accordion").accordion({
active:false,
collapsible: true,
autoHeight: false,
animated: 'swing'
});
In order to do this, I have found that I need to refresh the accordion using the following:
$('.accordion').accordion("refresh");
My problem is that when I try to refresh the inner accordion using:
$('.child-accordion').accordion("refresh");
I get the following:
Error: cannot call methods on accordion prior to initialization; attempted to call method 'refresh'
When I inspect the div that should be refreshed it has the following ids/classes:
DIV#shelf0sections.child-accordion.ui-accordion-content.ui-helper-reset.ui-...
I tried using the selector:
$('#shelf0sections .child-accordion').accordion("refresh");
instead which doesn't give an error, but nothing happens visually.
jsFiddle: http://jsfiddle.net/Mw9SA/3/
(Note that the first element in the list is just an example to see the nested accordion working, If you try to add sections to it, it won't work. Use the '+Shelf' button, then open the new accordion and use the '+Section' button.)
How about a more modular approach?
Fiddle or it didnt happen: http://jsfiddle.net/Varinder/24hsd/1/
Explanation
The idea is ( the same ) to create a brand new accordion element on the fly with correct events attached and appended somewhere in the DOM.
It's generaly more managable to have repeater HTML markup abstracted away in a template somewhere in DOM and use JS to reference it rather that building it from a string.
Heres the accordion template in the markup:
<div class="template">
<div class="accordion">
<h3 class="accordion-title">accordion title</h3>
<div class="accordion-content">
accordion content
</div>
</div>
</div>
Heres the full HTML markup - just in case:
<div class="page">
</div>
<div id="addShelf" class="button">+ Shelf</div>
<div id="addSection" class="button">+ Section</div>
<div class="template">
<div class="accordion">
<h3 class="accordion-title">accordion title</h3>
<div class="accordion-content">
accordion content
</div>
</div>
</div>
JS
Starting off by storing different accordion configurations:
var shelfConfig = {
collapsible: true,
autoHeight: false,
animated: "swing",
heightStyle: "content"
}
var shelfSectionConfig = {
active:false,
collapsible: true,
autoHeight: false,
animated: "swing"
}
Kepping a track of current accordion number and current accordion section number ( number of sections inside last accordion ) - might come in handy if you require a feature to remove an accordion shelf
var currentShelfNumber = 0;
var currentShelfSectionNumber = 0;
Chaching DOM elements, notice reference to the tempalte div
var $page = $(".page");
var $accordionTemplate = $(".template").children();
var $addSection = $("#addSection");
var $addShelf = $("#addShelf");
Creating a helper function that simply returns a cloned copy of the accordion template from the DOM
function getAccordionTemplate() {
return $accordionTemplate.clone();
}
Main function generateAccordion - it takes two arguments, accordionNumber to append current number in titles etc and accordionType to find out which accordion configuration to use.
With those parameters it will return a brand-spanking-new accordion with appropriate events attached which can then be append to the DOM
function generateAccordion( number, accordionType ) {
var $accordion = getAccordionTemplate();
var accordionTitle = "twerking bieber?";
if ( accordionType == "shelf" ) {
accordionTitle = "Shelf " + number;
} else {
accordionTitle = "Shelf Section";
}
$accordion.find("h3").text( accordionTitle );
var $accordionWithEvents = attachAccordionEvents( $accordion, accordionType );
return $accordionWithEvents;
}
Notice the call to another function attachAccordionEvents as the name suggests - this fella will attach events to the accordion element.
function attachAccordionEvents( $accordionElement, accordionType ) {
if ( accordionType == "shelf" ) {
$accordionElement.accordion( shelfConfig );
} else {
$accordionElement.accordion( shelfSectionConfig );
}
return $accordionElement;
}
Another helper function which makes sure "add section" button doesnt show up if there is no accordion shelf for it to work on
function manageSectionButton() {
if ( $page.children().length > 0 ) {
$addSection.show();
} else {
$addSection.hide();
}
}
Finaly events and logic:
$addShelf.on("click", function(e) {
e.preventDefault();
var newShelfNumber = currentShelfNumber + 1;
var $shelfElement = generateAccordion( newShelfNumber, "shelf" );
currentShelfNumber = newShelfNumber;
$page.append( $shelfElement );
manageSectionButton();
});
$addSection.on("click", function(e) {
e.preventDefault();
var newShelfSectionNumber = currentShelfSectionNumber + 1;
var $shelfSectionElement = generateAccordion( newShelfSectionNumber, "section" );
var $activeShelfElement = $page.children().last().find(".accordion-content");
$activeShelfElement.append( $shelfSectionElement );
});
... And thats about it.
Hope this helps,
Cheers

Is there a way to listen to Html Element Style changes - Backbone.JS

I have the following HTML template:
<li id="imovel" style="<%= display %>">
<div class="thumbnail">
<a href="#">
<img src="http://placehold.it/90x60"></img>
<%= textvar %>
</a>
</div>
</li>
The display is a display: none or display: block depending on some things.
I would like this view:
var imovelView = Backbone.View.extend({
el: $('#imovel')
,template: _.template( $("#listing_template").html())
,events: {
"change:display #imovel" : "activate"
}
,initialize: function(){
this.on('change:display', this.toggle, this);
this.collection.bind('add', this.render, this);
}
,activate: function(item){
alert("change display");
}
,render: function(item){
var showPerPage = $('#show_per_page').val();
var totalEntries = this.collection.length;
var pageMax = Math.ceil( showPerPage/totalEntries );
var displayValue = "display: none;";
if(totalEntries <= showPerPage){
displayValue = "display: block;";
}
var variables = { textvar: item.get("Lat"), display: displayValue };
var template = this.template( variables );
$('#max_page').val(pageMax);
$('#content').append( template );
resizeViewport();
}
});
When I do change:display in display I mean Listen to changes in the el style display property.
Is it possible to do that ?
Can you alter the code to trigger event on the element that is being hidden/shown? This is as much as adding two lines when display nonde/hide is set. $(el).trigger('display:changed') on the element that is changing it's display state. And then from Backbone.View just listen for this custom event on the element disapearingElement.on('display:changed', doSomething)
Any other solution will be supper messy and put a constant load on the browser. You'd have to create helper method which would use polling with setInterval/setTimeout that would check the state of element all the time and trigger a callback/custom event when it changes - this is the only way to implement it cross browser.
But honestly... don't leave messy code - if it's fine for you, ok - but think about people who will be working with this code when you are gone :P

Categories

Resources