How to automatically react to DOM changes? - javascript

I have a list of items in the DOM:
<ul id="my_list">
<li class="relevant">item 1 <a onclick="remove(1)">Remove</a></li>
<li class="relevant">item 2 <a onclick="remove(2)">Remove</a></li>
...
</ul>
The user can remove items from the list by clicking the respective 'remove' links in the item.
I want to reactively monitor this list for changes, so that when there are no items left it triggers a function. For example:
var num_list_items = $("#my_list li.relevant").length;
if( num_list_items == 0 ){
do_something();
}
My app is being built in Meteor, so would ideally like to know if there is any native functionality that can do this. If not, any other suggestions are welcome.

You can use a MutationObserver(link)
You'll instantiate one with something to do everytime the observed node mutates, if the mutation reports the desired condition you'll do something about it.
var observed = document.getElementById('my_list');
var whatIsObserved = { childList : true };
var mo = new MutationObserver(function(mutations){
mutations.forEach(function(mutation){
if(mutation.type === 'childList'){
//DOM Tree mutated
if(mutated.target.querySelectorAll('li.relevant').length === 0){
//mutated target has no childs.
}
}
});
});
mo.observe(observed, whatIsObserved);
Alternatively, have the remove function trigger your condition.

Related

Fire an event when a liste get new item

i have an empty list :
<ul id="select2-choices"></ul>
this list gets elements added to it using a ui ,so i get this :
<ul id="select2-choices">
<li>item1</li>
<li>item2</li>
</ul>
i want to fire an event , in order to call a function when that list get a new item :
$("#select2-choices").on("Thevent", function (e)){
self.SetDefaultTeam(e);
});
how to do that ?
You can use mutation observers as shown below. The code is commented. I created a button to mimic the addition of new items, but the mutation observer is the function that recognises that change in the DOM tree.
N.B. If you have access to the code that is adding the new li then it would be better to trigger your function from there.
Let me know if you were hoping for something else.
// Create a button to add options to mimic your functionality
$("#add-li").click(function() {
$("ul#select2-choices").append("<li>New</li>");
});
// Create mutation observer
var observer = new MutationObserver(function(mutations) {
// Something has changed in the #select2-choices
console.log("Change noticed in childList");
});
// Just look out for childList changes
var config = {
attributes: false,
childList: true,
characterData: false
};
// Select target
var target = document.querySelector('#select2-choices');
// Launch observer with above configuration
observer.observe(target, config);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="select2-choices">
</ul>
<button id="add-li">Add option</button>

ng2-dragula after adding new item it's getting displayed at the top

I am using ng2-dragula for drag and drop feature. I am seeing issue when I drag and drop first element(or any element) at the end and then try to add new item to the array using addNewItem button, new item is not getting added to the end. If i don't drop element to the end, new item is getting added at the end in UI.
I want new items to be displayed at the bottom in any scenario. Any help is appreciated.
This issue is not reproducible with Angular 7. I see this happening with Angular 9
JS
export class SampleComponent {
items = ['Candlestick','Dagger','Revolver','Rope','Pipe','Wrench'];
constructor(private dragulaService: DragulaService) {
dragulaService.createGroup("bag-items", {
removeOnSpill: false
});
}
public addNewItem() {
this.items.push('New Item');
}
}
HTML
<div class="container" [dragula]='"bag-items"' [(dragulaModel)]='items'>
<div *ngFor="let item of items">{{ item }}</div>
</div>
<button id="addNewItem" (click)="addNewItem()">Add New Item
I edited the stackblitz from the comment to help visualize the issue. This seems to be triggered when a unit is dragged to the bottom of the list. Updated stackblitz : https://stackblitz.com/edit/ng2-dragula-base-ykm8fz?file=src/app/app.component.html
ItemsAddedOutOfOrder
You can try to restore old item position on drop.
constructor(private dragulaService: DragulaService) {
this.subscription = this.dragulaService.drop().subscribe(({ name }) => {
this.dragulaService.find(name).drake.cancel(true);
});
}
Forked Stackblitz
Explanation
There is some difference between how Ivy and ViewEngine insert ViewRef at specific index. They relay on different beforeNode
Ivy always returns ViewContainer host(Comment node)ref if we add item to the end:
export function getBeforeNodeForView(viewIndexInContainer: number, lContainer: LContainer): RNode|
null {
const nextViewIndex = CONTAINER_HEADER_OFFSET + viewIndexInContainer + 1;
if (nextViewIndex < lContainer.length) {
const lView = lContainer[nextViewIndex] as LView;
const firstTNodeOfView = lView[TVIEW].firstChild;
if (firstTNodeOfView !== null) {
return getFirstNativeNode(lView, firstTNodeOfView);
}
}
return lContainer[NATIVE]; <============================= this one
}
ViewEngine returns last rendered node(last <li/> element)ref
function renderAttachEmbeddedView(
elementData: ElementData, prevView: ViewData|null, view: ViewData) {
const prevRenderNode =
prevView ? renderNode(prevView, prevView.def.lastRenderRootNode!) : elementData.renderElement;
...
}
The solution might be reverting the dragged element back to original container so that we can let built-in ngForOf Angular directive to do its smart diffing.
Btw, the same technique is used in Angular material DragDropModule. It remembers position of dragging element and after we drop item it inserts it at its old position in the DOM which is IMPORTANT.

setting one line item 'active' at a time in vue

