How to partially (re)apply bindings to descendants with knockout? - javascript

I created a custom knockout binding that wraps a given div in an expander. My custom binding moves the given content div to the contant-container div of the expander. After moving the content, the knockout bindings of the content child nodes would not work any more (e.g. click binding for a button inside the content div). Therefore I have to reapply the knockout bindings.
This works in most cases. However, If the content div contains for example a knockout foreach binding, reapplying the bindings means that some content is duplicated.
Example usage of the expander binding:
<div
data-bind="expander: { title: 'dataCollectionForms'}">
<div>
<div class="expander-content">
<button
data-bind="click: $root.addAction, text: 'Hinzufügen'"></button>
<div
data-bind="foreach: listOfButtons">
<button
data-bind="click: $root.buttonClickAction">
</button>
</div>
</div>
</div>
</div>
My code for moving the content div:
function moveExpanderContentToContentContainer($contentContainer, expanderContent) {
try {
//Move the expander content to the content container
//The content will not be cloned but moved. For more info see:
//https://api.jquery.com/append/
var $expanderContent = $(expanderContent);
$contentContainer.append($expanderContent);
$contentContainer.addClass('expander-content-container panel-body');
$expanderContent.addClass('expander-content');
ko.applyBindingsToDescendants(bindingContext, expanderContent);
} catch (appendException) {
var errorMessage = 'Could not append expander-content to expander-content-container.';
logger.logError(errorMessage, appendException, self, true);
}
}
If I remove the line
ko.applyBindingsToDescendants(bindingContext, expanderContent);
the click actions of my three buttons do not work any more:
If I keep the line, the click actions work but the buttons are duplicated:
=> How can I update the bindings of the moved content in a way that fixes
the click bindings and does not duplicate my buttons?
=> If this moving work flow does not work at all, what is a better way to create a custom knockout binding that wraps a given content in a collapsable expander?
I could find some related articles but no solution to my specific issue:
http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html
How to clear/remove observable bindings in Knockout.js?
ko.applyBindingsToNode vs. ko.applyBindingsToDescendants
Remove knockout js bindings on cloned element
https://github.com/knockout/knockout/issues/1821

I solved the issue by not moving the content div at all but building the expander around it.
My original strategy was to have a reusable expander view + view model and to move the content div from the original location to the newly composed expander view. Moving around the already bound content was no good idea, I guess.
The new strategy adapts the already existing divs and composes only the header for the expander.
Nevertheless thank you for your thoughts.

I use following code for recreating content of the passed DOM element (having view model + template selector):
function renderIntoElement(element, viewModel, templateSelector) {
templateSelector = templateName || "#myTemplateId";
var $element = $(element);
var templateHtml = $(templateSelector).text(),
$element.children().remove();
$element = $element.append(templateHtml),
ko.applyBindings(viewModel, $element.children()[0]);
}
Hope this helps.

Related

Detect length div after append

I'm having trouble detecting length and index of the appended div, I dug through a lot of things, and there's a solution with MutationObservers but somehow I'm continuously asking myself do I need it for this kind of problem,
However, let's get to the issue. I have a div populated dynamically structured like this,
<div class="array-placeholder">
<div id="tagContainer" class="row tag">
<!-- dynamic elements -->.....
</div>
</div>
when I click to this button it appends another dynamic element, see the code below :
$('body').off('click.addCollection').on('click.addCollection', function () {
//list and new widgets are just containers and datalist inside the array-placeholder div
let newElem = $(list.attr('data-widget-tags')).html(newWidget);
newElem.appendTo(list);
})
I've tried this solution to get the length of the array-placeholder div but it doesn't seem to work, the function is never launched:
$('body').on('DOMSubtreeModified', '.array-placeholder', function (event) {
console.log( $(".array-placeholder > div").length);
})
result
//nothing and when you click to the appended element via moveUpButton.closest(".tag") you 0 as index
do I have to implement MutationObserver to solve this if yes, could you guide me through this, thanks
DOMSubtreeModified
This event has been deprecated in favor of the Mutation Observer API
Yes you have to use Mutation Observer API. To implement that you can refer This
You can get all children using jquery's find('*') and then count their length;
$(".array-placeholder").find('*').length;
Or if you want only divs then do:
$(".array-placeholder").find('div').length;

how to set visible = false by knockout

