I am trying to create a list which can be reordered by dragging the items in it.
When I drag an element for the first time dragstart.trigger="drag($event)" invokes the drag(e). In drag(e) I set data of the element dragged.
On dropping the dragged element drop.trigger="drop($event)" invokes drop(e).
In drop(e) I get the dragged element and remove it from the list/parent element <ul>.
After that I insert the dragged element to the dropped location.
The problem is once a element is dragged. I cannot able to drag it again to different target because the dragstart.trigger="drag($event)" is not invoking the drag(e).
How can I invoke dragstart.trigger="drag($event)"?
<ul id="columns" drop.trigger="drop($event)" dragover.trigger="allowDrop($event)">
<li id="item1" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>A</header></li>
<li id="item2" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>B</header></li>
<li id="item3" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>C</header></li>
<li id="item4" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>D</header></li>
<li id="item5" class="column" draggable="true" dragstart.trigger="drag($event)" dragend.trigger="dragend($event)"><header>E</header></li>
</ul>
JS :
drag(e) {
console.log('handleDragStart');
// Target element is the source node.
this.dragSrcEl = e.currentTarget;
console.log('dragSrcEl :', this.dragSrcEl);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.currentTarget.outerHTML);
e.currentTarget.classList.add('dragElem');
return true;
}
allowDrop(e) {
console.log('handleDragover');
e.preventDefault();
}
dragend() {
console.log('handleDragEnd');
}
drop(e) {
console.log('handleDrop');
if (e.stopPropagation) {
e.stopPropagation();
}
// Don't do anything if dropping the same column we're dragging.
if (this.dragSrcEl != e.srcElement) {
e.currentTarget.removeChild(this.dragSrcEl);
let dropHTML = e.dataTransfer.getData('text/html');
e.srcElement.parentNode.insertAdjacentHTML('beforebegin',dropHTML)
}
e.currentTarget.classList.remove('over');
return false;
}
The reason dragstart is not invoked after reordering the elements, is because you're not really reordering them. You're actually removing the dragged element and then inserting a new copy of it.
This new copy is not handled by aurelia's composition engine, therefore not compiled, and so any aurelia-specific expressions in the html will not do anything. .trigger is simply a dead tag at that point.
Drag/drop is kind of a special beast and has never been particularly simple to implement in a natural way, especially when there's all kind of custom framework behavior attached to these elements.
You have 3 options here:
Do not use aurelia's trigger and instead just use el.addEventListener both when you first create them, and then when you create new copies.
Use aurelia's ViewEngine to re-compile (parts of) your view whenever you drop an element so that .trigger is processed which, under the hood, really kind of just does el.addEventListener anyway
Turn this into custom element with a repeat.for and let Aurelia handle the html side of things.
Now option 1 would certainly be the quickest way to get it to work, and option 2 would be slightly more robust and tricky to do, but both are quite hacky.
I'm a strong advocate of utilizing the framework rather than hacking around it, because things will be easier to maintain on the longterm and you can more easily add additional fancy behavior as the project evolves.
It may seem much more involved than what you are doing now, but by engaging more of the framework to handle the low-level stuff, you'll have "living" draggable elements with a fully functional Aurelia that you can do much more things with.
So here's just one example of how you might approach option 3:
In app.js, make your columns into a list of javascript objects:
items = [
{ text: "A", id: "item1" },
{ text: "B", id: "item2" },
{ text: "C", id: "item3" },
{ text: "D", id: "item4" },
{ text: "E", id: "item5" }
];
In app.html, pass those items to the columns custom element (to keep the html similar to your example i'll use as-element)
<template>
<require from="./resources/elements/columns"></require>
<ul as-element="columns" items.bind="items"></ul>
</template>
In resources/elements/columns.js, work against individual items viewmodels instead of against the html elements:
import { customElement, children, bindable } from "aurelia-templating";
#customElement("columns")
export class Columns {
// keeps a list of the viewmodels of the direct "li" children
#children("li") children;
// the columns
#bindable() items;
// the currently dragged column
dragColumn;
// the customEvent we dispatch from the child "column" element
handleColDragStart(e) {
// the viewmodel we passed into the customEvent
this.dragColumn = e.detail.column;
}
allowDrop(e) {
console.log("handleDragover");
e.preventDefault();
}
drop(e) {
console.log("handleDrop");
if (e.stopPropagation) {
e.stopPropagation();
}
// source drag index
let dragIdx = this.children.indexOf(this.dragColumn);
// if we can't resolve to a sibling (e.g. dropped on or outside the list),
// naively drop it at index 0 instead
let dropIdx = 0;
// try to find the drop target
let dropTarget = e.srcElement;
while (dropTarget !== document.body) {
let dropTargetVm = dropTarget.au && dropTarget.au.controller && dropTarget.au.controller.viewModel;
if (dropTargetVm) {
dropIdx = this.children.indexOf(dropTargetVm);
break;
} else {
dropTarget = dropTarget.parentElement;
}
}
if (dragIdx !== dropIdx) {
// only modify the order in the array of javascript objects;
// the repeat.for will re-order the html for us
this.items.splice(dropIdx, 0, this.items.splice(dragIdx, 1)[0]);
}
return false;
}
}
In resources/elements/columns.html, just listen for the customEvent we dispatch from the column element and other than that only handle drop:
<template id="columns" drop.trigger="drop($event)" dragover.trigger="allowDrop($event)">
<require from="./column"></require>
<li as-element="column" repeat.for="col of items" column.bind="col" coldragstart.trigger="handleColDragStart($event)">
</li>
</template>
In resource/elements/column.js handle the dragstart and dragend events, then dispatch a customEvent with a reference to the viewModel (so you don't have to deal with the html too much):
import { customElement, bindable } from "aurelia-templating";
import { inject } from "aurelia-dependency-injection";
#customElement("column")
#inject(Element)
export class Column {
el;
constructor(el) {
this.el = el;
}
#bindable() column;
dragstart(e) {
this.el.dispatchEvent(
new CustomEvent("coldragstart", {
bubbles: true,
detail: {
column: this
}
})
);
return true;
}
}
Finally, in resources/elements/column.html just listen for the dragstart event:
<template draggable="true" dragstart.trigger="dragstart($event)">
<header>${column.text}</header>
</template>
The part of this solution that might look a bit strange to you, also the part that I still consider a bit hacky, is where we try to get the ViewModel via el.au.controller.viewModel.
This is something you "just need to know". A custom element / html behavior always has an au property on it that contains a reference to the behavior instance with the controller, view, etc.
This is essentially the easiest (and sometimes the only) way to "get a hold of" aurelia when working directly against the html. With things like drag/drop I don't believe there is any way to avoid this, as there is unfortunately no native aurelia support for it.
Related
I need help with angular drag and drop. It's like I need to drag an icon to a canvas.
I had gone through many examples and this is the example I have reached. when I drag that object the copy of the object should be moved. I had looked at many examples, please anyone help.
our "fields" are object with text,top and left. So, you can create a function
changePosition(event:CdkDragEnd<any>,field)
{
console.log(field)
field.top=+field.top.replace('px','')+event.distance.y+'px'
field.left=+field.left.replace('px','')+event.distance.x+'px'
}
And you call in the .html
<div *ngFor="let field of fields;" cdkDrag (cdkDragEnded)="changePosition($event,field)"
style="position:absolute;z-index:10" [style.top]="field.top" [style.left]="field.left">
{{field.text}}
</div>
Updated the problem, as Ananthakrishna let me know is that you can drag out of the "dop-zone" one element in drop zone
We need use the event cdkDragDropped
<div *ngFor="let field of fields;" cdkDrag
(cdkDragDropped)="changePosition($event,field)"
style="position:absolute;z-index:10"
[style.top]="field.top"
[style.left]="field.left">
{{field.text}}
</div>
And, in our function changePosition "check" if is droppend inside. I use getBoundingClientRect of the elements relateds:
changePosition(event:CdkDragDrop<any>,field)
{
const rectZone=this.dropZone.nativeElement.getBoundingClientRect()
const rectElement=event.item.element.nativeElement.getBoundingClientRect()
let top=+field.top.replace('px','')+event.distance.y
let left=+field.left.replace('px','')+event.distance.x
const out=top<0 || left<0 ||
(top>(rectZone.height-rectElement.height)) ||
(left>(rectZone.width-rectElement.width))
if (!out) //If is inside
{
field.top=top+'px'
field.left=left+'px'
}
else{ //we can do nothing
this.fields=this.fields.filter(x=>x!=field) //or eliminate the object
}
}
See the forked stackblitz
It's very easy to achieve your goal with ng-dnd. You can check the examples and have a try.
Making a DOM element draggable
<div [dragSource]="source">
drag me
</div>
constructor(private dnd: DndService) { }
source = this.dnd.dragSource("DRAGME", {
beginDrag: () => ({ name: 'Jones McFly' }),
// other DragSourceSpec methods
});
Making a DOM element into a drop target
<div [dropTarget]="target">
drop on me
</div>
constructor(private dnd: DndService) { }
target = this.dnd.dropTarget("DRAGME", {
drop: monitor => {
console.log('dropped an item:', monitor.getItem()); // => { name: 'Jones McFly' }
}
})
I've got a CMS-like feature that has an article with multiple particles (called blocks). A particle can be a either a rich text field or a table. Based on the Block's discr attribute, a Quill or Handsontable instance should be initiated.
This works perfectly, until I reorder the blocks. When I've got a Quill instance and a Handsontable instance, after reordering them, the Quill gets a context menu from the Handsontable and the Quill instance gets a toolbar.
I'm new to Vue.js, but I already understand that happens. I've read List Rendering Caveats and Why isn’t the DOM updating?. The two div.chapterblock elements don't get reordered (like a jQuery-like application probably would do), but only their content changes. When I use the inspector, I see the .chapterblock#id and it's content changing, not moving. The (Quill/Handsontable/whatever) instance is bound to a specific DOM element and stays bound to the element, even if it changes.
But what I don't (yet) understand is how to solve the problem. How can I reorder items and keep the Quill/Handsontable instance on the right elements? Destroying and re-initializing the instances doesn't feel right.
My template:
<div class="chapterblock" v-for="(block, index) in blocks" v-bind:data-id="block.id">
<template v-if="block.discr == 'html'">
<div class="quill" v-html="block.content"></div>
</template>
<template v-if="block.discr == 'table'">
<script type="application/json" v-html="block.content"></script>
<div v-bind:id="'handsontable_' + block.id" class="handsontable-wrapper"></div>
</template>
<button v-if="index !== 0" v-on:click="move(block, 'up')">up</button>
<button v-if="index !== 1" v-on:click="move(block, 'down')">down</button>
</div>
Vue instance:
return new Vue({
//...
computed: {
blocks: function () {
return this.chapter.blocks.sort(function compare (a, b) {
if (a.position < b.position) {
return -1
}
if (a.position > b.position) {
return 1
}
return 0
})
}
},
methods: {
move: function (block, direction) {
if (direction === 'up') {
block.position = block.position - 1
} else if (direction === 'down') {
block.position = block.position + 1
}
// fetch to save position
}
}
Use the key attribute on the v-for loop to reorder the elements, instead of replacing their contents:
From https://v2.vuejs.org/v2/guide/list.html#key:
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item.
I would like to use a javascript loop to create multiple HTML wrapper elements and insert JSON response API data into some of the elements (image, title, url, etc...).
Is this something I need to go line-by-line with?
<a class="scoreboard-video-outer-link" href="">
<div class="scoreboard-video--wrapper">
<div class="scoreboard-video--thumbnail">
<img src="http://via.placeholder.com/350x150">
</div>
<div class="scoreboard-video--info">
<div class="scoreboard-video--title">Pelicans # Bulls Postgame: E'Twaun Moore 10-8-17</div>
</div>
</div>
</a>
What I am trying:
var link = document.createElement('a');
document.getElementsByTagName("a")[0].setAttribute("class", "scoreboard-video-outer-link");
document.getElementsByTagName("a")[0].setAttribute("url", "google.com");
mainWrapper.appendChild(link);
var videoWrapper= document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video-outer-link");
link.appendChild(videoWrapper);
var videoThumbnailWrapper = document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video--thumbnail");
videoWrapper.appendChild(videoThumbnailWrapper);
var videoImage = document.createElement('img');
document.getElementsByTagName("img")[0].setAttribute("src", "url-of-image-from-api");
videoThumbnailWrapper.appendChild(videoImage);
Then I basically repeat that process for all nested HTML elements.
Create A-tag
Create class and href attributes for A-tag
Append class name and url to attributes
Append A-tag to main wrapper
Create DIV
Create class attributes for DIV
Append DIV to newly appended A-tag
I'd greatly appreciate it if you could enlighten me on the best way to do what I'm trying to explain here? Seems like it would get very messy.
Here's my answer. It's notated. In order to see the effects in the snippet you'll have to go into your developers console to either inspect the wrapper element or look at your developers console log.
We basically create some helper methods to easily create elements and append them to the DOM - it's really not as hard as it seems. This should also leave you in an easy place to append JSON retrieved Objects as properties to your elements!
Here's a Basic Version to give you the gist of what's happening and how to use it
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com" });
let div = create("div", { id: "myDiv" });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com", textContent: "this text is a Link in the div" });
let div = create("div", { id: "myDiv", textContent: "this text is in the div! " });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
div {
border: 3px solid black;
padding: 5px;
}
<div id="mainWrapper"></div>
Here is how to do specifically what you asked with more thoroughly notated code.
//get main wrapper
let mainWrapper = document.getElementById("mainWrapper");
//make a function to easily create elements
//function takes a tagName and an optional object for property values
//using Object.assign we can make tailored elements quickly.
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//document.appendChild is great except
//it doesn't offer easy stackability
//The reason for this is that it always returns the appended child element
//we create a function that appends from Parent to Child
//and returns the compiled element(The Parent).
//Since we are ALWAYS returning the parent(regardles of if the child is specified)
//we can recursively call this function to great effect
//(you'll see this further down)
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//these are the elements you wanted to append
//notice how easy it is to make them!
//FYI when adding classes directly to an HTMLElement
//the property to assign a value to is className -- NOT class
//this is a common mistake, so no big deal!
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
//here's where the recursion comes in:
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//keep in mind that it might be easiest to read the ac functions backwards
//the logic is this:
//Append videoImage to videoThumbnailWrapper
//Append (videoImage+videoThumbnailWrapper) to videoWrapper
//Append (videoWrapper+videoImage+videoThumbnailWrapper) to link
//Append (link+videoWrapper+videoImage+videoThumbnailWrapper) to mainWrapper
let mainWrapper = document.getElementById('mainWrapper');
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//pretty fancy.
//This is just to show the output in the log,
//feel free to just open up the developer console and look at the mainWrapper element.
console.dir(mainWrapper);
<div id="mainWrapper"></div>
Short version
Markup.js's loops.
Long version
You will find many solutions that work for this problem. But that may not be the point. The point is: is it right? And you may using the wrong tool for the problem.
I've worked with code that did similar things. I did not write it, but I had to work with it. You'll find that code like that quickly becomes very difficult to manage. You may think: "Oh, but I know what it's supposed to do. Once it's done, I won't change it."
Code falls into two categories:
Code you stop using and you therefore don't need to change.
Code you keep using and therefore that you will need to change.
So, "does it work?" is not the right question. There are many questions, but some of them are: "Will I be able to maintain this? Is it easy to read? If I change one part, does it only change the part I need to change or does it also change something else I don't mean to change?"
What I'm getting at here is that you should use a templating library. There are many for JavaScript.
In general, you should use a whole JavaScript application framework. There are three main ones nowadays:
ReactJS
Vue.js
Angular 2
For the sake of honesty, note I don't follow my own advice and still use Angular. (The original, not Angular 2.) But this is a steep learning curve. There are a lot of libraries that also include templating abilities.
But you've obviously got a whole project already set up and you want to just plug in a template into existing JavaScript code. You probably want a template language that does its thing and stays out of the way. When I started, I wanted that too. I used Markup.js . It's small, it's simple and it does what you want in this post.
https://github.com/adammark/Markup.js/
It's a first step. I think its loops feature are what you need. Start with that and work your way to a full framework in time.
Take a look at this - [underscore._template]
It is very tiny, and useful in this situation.
(https://www.npmjs.com/package/underscore.template).
const targetElement = document.querySelector('#target')
// Define your template
const template = UnderscoreTemplate(
'<a class="<%- link.className %>" href="<%- link.url %>">\
<div class="<%- wrapper.className %>">\
<div class="<%- thumbnail.className %>">\
<img src="<%- thumbnail.image %>">\
</div>\
<div class="<%- info.className %>">\
<div class="<%- info.title.className %>"><%- info.title.text %></div>\
</div>\
</div>\
</a>');
// Define values for template
const obj = {
link: {
className: 'scoreboard-video-outer-link',
url: '#someurl'
},
wrapper: {
className: 'scoreboard-video--wrapper'
},
thumbnail: {
className: 'scoreboard-video--thumbnail',
image: 'http://via.placeholder.com/350x150'
},
info: {
className: 'scoreboard-video--info',
title: {
className: 'scoreboard-video--title',
text: 'Pelicans # Bulls Postgame: E`Twaun Moore 10-8-17'
}
}
};
// Build template, and set innerHTML to output element.
targetElement.innerHTML = template(obj)
// And of course you can go into forEach loop here like
const arr = [obj, obj, obj]; // Create array from our object
arr.forEach(item => targetElement.innerHTML += template(item))
<script src="https://unpkg.com/underscore.template#0.1.7/dist/underscore.template.js"></script>
<div id="target">qq</div>
I have deferred updates enabled.
I have two components.
The first is a list, which is simply implemented as a div with a foreach data binding:
<div class="list-people" data-bind="foreach: { data: people, afterRender: afterRenderPeople }">
<!-- ko component: { name: "listitem-person", params: { person: $data } } --><!-- /ko -->
</div>
The second is the list item:
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
afterRender is called for each item in the foreach.
My afterRenderPerson function is simple enough:
public afterRenderPerson = (elements: any[], data: Person) => {
let top = $(element[0]).offset().top;
scrollTo(top);
};
The problem is that when afterRenderPerson is called the sub-component listitem-person hasn't yet been rendered.
Which means the element array passed to afterRenderPerson has 4 nodes:
A text node containing \n i.e. a new line.
A comment node containing <!-- ko component: { name: "listitem-person", params: { person: $data } } -->.
A comment node containing <!-- /ko -->.
A text node containing \n i.e. a new line.
None of these are suitable for getting the top pixel, and even if they were, the sub-component being rendered could affect the layout at that location changing the value of the pixel I'm trying to get.
Unfortunately it seems that the documentation for foreach doesn't take in to account the delayed nature of components.
If you need to run some further custom logic on the generated DOM elements, you can use any of the afterRender/afterAdd/beforeRemove/beforeMove/afterMove callbacks described below.
Note: These callbacks are only intended for triggering animations related to changes in a list.
There are two workarounds I've come across, neither of which are great, but that's why they're workarounds and not solutions!
user3297291 gave the suggestion in a comment of making a scrollTo binding that's placed on the child components.
Only workaround I can think of is to define a custom scrollTo binding and include it in the component template... Quite easy to implement, but still feels hacky and makes your inner component harder to reuse. You might also want to track this feature request – user3297291
This would simply be a custom binding that conditionally executes some code based on a value provided to it.
The bindings aren't called until the HTML has been inserted in to the DOM. That's not perfect, as later changes to the DOM could affect the position of the inserted HTML elements, but it should work for many situations.
I wasn't very keen on having to modify the child components though, I preferred a solution when remained encapsulated in the parent component.
The second workaround is to check to see if the child component HTML element exists in the DOM by it's ID. Since I don't know when they will come in to existence this has to be done in some sort of loop.
A while loop isn't suitable as it'll run the check far too often, in a "tight" loop, so instead setTimeout is used.
setTimeout is a horrid hack, and it makes me feel dirty to use it, but it does work for this situation.
private _scrollToOffset = -100;
private _detectScrollToDelayInMS = 200;
private _detectScrollToCountMax = 40;
private _detectScrollToCount = 0;
private _detectScrollTo = (scrollToContainerSelector: string, scrollToChildSelector: string) => {
//AJ: If we've tried too many times then give up.
if (this._detectScrollToCount >= this._detectScrollToCountMax)
return;
setTimeout(() => {
let foundElements = $(scrollToChildSelector);
if (foundElements.length > 0) {
//AJ: Scroll to it
$(scrollToContainerSelector).animate({ scrollTop: foundElements.offset().top + this._scrollToOffset });
//AJ: Give it a highlight
foundElements.addClass("highlight");
} else {
//AJ: Try again
this._detectScrollTo(scrollToContainerSelector, scrollToChildSelector);
}
}, this._detectScrollToDelayInMS);
this._detectScrollToCount++;
};
I made sure to put a limit on how long it can run for, so if something goes wrong it won't loop forever.
It should probably be noted that there is an "Ultimate" solution to this problem, and that's TKO, AKA Knockout 4.
But that's not "production ready" yet.
How to know when a component has finished updating DOM?
brianmhunt commented on Jun 20
knockout/tko (ko 4 candidate) latest master branch has this.
More specifically, the applyBindings family of functions now return a Promise that resolves when sub-children (including asynchronous ones) are bound.
The API isn't set or documented yet, but the bones have been set up.
This appears to work. I made a binding handler that runs a callback in its init (it uses tasks.schedule to allow a rendering cycle). Attaching it at the parent level does not get the children rendered in time, but attaching it to the virtual element does.
I designed it to work with a function whose signature is like afterRender. Because it runs for each of the elements, the callback function has to test that the data is for the first one of them.
ko.options.deferUpdates = true;
ko.bindingHandlers.notify = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make it asynchronous, to allow Knockout to render the child component
ko.tasks.schedule(() => {
const onMounted = valueAccessor().onMounted;
const data = valueAccessor().data;
const elements = [];
// Collect the real DOM nodes (ones with a tagName)
for(let child=ko.virtualElements.firstChild(element);
child;
child=ko.virtualElements.nextSibling(child)) {
if (child.tagName) { elements.push(child); }
}
onMounted(elements, data);
});
}
};
ko.virtualElements.allowedBindings.notify = true;
function ParentVM(params) {
this.people = params.people;
this.afterRenderPeople = (elements, data) => {
console.log("Elements:", elements.map(e => e.tagName));
if (data === this.people[0]) {
console.log("Scroll to", elements[0].outerHTML);
//let top = $(element[0]).offset().top;
//scrollTo(top);
}
};
}
ko.components.register('parent-component', {
viewModel: ParentVM,
template: {
element: 'parent-template'
}
});
function ChildVM(params) {
this.Name = params.person;
}
ko.components.register('listitem-person', {
viewModel: ChildVM,
template: {
element: 'child-template'
}
});
vm = {
names: ['One', 'Two', 'Three']
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<template id="parent-template">
<div class="list-people" data-bind="foreach: people">
<!-- ko component: { name: "listitem-person", params: { person: $data } }, notify: {onMounted: $parent.afterRenderPeople, data: $data} -->
<!-- /ko -->
</div>
</template>
<template id="child-template">
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
</template>
<parent-component params="{ people: names }">
</parent-component>
I've been using an implementation of this Drag and Drop with AngularJS and jQuery UI:
http://www.smartjava.org/examples/dnd/double.html
With AngularJS 1.0.8 it works flawlessly. With 1.2.11, it doesn't.
When using AngularJS 1.2 and dragging an item from the left list to the right one the model for the destination list updates correctly. However the DOM doesn't update correctly. Here is the directive that's being used from the example:
app.directive('dndBetweenList', function($parse) {
return function(scope, element, attrs) {
// contains the args for this component
var args = attrs.dndBetweenList.split(',');
// contains the args for the target
var targetArgs = $('#'+args[1]).attr('dnd-between-list').split(',');
// variables used for dnd
var toUpdate;
var target;
var startIndex = -1;
// watch the model, so we always know what element
// is at a specific position
scope.$watch(args[0], function(value) {
toUpdate = value;
},true);
// also watch for changes in the target list
scope.$watch(targetArgs[0], function(value) {
target = value;
},true);
// use jquery to make the element sortable (dnd). This is called
// when the element is rendered
$(element[0]).sortable({
items:'li',
start:function (event, ui) {
// on start we define where the item is dragged from
startIndex = ($(ui.item).index());
},
stop:function (event, ui) {
var newParent = ui.item[0].parentNode.id;
// on stop we determine the new index of the
// item and store it there
var newIndex = ($(ui.item).index());
var toMove = toUpdate[startIndex];
// we need to remove him from the configured model
toUpdate.splice(startIndex,1);
if (newParent == args[1]) {
// and add it to the linked list
target.splice(newIndex,0,toMove);
} else {
toUpdate.splice(newIndex,0,toMove);
}
// we move items in the array, if we want
// to trigger an update in angular use $apply()
// since we're outside angulars lifecycle
scope.$apply(targetArgs[0]);
scope.$apply(args[0]);
},
connectWith:'#'+args[1]
})
}
});
Does something need to be updated for this to work properly with Angular 1.2? I feel like it has something to do with the scope.$apply but am not sure.
I see this is an older question, but I recently ran into the exact same issue with the Drag and Drop example. I don’t know what has changed between angular 1.0.8 and 1.2, but it appears to be the digest cycle that causes problems with the DOM. scope.$apply will trigger a digest cycle, but scope.$apply in and of itself is not the issue. Anything that causes a cycle can cause the DOM t get out of sync with the model.
I was able to find a solution to the the problem using the ui.sortable directive. The specific branch that I used is here: https://github.com/angular-ui/ui-sortable/tree/angular1.2. I have not tested with other branches.
You can view a working example here:
http://plnkr.co/edit/atoDX2TqZT654dEicqeS?p=preview
Using the ui-sortable solution, the ‘dndBetweenList’ directive gets replaced with the ui-sortable directive. Then there are a few changes to make.
In the HTML
<div class="row">
<div class="span4 offset2">
<ul ui-sortable="sortableOptions" ng-model="source" id="sourceList" ng-class="{'minimalList':sourceEmpty()}" class="connector">
<li class="alert alert-danger nomargin" ng-repeat="item in source">{{item.value}}</li>
</ul>
</div>
<div class="span4">
<ul ui-sortable="sortableOptions" id="targetList" ng-model="model" ng-class="{'minimalList':sourceEmpty()}" class="connector">
<li class="alert alert-info nomargin" ng-repeat="item in model">{{item.value}}</li>
</ul>
</div>
</div>
Note the dnd-between-list directive is no longer needed and is replaced with the ui-sortable.
In the module inject the ui-sortable, and in the controller specify that sortable options. The sortable accepts the same options as the jquery sortable.
app.js
var app = angular.module('dnd', ['ui.sortable']);
ctrl-dnd.js
$scope.sortableOptions = {
connectWith: '.connector'
}
Only the additions to the controller are shown. Note that I added a .connector class on the ul. In the sortable I use .connector for the connectWith option.