I have a small unordered list in a vue template:
<ul style="border-bottom:none !important; text-decoration:none">
<li class="commentToggle" v-bind:class="{active:commentActive}" v-on:click="setInputName('new')">New Comment</li>
<li class="commentToggle" v-bind:class="{active:commentActive}" v-on:click="setInputName('note')">Note</li>
</ul>
and I then have a data variable for commentActive and a function I call to set the input name:
data () {
return {
commentActive: false,
}
},
methods: {
setInputName(str) {
this.inputName = str;
this.commentActive = true;
},
}
And this is functional but it is obviously setting BOTH inputs to active when I click on one of them. How do I alter this to only set the clicked line item active?
You need to use a unique identifier to determine which comment is active. In the most rudimentary way using your current setup:
<li class="commentToggle"
v-bind:class="{active:commentActive === 'new'}"
v-on:click="setInputName('new')">
New Comment
</li>
<li class="commentToggle"
v-bind:class="{active:commentActive === 'note'}"
v-on:click="setInputName('note')">
Note
</li>
setInputName(str) {
this.inputName = str;
this.commentActive = str;
},
The adjustment that we made is to ensure that the commentActive matches the str that we've used. You can change that identifier to anything else, and pass additional arguments if you so choose.

How to disable all parent and childs if one parent is selected?

Here in first condition i was able to disbale all parents except current parent that is selected.
checkbox.js
if (geoLocation.id === 5657){
var getParent = geoLocation.parent();
$.each(geoLocation.parent(),function(index,location) {
if (location.id !== geoLocation.id) {
var disableItemId = 'disabled' + location.id;
// Get **strong text**the model
var model = $parse(disableItemId);
// Assigns a value to it
model.assign($scope, true);
}
}
);
At this point i am trying to disbale all the child for the parents that are disabled in above condition. How to achieve that task with below code any help will be appreciated.
So far tried code...
$.each(geoLocation.parent().items,function(index,location) {
if(location.id !== geoLocation.id){
var disableItemId = 'disabled' + location.children.data;
// Get the model
var model = $parse(disableItemId);
// Assigns a value to it
model.assign($scope, true);
}
});
console.log(getParent);
}
If you plan to use angular, better express all of this in a model structure, and use ng-click and ng-disabled to achieve what you need.
Template:
<ul>
<li ng-repeat="item in checkboxes">
<input type="checkbox" ng-disabled="item.disabled" ng-click="disableOthers(item)"> {{item.name}}
</li>
</ul>
Controller:
$scope.disableOthers = function (item) {
// iterate over parents and childs and mark disabled to true
};

Drag and drop sortable ng:repeats in AngularJS?

Is it at all easy to use jQuery.sortable on ng-repeat elements in AngularJS?
It would be awesome if re-ordering the items automatically propagated that ordering back into the source array. I'm afraid the two systems would fight though. Is there a better way to do this?
Angular UI has a sortable directive,Click Here for Demo
Code located at ui-sortable, usage:
<ul ui-sortable ng-model="items" ui-sortable-update="sorted">
<li ng-repeat="item in items track by $index" id="{{$index}}">{{ item }}</li>
</ul>
$scope.sorted = (event, ui) => { console.log(ui.item[0].getAttribute('id')) }
I tried to do the same and came up with the following solution:
angular.directive("my:sortable", function(expression, compiledElement){
return function(linkElement){
var scope = this;
linkElement.sortable(
{
placeholder: "ui-state-highlight",
opacity: 0.8,
update: function(event, ui) {
var model = scope.$tryEval(expression);
var newModel = [];
var items = [];
linkElement.children().each(function() {
var item = $(this);
// get old item index
var oldIndex = item.attr("ng:repeat-index");
if(oldIndex) {
// new model in new order
newModel.push(model[oldIndex]);
// items in original order
items[oldIndex] = item;
// and remove
item.detach();
}
});
// restore original dom order, so angular does not get confused
linkElement.append.apply(linkElement,items);
// clear old list
model.length = 0;
// add elements in new order
model.push.apply(model, newModel);
// presto
scope.$eval();
// Notify event handler
var onSortExpression = linkElement.attr("my:onsort");
if(onSortExpression) {
scope.$tryEval(onSortExpression, linkElement);
}
}
});
};
});
Used like this:
<ol id="todoList" my:sortable="todos" my:onsort="onSort()">
It seems to work fairly well. The trick is to undo the DOM manipulation made by sortable before updating the model, otherwise angular gets desynchronized from the DOM.
Notification of the changes works via the my:onsort expression which can call the controller methods.
I created a JsFiddle based on the angular todo tutorial to shows how it works: http://jsfiddle.net/M8YnR/180/
This is how I am doing it with angular v0.10.6. Here is the jsfiddle
angular.directive("my:sortable", function(expression, compiledElement){
// add my:sortable-index to children so we know the index in the model
compiledElement.children().attr("my:sortable-index","{{$index}}");
return function(linkElement){
var scope = this;
linkElement.sortable({
placeholder: "placeholder",
opacity: 0.8,
axis: "y",
update: function(event, ui) {
// get model
var model = scope.$apply(expression);
// remember its length
var modelLength = model.length;
// rember html nodes
var items = [];
// loop through items in new order
linkElement.children().each(function(index) {
var item = $(this);
// get old item index
var oldIndex = parseInt(item.attr("my:sortable-index"), 10);
// add item to the end of model
model.push(model[oldIndex]);
if(item.attr("my:sortable-index")) {
// items in original order to restore dom
items[oldIndex] = item;
// and remove item from dom
item.detach();
}
});
model.splice(0, modelLength);
// restore original dom order, so angular does not get confused
linkElement.append.apply(linkElement,items);
// notify angular of the change
scope.$digest();
}
});
};
});
Here's my implementation of sortable Angular.js directive without jquery.ui :
https://github.com/schartier/angular-sortable
you can go for ng-sortable directive which is lightweight and it does not uses jquery. here is link ng-sortable drag and drop elements
Demo for ng-sortable

Categories