Perform a drag and drop in serenity-js - drag-and-drop

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

Related

How to get data from an API only once (on app creation, outside component or view) in Vue3 SPA, with Pinia store

Is it possible and is it a good practice to avoid fetching data from an API every time the router view is loaded or the component is Mounted?
The thing is that some data rarely changes (like a dropdown options list, imagine allowed animal picks for my app) and it's logical not to send a request every time to a server, instead on app creation would be sufficient.
Tried in App.vue, is that a common thing?
IN APP.vue
import { computed, onMounted, onUpdated, ref } from 'vue';
onMounted(()=>{
axios.get('/data')....
.then((res)=>{
store.property = res.data
...
})
})
I think having it on mount in the App.vue component is acceptable since the App component would not be remounted.
The ideal setup, however, depends on some other parameters like size of application and size of team that's maintaining it. In a large applications you might want to organize things in amore structured and consistent way so you and other folks working on the code know where to find things.
You could consider moving the API call into the pinia action.
store.loadMyData()
// instead of
axios.get('/data')
.then((res)=>{
store.property = res.data;
})
That way you have fewer lines of code in the component. Having "lean" components and moving "business logic" out of components usually makes for better organization which makes it easier to maintain.
Within the action, you can track the state of the API
const STATES = {
INIT: 0,
DONE: 1,
WIP: 2,
ERROR: 3
}
export const useMyApiStore = defineStore('myapi', {
state: () => ({
faves: [],
favesState: STATES.INIT
}),
actions: {
loadMyData() {
this.store.favesState = STATES.WIP;
axios.get('/data')
.then((res) => {
this.store.property = res.data;
this.store.favesState = STATES.DONE;
})
.catch((e) => {
this.store.favesState = STATES.ERROR;
})
},
},
getters: {
isLoaded(){
return this.store.favesState === STATES.DONE;
}
isLoading(){
return this.store.favesState === STATES.WIP;
}
}
})
This is, obviously, more verbose, but allows for the components to be smaller and contain less logic. Then, for example, in your component you can use the getter isLoading to display a loading indicator, or use isLoaded inside another component to determine whether to show it.
Yes, this is a oft used way to load some data into the Vue App.
You could also load data before the Mounting in beforeMount() or created() Lifecycle Hooks (see Vue Lifecycle Diagram) to prevent unnecessary HTML updates.

Is there a onScroll event for ag-grid

I am looking for a scroll event on ag-grid, I want to know when the scroll reaches the end and load the next set of rows, I know if you set the infinite scroll mode then ag-grid calles the getRows method, but in my application I do not get the next set of rows right away, I make a call to the server and server sends a separate message to the client with the new set of rows
After getting in deep, I found the perfect solution to this problem.
Please note here I am used AngularJS, But very easy to understand.
onBodyScroll:function(params) {
var bottom_px = $scope.gridOptions.api.getVerticalPixelRange().bottom;
var grid_height = $scope.gridOptions.api.getDisplayedRowCount() * $scope.gridOptions.api.getSizesForCurrentTheme().rowHeight;
if(bottom_px == grid_height)
{
alert('Bottom')
}
},
There's a grid event called 'onBodyScroll' which you can attach an event handler to it.
This event is somewhat secret as it was not there on their GridOptions type before version 18, even though it does work.
see this comment: https://github.com/ag-grid/ag-grid-enterprise/issues/89#issuecomment-264477535
They do have this event in document tho: https://www.ag-grid.com/javascript-grid-events/#miscellaneous
BodyScrollEvent
bodyScroll - The body was scrolled horizontally or vertically.
onBodyScroll = (event: BodyScrollEvent) => void;
interface BodyScrollEvent {
// Event identifier
type: string;
api: GridApi;
columnApi: ColumnApi;
direction: ScrollDirection;
left: number;
top: number;
}
You should be able to do that thing (loading the data from the server) as per below example.
First of all, define your dataSource.
const dataSource: IServerSideDatasource = {
getRows: (params: IServerSideGetRowsParams) => this._getRows(params, [])
};
this.gridApi.setServerSideDatasource(dataSource);
Declare _getRows method like this.
private _getRows(params: IServerSideGetRowsParams, data: any[]) {
this.gridApi.showLoadingOverlay();
service.getData(params) // the payload your service understands
.subscribe((result: any[]) => {
params.successCallback(result, -1);
params.failCallback = () => console.log('some error occured while loading new chunk of data');
this.gridApi.hideOverlay();
},
error => this._serverErrorHandler(error)
);
}
This is pretty much self-explanatory. Let's me know if anything is unclear to you.
BTW, I've used typescript for the example, javascript example would be kind of the same for ag-grid-react

Angular 2 + ngrx(redux) + forms

