Giới thiệu về directive dndLists trong Angular

Tổng quan

Hôm nay mình sẽ giới thiệu đến các bạn directive dndLists - hỗ trợ kéo thả item giữa các list trong angular 1. Và mình sẽ làm 1 bảng KANBAN sử dụng directive trên. Trước khi bắt đầu, các bạn nên tìm hiểu sơ về dndLists trước nhé (tất nhiên các bạn nên biết sơ về directive là gì trong angular đã nhé, các bạn có thể tìm hiểu tại đây.

Demo

Đầu tiên chúng ta cần tạo app trước và import các thư viện cần thiết như (Angular JS, dndLists) và một vài thư viện hỗ trợ khác. Ở đây mình dùng npm để cài các package cần thiết, các bạn cũng có thể sử dụng bower nhé.

Cài thư viện và init project

Bạn cần tạo 1 thư mục để chứa sourcecode. Ở đây mình tạo thư mục với tên là my-app. Tiếp theo chúng ta mở terminal (Ubuntu), Windows PowerShell (Win 10) lên và dẫn đường dẫn đến thư mục chúng ta vừa tạo. Chạy lệnh npm init để tạo file package.json, bạn sẽ được hỏi một vài câu hỏi, cứ điền thông tin hoặc không điền rồi cứ bấm Enter đến khi nào hết. Tiếp đến, chúng ta sẽ cài các package như mình đã nói ở trên, ở đây mình sẽ cài: angular, angular-animate, angular-drag-and-drop-lists, angular-sanitize, bootstrap, font-awesome, lodash bằng các lệnh sau:

npm install angular --save
npm install angular-animate --save
npm install angular-drag-and-drop-lists --save
npm install angular-sanitize --save
npm install bootstrap --save
npm install font-awesome --save
npm install lodash --save

--save ở đây chỉ là option thêm, nếu các bạn thêm vào thì những người build app của các bạn chỉ cần chạy một lệnh duy nhất npm install là có thể down các package mà bạn đã down. Vậy là phần cài đặt thư viện đã xong, bây giờ các bạn hãy tạo một file index.html, các bạn cũng có thể tạo một thư mục riêng ở trong thư mục my-app để chứa sourcecode, ở đây mình đã tạo thư mục src. Ở đây các bạn chỉ cần viết cấu trúc như một file html thuần bình thường gồm các tag như <html>, <head>, <body>, sau đó require các file js, css của các package mà mình đã lưu ở trên vào (tất cả đều nằm trong thư mục node_modules cả nhé. Cơ bản ta sẽ có được file index.html như sau:

<html>
  <head>
    <meta charset="utf-8"/>
    <script src="../node_modules/angular/angular.js"></script>
    <script src="../node_modules/angular-animate/angular-animate.js"></script>
    <script src="../node_modules/angular-sanitize/angular-sanitize.js"></script>
    <script src="../node_modules/angular-drag-and-drop-lists/angular-drag-and-drop-lists.js"></script>
    <script src="../node_modules/lodash/lodash.js"></script>
    <link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="../node_modules/font-awesome/css/font-awesome.css" rel="stylesheet">
  </head>
  <body>
  </body>
</html>

Bắt đầu code

Như phần trên, chúng ta đã tạo được một file index.html cơ bản, tiếp theo các bạn thêm directive ng-appng-controller vào nhé

...
<body ng-app="myApp" ng-controller="kanBanCtrl as ctrl">
</body>
...

Tiếp theo chúng ta sẽ phải tạo một file javascript để xử lý các sự kiện bên controller, tất nhiên đừng quên import nó vào file index nhé. Ở đây mình tạo file app.js với nội dung sau:

var app = angular.module('myApp', ['ngAnimate', 'ngSanitize', 'dndLists'])

app.controller('kanBanCtrl', kanBanCtrl);

function kanBanCtrl() {
  var ctrl = this;
  console.log('Hello World!');
}

Ở đây chúng ta cần thêm các directive đã cài ở trên như ngAnimate, ngSanitize, dndLists vào thì mới có thể sử dụng được nhé. Bây giờ mở file index.html bằng trình duyệt và check ở console nào.

Tiếp theo chúng ta sẽ tạo view nhé. Trước hết chúng ta tạo khởi tạo các biến và gán dữ liệu vào ở bên javascript nhé, mình sẽ có một list các task với cấu trúc như sau:

ctrl.taskList = [
    {tasks: [{name: 'Task 1', selected: false}, {name: 'Task 2', selected: false}, {name: 'Task 3', selected: false}], label: 'TO DO', class: 'info', dragging: false},
    {tasks: [{name: 'Task 4', selected: false}, {name: 'Task 5', selected: false}, {name: 'Task 6', selected: false}], label: 'DOING', class: 'primary', dragging: false},
    {tasks: [{name: 'Task 7', selected: false}, {name: 'Task 8', selected: false}, {name: 'Task 9', selected: false}], label: 'PENDING', class: 'warning', dragging: false},
    {tasks: [{name: 'Task 10', selected: false}, {name: 'Task 13', selected: false}, {name: 'Task 12', selected: false}], label: 'DONE', class: 'success', dragging: false}
  ]

Trong đó:

  • Mỗi list task ứng với một cột sẽ là một object gồm các thuộc tính như:
    1. tasks: Chứa các task trong cột, trong đó mỗi task sẽ có hai thuộc tính đơn giản như name (tên của task), selected: để biết task đó có đang được pick hay không.
    2. label: Ứng với nhãn của mỗi cột.
    3. class: Thuộc tính này chỉ là option class cho mỗi cột.
    4. dragging: Để biết cột đó đang có phần tử được drag hay không. Đây chỉ là cấu trúc cơ bản, các bạn có thể tùy chỉnh sao cho phù hợp. Tiếp theo, chúng ta sẽ tạo các cột tương ứng với các cột của bảng KANBAN, ở đây mình sẽ có có code html như sau:
<body ng-app="myApp" ng-controller="kanBanCtrl as ctrl">
    <div class="container-fluid">
      <div class="row">
        <div class="col-md-3" ng-repeat="list in ctrl.taskList">
          <div class="panel panel-{{list.class}}">
            <div class="panel-heading list-header" ng-bind="list.label"></div>
            <div class="panel-body task-list">
              <ul>
                <li ng-repeat="task in list.tasks">
                    <i class="fa" aria-hidden="true"
                      ng-class="{'fa-hand-o-right': list.label === 'TO DO', 'fa-cogs': list.label === 'DOING',
                        'fa-exclamation-circle': list.label === 'PENDING', 'fa-check-square-o': list.label === 'DONE'}">
                    </i>
                    <span ng-bind="task.name"></span>
                </li>
              </ul>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>

Với đoạn code trên, khi chạy lại chúng ta sẽ được kết quả như sau: .

Tiếp theo chúng ta cùng sử dụng directive dndLists để thực hiện việc kéo thả các task qua lại giữa các cột nào. Ta sẽ có đoạn code sau:

<ul dnd-list dnd-drop="ctrl.onDrop(list, item, index)">
    <li ng-repeat="task in list.tasks" dnd-draggable="ctrl.getSelectedTasksIncluding(list, task)"
      dnd-dragstart="ctrl.onDragstart(list, event)" dnd-moved="ctrl.onMoved(list)"
      dnd-dragend="list.dragging = false" dnd-selected="task.selected = !task.selected"
      ng-class="{'selected': task.selected}" ng-hide="list.dragging && task.selected">
        <i class="fa" aria-hidden="true"
          ng-class="{'fa-hand-o-right': list.label === 'TO DO', 'fa-cogs': list.label === 'DOING',
            'fa-exclamation-circle': list.label === 'PENDING', 'fa-check-square-o': list.label === 'DONE'}">
        </i>
        <span ng-bind="task.name"></span>
    </li>
