1 | Official **Angular** wrapper for [`dragula`](https://github.com/bevacqua/dragula).
|
2 |
|
3 | [![npm version](https://badge.fury.io/js/ng2-dragula.svg)](http://badge.fury.io/js/ng2-dragula) [![npm downloads](https://img.shields.io/npm/dm/ng2-dragula.svg)](https://npmjs.org/ng2-dragula)
|
4 | [![Build Status](https://travis-ci.org/valor-software/ng2-dragula.svg?branch=master)](https://travis-ci.org/valor-software/ng2-dragula)
|
5 | [![codecov](https://codecov.io/gh/valor-software/ng2-dragula/branch/master/graph/badge.svg)](https://codecov.io/gh/valor-software/ng2-dragula)
|
6 |
|
7 | ![Logo](https://raw.githubusercontent.com/bevacqua/dragula/master/resources/logo.png)
|
8 |
|
9 | > Drag and drop so simple it hurts
|
10 |
|
11 |
|
12 | # Demo
|
13 |
|
14 | Try out the [demo](http://valor-software.github.io/ng2-dragula/index.html)!
|
15 |
|
16 | [//]: # (Or play with [this starter in your browser][stackblitz] on StackBlitz.)
|
17 |
|
18 | [stackblitz]: https://stackblitz.com/edit/ng2-dragula-base?file=src/app/app.component.html
|
19 |
|
20 | ![Demo](https://raw.githubusercontent.com/bevacqua/dragula/master/resources/demo.png)
|
21 |
|
22 | * [Install](#install)
|
23 | * [Setup](#setup)
|
24 | * [Usage](#usage)
|
25 | * [Directive](#directive)
|
26 | * [Grouping containers](#grouping-containers)
|
27 | * [Saving changes to arrays with dragulaModel](#saving-changes-to-arrays-with-dragulamodel)
|
28 | * [DragulaService](#dragulaservice)
|
29 | * [Drake options](#drake-options)
|
30 | * [Events](#events)
|
31 | * [Special events for ng2-dragula](#special-events-for-ng2-dragula)
|
32 | * [Classic Blunders](#classic-blunders)
|
33 | * [Development](#development)
|
34 |
|
35 | # Dependencies
|
36 |
|
37 | Latest version available for each version of Angular
|
38 |
|
39 | | ng2-dragula | Angular |
|
40 | | ---------- |---------|
|
41 | | 2.1.1 | <= 9.x |
|
42 | | current | 16.x.x |
|
43 |
|
44 | # Install
|
45 |
|
46 | You can get it on npm.
|
47 |
|
48 | ```sh
|
49 | npm install ng2-dragula
|
50 | # or
|
51 | yarn add ng2-dragula
|
52 | ```
|
53 |
|
54 | # Setup
|
55 |
|
56 | ### 1. Important: add the following line to your `polyfills.ts`:
|
57 |
|
58 | ```ts
|
59 | (window as any).global = window;
|
60 | ```
|
61 |
|
62 | This is a temporary workaround for
|
63 | [#849](https://github.com/valor-software/ng2-dragula/issues/849), while upstream
|
64 | dragula still relies on `global`.
|
65 |
|
66 | ### 2. Add `DragulaModule.forRoot()` to your application module.
|
67 |
|
68 | ```typescript
|
69 | import { DragulaModule } from 'ng2-dragula';
|
70 | @NgModule({
|
71 | imports: [
|
72 | ...,
|
73 | DragulaModule.forRoot()
|
74 | ],
|
75 | })
|
76 | export class AppModule { }
|
77 | ```
|
78 |
|
79 | On any child modules (like lazy loaded route modules), just use `DragulaModule`.
|
80 |
|
81 | ### 3. Add the CSS to your project
|
82 |
|
83 | You'll also need to add Dragula's CSS stylesheet `dragula.css` to your
|
84 | application (e.g. in `styles.scss`). The following is **slightly better** than
|
85 | `node_modules/dragula/dist/dragula.css` (it includes `pointer-events: none`
|
86 | ([#508](https://github.com/valor-software/ng2-dragula/issues/508)) and
|
87 | [this fix](https://github.com/bevacqua/dragula/issues/373)),
|
88 | but you may wish to make your own modifications.
|
89 |
|
90 | ```scss
|
91 | /* in-flight clone */
|
92 | .gu-mirror {
|
93 | position: fixed !important;
|
94 | margin: 0 !important;
|
95 | z-index: 9999 !important;
|
96 | opacity: 0.8;
|
97 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
|
98 | filter: alpha(opacity=80);
|
99 | pointer-events: none;
|
100 | }
|
101 | /* high-performance display:none; helper */
|
102 | .gu-hide {
|
103 | left: -9999px !important;
|
104 | }
|
105 | /* added to mirrorContainer (default = body) while dragging */
|
106 | .gu-unselectable {
|
107 | -webkit-user-select: none !important;
|
108 | -moz-user-select: none !important;
|
109 | -ms-user-select: none !important;
|
110 | user-select: none !important;
|
111 | }
|
112 | /* added to the source element while its mirror is dragged */
|
113 | .gu-transit {
|
114 | opacity: 0.2;
|
115 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
|
116 | filter: alpha(opacity=20);
|
117 | }
|
118 | ```
|
119 |
|
120 | ### Then you're ready to go
|
121 |
|
122 | Here's a super quick sample to get you started:
|
123 |
|
124 | ```typescript
|
125 | @Component({
|
126 | selector: "sample",
|
127 | template:`
|
128 | <div>
|
129 | <div class="wrapper">
|
130 | <div class="container" dragula="DRAGULA_FACTS">
|
131 | <div>You can move these elements between these two containers</div>
|
132 | <div>Moving them anywhere else isn't quite possible</div>
|
133 | <div>There's also the possibility of moving elements around in the same container, changing their position</div>
|
134 | </div>
|
135 | <div class="container" dragula="DRAGULA_FACTS">
|
136 | <div>This is the default use case. You only need to specify the containers you want to use</div>
|
137 | <div>More interactive use cases lie ahead</div>
|
138 | <div>Make sure to check out the <a href="https://github.com/bevacqua/dragula#readme">documentation on GitHub!</a></div>
|
139 | </div>
|
140 | </div>
|
141 | </div>
|
142 | `
|
143 | })
|
144 | class Sample {}
|
145 | ```
|
146 |
|
147 | # Usage
|
148 |
|
149 | This package isn't very different from `dragula` itself. I'll mark the
|
150 | differences here, but please refer to the documentation for
|
151 | [dragula](https://github.com/bevacqua/dragula) if you need to learn more about
|
152 | `dragula` itself.
|
153 |
|
154 | ## Directive
|
155 |
|
156 | There's a `dragula` directive that makes a container's direct children
|
157 | draggable. You must supply a string. Both syntaxes, `dragula="VAMPIRES"` or
|
158 | `[dragula]="'VAMPIRES'"`, work equally well.
|
159 |
|
160 | ```html
|
161 | <ul dragula="VAMPIRES">
|
162 | <li>Dracula</li>
|
163 | <li>Kurz</li>
|
164 | <li>Vladislav</li>
|
165 | <li>Deacon</li>
|
166 | </ul>
|
167 | ```
|
168 |
|
169 | ## Grouping containers
|
170 |
|
171 | You can group containers together by giving them the same group name. When you
|
172 | do, the children of each container can be dragged to any container in the same
|
173 | group.
|
174 |
|
175 | ```html
|
176 | <div dragula="VAMPIRES">
|
177 | <!-- vamps in here -->
|
178 | </div>
|
179 | <div dragula="VAMPIRES">
|
180 | <!-- vamps in here -->
|
181 | </div>
|
182 |
|
183 | <div dragula="ZOMBIES">
|
184 | <!-- but zombies in here! -->
|
185 | </div>
|
186 | ```
|
187 |
|
188 | If you want to make sure you are using the same type string in different places,
|
189 | use the `[dragula]` syntax to pass a string variable from your component:
|
190 |
|
191 | ```html
|
192 | <div [dragula]="Vampires"></div>
|
193 | <div [dragula]="Vampires"></div>
|
194 | ```
|
195 | ```ts
|
196 | class MyComponent {
|
197 | Vampires = "VAMPIRES";
|
198 | }
|
199 | ```
|
200 |
|
201 | ## Saving changes to arrays with `[(dragulaModel)]`
|
202 |
|
203 | If your container's children are rendered using `ngFor`, you may wish to read what your users have done. If you provide the same array to the `[(dragulaModel)]` attribute on the **container** element, any changes will be synced back to the array.
|
204 |
|
205 | **NOTE: v2 changes the behaviour of [dragulaModel]. It no longer mutates the arrays you give it, but will shallow clone them and give you the results.** Use two-way binding with `[(dragulaModel)]="..."`, or use the DragulaService `dropModel` and `removeModel` events to save the new models produced.
|
206 |
|
207 | ```html
|
208 | <ul dragula="VAMPIRES" [(dragulaModel)]="vampires">
|
209 | <li *ngFor="let vamp of vampires">
|
210 | {{ vamp.name }} likes {{ vamp.favouriteColor }}
|
211 | </li>
|
212 | </ul>
|
213 | ```
|
214 |
|
215 | You do not, of course, *have* to sync the changes back. The `[(dragulaModel)]` syntax is equivalent to:
|
216 |
|
217 | ```html
|
218 | <ul dragula="VAMPIRES" [dragulaModel]="vampires" (dragulaModelChange)="vampires = $event">
|
219 | ...
|
220 | </ul>
|
221 | ```
|
222 |
|
223 | Note: **DO NOT** put any other elements inside the container. The library relies
|
224 | on having the index of a DOM element inside a container mapping directly to
|
225 | their associated items in the array. Everything will be messed up if you do
|
226 | this.
|
227 |
|
228 | On top of the normal Dragula events, when `[(dragulaModel)]` is provided, there are two extra events: `dropModel` and `removeModel`. Further details are available under `Events`
|
229 |
|
230 | ## Drake options
|
231 |
|
232 | If you need to configure the `drake` _(there's exactly one `drake` per `group`)_, you can use the `DragulaService`.
|
233 |
|
234 | ```ts
|
235 | import { DragulaService } from 'ng2-dragula';
|
236 |
|
237 | class ConfigExample {
|
238 | constructor(private dragulaService: DragulaService) {
|
239 | dragulaService.createGroup("VAMPIRES", {
|
240 | removeOnSpill: true
|
241 | });
|
242 | }
|
243 | }
|
244 | ```
|
245 |
|
246 | See below for more info on options.
|
247 |
|
248 | ## `DragulaService`
|
249 |
|
250 | This service exposes a few different methods with which you can interact with `dragula`.
|
251 |
|
252 | ### `dragulaService.createGroup(name, options)`
|
253 |
|
254 | NOTE: formerly known as `setOptions()`
|
255 |
|
256 | Creates a group named `name`, with an
|
257 | [options](#dragulaoptions) object.
|
258 |
|
259 | ### `dragulaService.find(name: string)`
|
260 |
|
261 | Returns a `Group` named `name`, if there is one. A `Group` contains the following
|
262 | properties.
|
263 |
|
264 | - `name` is the name that identifies the group
|
265 | - `drake` is the raw `drake` instance itself
|
266 | - `options` is the options object used to create the drake. Modifying it won't
|
267 | do anything useful.
|
268 |
|
269 | ### `dragulaService.destroy(name)`
|
270 |
|
271 | Destroys a `Group` named `name` and its associated `drake` instance. Silently
|
272 | returns if the group does not exist.
|
273 |
|
274 | ### DragulaOptions
|
275 |
|
276 | Refer to the documentation for
|
277 | [dragula](https://github.com/bevacqua/dragula#readme) to learn more about the
|
278 | native options.
|
279 |
|
280 | All of the native options work with ng2-dragula. However, there is one addition:
|
281 |
|
282 | #### `copyItem: <T>(item: T) => T`
|
283 |
|
284 | When you have:
|
285 |
|
286 | * `[(dragulaModel)]`
|
287 | * `copy` is `true` or a *function that returns true*
|
288 |
|
289 | ... ng2-dragula will have to create a clone of the JS object you picked up. In
|
290 | previous versions of `ng2-dragula`, there was a terribly buggy,
|
291 | one-size-fits-all clone function. From v2 onwards, you **MUST** provide your own
|
292 | `copyItem` function.
|
293 |
|
294 | If you have a simple object with no nested values, it could be as simple as:
|
295 |
|
296 | ```ts
|
297 | {
|
298 | copy: ...,
|
299 | copyItem: (item: MyType) => ({ ...item })
|
300 | }
|
301 | ```
|
302 |
|
303 | There is a complete example using a `Person` class on the demo page.
|
304 |
|
305 | ## Events
|
306 |
|
307 | Whenever a `drake` instance is created with the `dragula` directive, there are
|
308 | several events you can subscribe to via `DragulaService`. Each event emits
|
309 | a typed object, which you can use to get information about what happened.
|
310 |
|
311 | Refer to [the Drake events
|
312 | documentation](https://github.com/bevacqua/dragula#drakeon-events) for more
|
313 | information about the different events available. Each event follows this
|
314 | format:
|
315 |
|
316 | ```yml
|
317 | Event named: 'drag'
|
318 |
|
319 | Native dragula:
|
320 | Use: drake.on('drag', listener)
|
321 | Listener arguments: (el, source)
|
322 |
|
323 | ng2-dragula:
|
324 | Method: DragulaService.drag(groupName?: string): Observable<...>
|
325 | Observable of: { name: string; el: Element; source: Element; }
|
326 | ```
|
327 |
|
328 | Each supports an optional parameter, `groupName?: string`, which filters events
|
329 | to the group you're interested in. This is usually better than getting all
|
330 | groups in one observable.
|
331 |
|
332 | The sample below illustrates how you can use destructuring to pull values from
|
333 | the event, and unsubscribe when your component is destroyed.
|
334 |
|
335 | ```html
|
336 | <div dragula="VAMPIRES"></div>
|
337 | ```
|
338 |
|
339 | ```ts
|
340 | import { Subscription } from 'rxjs';
|
341 | import { DragulaService } from 'ng2-dragula';
|
342 |
|
343 | export class MyComponent {
|
344 | // RxJS Subscription is an excellent API for managing many unsubscribe calls.
|
345 | // See note below about unsubscribing.
|
346 | subs = new Subscription();
|
347 |
|
348 | constructor(private dragulaService: DragulaService) {
|
349 |
|
350 | // These will get events limited to the VAMPIRES group.
|
351 |
|
352 | this.subs.add(this.dragulaService.drag("VAMPIRES")
|
353 | .subscribe(({ name, el, source }) => {
|
354 | // ...
|
355 | })
|
356 | );
|
357 | this.subs.add(this.dragulaService.drop("VAMPIRES")
|
358 | .subscribe(({ name, el, target, source, sibling }) => {
|
359 | // ...
|
360 | })
|
361 | );
|
362 | // some events have lots of properties, just pick the ones you need
|
363 | this.subs.add(this.dragulaService.dropModel("VAMPIRES")
|
364 | // WHOA
|
365 | // .subscribe(({ name, el, target, source, sibling, sourceModel, targetModel, item }) => {
|
366 | .subscribe(({ sourceModel, targetModel, item }) => {
|
367 | // ...
|
368 | })
|
369 | );
|
370 |
|
371 | // You can also get all events, not limited to a particular group
|
372 | this.subs.add(this.dragulaService.drop()
|
373 | .subscribe(({ name, el, target, source, sibling }) => {
|
374 | // ...
|
375 | })
|
376 | );
|
377 | }
|
378 |
|
379 | ngOnDestroy() {
|
380 | // destroy all the subscriptions at once
|
381 | this.subs.unsubscribe();
|
382 | }
|
383 | }
|
384 |
|
385 | ```
|
386 |
|
387 | **NOTE: You should always unsubscribe each time you listen to an event.** This
|
388 | is especially true for a component, which should tear itself down completely in
|
389 | `ngOnDestroy`, including any subscriptions. It might not be necessary if you
|
390 | have a global singleton service (which is never destroyed) doing the
|
391 | subscribing.
|
392 |
|
393 | You can also engineer your use of events to avoid subscribing in the first
|
394 | place:
|
395 |
|
396 | ```ts
|
397 | import { merge } from 'rxjs';
|
398 | import { mapTo, startWith } from 'rxjs/operators';
|
399 |
|
400 | dragStart$ = this.dragulaService.drag("VAMPIRES").pipe(mapTo(true));
|
401 | dragEnd$ = this.dragulaService.dragend("VAMPIRES").pipe(mapTo(false));
|
402 | isDragging$ = merge(dragStart$, dragEnd$).pipe(startWith(false));
|
403 |
|
404 | // html: [class.dragging]="isDragging$ | async"
|
405 | ```
|
406 |
|
407 | ## Special Events for `ng2-dragula`
|
408 |
|
409 | The `dropModel(name?: string)` and `removeModel(name?: string)` events are only active when you have supplied `[dragulaModel]`.
|
410 |
|
411 | | Event Name | Listener Arguments | Event Description |
|
412 | | :-------------- | :-------------------------: | :--------------------------------------------------------------------------------------- |
|
413 | | dropModel | { type, el, target, source, item, sourceModel, targetModel, sourceIndex, targetIndex } | same as normal drop, but with updated models + the item that was dropped |
|
414 | | removeModel | { type, el, container, source, item, sourceModel, sourceIndex } | same as normal remove, but with updated model + the item that got removed |
|
415 |
|
416 | # Classic Blunders
|
417 |
|
418 | There are a number of very common issues filed against this repo. You will be
|
419 | mocked terribly if you file a bug and it turns out you made one of these
|
420 | blunders and it wasn't a bug at all.
|
421 |
|
422 | ### 1. Do not put `[dragula]` or `[(dragulaModel)]` on the same element as `*ngFor`.
|
423 |
|
424 | **WRONG:**
|
425 |
|
426 | ```html
|
427 | <div class="container">
|
428 | <div *ngFor="let x of list"
|
429 | dragula="WRONG" [(dragulaModel)]="list">...</div>
|
430 | </div>
|
431 | ```
|
432 |
|
433 | **RIGHT:**
|
434 |
|
435 | ```html
|
436 | <div class="container" dragula="RIGHT" [(dragulaModel)]="list">
|
437 | <div *ngFor="let x of list">...</div>
|
438 | </div>
|
439 | ```
|
440 |
|
441 | ### 2. Do not add any child elements that aren't meant to be draggable
|
442 |
|
443 | **WRONG:**
|
444 | ```html
|
445 | <div class="container" dragula="WRONG" [(dragulaModel)]="list">
|
446 | <h2>WRONG: This header will mess up everything, and you will
|
447 | get really weird bugs on drop</h2>
|
448 | <div *ngFor="let x of list">...</div>
|
449 | </div>
|
450 | ```
|
451 |
|
452 | **RIGHT:**
|
453 | ```html
|
454 | <h2>This header will not be draggable or affect drags at all.</h2>
|
455 | <div class="container" dragula="RIGHT" [(dragulaModel)]="list">
|
456 | <div *ngFor="let x of list">...</div>
|
457 | </div>
|
458 | ```
|
459 |
|
460 | # Alternatives
|
461 |
|
462 | There are hundreds of other libraries that do this. Some notable ones:
|
463 |
|
464 | - [@angular-skyhook](https://cormacrelf.github.io/angular-skyhook/examples/), specifically with the sortable. Also by me (@cormacrelf).
|
465 | - [@angular/cdk/drag-drop](https://material.angular.io/cdk/categories) -- no documentation yet, but will presumably be well-supported.
|
466 |
|
467 | # Development
|
468 |
|
469 | - You must use Yarn >= 1.3. It includes the 'workspaces' feature.
|
470 | - Please use [Conventional Commits](https://conventionalcommits.org/) in your commit messages.
|
471 |
|
472 | #### setup
|
473 |
|
474 | ```sh
|
475 | yarn
|
476 | yarn workspace ng2-dragula build
|
477 | ```
|
478 |
|
479 | #### run tests
|
480 |
|
481 | ```sh
|
482 | yarn workspace ng2-dragula test
|
483 | # or
|
484 | yarn workspace ng2-dragula test:headless
|
485 | ```
|
486 |
|
487 | #### run demo server
|
488 |
|
489 | ```sh
|
490 | # listens for changes in the library and rebuilds on save
|
491 | yarn watch
|
492 | # runs demo server
|
493 | yarn workspace demo start
|
494 | ```
|
495 |
|
496 | #### Publishing a new version
|
497 |
|
498 | ```
|
499 | yarn lerna publish
|
500 | ```
|
501 |
|
502 | ## Credits
|
503 |
|
504 | - `v1`: Nathan Walker (@NathanWalker)
|
505 | - `v1.x`: Dmitriy Shekhovtsov (@valorkin)
|
506 | - `v2`: Cormac Relf (@cormacrelf)
|
507 |
|