How do you handle Angular 2 forms in unidirectional data flow? Especially with validation between several parent/child components?
I am using ngrx/store and model driven forms with form builder.. Is it possible to do something similar like form reducer in React and make it as a part of Store?
Do you have some articles about it?
I have created a library called ngrx-forms that does exactly what you want. You can get it on npm via:
npm install ngrx-forms --save
I recommend checking out the full README on the github page, but below you can find some examples of what you need to do to get the library up and running once installed.
Import the module:
import { StoreModule } from '#ngrx/store';
import { NgrxFormsModule } from 'ngrx-forms';
import { reducers } from './reducer';
#NgModule({
declarations: [
AppComponent,
],
imports: [
NgrxFormsModule,
StoreModule.forRoot(reducers),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Add a group state somewhere in your state tree via createFormGroupState and call the formGroupReducer inside your reducer:
import { Action } from '#ngrx/store';
import { FormGroupState, createFormGroupState, formGroupReducer } from 'ngrx-forms';
export interface MyFormValue {
someTextInput: string;
someCheckbox: boolean;
nested: {
someNumber: number;
};
}
const FORM_ID = 'some globally unique string';
const initialFormState = createFormGroupState<MyFormValue>(FORM_ID, {
someTextInput: '',
someCheckbox: false,
nested: {
someNumber: 0,
},
});
export interface AppState {
someOtherField: string;
myForm: FormGroupState<MyFormValue>;
}
const initialState: AppState = {
someOtherField: '',
myForm: initialFormState,
};
export function appReducer(state = initialState, action: Action): AppState {
const myForm = formGroupReducer(state.myForm, action);
if (myForm !== state.myForm) {
state = { ...state, myForm };
}
switch (action.type) {
case 'some action type':
// modify state
return state;
default: {
return state;
}
}
}
Expose the form state inside your component:
import { Component } from '#angular/core';
import { Store } from '#ngrx/store';
import { FormGroupState } from 'ngrx-forms';
import { Observable } from 'rxjs/Observable';
import { MyFormValue } from './reducer';
#Component({
selector: 'my-component',
templateUrl: './my-component.html',
})
export class MyComponent {
formState$: Observable<FormGroupState<MyFormValue>>;
constructor(private store: Store<AppState>) {
this.formState$ = store.select(s => s.myForm);
}
}
Set the control states in your template:
<form novalidate [ngrxFormState]="(formState$ | async)">
<input type="text"
[ngrxFormControlState]="(formState$ | async).controls.someTextInput">
<input type="checkbox"
[ngrxFormControlState]="(formState$ | async).controls.someCheckbox">
<input type="number"
[ngrxFormControlState]="(formState$ | async).controls.nested.controls.someNumber">
</form>
This is a fairly old question, but I couldn't find a great solution in my own quest for working with ngrx + reactive forms in Angular. As a result, I'll post my research here with hope that it may help someone else. My solution can be broken down into two parts, and I pray you (oh weathered soul) find it applicable to your problem:
1) Monitor the form element/s (for example, "keyup" event for a typical text input), and update the State from that event. This strategy comes straight out of the book search component in the ngrx example app. We can now successfully populate the State as our form changes. Awesome! 50% done!
2) The angular reactive forms guide demonstrates creating the form group in the constructor. I have seen some other people do it inside ngOnInit, but this is too late in the lifecycle for our needs (I tried, I failed). Now that we have our form group established, setup ngOnChanges to capture any changes pushed from the state, and then update the form group using patchValue. For example:
ngOnChanges(changes: SimpleChanges) {
if (changes.valueICareAbout1) {
this.myForm.patchValue({
valueICareAbout1: changes.valueICareAbout1.currentValue
});
}
if (changes.valueICareAbout2) {
this.myForm.patchValue({
valueICareAbout2: changes.valueICareAbout2.currentValue
});
}
}
In the applications I built with Angular 2, the following guideline seemed to work well:
Parent components pass data down to children via data binding. Child components request data changes by emitting output events to parent components. It is the parent components responsibility to act accordingly.
In a hierarchical component structure, data changes are handled by the lowest component that depends on the data. If there's another component higher up or a sibling that depends on the same data item, pass changes up by emitting events and leave the handling to a higher component.
This scheme works well because, for any data that is relevant to more than one component, there is a single component responsible for performing changes. Changes bubble down automatically. Components are reusable, and changes in the component tree can be easily adapted.
With regard to validation, any component in the ladder between the lowest component emitting a data change request up to the highest component that finally handles the change, any component can effectively cancel the change by not passing it higher up. In most applications, I'd opt for validating data changes at the origin of the change though.
Naturally, child components can still have internal state and need not communicate changes - unless changes are relevant to the parent component.
Form data is inherently a very local state, especially for Angular since ngModel binds to local component variables. The top devs that I know recommend keeping the data for the form localized to that component (ie just use ngModel with local variables). This is because un-submitted form data is almost never shared by various components across your whole application. When the user submits the form then you can dispatch an action with a payload containing the form data to a parent component, to the store, or even to an ngrx/effect to be posted to a server.

Angular 2 drag and drop directive extremely slow

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.

Is it fine to mutate attributes of React-controlled DOM elements directly?

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.