I've got a template to manage important property in my collection. It's a simple list filtered by this property with a toggle that allows changing its value:
<template name="list">
{{#each items}}
<div class="box" data-id="{{_id}}">
{{name}}
<span class="toggle">Toggle</span>
</div>
{{/each}}
</template>
Template.list.items = function() {
return Items.find({property: true}, {sort: {name: 1}});
};
Template.list.events({
'click .toggle': function(e) {
var item = Items.findOne( $(e.target).closest('.box').data('id') );
Items.update(item._id, {$set: {
property: !item.property;
}});
},
});
Quite simple. Now, obviously, when I click the toggle for an item, this item property turns to false and it's removed from the list. However, to enable easy undo, I'd like the item to stay in the list. Ideally, until user leaves the page, but a postponed fadeout is acceptable.
I don't want to block the reactivity completely, new items should appear on the list, and in case of name change it should be updated. All I want is for the removed items to stay for a while.
Is there an easy way to achieve this?
I would store the removed items in a new array, and do something like this:
var removed = []; // Contains removed items.
Template.list.created = function() {
// Make it empty in the beginning (if the template is used many times sequentially).
removed = []
};
Template.list.items = function() {
// Get items from the collection.
var items = Items.find({property: true}).fetch();
// Add the removed items to it.
items = items.concat(removed)
// Do the sorting.
items.sort(function(a, b){ return a.name < b.name}) // May be wrong sorter, but any way.
return items
};
Template.list.events({
'click .toggle': function(e) {
var item = Items.findOne( $(e.target).closest('.box').data('id') );
Items.update(item._id, {$set: {
property: !item.property;
}});
// Save the removed item in the removed list.
item.property = !item.property
item.deleted = true // To distinguish them from the none deleted ones.
removed.push(item)
},
});
That should work for you, wouldn't it? But there may be a better solution.
Related
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.
I have a nested template, using a ReactiveDict to store the data, which is an object that includes variables (color, type...) and an array of children nodes.
I'm having an issue on refresh: the array displays reactively, but when I update the array, it does not properly render.
in short (cleaned up code):
<body>
{{#with data}}
{{>nested}}
{{/with}}
</body>
<template name="nested">
<div>{{color}}<div>
<div class="ui dropdown">
<!-- drop down stuff goes here-->
</div>
{{#if children}}
{{#each children}}
{{>nested scope=this}}
{{/each}}
{{/if}}
</template>
Template.body.helpers({
"data": { color: "blue",
children: [{color: "green", children: [{color: "teal"}]},
{color:"red", children:[{color: "cyan"}],{color: "magenta"}]]}}
})
Template.nested.onCreated(function(){
this.scope = new ReactiveDict();
this.scope.set('scope', this.data.scope);
})
Template.nested.helpers({
"color": function () { Template.instance().scope.get('scope').color;},
"children": function () {
return Template.instance().scope.get('scope').children;
}
})
Template.nested.events({
"click .ui.dropdown > .menu > .item": function(e, t) {
e.preventDefault();
e.stopPropagation();
var data = t.scope.get('scope');
//do processing stuff here...
updatedArray = myFunction();
data['children'] = updatedArray;
t.scope.set('scope', data);
}
})
So what's happening is that on update the elements alreayd present do not update, and if there are elements added they show up.
if there are elements removed, they elements will be removed but the data in the variables (color here) does not get updated.
To make it work so far, I had to do the following:
Template.nested.events({
"click .ui.dropdown > .menu > .item": function(e, t) {
e.preventDefault();
e.stopPropagation();
var data = t.scope.get('scope');
//do processing stuff here...
updatedArray = myFunction();
delete data.children;
t.scope.set('scope', data);
Meteor.setTimeout(function() {
data['children'] = updatedArray;
t.scope.set('scope', data);
},10);
}
})
That works but it's a total hack forcing the array to nothing and then refreshing after a short timeout.
How am I supposed to do this the proper way?
PS: I tried using allDeps.changed() on the ReactiveDict, and i tried forcing a re-render but it's in the render loop so it won't render the view twice.
Can't seem to understand why the array elements are not updated. I know when using collections MiniMongo checks for _id's of the objects to see if they changed or not, but there are no _id in my objects. I also tried to add one but without much luck
well I guess I asked just before figuring it out...
the '_id' thing did the trick. I had tried before but I was actually using the same _id for the same elements so they did not appear to change (duh!)
So by adding a { "_id": Meteor.uuid() } in my generated objects, the update works fine.
After trying out a number of methods, I have searched high and low for a good solution to a seemingly easy problem.
I have a sortable list of chapters and I'm using averaging to make jquery sortable work for a meteor application. The dragging and dropping sortable part is easy (using averaging), but applying the correct (whole number) order of the chapters has been tricky.
A common application would be chapter numbers applied to a table of contents.
What I've tried:
Rewriting the stop: function using a for loop that accounts for the different scenarios. Drag to front, drag to rear
This can get complex as the numbers of chapters increase. I felt it was too complex a solution.
Using jQuery to identify the order of the items and apply the numbers according to their sorted order in the browser. I think this will work, but I haven't been able to figure out what jquery functions to use and where after trying several out. Disclaimer: I'm new to Spacebars and haven't used much jquery.
chaptersList html:
<template name="chaptersList">
<div id="items">
{{#each publishedChapters}}
{{> chapterItem}}
{{/each}}
</div><!--items-->
</template>
chaptersList js:
Template.chaptersList.rendered = function() {
this.$('#items').sortable({
stop: function(e, ui) {
// get the dragged html element and the one before
// and after it
el = ui.item.get(0)
before = ui.item.prev().get(0)
after = ui.item.next().get(0)
if(!before) {
//if it was dragged into the first position grab the
// next element's data context and subtract one from the order
newOrder = 1;
} else if(!after) {
//if it was dragged into the last position grab the
// previous element's data context and add one to the order
newOrder = Blaze.getData(before).order + 1
}
else
//else take the average of the two orders of the previous
// and next elements
newOrder = (Blaze.getData(after).order +
Blaze.getData(before).order)/2
newOrder = Math.round(newOrder)
//update the Items' orders
Chapters.update({_id: Blaze.getData(el)._id}, {$set: {order: newOrder}})
}
})
}
Template.chaptersList.helpers({
publishedChapters: function() {
return Chapters.find({ published: true },
{ sort: {order: 1}
});
},
items: function() {
return Chapters.find({}, {
sort: {order: 1 }
})
}
});
chapterItem html:
<template name="chapterItem">
<div class="item">
<h3 class="headroom-10 chapter-title-small">
{{title}}
</h3>
<p class="chapter-text">{{#markdown}}{{chapterTease}}{{/markdown}}</p>
{{#if ownChapter}}
Edit
<span class="delete">
Delete
</span>
{{/if}}
</div>
</template>
Thank you for your valuable insight.
I went through this same issue and the trick to get it working cleanly in my case was to cancel the sortable action from inside the sort and let Blaze take over once the item was dropped. Otherwise the sortable ordering and Blaze ordering end up wrestling one another.
I save my new set of ordered items as a single batch, but that's not necessarily required.
Template.builder.rendered = function(){
var self = this;
this.$("ol.sortable").sortable({
axis: "y",
containment: "ol.sortable",
tolerance: "pointer",
update: function(event, ui){
var
items = $(this).find("li"),
picks = _.map(items, function(item){
var obj = Blaze.getData(item);
obj.position = $(item).index() + 1;
return obj;
});
self.$('ol.sortable').sortable('cancel');
Meteor.call(
"orderPicks",
team,
picks,
function(error, result){}
);
}
});
}
And on the server:
Meteor.methods({
orderPicks: function(team, picks){
Teams.update(team,
{ $set: {picks: picks} }
);
return true;
}
});
I believe I picked this up from another SO thread; I can't remember. In any case it works seamlessly for a modestly sized set of items.
HTML:
<template name="builder">
<ol class="builder sortable">
{{#each team.picks}}
<li>{{position}}. {{name}}</li>
{{/each}}
</ol>
</template>
This answer is a follow up to the conversation with #jeremy above, adapting a nested array approach to individual documents.
It is not currently working, but by posting it here, I hope to be able to gain insight, get it working and offer another solution.
Right now, the sorting order is not changing when an item is dragged and dropped. If you can see why, let me know or you can also edit the answer:
Add id to li:
chapterId: function() {
return Chapters.findOne(this._id)._id
}
HTML:
<li class="item headroom-20" id="{{chapterId}}">
Sortable JS:
Template.chaptersList.rendered = function(){
var self = this;
this.$("ol.items").sortable({
axis: "y",
containment: "ol.items",
tolerance: "pointer",
update: function(event, ui){
var items = $(".items").find("li");
var publishedChaptersCount = items.length;
var sortedIds = [];
for (var i = 0; i < publishedChaptersCount; i++) {
var obj = $(items[i]).attr('id');
sortedIds.push(obj);
}
self.$('ol.items').sortable('cancel');
Meteor.call(
"chapterOrderUpdate",
sortedIds,
function(error, result){}
);
}
});
}
Meteor Method:
Meteor.methods({
chapterOrderUpdate: function(sortedIds){
var publishedChaptersCount = sortedIds.length;
for (var i = 0; i < publishedChaptersCount; i++) {
var chapterId = sortedIds[i];
Chapters.update({_id: chapterId}, { $set: { order: i+1 } })
}
}
});
In a basic table structure, I want to be able to display a set of data from an array of objects one at a time. Clicking on a button or something similar would display the next object in the array.
The trick is, I don't want to use the visible tag and just hide the extra data.
simply you can just specify property that indicate the current element you want to display and index of that element inside your observableArray .. i have made simple demo check it out.
<div id="persons"> <span data-bind="text: selectedPerson().name"></span>
<br/>
<button data-bind="click: showNext" id="btnShowNext">Show Next</button>
<br/>
</div>
//here is the JS code
function ViewModel() {
people = ko.observableArray([{
name: "Bungle"
}, {
name: "George"
}, {
name: "Zippy"
}]);
showNext = function (person) {
selectedIndex(selectedIndex() + 1);
};
selectedIndex = ko.observable(0);
selectedPerson = ko.computed(function () {
return people()[selectedIndex()];
});
}
ko.applyBindings(new ViewModel());
kindly check this jsfiddle
Create observable property for a single object, then when clicking next just set that property to other object and UI will be updated.
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