diff --git a/.gitignore b/.gitignore index 86d943a..301cefc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ speed-measure-plugin*.json *.sublime-workspace # IDE - VSCode +.vscode/ .vscode/* !.vscode/settings.json !.vscode/tasks.json diff --git a/angular.json b/angular.json index 1c7e6d6..aa8bc82 100644 --- a/angular.json +++ b/angular.json @@ -28,6 +28,7 @@ "src/assets" ], "styles": [ + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "./node_modules/bootstrap/dist/css/bootstrap.min.css", "./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css", "src/styles.scss" @@ -94,6 +95,7 @@ "src/assets" ], "styles": [ + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "./node_modules/bootstrap/dist/css/bootstrap.min.css", "./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css", "src/styles.scss" @@ -129,5 +131,8 @@ } } }, - "defaultProject": "ng-peti" + "defaultProject": "ng-peti", + "cli": { + "analytics": false + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 90e9696..aa3845c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -199,6 +199,22 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.1.1.tgz", "integrity": "sha512-IvKv8sV0ymbzDEX2ZLW+F6nOTQqDYallHexuzRVT9txvNE8TNHyySvLcyC5dTmX9fj9LA72NZ6nFyhxq0LFvtQ==" }, + "@angular/cdk": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.4.tgz", + "integrity": "sha512-iw2+qHMXHYVC6K/fttHeNHIieSKiTEodVutZoOEcBu9rmRTGbLB26V/CRsfIRmA1RBk+uFYWc6UQZnMC3RdnJQ==", + "requires": { + "parse5": "^5.0.0" + }, + "dependencies": { + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "optional": true + } + } + }, "@angular/cli": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-9.1.1.tgz", @@ -470,6 +486,11 @@ "integrity": "sha512-T+/0X2VnmgW/vzynqYTVv29qtebNvrCB/yJqtNIlqXvBjcB8XRRwZPDZvRyl5BiwEPSsJnjdRFNH9krQHxYp+g==", "dev": true }, + "@angular/material": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-9.2.4.tgz", + "integrity": "sha512-LkoTXE6B0slvMhvfZDdPWaz4yaYLkaAp5VSPunI9pxGsPxzqEV9e210wC1/sjG/76Nk8Ep7/2z9XKac8Q9bMwA==" + }, "@angular/platform-browser": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.1.1.tgz", diff --git a/package.json b/package.json index c512cf5..32704fc 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "private": true, "dependencies": { "@angular/animations": "~9.1.1", + "@angular/cdk": "^9.2.4", "@angular/common": "~9.1.1", "@angular/compiler": "~9.1.1", "@angular/core": "~9.1.1", "@angular/forms": "~9.1.1", + "@angular/material": "^9.2.4", "@angular/platform-browser": "~9.1.1", "@angular/platform-browser-dynamic": "~9.1.1", "@angular/router": "~9.1.1", diff --git a/src/app/app.component.html b/src/app/app.component.html index f348a5a..c34ed49 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -4,27 +4,3 @@
- -
-
-
-
- - - - -
-
-
- Valido {{profileForm.valid}} - -
- Touched {{profileForm.touched}} -
-
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b31f041..0dfc324 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,32 +1,17 @@ import { Component } from '@angular/core'; -import { FormGroup, FormControl, Form, Validators, AbstractControl } from '@angular/forms'; +import { + FormGroup, + FormControl, + Validators, + AbstractControl, + ValidatorFn, +} from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + styleUrls: ['./app.component.scss'], }) export class AppComponent { title = 'Todo'; - profileForm = new FormGroup({ - firstName: new FormControl('', [Validators.required, Validators.maxLength(10)]), - lastName: new FormControl('', [Validators.required, StartsWithAValidator]), - }) - constructor () { - this.profileForm.valueChanges.subscribe(value => console.log(value)); - } - - onSubmit() { - console.log(this.profileForm.value); - } - initialize() { - this.profileForm.reset() - } } - -export function StartsWithAValidator(control: AbstractControl) { - if (!control.value.startsWith('A')) { - return { startsWithA: true }; - } - return null; -} \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2b82464..19ea5af 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,5 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; - import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -8,9 +7,15 @@ import { TodoAppComponent } from './todo-app/todo-app.component'; import { TodoFormComponent } from './todo-form/todo-form.component'; import { TodoListComponent } from './todo-list/todo-list.component'; import { TodoFooterComponent } from './todo-footer/todo-footer.component'; -import { TodoService } from './todo.service'; import { StatsComponent } from './stats/stats.component'; import { ReactiveFormsModule } from '@angular/forms'; +import { MatTableModule } from '@angular/material/table'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; @NgModule({ declarations: [ @@ -25,9 +30,16 @@ import { ReactiveFormsModule } from '@angular/forms'; BrowserModule, AppRoutingModule, BrowserAnimationsModule, - ReactiveFormsModule + ReactiveFormsModule, + MatTableModule, + MatButtonModule, + MatCheckboxModule, + MatIconModule, + MatProgressBarModule, + MatFormFieldModule, + MatInputModule, ], providers: [], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/custom-error-matcher.ts b/src/app/custom-error-matcher.ts new file mode 100644 index 0000000..d824f91 --- /dev/null +++ b/src/app/custom-error-matcher.ts @@ -0,0 +1,8 @@ +import { FormControl, NgForm, FormGroupDirective } from '@angular/forms'; +import { ErrorStateMatcher } from '@angular/material/core'; + +export class CustomErrorStateMatcher implements ErrorStateMatcher { + isErrorState(control: FormControl, form: NgForm | FormGroupDirective | null) { + return control && control.invalid && control.touched; + } +} diff --git a/src/app/custom-validators.ts b/src/app/custom-validators.ts new file mode 100644 index 0000000..e1ba977 --- /dev/null +++ b/src/app/custom-validators.ts @@ -0,0 +1,5 @@ +import { ValidatorFn, AbstractControl, Validators } from '@angular/forms'; + +export const isUrl = Validators.pattern( + /^(http[s]?:\/\/){0,1}(www\.){0,1}[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,5}[\.]{0,1}/ +); diff --git a/src/app/local-storage.service.spec.ts b/src/app/local-storage.service.spec.ts deleted file mode 100644 index ba1dbd4..0000000 --- a/src/app/local-storage.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { LocalStorageService } from './local-storage.service'; - -describe('LocalStorageService', () => { - let service: LocalStorageService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(LocalStorageService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/local-storage.service.ts b/src/app/local-storage.service.ts deleted file mode 100644 index 204c7fc..0000000 --- a/src/app/local-storage.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class LocalStorageService { - // https://developer.mozilla.org/es/docs/Web/API/Window/localStorage - constructor() { } - getName() { - return 'LocalStorageService' - } -} diff --git a/src/app/model/todo-item.ts b/src/app/model/todo-item.ts index bdb4f8b..7738327 100644 --- a/src/app/model/todo-item.ts +++ b/src/app/model/todo-item.ts @@ -1,7 +1,8 @@ export class TodoItem { id: number; description: string; - isCompleted: boolean = false; + url: string; + isCompleted = false; toggleCompleted() { this.isCompleted = !this.isCompleted; diff --git a/src/app/stats/stats.component.html b/src/app/stats/stats.component.html index b599e29..05aa977 100644 --- a/src/app/stats/stats.component.html +++ b/src/app/stats/stats.component.html @@ -1,6 +1 @@ -
-{{completedPercentage()}} % -
+ diff --git a/src/app/stats/stats.component.scss b/src/app/stats/stats.component.scss index 3543841..e69de29 100644 --- a/src/app/stats/stats.component.scss +++ b/src/app/stats/stats.component.scss @@ -1,6 +0,0 @@ -.progressbar { - background-color: green; - height: 50px; - font-size: x-large; - transition: width 0.3s; -} diff --git a/src/app/stats/stats.component.ts b/src/app/stats/stats.component.ts index 1ec80aa..99ed0c2 100644 --- a/src/app/stats/stats.component.ts +++ b/src/app/stats/stats.component.ts @@ -4,17 +4,16 @@ import { TodoService } from '../todo.service'; @Component({ selector: 'app-stats', templateUrl: './stats.component.html', - styleUrls: ['./stats.component.scss'] + styleUrls: ['./stats.component.scss'], }) -export class StatsComponent implements OnInit { +export class StatsComponent { + constructor(private service: TodoService) {} - constructor( - private service: TodoService - ) { } - - ngOnInit(): void { - } completedPercentage() { - return Math.round(this.service.completedSize() / this.service.list.length * 100) || 0 + return ( + Math.round( + (this.service.completedSize() / this.service.list.length) * 100 + ) || 0 + ); } } diff --git a/src/app/todo-app/todo-app.component.html b/src/app/todo-app/todo-app.component.html index 786cc57..3c1288b 100644 --- a/src/app/todo-app/todo-app.component.html +++ b/src/app/todo-app/todo-app.component.html @@ -1,12 +1,15 @@ - + - - - \ No newline at end of file + + + diff --git a/src/app/todo-app/todo-app.component.ts b/src/app/todo-app/todo-app.component.ts index cc42271..6981d90 100644 --- a/src/app/todo-app/todo-app.component.ts +++ b/src/app/todo-app/todo-app.component.ts @@ -1,29 +1,36 @@ -import { Component, OnInit } from '@angular/core'; -import {TodoItem} from '../model/todo-item'; -import { element } from 'protractor'; +import { Component } from '@angular/core'; +import { TodoItem } from '../model/todo-item'; import { TodoService } from '../todo.service'; +import { MatTableDataSource } from '@angular/material/table'; /** */ @Component({ selector: 'app-todo', templateUrl: './todo-app.component.html', styleUrls: ['./todo-app.component.scss'], }) -export class TodoAppComponent { - - constructor( - private service: TodoService - ) {} +export class TodoAppComponent { + constructor(private service: TodoService) {} + itemToEdit: TodoItem = null; getList() { return this.service.list; } + getDataSource() { + return new MatTableDataSource(this.service.list); + } onTodoItemRemoved(id) { this.service.remove(id); } onItemStateChanged(item: TodoItem) { item.toggleCompleted(); } - onTodoItemCreated(task) { - this.service.add(task) + onTodoItemCreated(task: TodoItem) { + this.service.add(task); + } + onTodoItemUpdated(task: TodoItem) { + this.service.update(task); + } + onItemEdit(item: TodoItem) { + this.itemToEdit = item; } } diff --git a/src/app/todo-footer/todo-footer.component.html b/src/app/todo-footer/todo-footer.component.html index a0f7423..fcebda2 100644 --- a/src/app/todo-footer/todo-footer.component.html +++ b/src/app/todo-footer/todo-footer.component.html @@ -1,2 +1,4 @@ -

Tasks Completed {{completedSize()}}

-

Tasks To do {{incompletedSize()}}

+ diff --git a/src/app/todo-footer/todo-footer.component.scss b/src/app/todo-footer/todo-footer.component.scss index e69de29..6155f23 100644 --- a/src/app/todo-footer/todo-footer.component.scss +++ b/src/app/todo-footer/todo-footer.component.scss @@ -0,0 +1,7 @@ +.footer { + margin-top: 0.5em; +} + +h2 { + margin: 0; +} diff --git a/src/app/todo-footer/todo-footer.component.ts b/src/app/todo-footer/todo-footer.component.ts index 6e1af04..2e58acb 100644 --- a/src/app/todo-footer/todo-footer.component.ts +++ b/src/app/todo-footer/todo-footer.component.ts @@ -1,30 +1,23 @@ import { Component, OnInit, Input } from '@angular/core'; -import { TodoItem } from '../model/todo-item'; import { TodoService } from '../todo.service'; @Component({ selector: 'app-todo-footer', templateUrl: './todo-footer.component.html', - styleUrls: ['./todo-footer.component.scss'] + styleUrls: ['./todo-footer.component.scss'], }) -export class TodoFooterComponent implements OnInit { +export class TodoFooterComponent { countTodo = 0; countCompleted = 0; @Input() list; - constructor( - private service: TodoService - ) { } + constructor(private service: TodoService) {} - ngOnInit() { - - } incompletedSize() { - this.countTodo = this.service.incompletedSize() + this.countTodo = this.service.incompletedSize(); return this.countTodo; } completedSize() { - this.countCompleted =this.service.completedSize() + this.countCompleted = this.service.completedSize(); return this.countCompleted; } - } diff --git a/src/app/todo-form/todo-form.component.html b/src/app/todo-form/todo-form.component.html index ec0ad47..9fed148 100644 --- a/src/app/todo-form/todo-form.component.html +++ b/src/app/todo-form/todo-form.component.html @@ -1,10 +1,28 @@ - - - - - - +
+ + Description + + + + Url + + + +
diff --git a/src/app/todo-form/todo-form.component.scss b/src/app/todo-form/todo-form.component.scss index 213bb36..c4af853 100644 --- a/src/app/todo-form/todo-form.component.scss +++ b/src/app/todo-form/todo-form.component.scss @@ -1,3 +1,11 @@ -.add-todo { - width: 80% +.form-field { + width: 80%; +} + +form { + margin-bottom: 0.5em; +} + +#btn-submit { + margin-left: 1em; } diff --git a/src/app/todo-form/todo-form.component.ts b/src/app/todo-form/todo-form.component.ts index 3366020..14cc97e 100644 --- a/src/app/todo-form/todo-form.component.ts +++ b/src/app/todo-form/todo-form.component.ts @@ -1,24 +1,62 @@ -import { Component, OnInit, Output, EventEmitter} from '@angular/core'; +import { + Component, + Output, + EventEmitter, + Input, + OnChanges, +} from '@angular/core'; import { TodoItem } from '../model/todo-item'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; +import * as customValidators from '../custom-validators'; +import { CustomErrorStateMatcher } from '../custom-error-matcher'; @Component({ selector: 'app-todo-form', templateUrl: './todo-form.component.html', - styleUrls: ['./todo-form.component.scss'] + styleUrls: ['./todo-form.component.scss'], }) -export class TodoFormComponent { +export class TodoFormComponent implements OnChanges { + @Input() itemToEdit: TodoItem = null; + @Output() add = new EventEmitter(); + @Output() update = new EventEmitter(); - @Output() add = new EventEmitter(); + errorMatcher = new CustomErrorStateMatcher() - save(description){ - if(!description.value || description.value === '') { - return; + editMode = false; + taskForm = new FormGroup({ + description: new FormControl('', [Validators.required]), + url: new FormControl('', [Validators.required, customValidators.isUrl]), + }); + + ngOnChanges(): void { + if (this.itemToEdit) { + this.editMode = true; + this.taskForm.patchValue(this.itemToEdit); } - let task = new TodoItem(); - task.description = description.value; - task.isCompleted = false; - this.add.emit(task); - description.value = ''; } -} + get description() { + return this.taskForm.get('description'); + } + get url() { + return this.taskForm.get('url'); + } + + onSubmit() { + const task = new TodoItem(); + task.description = this.description.value; + task.url = this.url.value; + + if (this.editMode) { + task.id = this.itemToEdit.id; + task.isCompleted = this.itemToEdit.isCompleted; + this.update.emit(task); + } else { + task.isCompleted = false; + this.add.emit(task); + } + this.itemToEdit = null; + this.editMode = false; + this.taskForm.reset(); + } +} diff --git a/src/app/todo-list/todo-list.component.html b/src/app/todo-list/todo-list.component.html index 127ec76..2d67a74 100644 --- a/src/app/todo-list/todo-list.component.html +++ b/src/app/todo-list/todo-list.component.html @@ -1,11 +1,32 @@ -
    -
  • - {{task.description}} - - - - -
  • -
+ + + Completed + + + + + + Description + {{ task.description }} + + + Url + {{ task.url }} + + + Actions + + + + + + + + diff --git a/src/app/todo-list/todo-list.component.scss b/src/app/todo-list/todo-list.component.scss index 0096849..e69de29 100644 --- a/src/app/todo-list/todo-list.component.scss +++ b/src/app/todo-list/todo-list.component.scss @@ -1,15 +0,0 @@ -.completed { - background-color: greenyellow; -} -.list { - padding: 0; -} -.list-item { - margin: 0; - list-style-type: none; - font-size: 2rem; - border-bottom: 1px solid lightgrey; - width: 80%; - display: flex; - justify-content: space-between; -} diff --git a/src/app/todo-list/todo-list.component.ts b/src/app/todo-list/todo-list.component.ts index fc5d8f0..7ac7a12 100644 --- a/src/app/todo-list/todo-list.component.ts +++ b/src/app/todo-list/todo-list.component.ts @@ -1,26 +1,30 @@ -import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; import { TodoItem } from '../model/todo-item'; +import { MatTableDataSource } from '@angular/material/table'; @Component({ selector: 'app-todo-list', templateUrl: './todo-list.component.html', - styleUrls: ['./todo-list.component.scss'] + styleUrls: ['./todo-list.component.scss'], }) -export class TodoListComponent implements OnInit { - @Input() list: any[]; - @Output() itemRemoved = new EventEmitter(); - @Output() itemStateChanged = new EventEmitter(); - constructor() { } +export class TodoListComponent { + // https://stackoverflow.com/questions/49141809/angular-input-change-detection-performance-with-mat-table-data-source + @Input() dataSource: MatTableDataSource; + @Output() itemRemoved = new EventEmitter(); + @Output() itemStateChanged = new EventEmitter(); + @Output() itemEdit = new EventEmitter(); - ngOnInit() { - } - removeItem(id) { + columnsToDisplay = ['isCompleted', 'description', 'url', 'actions']; + + removeItem(id: number) { this.itemRemoved.emit(id); } - completeTask(item:TodoItem) { + completeTask(item: TodoItem) { this.itemStateChanged.emit(item); - } -} \ No newline at end of file + editItem(item: TodoItem) { + this.itemEdit.emit(item); + } +} diff --git a/src/app/todo.service.ts b/src/app/todo.service.ts index 9bba8cc..579ef4a 100644 --- a/src/app/todo.service.ts +++ b/src/app/todo.service.ts @@ -1,16 +1,13 @@ import { Injectable } from '@angular/core'; -import { LocalStorageService } from './local-storage.service'; +import { TodoItem } from './model/todo-item'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TodoService { - list = []; lastItemId = 0; - constructor(private storage: LocalStorageService) { } - add(task) { const id = this.lastItemId; task.id = id; @@ -18,20 +15,20 @@ export class TodoService { this.lastItemId += 10; } + update(task: TodoItem) { + const index = this.list.findIndex((element) => element.id === task.id); + this.list.splice(index, 1, task); + } + remove(id) { const index = this.list.findIndex((element) => element.id === id); this.list.splice(index, 1); } incompletedSize() { - return this.list.filter(item => !item.isCompleted).length; - + return this.list.filter((item) => !item.isCompleted).length; } completedSize() { - return this.list.filter(item => item.isCompleted).length ; - } - - getName() { - return 'TodoService 123' + this.storage.getName(); + return this.list.filter((item) => item.isCompleted).length; } } diff --git a/src/index.html b/src/index.html index 62a7817..28d80b3 100644 --- a/src/index.html +++ b/src/index.html @@ -6,8 +6,10 @@ + + - + diff --git a/src/styles.scss b/src/styles.scss index afb57e2..66a9bc1 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -2,3 +2,6 @@ .btn { margin: 0.5rem; } + +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }