I am trying to implement a custom drag and drop directive. It works, but it is extremely slow, and I think the slowness can be tracked to Angular 2 because I've never encountered this slowness before. The slowness only occurs when I attach an event listener to the dragover or drag events (i.e. the events which are sent frequently), even if I do nothing but return false in them.
Here's my directive code:
import {Directive, ElementRef, Inject, Injectable} from 'angular2/core';
declare var jQuery: any;
declare var document: any;
#Directive({
selector: '.my-log',
host: {
'(dragstart)': 'onDragStart($event)',
'(dragover)': 'onDragOver($event)',
'(dragleave)': 'onDragLeave($event)',
'(dragenter)': 'onDragEnter($event)',
'(drop)': 'onDrop($event)',
}
})
#Injectable()
export class DraggableDirective {
refcount = 0;
jel;
constructor( #Inject(ElementRef) private el: ElementRef) {
el.nativeElement.setAttribute('draggable', 'true');
this.jel = jQuery(el.nativeElement);
}
onDragStart(ev) {
ev.dataTransfer.setData('Text', ev.target.id);
}
onDragOver(ev) {
return false;
}
onDragEnter(ev) {
if (this.refcount === 0) {
this.jel.addClass('my-dragging-over');
}
this.refcount++;
}
onDragLeave(ev) {
this.refcount--;
if (this.refcount === 0) {
this.jel.removeClass('my-dragging-over');
}
}
onDrop(ev) {
this.jel.removeClass('my-dragging-over');
this.refcount = 0;
}
}
Here's the relevant style sheet excerpt:
.my-log.my-dragging-over {
background-color: yellow;
}
As you can see all I'm doing is highlighting the element being dragged over in yellow. And it works fast when I don't handle the dragover event, however I must handle it to support dropping. When I do handle the dragover event, everything slows down to unbearable levels!!
EDIT I am using angular beta 2.0.0-beta.8
EDIT #2 I tried profiling the code using chrome's profiler, these are the results:
Look at the marked line, it is strangely suspicious...
EDIT #3 Found the problem: it was indeed due to Angular 2's change detection. The drag and drop operation in my case is done on a very dense page with a lot of bindings and directives. When I commented out everything except the given list, it worked fast again... Now I need your help in finding a solution to this!
Just went through some trouble with the same problem. Even with efficient ngFor code, drag and drop can still be crazy slow if you have a large number of draggable items.
The trick for me was to make all drag and drop event listeners run outside of Angular with ngZone, then make it run back in Angular when dropped. This makes Angular avoid checking for detection for every pixel you move the draggable item around.
Inject:
import { Directive, ElementRef, NgZone } from '#angular/core';
constructor(private el: ElementRef, private ngZone: NgZone) {}
Initializing:
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
el.addEventListener('dragenter', (e) => {
// do stuff with e or el
});
...
On drop:
el.addEventListener('drop', (e) => {
this.ngZone.run(() => {
console.log("dropped");
})
})
Thanks to everybody for this discussion.
End up with simple solution which works like a charm:
constructor(private cd: ChangeDetectorRef) {
}
drag(event: DragEvent): void {
this.cd.detach();
// Begin the job (use event.dataTransfer)
}
allowDrop(event: DragEvent): void {
event.preventDefault();
}
drop(event: DragEvent): void {
event.preventDefault();
this.cd.reattach();
// Do the job
}
Answering my own question (problem was solved).
The slowness problem was due to inefficient data bindings in my markup, which caused Angular to waste a lot of time calling functions on my view model. I had many bindings of this sort:
*ngFor="#a of someFunc()"
This caused Angular to be unsure whether data has changed or not, and the function someFunc was getting called again and again after every run of onDragOver (which is a about once every 350ms) even though data was not changing during the drag and drop process. I changed these bindings to refer to simple properties in my class, and moved the code that populates them where it was supposed to be. Everything started moving lightning fast again!
I had a similar issue recently. It was in an Angular 6 environment using reactive forms. This is how I solved it for my situation:
Basically and briefly, I turned off change detection on that component while dragging was taking place.
import ChangeDetectorRef:
import { ChangeDetectorRef } from '#angular/core';
inject it into the constructor:
constructor(private chngDetRef: ChangeDetectorRef) { //...
detach it on dragStart:
private onDragStart(event, dragSource, dragIndex) {
// ...
this.chngDetRef.detach();
// ...
reattach it on drop and dragEnd:
private onDrop(event, dragSource, dragIndex) {
// ...
this.chngDetRef.reattach();
// ...
private onDragEnd(event, dragIndex) {
// ...
this.chngDetRef.reattach();
// ...
If you have a lot of parent or layered components, you may have to do something about their change detection as well in order to see a substantial improvement.
This is a follow up to an old post, but drag and drop is "still an issue. My particular problem involved a page with over 130 components on it and drag and drop was abysmal. I tried the various suggestions offered in this and other posts with only minimal improvement.
Finally, I decided that rather than the ngZone solution, I would try changing (dragOver)="function()" to the native ondragover="event.preventDefault()". I let all the other event handlers (i.e. dragStart, dragEnter, dragLeave, dragDrop, dragEnd) go through Angular as was needed. My drag and drop response went from seconds to milliseconds.
It would be great anyone could provide an alternative dragOver event handler that bypasses change detection.
I had a similar issue, also my drag and drop became very slow when I did put multiple drag zones inside a *ngFor.
I solved this by changing the change detection strategy to OnPush of the child component.
Then on every time when an item get dragged, do markForCheck().
constructor(private changeDetectorRef: ChangeDetectorRef) {}
// Callback function
public onDrag() {
this.changeDetectorRef.markForCheck();
}
Issue for me was that Development mode was turned on even in production. When i compiled it with ng build --evn-prod drag and drop is suddenly blazing fast.
I had the same problem with drag & drop in my angular project - detectChanges(reattach(), deTached ..), outSide Angular (ngZone) couldn't solve this problem.
Now I solved this problem by using jquery , I bond events in constructor for my div content.
constructor() {
$(document).delegate('#jsDragZone', 'dragenter', function (e) {
console.log('here your logic')
});
}
in this way you can implement other events too (dragleave, drop, 'dragover'). It's worked very nice and fast for me.
Related
I've been trying to dynamically insert data from Firestore into my component.
Currently, I'm using the firstUpdated() lifecycle. My code works but it fell like there's a better way of doing this.
This is my current component.
static get properties() {
return {
firebaseData: {type:Object},
}
}
constructor() {
super()
this.firebaseData = {}
}
firstUpdated() {
firestore.doc(`...`).get()
.then(doc => {this.firebaseData = doc.data()})
})
.catch(err => console.error(err))
}
render() {
return html `${firebaseData.title}`
}
I was hope someone with more experience would be open to sharing their knowledge. Thanks in advance!
firstUpdated should be used when you need to interact with shadow DOM elements inside your web component, as they aren't created until then. It's the earliest moment when you can be sure your component DOM exists.
I would prefer to do the firebase call earlier, even in the constructor.
The idea is, your firebase call isn't dependent of the rendering, so you could directly do it at the earliest moment, and as in the callback of the function you update the firebaseData property, a new rendering cycle will be done then.
I'm searching for a way to perform a drag and drop with serenity-js (http://serenity-js.org/) but I can't find any examples. What I can find is the protractor way, but because protractor is baked in the serenity-js framework I was wondering how to get this working. (Sorry I'm a novice in TS and javascript)
UPDATE:
I implemented the HTML5 workaround from Jan Molak but I got the following error message:
And this is my Task implementation:
import { Execute, Target } from 'serenity-js/lib/screenplay-protractor';
import { PerformsTasks, Task } from 'serenity-js/lib/screenplay';
const dragAndDropScript = require('html-dnd').code; // tslint:disable-
line:no-var-requires
export class DragAndDrop implements Task {
static with(draggable: Target, dropzone: Target) {
return new DragAndDrop(draggable, dropzone);
}
performAs(actor: PerformsTasks): PromiseLike<void> {
return actor.attemptsTo(
Execute.script(dragAndDropScript).withArguments(this.draggable,
this.dropzone)
);
}
constructor(private draggable: Target, private dropzone: Target) {
}
}
There's no built-in interaction for that just yet, but Serenity/JS is quite easy to extend, so you could create a custom interaction (and maybe even submit it as a pull request?).
Below are the steps I'd take to create a custom interaction.
1. Research the protractor way
To start with, think about how you'd implement this functionality using the plain-old Protractor?
Protractor's API documentation suggests the following options:
// Dragging one element to another.
browser.actions().
mouseDown(element1).
mouseMove(element2).
mouseUp().
perform();
// You can also use the `dragAndDrop` convenience action.
browser.actions().
dragAndDrop(element1, element2).
perform();
// Instead of specifying an element as the target, you can specify an offset
// in pixels. This example double-clicks slightly to the right of an element.
browser.actions().
mouseMove(element).
mouseMove({x: 50, y: 0}).
doubleClick().
perform();
As you can see, all the above examples rely on the browser.actions() API, so we'll need to find a way of getting hold of that.
But before diving there, let's try to design our new interaction from the outside-in, and think about the interface we'd like to have.
2. Define a DSL you'd like to use
Let's say I wanted to have a Serenity/JS, Screenplay-style interaction based on the second example from the Protractor docs:
browser.actions().
dragAndDrop(element1, element2).
perform();
providing the following interface:
actor.attemptsTo(
DragAndDrop(element1).onto(element2);
)
This means that I could define my interaction's DSL as follows:
import { Target } from 'serenity-js/lib/screenplay-protractor';
export const DragAndDrop = (draggable: Target) => ({
onto: (dropzone: Target) => ...
})
This will give me the syntax of DragAndDrop(draggable).onto(dropzone) that I wanted to have.
The next step is for DragAndDrop(draggable).onto(dropzone) call to return an actual interaction.
3. Define the interaction
You can define an interaction using the following short-hand syntax:
import { Interaction } from 'serenity-js/lib/screenplay-protractor';
Interaction.where(`#actor drags ${draggable} onto ${dropzone}`, actor => {
// interaction body
});
Serenity/JS provides an "ability" to BrowseTheWeb.
This ability is a Screenplay Pattern-style wrapper around the protractor object, which means that you can use it to access protractor-specific APIs.
So provided that you've given your actor the ability to BrowseTheWeb:
import { Actor, BrowseTheWeb } from 'serenity-js/lib/screenplay-protractor';
const Alice = Actor.named('Alice').whoCan(BrowseTheWeb.using(protractor.browser));
you can access it in your interaction body:
Interaction.where(`#actor drags ${draggable} onto ${dropzone}`, actor => {
return BrowseTheWeb.as(actor).actions().
dragAndDrop(..., ...).
perform();
});
One more missing step is that protractor's browser.actions.dragAndDrop(..., ...) method expects you to provide an instance of a WebElement, rather than a Serenity/JS-specific Target.
This means that we need to resolve the Target before we pass it on:
Interaction.where(`#actor drags ${draggable} onto ${dropzone}`, actor => {
const browse = BrowseTheWeb.as(actor),
draggableElement = browse.locate(draggable),
dropzoneElement = browse.locate(dropzone);
return browse.actions().
dragAndDrop(draggableElement, dropzoneElement).
perform();
});
4. Putting it all together
Given all the above, the resulting implementation could look as follows:
import { Actor, BrowseTheWeb, Interaction, Target } from 'serenity-js/lib/screenplay-protractor';
export const DragAndDrop = (draggable: Target) => ({
onto: (dropzone: Target) => Interaction.where(
`#actor drags ${draggable} onto ${dropzone}`,
actor => {
const browse = BrowseTheWeb.as(actor),
draggableElement = browse.locate(draggable),
dropzoneElement = browse.locate(dropzone);
return browse.actions().
dragAndDrop(draggableElement, dropzoneElement).
perform();
})
})
HTML5 Drag and Drop and Chrome
Please note that the above implementation might not work in Chromedriver with HTML5 drag and drop unless this defect is fixed.
Alternatively, you can install the html-dnd module and implement a Screenplay-style task as follows (you'll need Serenity/JS 1.9.3 or later):
import { Execute, Target, Task } from 'serenity-js/lib/screenplay-protractor';
const dragAndDropScript = require('html-dnd').code; // tslint:disable-line:no-var-requires
export const DragAndDrop = (draggable: Target) => ({
onto: (dropzone: Target) => Task.where(`#actor drags ${draggable} onto ${dropzone}`,
Execute.script(dragAndDropScript).withArguments(draggable, dropzone),
),
});
Hope this helps and thanks for joining the Serenity/JS community :-)
Jan
I'd like to use headroom.js with React. Headroom.js docs say:
At it's most basic headroom.js simply adds and removes CSS classes from an element in response to a scroll event.
Would it be fine to use it directly with elements controlled by React? I know that React fails badly when the DOM structure is mutated, but modifying just attributes should be fine. Is this really so? Could you show me some place in official documentation saying that it's recommended or not?
Side note: I know about react-headroom, but I'd like to use the original headroom.js instead.
EDIT: I just tried it, and it seems to work. I still don't know if it will be a good idea on the long run.
If React tries to reconcile any of the attributes you change, things will break. Here's an example:
class Application extends React.Component {
constructor() {
super();
this.state = {
classes: ["blue", "bold"]
}
}
componentDidMount() {
setTimeout(() => {
console.log("modifying state");
this.setState({
classes: this.state.classes.concat(["big"])
});
}, 2000)
}
render() {
return (
<div id="test" className={this.state.classes.join(" ")}>Hello!</div>
)
}
}
ReactDOM.render(<Application />, document.getElementById("app"), () => {
setTimeout(() => {
console.log("Adding a class manually");
const el = document.getElementById("test");
if (el.classList)
el.classList.add("grayBg");
else
el.className += ' grayBg';
}, 1000)
});
And here's the demo: https://jsbin.com/fadubo/edit?js,output
We start off with a component that has the classes blue and bold based on its state. After a second, we add the grayBg class without using React. After another second, the component sets its state so that the component has the classes blue, bold, and big, and the grayBg class is lost.
Since the DOM reconciliation strategy is a black box, it's difficult to say, "Okay, my use case will work as long as React doesn't define any classes." For example, React might decide it's better to use innerHTML to apply a large list of changes rather than setting attributes individually.
In general, if you need to do manual DOM manipulation of a React component, the best strategy is to wrap the manual operation or plugin in its own component that it can 100% control. See this post on Wrapping DOM Libs for one such example.
I want to block scrolling page "out of the iPhone screen" (when gray Safari's background behind the page border is visible). To do this, I'm cancelling touchmove event:
// Disables scrolling the page out of the screen.
function DisableTouchScrolling()
{
document.addEventListener("touchmove", function TouchHandler(e) { e.preventDefault(); }, true);
}
Unfortunately, this also disables mousemove event: when I tap on a button then move my finger out of it, then release the screen, the button's onclick event is triggered anyway.
I've tried mapping touch events on mouse events, as desribed here: http://ross.posterous.com/2008/08/19/iphone-touch-events-in-javascript/, but to no avail (the same behavior).
Any ideas?
From what I understand of your question, you've attempted to combine the code you've presented above with the code snippet provided by Ross Boucher on Posterous. Attempting to combine these two snippets back-to-back won't work, because in disabling touchmove, you've also disabled the shim that allows mousemove to work via his sample.
This question and its answers sketch out a workable solution to your problem. You should try these two snippets to see if they resolve your issue:
This snippet, which disables the old scrolling behavior:
elementYouWantToScroll.ontouchmove = function(e) {
e.stopPropagation();
};
Or this one, from the same:
document.ontouchmove = function(e) {
var target = e.currentTarget;
while(target) {
if(checkIfElementShouldScroll(target))
return;
target = target.parentNode;
}
e.preventDefault();
};
Then, drop in the code on Posterous:
function touchHandler(event)
{
var touches = event.changedTouches,
first = touches[0],
type = "";
switch(event.type)
{
case "touchstart": type = "mousedown"; break;
case "touchmove": type="mousemove"; break;
case "touchend": type="mouseup"; break;
default: return;
}
//initMouseEvent(type, canBubble, cancelable, view, clickCount,
// screenX, screenY, clientX, clientY, ctrlKey,
// altKey, shiftKey, metaKey, button, relatedTarget);
var simulatedEvent = document.createEvent("MouseEvent");
simulatedEvent.initMouseEvent(type, true, true, window, 1,
first.screenX, first.screenY,
first.clientX, first.clientY, false,
false, false, false, 0/*left*/, null);
first.target.dispatchEvent(simulatedEvent);
event.preventDefault();
}
And that should do it for you. If it doesn't, something else isn't working with Mobile Safari.
Unfortunately I haven't had the time to check out to above yet but was working on an identical problem and found that the nesting of elements in the DOM and which relation you apply it to affects the handler a lot (guess the above solves that, too - 'var target = e.currentTarget').
I used a slightly different approach (I'd love feedback on) by basically using a class "locked" that I assign to every element which (including all its children) i don't want the site to scroll when someone touchmoves on it.
E.g. in HTML:
<header class="locked">...</header>
<div id="content">...</div>
<footer class="locked"></div>
Then I have an event-listener running on that class (excuse my lazy jquery-selector):
$('.ubq_locked').on('touchmove', function(e) {
e.preventDefault();
});
This works pretty well for me on iOs and Android and at least gives me the control to not attach the listener to an element which I know causes problems. You do need to watch your z-index values by the way.
Plus I only attach the listener if it is a touch-device, e.g. like this:
function has_touch() {
var isTouchPad = (/hp-tablet/gi).test(navigator.appVersion);
return 'ontouchstart' in window && !isTouchPad;
}
This way non-touch devices will not be affected.
If you don't want to spam your HTML you could of course just write the selectors into an array and run through those ontouchmove, but I would expect that to be more costly in terms of performance (my knowledge there is limited though). Hope this can help.
I have some troubles with jQuery.
I have a set of Divs with .square classes. Only one of them is supposed to have an .active class. This .active class may be activated/de-activated onClick.
Here is my code :
jQuery().ready(function() {
$(".square").not(".active").click(function() {
//initialize
$('.square').removeClass('active');
//activation
$(this).addClass('active');
// some action here...
});
$('.square.active').click(function() {
$(this).removeClass('active');
});
});
My problem is that the first function si called, even if I click on an active .square, as if the selector was not working. In fact, this seems to be due to the addClass('active') line...
Would you have an idea how to fix this ?
Thanks
Just to give something different from the other answers. Lonesomeday is correct in saying the function is bound to whatever they are at the start. This doesn't change.
The following code uses the live method of jQuery to keep on top of things. Live will always handle whatever the selector is referencing so it continually updates if you change your class. You can also dynamically add new divs with the square class and they will automatically have the handler too.
$(".square:not(.active)").live('click', function() {
$('.square').removeClass('active');
$(this).addClass('active');
});
$('.square.active').live('click', function() {
$(this).removeClass('active');
});
Example working: http://jsfiddle.net/jonathon/mxY3Y/
Note: I'm not saying this is how I would do it (depends exactly on your requirement) but it is just another way to look at things.
This is because the function is bound to elements that don't have the active class when you create them. You should bind to all .square elements and take differing actions depending on whether the element has the class active:
$(document).ready(function(){
$('.square').click(function(){
var clicked = $(this);
if (clicked.hasClass('active')) {
clicked.removeClass('active');
} else {
$('.square').removeClass('active');
clicked.addClass('active');
}
});
});