</ul>

Mình sẽ giải thích cụ thể nhé:

  • dnd-list: Đây là thuộc tính bắt buộc có, sẽ có giá trị là mảng của các phần tử được drop vào. Hoặc ta có thể để trống khi sử dụng kèm với dnd-drop.
  • dnd-drop: Như mình đã nói ở trên, đây là một option được gọi khi có một phần tử được drop vào mảng. Đây là các thuộc tính mình dùng cho thẻ ul. Ở thẻ li sẽ có các option như:
  • dnd-draggable: Nếu như dnd-list là thuộc tính bắt buộc dùng cho list các items, thì đây sẽ là thuộc tính bắt buộc đối với mỗi item.
  • dnd-dragstart: Đây là một callback bắt sự kiện khi có một phần tử bị dragged.
  • dnd-dragend: Đây là một callback trái ngược với dnd-dragstart, dùng để bắt sự kiện khi kết thúc dragged một phần tử.
  • dnd-selected: Đây là một callback khác, dùng để bắt sự kiện khi có một phần tử được click vào nhưng không dragged.
  • dnd-moved: Đây là callback được gọi khi có một phần tử bị di chuyển, và thường thì chúng ta phải tự xóa phần tử đó ra khỏi list ban đầu vì directive này không làm điều đó giúp chúng ta. Dựa vào những điều trên, chúng ta có thể viết code cho các hàm xử lý bên controller như sau:
// Hàm này sẽ xử lý việc gán trường *selected* của phần tử đang được dragged bằng true, và lấy các phần tử khác đang được select (selected = true) để thực hiện drop.
ctrl.getSelectedTasksIncluding = function(list, task) {
    task.selected = true;
    return _.filter(list.tasks, ['selected', true]);
  };

// Hàm này sẽ gán trường *dragging* của list có phần tử đang được dragged bằng true.
  ctrl.onDragstart = function(list, event) {
    list.dragging = true;
  };

// Hàm này sẽ gán trường *selected* của tất cả các phần tử trong mảng thành false
// Thường thì khi chúng ta drag, các phần tử được dragged sẽ có trường *selected* bằng true, vì thế sau khi drop, chúng ta phải gán lại trường *selected* của các phần tử đó bằng false.
// Sau đó chúng ta sẽ thực hiện việc thêm các phần tử được dragged đó vào list mới.
// Ở đây index chính là vị trí mà chúng ta drop vào trong mảng, vì vậy để chèn các phần tử đó vào chúng ta sẽ thực hiện như bên dưới.
  ctrl.onDrop = function(list, tasks, index) {
    _.forEach(tasks, function(task) { task.selected = false; });
    list.tasks = list.tasks.slice(0, index).concat(tasks).concat(list.tasks.slice(index));
    return true;
  }

// Như mình đã giải thích ở trên, hàm này sẽ có tác dụng loại bỏ phần tử đang được dragged, bằng cách loại bỏ những phần tử có trường *selected* bằng true.
  ctrl.onMoved = function(list) {
    list.tasks = _.filter(list.tasks, ['selected', false]);
  };

Với đoạn code như trên chúng ta đã có thể drop-drag các phần tử qua lại giữa các list rồi, cùng chạy lại và test xem nhé :kiss_mm:. Tại vì mình chỉ làm ở client nên không có data, phải tạo data giả. Các bạn có thể kết nối đến server rồi lấy dữ liệu và trả về nhé. Hi vọng qua bài này có thể giúp các bạn biết thêm một directive hỗ trợ việc drag and drop list trong angular. Cảm ơn các bạn đã tham khảo bài viết này. Nếu muốn góp ý hay thắc mắc, vui lòng để lại ở phần bình luận nhé :kiss_mm:.

Tham khảo

Các bạn cũng có thể tìm hiểu thêm về directive này tại đây.