Drag and drop con columnas dinámicas
Clase 14 de 24 • Curso de Maquetación con Angular CDK y Tailwind CSS
Contenido del curso
Clase 14 de 24 • Curso de Maquetación con Angular CDK y Tailwind CSS
Contenido del curso
Cristian Danilo Motta Herrera
Jose Armando Acevedo Angarita
Ángel David Roque Ayala
Camilo Alexander Arias Guerrero
Marco Astudillo
Alexis Duque
Pablo Torres Pérez
Darwin Rodríguez
Pablo Torres Pérez
Emilia Margarethe Ericksson
José Nicolás Aristizabal Ramírez
Diego Inostroza
David Matias Casco Lobos
Diego Lozano
Ángel David Roque Ayala
Alberto Cruz
Max Andy Diaz Neyra
Max Andy Diaz Neyra
Max Andy Diaz Neyra
Wilberk Ledezma
Adrian Lima
Elioenai Garcia
Emilio Nicolás Mendoza Patti
Adrian Silva
LUIS ANTONIO CALVO QUISPE
LUIS ANTONIO CALVO QUISPE
LUIS ANTONIO CALVO QUISPE
¿no se le puede dar like a un video?
¡ este curso está genial !
Lo puedes hacer cuando termines el curso
Customizar el scroll bar
En el archivo styles.scss
/* Agrega estilos personalizados para la barra de desplazamiento */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-thumb { background-color: rgba(156, 163, 175, var(--tw-bg-opacity)); border-radius: 4px; } ::-webkit-scrollbar-track { background-color: rgba(229, 231, 235, var(--tw-bg-opacity)); border-radius: 4px; } /* Agrega un efecto hover a la barra de desplazamiento */ ::-webkit-scrollbar-thumb:hover { background-color: rgba(107, 114, 128, var(--tw-bg-opacity)); }
si quieren mover las listas pueden utilizar cdkDropListOrientation https://material.angular.io/cdk/drag-drop/overview#list-orientation.
Mi solución por si a alguien se le dificulta.
<div class="flex grow overflow-x-scroll h-full" cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropHorizontal($event)" > <div class="flex items-start w-full" cdkDropListGroup> <div cdkDrag class="rounded bg-gray-200 w-72 p-2 mr-3 shrink-0" *ngFor="let colum of columns" >
Faltaba el código.
dropHorizontal (event: CdkDragDrop<Column[]>) { moveItemInArray(this.columns, event.previousIndex, event.currentIndex); }
Les comparto como va mi solución del reto hasta el momento.
Hola, cómo lo resolviste?
Yo coloqué la directiva cdkDrap y en un nivel más arriba coloqué las otras directivas pero se rompe la estructura
Hola, acabo de ver el comentario, jeje. Espero ya lo hayas resuelto. Si no, te cuento como lo resolví. Además del 'cdkDrag' que comentas en el div donde colocamos el *ngFor para las columnas, agregué un 'cdkDropList' al div padre donde colocamos 'cdkDropListGroup'. Finalmente, en ese mismo div, coloqué cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropColumn($event)" Quedando así
<div cdkDropListGroup cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropColumn($event)" [cdkDropListData]="this.columns" class="example-list overflow-hidden" class="flex items-start w-full h-full overflow-x-scroll scrollbar-thumb-rounded-full scrollbar-track-rounded-full scrollbar scrollbar-thumb-blue-200 scrollbar-track-blue-400" > <div class="rounded bg-gray-200 w-72 p-2 mr-3 shrink-0" *ngFor="let column of columns; let i=index;" (click)="getIndex(i)" cdkDrag > <div class="example-box flex flex-row cursor-move flex-grow" class="flex justify-between py-1" > <h3 class="text-sm font-bold ml-2">{{ column.title }}</h4> <button class="flex" aria-expanded="false" type="button" cdkOverlayOrigin #trigger="cdkOverlayOrigin" (click)="toggle(trigger)" > <fa-icon [icon]="faEllipsisH"></fa-icon> </button> </div> <div class="text-sm mt-2 min-h-[2.5rem]" cdkDropList (cdkDropListDropped)="drop($event)" [cdkDropListData]="column.todos" > <div (click)="openDialog(todo)" *ngFor="let todo of column.todos" cdkDrag class="bg-white shadow p-2 rounded mt-2 border-b border-x-gray-300 cursor-pointer hover:bg-gray-400" > {{ todo.title }} </div> <button (click)="addNewTask = !addNewTask"> <fa-icon [icon]="faPlus">Añade una tarjeta</fa-icon> </button> <input class="w-full" type="text" placeholder="Introduzca un título para esta tarjeta" [formControl]="list" *ngIf="addNewTask && ni === i" /> <!-- {{ ni == i }} --> <button class="w-4/12 mt-2 bg-sky-500 rounded-sm text-white" *ngIf="addNewTask && ni === i" (click)="addTask(column.todos)" > Agregar </button> </div> </div> <button (click)="addColumn()" class="bg-white rounded p-1 font-bold w-72 shrink-0" > + Columna </button> </div>
Y en el ts muy similar a lo visto en clase (por no decir que igual, jeje). Solo me falta trabajar un poco en el tipado del CdkDragDrop<any[]>
dropColumn(event: CdkDragDrop<any[]>) { console.log(event); if (event.previousContainer === event.container) { moveItemInArray(this.columns, event.previousIndex, event.currentIndex); } else { transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex ) } }
comparto un poco de mi avance :)
Así vamos ... excelente curso.
Si están usando la sintaxis de angular 17:
<div class="flex flex-col h-screen"> <app-nav-bar></app-nav-bar> <div class="w-full grow bg-sky-600 px-4 pb-4"> <div class="flex flex-col h-full"> <div> <h2 class="text-xl font-bold text-white my-4">Demo Board</h3> </div> <div class="flex grow items-start w-full h-full overflow-x-scroll" cdkDropListGroup> @for (column of columns; track column) { <div class="rounded bg-gray-200 w-72 p-2 mr-3 shrink-0"> <h3 class="text-sm font-bold ml-2"> {{column.title}} </h4> <div class="text-sm mt-2 min-h-[2.5rem]" cdkDropList (cdkDropListDropped)="drop($event)" [cdkDropListData]="column.tasks"> @for (task of column.tasks; track task.id) { <div (click)="openDialog()" cdkDrag class="bg-white shadow p-2 rounded mt-2 border-b border-x-gray-300 cursor-pointer hover:bg-gray-400"> {{ task.title }} </div> } </div> </div> } <button (click)="addColumn()">Add Column</button> </div> </div> </div> </div>
Me está gustando mucho el curso
Para conectar Overlays a elementos dinámicos pueden hacer lo siguiente: Declarar el trigger en .TS:
triggerOrigin: any;
Ese será la entrada de la propiedad 'cdkConnectedOverlayOrigin'
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="triggerOrigin"
Luego crean un método Toggle:
toggle(trigger: any) { this.triggerOrigin = trigger; this.isOpen = !this.isOpen }
Luego tomamos el Toggle como parámetro pasándolo al template:
<button class="btn btn-primary" cdkOverlayOrigin #trigger="cdkOverlayOrigin" (click)="toggle(trigger)">Open overlay</button> ... <div style="margin-top:100px"> <button *ngFor="let btn of btns" cdkOverlayOrigin #trigger="cdkOverlayOrigin" (click)="toggle(trigger)">{{btn}}</button> </div>
Y listo, ya tendrían su Overlay en elementos dinámicos.
Tenia problemas para agregar la directiva [cdkDropListConnectedTo] de forma dinamica, asi que lo solucione de la siguiente forma:
Ts:
Agregue una nueva propiedad a la interfaz
export interface Column { id: string; title: string; todos: ToDo[]; }
y una vez agregado un id a cada columna agrege un getter para obtener la lista de los ids:
get columnsIds() { return this.columns.map(column => column.id); }
Y finalmente agregue cada id a cada elemento del dom y la directiva cdkDropListConnectedTo:
<div class="text-sm mt-2 min-h-[2.5rem]" cdkDropList (cdkDropListDropped)="drop($event)" [cdkDropListData]="column.todos" [id]="column.id" [cdkDropListConnectedTo]="columnsIds" >
Excelente! Gracias por el aporte
Mi solucion para el desafio de mover las columnas en Angular 19:
import { Component, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CdkDragDrop, CdkDrag, CdkDropList, moveItemInArray, CdkDropListGroup, transferArrayItem, } from '@angular/cdk/drag-drop'; import { NavbarComponent } from '../../components/navbar/navbar.component'; import { Column, Todo } from '../../models/todo.model'; @Component({ selector: 'app-board', imports: [ CdkDrag, CdkDropList, CdkDropListGroup, NavbarComponent, FormsModule, ], templateUrl: './board.component.html', styles: [ ` /* Animate items as they're being sorted. */ .cdk-drop-list-dragging .cdk-drag { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } /* Animate an item that has been dropped. */ .cdk-drag-animating { transition: transform 300ms cubic-bezier(0, 0, 0.2, 1); } `, ], }) export class BoardComponent { newColumnName = signal(''); newTodoName = signal(''); isOpenFormColumn = signal(false); columns = signal<Column[]>([ { title: 'To Do', todos: [ { id: '1', title: 'Make dishes', }, { id: '2', title: 'Buy a unicorn', }, ], }, { title: 'Doing', todos: [ { id: '3', title: 'Watch Angular Path in Platzi', }, ], }, { title: 'Done', todos: [ { id: '4', title: 'Play video games', }, ], }, ]); drop(event: CdkDragDrop<Todo[]>) { transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex, ); // moveItemInArray(this.todos, event.previousIndex, event.currentIndex); } dropHorizontal(event: CdkDragDrop<Column[]>) { moveItemInArray(this.columns(), event.previousIndex, event.currentIndex); } addColumn() { this.columns.update((prev) => [ ...prev, { title: this.newColumnName(), todos: [], }, ]); this.newColumnName.set(''); } openFormColum() { this.isOpenFormColumn.set(true); } closeFormColum() { this.isOpenFormColumn.set(false); } addTodo(title: string) { const index = this.columns().findIndex((column) => column.title === title); this.columns.update((prev) => { const newTodos: Todo[] = [ ...prev[index].todos, { id: Math.random().toString(), title: this.newTodoName(), }, ]; prev[index].todos = newTodos; return prev; }); this.newTodoName.set(''); this.closeFormTodo(title); } openFormTodo(title: string) { this.columns.update((prev) => prev.map((column) => { if (column.title === title) { column.isOpemFormCard = true; return column; } return column; }), ); } closeFormTodo(title: string) { this.columns.update((prev) => prev.map((column) => { if (column.title === title) { column.isOpemFormCard = false; return column; } return column; }), ); } }
Aqui les dejo mi model:
export interface Todo { id: string; title: string; } export interface Column { title: string; todos: Todo[]; isOpemFormCard?: boolean; }
Mi HTML:
<div class="flex flex-col h-screen"> <app-navbar></app-navbar> <div class="w-full grow bg-sky-600 px-4 pb-4"> <div class="flex flex-col h-full"> <div> <h2 class="text-xl font-bold text-white my-4">Demo Board</h3> </div> <div class="flex grow items-start w-full h-full overflow-x-scroll" cdkDropListGroup cdkDropList (cdkDropListDropped)="dropHorizontal($event)" [cdkDropListData]="columns()" cdkDropListOrientation="horizontal" > @for (column of columns(); track column.title) { <div class="rounded bg-gray-200 w-72 p-2 mr-3 shrink-0" cdkDrag> <div class="flex justify-between py-1"> <h3 class="text-sm font-bold ml-2">{{ column.title }}</h4> </div> <div class="text-sm py-2" cdkDropList (cdkDropListDropped)="drop($event)" [cdkDropListData]="column.todos" > @for (todo of column.todos; track todo.id) { <div cdkDrag class="bg-white shadow p-2 rounded mt-2 border-b border-x-gray-300 cursor-pointer hover:bg-gray-400" > {{ todo.title }} </div> } </div> @if (column.isOpemFormCard) { <div class="flex flex-col bg-gray-200 py-1 shrink-0 rounded-lg space-y-2" > <input type="text" class="rounded-lg" placeholder="New todo name" [(ngModel)]="newTodoName" /> <div> <button (click)="addTodo(column.title)" class="px-2 py-1 text-gray-800 bg-sky-600 rounded-lg" > + Add card </button> <button (click)="closeFormTodo(column.title)" class="px-2 py-1 text-gray-500" > x </button> </div> </div> } @else { <button (click)="openFormTodo(column.title)" class="px-2 py-1 text-gray-500" > + Add a card </button> } </div> } @if (isOpenFormColumn()) { <div class="flex flex-col w-72 px-1 bg-gray-200 py-1 shrink-0 mr-3 rounded-lg space-y-2" > <input type="text" class="rounded-lg" placeholder="New column name" [(ngModel)]="newColumnName" /> <div> <button (click)="addColumn()" class="px-2 py-1 text-gray-800 bg-sky-600 rounded-lg" > + Add column </button> <button (click)="closeFormColum()" class="px-2 py-1 text-gray-500" > x </button> </div> </div> } @else { <button class="bg-gray-200 py-2 px-5 w-72 rounded-lg bg-opacity-45 shrink-0 text-left outline-none mr-3" (click)="openFormColum()" > + Add column </button> } </div> </div> </div> </div>
Nicobytes, es uno de los mejores profesores en esta plataforma.
Para los que esten usando Angular 17 con signals, asi fue como logre arrastrar las columnas entre si:
En el HTML coloqué los siguientes atributos:
No hay que olvidarnos que como nuestro "columns" es un signal, tenemos que subscribirnos a el "[cdkDropListData]="columns()""
Y en el div de cada columna es decir la que tiene la clase "rounded" coloqué el "cdkDrag"
<div class="flex grow items-start w-full h-full overflow-x-scroll" cdkDropListGroup cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropColumns($event)" [cdkDropListData]="columns" > @for (column of columns(); track column) { <div class="rounded bg-gray-200 w-72 p-2 mr-3 shrink-0" cdkDrag> <div class="flex justify-between py-1"> <h3 class="text-sm font-bold ml-2">{{ column.title }}</h4> </div> ```Por ultimo en el TypeScript simplemente cree la función dropColumns que mueve los arrays dentro de las columnas: ```js dropColumns(event: CdkDragDrop<Column[]>) { moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); } ```Recuerda que"columns" debe de ser un signal: ```ts columns = signal<Column[]>([]); addColumn() { this.columns.update((columns) => [...columns, {title: 'New Column', todos: []}]) } ```Gracias :)
Para resolver el reto de Drag&Drop horizontal, se pueden apoyar de la siguiente documentación, realmente no es muy complicado. Saludos 😉
https://material.angular.io/cdk/drag-drop/overview#cdk-drag-drop-horizontal-sorting
Va quedando bien, lo que sigue es hacer las funcionalidades de Add card, rename list, delete list
Mi solución a los retos: Para el scroll cree un component.scss:
/* Estilos para navegadores webkit */ /* Cambiar el color del pulgar y de la pista del scroll */ ::-webkit-scrollbar-track { background-color: rgba(0, 0, 0, 0.11); border-radius: 10px; } ::-webkit-scrollbar { width: 8px; background-color: gray; border-radius: 10px; } ::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.432); border-radius: 10px; }
Para el input: En component.ts:
newTitle = new FormControl(''); isAddingCol = false; addColumn(){ if (this.isAddingCol === false){ this.isAddingCol = true; // para el focus del input, se le asigna una vez ya este en pantalla setTimeout(() => { document.getElementById('newTitle')?.focus(); }, 10); }else{ this.columns.push({ title: this.newTitle.value!, todos: [], }) this.isAddingCol = false; this.newTitle.setValue(""); } }
Reto Cumplido!
En las respuestas les pasare el código …, si pueden mejorar algo, me gustaría que lo comentaran
html
<div class="flex flex-col h-screen"> <app-navbar></app-navbar> <div class="w-full grow bg-sky-600 px-4 pb-4"> <div class="flex flex-col h-full"> <div> <h2 class="text-xl font-bold text-white my-4">Demo Board</h3> </div> <div class="flex grow overflow-x-scroll h-full" cdkDropList cdkDropListOrientation="horizontal" (cdkDropListDropped)="dropHorizontal($event)"> <div class="flex grow items-start w-full h-full overflow-x-scroll" cdkDropListGroup> <div class="rounded bg-gray-200 w-72 p-2 mr-3 shrink-0" *ngFor="let column of columns" cdkDrag> <div class="flex justify-between py-1 pr-2"> <h3 class="text-sm font-bold ml-2">{{ column.title }}</h4> <button type="button" (click)="column.isOpenList = !column.isOpenList" cdkOverlayOrigin #overlayList="cdkOverlayOrigin"> <fa-icon [icon]="faEllipsis"></fa-icon> </button> <ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="overlayList" [cdkConnectedOverlayOpen]="column.isOpenList"> <div class="divide-y bg-white w-72 p-2 space-y-2"> <div class="flex justify-center relative"> <p>List Actions</p> <button class="absolute right-0 mr-2" (click)="column.isOpenList = !column.isOpenList"> <fa-icon class="w-full" [icon]="faXmark"></fa-icon> </button> </div> <div class="text-slate-700"> <p class="p-2">Add card...</p> <p class="p-2">Copy list...</p> <p class="p-2">Move list...</p> <p class="p-2">Watch</p> </div> <div class="text-slate-700"> <p class="p-2">Sort by...</p> </div> </div> </ng-template> </div> <div class="text-sm mt-2 min-h-[2.5rem]" cdkDropList (cdkDropListDropped)="onDrop($event)" [cdkDropListData]="column.todos"> <div *ngFor="let todo of column.todos" cdkDrag class="bg-white shadow p-2 rounded mt-2 border-b border-x-gray-300 cursor-pointer hover:bg-gray-400"> {{ todo.title }} </div> </div> <cdk-accordion> <cdk-accordion-item #accordionAdd="cdkAccordionItem"> <button class="w-full pt-3 pb-2 px-2 font-semibold text-slate-500 text-sm flex justify-between" type="button" [style.display]="accordionAdd.expanded ? 'none' : ''" (click)="accordionAdd.toggle()"> + Add a card <fa-icon [icon]="faImages"></fa-icon> </button> <div [style.display]="accordionAdd.expanded ? '' : 'none'"> <input class="w-full min-h-[5.0rem] p-2 mt-2 bg-white rounded border-b border-x-gray-300" placeholder="Enter a title for this card ..." [(ngModel)]="isNameTodo"/> <div class="flex items-center justify-between mt-2"> <div class="flex space-x-4"> <app-btn [colorBtn]="'sky'" (click)="addTodo(column.title)" (click)="accordionAdd.toggle()">Add card</app-btn> <button (click)="accordionAdd.toggle()"> <fa-icon class="w-full" [icon]="faXmark"></fa-icon> </button> </div> <fa-icon class="mr-2" [icon]="faEllipsis"></fa-icon> </div> </div> </cdk-accordion-item> </cdk-accordion> </div> <cdk-accordion> <cdk-accordion-item #accordionAddColumn="cdkAccordionItem"> <button class="rounded bg-gray-200 w-72 p-2 mr-3 shrink-0 text-sm font-bold text-left" type="button" (click)="accordionAddColumn.toggle()" [style.display]="accordionAddColumn.expanded ? 'none' : ''"> + Add Column </button> <div class="rounded bg-gray-200 w-72 p-2 mr-3 shrink-0" [style.display]="accordionAddColumn.expanded ? '' : 'none'"> <input class="w-full min-h-[5.0rem] p-2 mt-2 bg-white rounded border-b border-x-gray-300" placeholder="Enter a title for this column ..." [(ngModel)]="isNameColumn"/> <div class="flex items-center justify-between mt-2"> <div class="flex space-x-4"> <app-btn [colorBtn]="'sky'" (click)="addColumn()" (click)="accordionAddColumn.toggle()">Add column</app-btn> <button (click)="accordionAddColumn.toggle()"> <fa-icon class="w-full" [icon]="faXmark"></fa-icon> </button> </div> <fa-icon class="mr-2" [icon]="faEllipsis"></fa-icon> </div> </div> </cdk-accordion-item> </cdk-accordion> </div> </div> </div> </div> </div>
Ts
import { Component } from '@angular/core'; import { faImages } from '@fortawesome/free-regular-svg-icons'; import { faEllipsis,faXmark } from '@fortawesome/free-solid-svg-icons'; import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; import { Todo } from 'src/app/model/Todo.model'; import { Column } from 'src/app/model/Column.model'; @Component({ selector: 'app-board', templateUrl: './board.component.html', styleUrls: ['./board.component.scss'] }) export class BoardComponent { faImages = faImages; faEllipsis = faEllipsis; faXmark = faXmark; // Auxiliar isOpenList = false; isNameTodo = ''; isNameColumn = ''; columns: Column[] = [ { title: 'Todo', isOpenList: false, todos: [ { id:'1', title:'Task 1' }, { id:'2', title:'Task 2' }, { id:'3', title:'Task 3' } ] }, { title: 'Doing', isOpenList: false, todos: [ { id:'4', title:'Task 4' }, { id:'5', title:'Task 5' }, { id:'6', title:'Task 6' } ] }, { title: 'Done', isOpenList: false, todos: [ { id:'7', title:'Task 7' }, { id:'8', title:'Task 8' }, { id:'9', title:'Task 9' } ] } ] onDrop(event: CdkDragDrop<any[]>){ if (event.previousContainer === event.container) { moveItemInArray(event.container.data, event.previousIndex, event.currentIndex) } else { transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex ) } } addColumn(){ this.columns.push({ title: this.isNameColumn, isOpenList: false, todos: [] }) } addTodo(columnCurrent: string){ // Obtener el índice de la columna "Doing" const todoIndex = this.columns.findIndex(column => column.title === columnCurrent); let lastTodoId = '0'; // Inicializar el ID en '0' // Obtener el último ID dentro del arreglo de todos de la columna del todo, si es hay items if (this.columns[todoIndex].todos.length > 0) { lastTodoId = this.columns[todoIndex].todos[this.columns[todoIndex].todos.length - 1].id; } // Convertir el último ID en un número entero y sumarle 1 const newId = parseInt(lastTodoId) + 1; // Crear un nuevo todo con el ID dinámico y el título ingresado const newTodo = { id: newId.toString(), title: this.isNameTodo }; // Agregar el nuevo todo al arreglo de todos de la columna "Doing" console.log(newTodo); this.columns[todoIndex].todos.push(newTodo); this.isNameTodo = ''; } dropHorizontal (event: CdkDragDrop<any[]>) { console.log('weii') moveItemInArray(this.columns, event.previousIndex, event.currentIndex); } }