I have posted more detailed question this is much clear and straightforward
here
hello, I am trying to set the value of list element to false by using knockout
this is my HTML
<li>
<a onclick="log(this)" data-bind="visible: true, text: $data"></a>
</li>
is there a way to say something like this :
myViewModel.items()[i].setVisible(false);
Don't set the visible binding to true set it to the variable you define in your viewModel. Also you can access individual elements of an observable array through the foreach binding. Lastly, if you want to use $data you can access the property of the individual array object directly using the "." operator. The documentation I referenced at the end of my post has more information. See below:
<div data-bind="foreach: shouldShowMessageArray">
<div data-bind="visible: $data.shouldShowMessage">
Message goes here.
</div>
</div>
<script type="text/javascript">
var myViewModel;
$(document).ready(function(){
myViewModel = new viewModel();
function viewModel() {
this.shouldShowMessage = ko.observable(false) // Message initially visible
this.shouldShowMessageArray = ko.observableArray([
{shouldShowMessage: ko.observable(true)},
{shouldShowMessage: ko.observable(false)}
]);
}
ko.applyBindings(myViewModel);
});
</script>
Knockout foreach / $data documentation
If you are looking to load the page with a starting value of visible:false, you may run into some issues with the "visible" binding.
I have had trouble with the "visible" binding when I want an element to be hidden on page load, and then made visible after some action taken by the user. You will see a flash of the hidden element while your knockout.js logic loads if you are using the visible binding. If you try to use inline CSS to set display:none on the element, then it will never become visible even when the KO logic results in a visible:true value. This is because KO applies the CSS rules after it has handled changing the element.style.display value.
One solution to this issue that I have found is to set up a CSS class that well set display to none, and then use a data binding to conditionally apply that CSS rule based on an observable.
CSS:
.hideMe { display: none; }
HTML:
<div class="hideContent" data-bind="css: { hideContent: !yourObservable() }">
<p>Content</p>
</div>

Angular not updating css after ng-repeat

I am having a problem updating css on an element in Angular. I have a div that contains elements generated by ng-repeat. The div containing these elements has a set height and has the ng-scrollbars directive attached to it. ng-scrollbars creates a nice scrollbar inside a div when the elements in the div exceed the height of the their container. To do this it creates nested internal divs. So in this case, if the height of the nested internal divs is larger than their container then a scrollbar is made so that you can scroll through the elements.
So lets say I add or remove items to the ng-repeat list. When this happens I would like additional css to be added to the top level div only if the internal div made by ng-scrollbars exceed the height of the top level div. (the height of the internal div is the actual height of the ng-repeat list because it contains the ng-repeat elements)
To do this I created two directives, one that broadcasts an event and one that receives it. The one that broadcasts the event is attached to every ng-repeat element and is broadcasted when the element is created or destroyed. This is to check when changes happen to the ng-repeat list.
The one that receives the broadcast is attached to the top level div container (the one that contains the div made by ng-scrollbar) and checks its height against the internal div made by ng-scrollbar, if the height of the internal div is greater than the top level div then add some css.
I'm sorry if this seems confusing, here's my code doing this with comments attached that I hope explain it better:
app.directive('gtScrollLb', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) { //elem is the top level container of the ng-repeat elements
var scrollDiv = elem.children()[0]; //this is an internal div created by ng-scrollbars
var anotherScrollDiv = $(scrollDiv).children("#" + $(scrollDiv).attr("id") + "_container"); //another internal div created by ng-scrollbars
var id = attrs.gtScrollLb;
scope.$on(id + "-repeat-change",
function(newVal, oldVal) {
//check if ng-scrollbar div that now contains the ng-repeat elements
//has a greater height than the top level container
if ($(anotherScrollDiv).height() >= elem.height()) {
elem.css("border-bottom", "1px solid darkgrey");
} else {
elem.css("border-bottom", "");
}
}
);
}
};
});
app.directive('gtRepeatable', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
var id = attrs.gtRepeatable;
scope.$emit(id + "-repeat-change"); //event that item has been added by ng-repeat
var parent = scope.$parent;
elem.bind("$destroy",function(){
parent.$broadcast(id + "-repeat-change");//event that item has been destroyed by ng-repeat
});
}
}
});
The problem is that the ng-repeat elements are added and destroyed before the height of their internal container is determined again. So the height being checked when the change event is fired is actually the old height of the internal container.
I need to somehow check the new height. I have been trying to solve this for a long time now, any help would be greatly appreciated.
Edit
In a nutshell, I need an even to fire or $watch to happen after ng-repeat has finished and the DOM has rendered. The problem I'm having here only happens because the event is fired before the DOM is rendered.
Edit 2
For #vittore who flagged this as a duplicate. If you had actually read my question in detail you would see that the one you proposed as the duplicate had nothing to do with my actual problem. There is no problem with ng-scrollbars here. ng-scrollbars is working fine. But it was necessary to include it in the description because it effects how I'm going about solving my problem.
There is a way to get event when last loop element was renderd. Check this link: AngularJS - Manipulating the DOM after ng-repeat is finished
Also here is exact question you have : ng-scrollbar is not working with ng-repeat
and important part:
$scope.$on('loopLoaded', function(evt, index) {
if (index == $scope.me.length-1) {
$scope.$broadcast('rebuild:me');
}
});
To get this to work I ended up using MutationObservers. I had to use two, one for attribute changes and one for childList changes.
MutationObservers should be used sparingly, they will slow down your app and cause serious bugs if left unchecked. I accidentally caused an infinite loop doing an async call.
It's best to only connect them right before the change to the DOM is made and have them disconnect themselves once they make the change.

