Создание приложения с функциями перетаскивания, от дел к выполнению
Перетаскивание - это функция многих интерактивных веб-приложений. Он предоставляет пользователям интуитивно понятный способ манипулировать своими данными. Для приложений Angular легко добавить функцию перетаскивания.
В этой части мы создадим приложение с двумя столбцами: столбец to-do и столбец Готово. Вы можете перетаскивать между ними два, чтобы изменить статус с дел на выполненное и наоборот.
Для создания приложения мы используем библиотеку Angular Material, чтобы приложение выглядело хорошо и с легкостью предоставляли возможность перетаскивания в наше приложение. У него также будет меню навигации и верхняя панель.
Чтобы начать сборку приложения, мы устанавливаем Angular CLI, запустив npm i @angular/cli
. Мы должны включить маршрутизацию и использовать SCSS, когда вас об этом попросят.
Затем мы создаем новый проект Angular, запустив ng new todo-app
. После этого добавляем нужные нам библиотеки, запустив npm i@angular/cdk @angular/material @ngrx/store
.
Это добавит в наше приложение Angular Material и магазин NGRX. Мы будем широко использовать Flux с этим приложением. Затем мы добавляем наши компоненты и службы, выполнив следующее:
ng g component addTodoDialog ng g component homePage ng g component toolBar ng g service todo
Мы добавляем шаблон для магазина NGRX, запустив ng add @ngrx/store
.
Теперь мы можем построить логику для нашего приложения. В add-todo-dialog.component.ts
мы добавляем:
import { Component, OnInit } from '@angular/core'; import { NgForm } from '@angular/forms'; import { MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { TodoService } from '../todo.service'; import { SET_TODOS } from '../reducers/todo-reducer'; @Component({ selector: 'app-add-todo-dialog', templateUrl: './add-todo-dialog.component.html', styleUrls: ['./add-todo-dialog.component.scss'] }) export class AddTodoDialogComponent implements OnInit { todoData: any = <any>{ done: false }; constructor( public dialogRef: MatDialogRef<AddTodoDialogComponent>, private todoService: TodoService, private store: Store<any> ) { } ngOnInit() { } save(todoForm: NgForm) { if (todoForm.invalid) { return; } this.todoService.addTodo(this.todoData) .subscribe(res => { this.getTodos(); this.dialogRef.close(); }) } getTodos() { this.todoService.getTodos() .subscribe(res => { this.store.dispatch({ type: SET_TODOS, payload: res }); }) } }
Этот код предназначен для диалогового окна, которое позволяет нам добавлять элементы в список, затем получать последние элементы и помещать их в магазин.
В add-todo-dialog.component.html
мы добавляем:
<h2>Add Todo</h2> <form #todoForm='ngForm' (ngSubmit)='save(todoForm)'> <mat-form-field> <input matInput placeholder="Description" required #description='ngModel' name='description' [(ngModel)]='todoData.description'> <mat-error *ngIf="description.invalid && (description.dirty || description.touched)"> <div *ngIf="content.errors.required"> Description is required. </div> </mat-error> </mat-form-field> <br> <button mat-raised-button type='submit'>Add</button> </form>
Это форма для добавления задачи. Для простоты у него есть только одно поле - описание.
В add-todo-dialog.component.scss
, чтобы изменить ширину поля формы, мы помещаем:
form { mat-form-field { width: 100%; margin: 0 auto; } }
Далее мы создаем нашу домашнюю страницу. Здесь будут находиться два списка. Пользователь может перетаскивать между двумя списками, чтобы изменить статус задачи.
В home-page.component.ts
мы помещаем:
import { Component, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { AddTodoDialogComponent } from '../add-todo-dialog/add-todo-dialog.component'; import { TodoService } from '../todo.service'; import { Store, select } from '@ngrx/store'; import { SET_TODOS } from '../reducers/todo-reducer'; import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; @Component({ selector: 'app-home-page', templateUrl: './home-page.component.html', styleUrls: ['./home-page.component.scss'] }) export class HomePageComponent implements OnInit { allTasks: any[] = []; todo: any[] = []; done: any[] = []; constructor( public dialog: MatDialog, private todoService: TodoService, private store: Store<any> ) { store.pipe(select('todos')) .subscribe(allTasks => { this.allTasks = allTasks || []; this.todo = this.allTasks.filter(t => !t.done); this.done = this.allTasks.filter(t => t.done); }) } ngOnInit() { this.getTodos(); } openAddTodoDialog() { const dialogRef = this.dialog.open(AddTodoDialogComponent, { width: '70vw', data: {} }) dialogRef.afterClosed().subscribe(result => { console.log('The dialog was closed'); }); } getTodos() { this.todoService.getTodos() .subscribe(res => { this.store.dispatch({ type: SET_TODOS, payload: res }); }) } drop(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); } let data = event.container.data[0]; data.done = !data.done; this.todoService.editTodo(data) .subscribe(res => { }) } removeTodo(index: number, tasks: any[]) { const todoId = tasks[index].id; this.todoService.removeTodo(todoId) .subscribe(res => { this.getTodos(); }) } }
У нас есть функции для обработки удаления элементов списка дел, и мы позволяем пользователям открывать диалоговое окно добавить дело, которое мы создали ранее с помощью функции openAddTodoDialog
.
Пользователь также может удалить задачи на этой странице с помощью функции removeTodo
. Функция drop
обрабатывает переход между списками, а также переключает статус элемента списка дел.
Порядок этого важен. Блок if...else
должен стоять перед вызовом функции editTodo
, потому что иначе элемент не будет в массиве event.container.data
.
removeTodo
принимает как index
, так и список tasks
, потому что он используется как для массивов todo
, так и done
.
В home-page.component.html
мы добавляем:
<div class="center"> <h1>Todos</h1> <button mat-raised-button (click)='openAddTodoDialog()'>Add Todo</button> </div> <div class="content"> <div class="todo-container"> <h2>To Do</h2> <div cdkDropList #todoList="cdkDropList" [cdkDropListData]="todo" [cdkDropListConnectedTo]="[doneList]" class="todo-list" (cdkDropListDropped)="drop($event)"> <div class="todo-box" *ngFor="let item of todo; let i = index" cdkDrag> {{item.description}} <a class="delete-button" (click)='removeTodo(i, todo)'> <i class="material-icons"> close </i> </a> </div> </div> </div> <div class="done-container"> <h2>Done</h2> <div cdkDropList #doneList="cdkDropList" [cdkDropListData]="done" [cdkDropListConnectedTo]="[todoList]" class="todo-list" (cdkDropListDropped)="drop($event)"> <div class="todo-box" *ngFor="let item of done; let i = index" cdkDrag> {{item.description}} <a class="delete-button" (click)='removeTodo(i, done)'> <i class="material-icons"> close </i> </a> </div> </div> </div> </div>
Этот шаблон предоставляет два списка, которые пользователь может перетаскивать между собой для переключения статуса. Также есть кнопка «x» для каждого элемента списка, позволяющая пользователям удалить этот элемент.
В home-page.component.scss
мы добавляем:
$gray: gray; .content { display: flex; align-items: flex-start; margin-left: 2vw; div { width: 45vw; } } .todo-container { width: 400px; max-width: 100%; margin: 0 25px 25px 0; display: inline-block; vertical-align: top; } .todo-list { border: solid 1px $gray; min-height: 70px; background: white; border-radius: 4px; overflow: hidden; display: block; } .todo-box { padding: 20px 10px; border-bottom: solid 1px $gray; color: rgba(0, 0, 0, 3); display: flex; flex-direction: row; align-items: center; justify-content: space-between; box-sizing: border-box; cursor: move; background: white; font-size: 14px; height: 70px; } .cdk-drag-preview { box-sizing: border-box; border-radius: 4px; box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 2), 0 8px 10px 1px rgba(0, 0, 0, 1), 0 3px 14px 2px rgba(0, 0, 0, 6); } .cdk-drag-placeholder { opacity: 0; } .cdk-drag-animating { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } .todo-box:last-child { border: none; } .todo-list.cdk-drop-list-dragging .todo-box:not(.cdk-drag-placeholder) { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } .delete-button { cursor: pointer; }
Чтобы стилизовать списки и поля, чтобы у них была граница и тень.
Переходим к добавлению редукторов для нашего магазина. Мы создаем файл с именем menu-reducer.ts
, запустив ng g class menuReducer
.
Там мы добавляем:
export const SET_MENU_STATE = 'SET_MENU_STATE'; export function MenuReducer(state: boolean, action) { switch (action.type) { case SET_MENU_STATE: return action.payload; default: return state; } }
Точно так же мы создаем файл с именем todo-reducer.ts
, запустив ng g class todoReducer
, и добавляем:
const SET_TODOS = 'SET_TODOS'; function todoReducer(state, action) { switch (action.type) { case SET_TODOS: state = action.payload; return state; default: return state } } export { todoReducer, SET_TODOS };
В reducers/index.ts
мы помещаем:
import { MenuReducer } from './menu-reducer'; import { todoReducer } from './todo-reducer'; export const reducers = { menuState: MenuReducer, todos: todoReducer };
Чтобы редукторы можно было передать в StoreModule
, когда мы импортируем его в app.module.ts
. Вместе эти три файла создадут хранилище, которое мы используем для хранения всех текущих задач.
Далее, в tool-bar.component.ts
, мы помещаем:
import { Component, OnInit } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { SET_MENU_STATE } from '../reducers/menu-reducer'; @Component({ selector: 'app-tool-bar', templateUrl: './tool-bar.component.html', styleUrls: ['./tool-bar.component.scss'] }) export class ToolBarComponent implements OnInit { menuOpen: boolean; constructor( private store: Store<any> ) { store.pipe(select('menuState')) .subscribe(menuOpen => { this.menuOpen = menuOpen; }) } ngOnInit() { } toggleMenu() { this.store.dispatch({ type: SET_MENU_STATE, payload: !this.menuOpen }); } }
Чтобы пользователи могли включать и выключать левое меню. Затем в tool-bar.component.html
мы помещаем:
<mat-toolbar> <a (click)='toggleMenu()' class="menu-button"> <i class="material-icons"> menu </i> </a> Todo App </mat-toolbar>
Добавить верхнюю панель инструментов и меню.
В tool-bar.component.scss
мы добавляем:
.menu-button { margin-top: 6px; margin-right: 10px; cursor: pointer; } .mat-toolbar { background: #009688; color: white; }
Чтобы добавить некоторый интервал к нашей кнопке меню и тексту заголовка.
В app-routing.module.ts
мы заменяем существующий код на:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HomePageComponent } from './home-page/home-page.component'; const routes: Routes = [ { path: '', component: HomePageComponent }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Чтобы пользователи могли видеть домашнюю страницу.
Затем в app.component.ts
мы помещаем:
import { Component, HostListener } from '@angular/core'; import { SET_MENU_STATE } from './reducers/menu-reducer'; import { Store, select } from '@ngrx/store'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { menuOpen: boolean; constructor( private store: Store<any>, ) { store.pipe(select('menuState')) .subscribe(menuOpen => { this.menuOpen = menuOpen; }) } @HostListener('document:click', ['$event']) public onClick(event) { const isOutside = !event.target.className.includes("menu-button") && !event.target.className.includes("material-icons") && !event.target.className.includes("mat-drawer-inner-container") if (isOutside) { this.menuOpen = false; this.store.dispatch({ type: SET_MENU_STATE, payload: this.menuOpen }); } } }
Для выключения меню, когда пользователь щелкает за пределами кнопки меню и меню. В app.component.html
мы добавляем:
<mat-sidenav-container class="example-container"> <mat-sidenav mode="side" [opened]='menuOpen'> <ul> <li> <b> New York Times </b> </li> <li> <a routerLink='/'>Home</a> </li> </ul> </mat-sidenav> <mat-sidenav-content> <app-tool-bar></app-tool-bar> <div id='content'> <router-outlet></router-outlet> </div> </mat-sidenav-content> </mat-sidenav-container>
Чтобы добавить меню, левую панель навигации и элемент router-outlet
, чтобы люди могли видеть маршруты, которые мы определили.
В app.component.scss
мы добавляем:
#content { padding: 20px; min-height: 100vh; } ul { list-style-type: none; margin: 0; li { padding: 20px 5px; } }
Чтобы добавить отступы на страницы и изменить стиль списка элементов в левом боковом меню.
В environment.ts
мы добавляем:
export const environment = { production: false, apiUrl: 'http://localhost:3000' };
Чтобы добавить URL-адрес нашего API.
В styles.scss
мы добавляем:
/* You can add global styles to this file, and also import other style files */ @import "~@angular/material/prebuilt-themes/indigo-pink.css"; body { font-family: "Roboto", sans-serif; margin: 0; } form { mat-form-field { width: 95vw; margin: 0 auto; } } .center { text-align: center; }
Импортировать тему Material Design и изменить ширину поля формы.
В app.module.ts
мы заменяем существующий код на:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { HomePageComponent } from './home-page/home-page.component'; import { StoreModule } from '@ngrx/store'; import { reducers } from './reducers'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { ToolBarComponent } from './tool-bar/tool-bar.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatButtonModule } from '@angular/material/button'; import { HttpClientModule } from '@angular/common/http'; import { MatSelectModule } from '@angular/material/select'; import { MatCardModule } from '@angular/material/card'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; import { MatIconModule } from '@angular/material/icon'; import { MatGridListModule } from '@angular/material/grid-list'; import { AddTodoDialogComponent } from './add-todo-dialog/add-todo-dialog.component'; import { DragDropModule } from '@angular/cdk/drag-drop'; @NgModule({ declarations: [ AppComponent, HomePageComponent, ToolBarComponent, AddTodoDialogComponent ], imports: [ BrowserModule, AppRoutingModule, StoreModule.forRoot(reducers), FormsModule, MatSidenavModule, MatToolbarModule, MatInputModule, MatFormFieldModule, BrowserAnimationsModule, MatButtonModule, MatMomentDateModule, HttpClientModule, MatSelectModule, MatCardModule, MatListModule, MatMenuModule, MatIconModule, MatGridListModule, DragDropModule ], providers: [ ], bootstrap: [AppComponent], entryComponents: [ AddTodoDialogComponent ] }) export class AppModule { } text-align: center; }
В todo.service.ts
мы помещаем:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root' }) export class TodoService { constructor( private http: HttpClient ) { } getTodos() { return this.http.get(`${environment.apiUrl}/todos`); } addTodo(data) { return this.http.post(`${environment.apiUrl}/todos`, data); } editTodo(data) { return this.http.put(`${environment.apiUrl}/todos/${data.id}`, data); } removeTodo(id) { return this.http.delete(`${environment.apiUrl}/todos/${id}`); } }
Эти функции позволяют нам выполнять операции CRUD для наших дел, делая запросы к нашему JSON API, который мы добавим с помощью пакета JSON Server Node.js.
Данные будут сохранены в файл JSON, поэтому нам не нужно заставлять нашу собственную серверную часть добавлять их, чтобы сохранить некоторые простые данные. Устанавливаем сервер, запустив npm i -g json-server
.
Как только это будет сделано, перейдите в каталог проекта и запустите json-server --watch db.json
. В db.json
мы помещаем:
{ "todos": [] }
Чтобы мы могли использовать эти конечные точки для сохранения данных в db.json
.
После того, как все сделано, получаем: