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 @@
-
-
-
-
-
-
+
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; }