Append <div> using jquery's forEach and templatePlugin

I'm struggeling for some hours with the following problem:
Iterating through a object using jquery's forEach method
For each object element, a template should be used, which also fills the information in the template. The plugin, which I am using: jquery-tempalte
For each object the template should be appended to a <div>
Unfortunately only the last object element is visualized and loaded to the DOM. All other divs, which are created during the steps before, were overwritten.
Find here an jsFiddle, which demonstrates my problem: http://jsfiddle.net/7oq053od/1/
Do you have any idea, what I am missing?
Would be great to get some feedback :)
From Options section on the github webpage you can see this third parameter with the append property set as true.
Just consider add it to your script like this :
$.each(results.features, function (key, value) {
$("#homeView").loadTemplate($("#template"), {
geoFenceName: value.attributes.title,
geoFenceDescription: value.attributes.description
}, {append: true});
});
Here is a working JsFiddle.
For appending multiple div's . you should create a main container that contains all the child div's and when displaying, each div element will be created and append to main container everytime.
Here's a sample code:
<div id="container"></div>
//now in javascript create child div elements and append to main container
<script>
for(var items in data)
{
var newChild = $(document.createElement('div'));
$(newChild).css({
//some css code
});
//now appending it to main container
$('#container').append(newChild);
}
</script>
It seems the template plugin applies the templated values to the same element. If you clone the element, your script works.
$.each(results.features, function (key, value) {
$("#homeView").clone().loadTemplate($("#template"), {
geoFenceName: value.attributes.title,
geoFenceDescription: value.attributes.description
}).appendTo('#fences');
});
See updated fiddle here
The problem is that the loadTemplate function overwrites the div. You can fix that by cloning the template container and then append it to '#homeWiew'.
I created a fork from your jsfiddle that worked for me:
http://jsfiddle.net/6ksdnejw/

What is the angular way of hiding all specific divs with the same class

I want to do a simple thing:
I have an app, which has certain divs that in need to show (Only the specific one) and hide if clicked somewhere outside of it (All with the specific class for example).
This is easy using jquery:
$('some-class').style('display','none') //psuedo code here
How should i do the same with angular js?
A specific reason to do so:
I want to build a simple select drop-down (I don't want to use one that exist, i want to understand this...), when i click one, it suppose to open a div right beneath the button, when i click outside of it, it suppose to close not only this one, but some other elements like it in the app.
one point worth mentioning: Not all my select boxes are pre-rendered, some are dynamically added (inside directive), so not all of the are in the $scope, but inside them directives which has isolated scope.
Its better to make directives for these kind of things:
Make a directive for toggleDisplay as following
app.directive('toggleDisplay', function() {
return function(scope, element) {
element.bind('click', function() {
element.toggleClass('unneeded-class'); // or something else
});
};
});
and then you can apply this directive to any element in the html:
<div toggle-display></div>
You can make a drop down logic or kindof accordion etc following this pattern
How do i listen to a click anywhere in the app, that when i click it
and "that" element is not the select box, close all the select boxes?
let suppose you have that opend/dispalyed div that you want to hide when you click outside of it . give it a class "divvy" and attach the following directive to its wrapping container:
app.directive('toggleFocus', function() {
return function(scope, element) {
element.bind('click', function() {
if(!element.hasClass('divvy'))
angular.element(document.querySelector('.divvy')).css({display:'none'})
});
};
});
html:
<div toggle-focus>
<div class="divvy"></div>
</div>
It's in the angular documentation: https://docs.angularjs.org/api/ng/directive/ngShow
<!-- when $scope.myValue is truthy (element is visible) -->
<div ng-show="myValue"></div>
<!-- when $scope.myValue is falsy (element is hidden) -->
<div ng-show="myValue" class="ng-hide"></div>
just attach ng-show="whateverValue" to each div you want to hide/show based on whether "whateverValue" is true/false

Categories