From 4f01e351aa3eda08cf1d95e0e65c970d9fdb1422 Mon Sep 17 00:00:00 2001 From: Jipa Dragos Date: Thu, 12 Aug 2021 00:42:41 +0300 Subject: [PATCH 1/4] first nigh' --- #serve# | 5 +++++ src/app/app.component.css | 7 +++++++ src/app/app.component.html | 4 ++++ src/app/app.component.ts | 20 ++++++++++++++++++++ src/app/app.module.ts | 18 ++++++++++++++++++ src/app/media-item.component.css | 0 src/app/media-item.component.html | 12 ++++++++++++ src/app/media-item.component.ts | 17 +++++++++++++++++ src/main.ts | 4 ++++ 9 files changed, 87 insertions(+) create mode 100644 #serve# create mode 100644 src/app/app.component.css create mode 100644 src/app/app.component.html create mode 100644 src/app/app.component.ts create mode 100644 src/app/app.module.ts create mode 100644 src/app/media-item.component.css create mode 100644 src/app/media-item.component.html create mode 100644 src/app/media-item.component.ts diff --git a/#serve# b/#serve# new file mode 100644 index 000000000..c482a159a --- /dev/null +++ b/#serve# @@ -0,0 +1,5 @@ +A + + +BBBBBBBBBBBBBBasd +BBAABBBB \ No newline at end of file diff --git a/src/app/app.component.css b/src/app/app.component.css new file mode 100644 index 000000000..288225aed --- /dev/null +++ b/src/app/app.component.css @@ -0,0 +1,7 @@ +p { + color: #ffffff; +} +.description { + font-style: italic; + color: green; +} diff --git a/src/app/app.component.html b/src/app/app.component.html new file mode 100644 index 000000000..aaf5946e0 --- /dev/null +++ b/src/app/app.component.html @@ -0,0 +1,4 @@ +
+

ce zici bai

+ +
\ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts new file mode 100644 index 000000000..fe95c4700 --- /dev/null +++ b/src/app/app.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + firstMediaItem = { + id: 1, + name: 'Firebug', + medium: 'Series', + category: 'SF', + year: 2021, + watchedOn: 123123, + isFavorite: false + } + + onMediaItemDelete(mediaItem) { } +} \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 000000000..a3385e687 --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { AppComponent } from './app.component' +import { MediaItemComponent } from './media-item.component'; +@NgModule({ + imports: [ + BrowserModule + ], + declarations: [ + AppComponent, + MediaItemComponent + ], + bootstrap: [ + AppComponent + ] +}) + +export class AppModule { } diff --git a/src/app/media-item.component.css b/src/app/media-item.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/media-item.component.html b/src/app/media-item.component.html new file mode 100644 index 000000000..1060f4349 --- /dev/null +++ b/src/app/media-item.component.html @@ -0,0 +1,12 @@ +

The REdemptino

+
true
+
Action
+
2021
+
+ + remove + + + watch + +
\ No newline at end of file diff --git a/src/app/media-item.component.ts b/src/app/media-item.component.ts new file mode 100644 index 000000000..7277516de --- /dev/null +++ b/src/app/media-item.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'mw-media-item', + templateUrl: './media-item.component.html', + styleUrls: ['./media-item.component.css'] +}) + +export class MediaItemComponent { + @Input() mediaItem; + @Output() delete = new EventEmitter(); + + onDelete() { + console.log('deleted'); + this.delete.emit(this.mediaItem); + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index e69de29bb..e60eb8198 100644 --- a/src/main.ts +++ b/src/main.ts @@ -0,0 +1,4 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app/app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); \ No newline at end of file From a1b5218cbeecde07d70c1c78cf037d537ee5f4b4 Mon Sep 17 00:00:00 2001 From: Jipa Dragos Date: Thu, 12 Aug 2021 18:02:41 +0300 Subject: [PATCH 2/4] forms - validations - errorHandling --- README.md | 42 +++++++++------- src/app/app.component.css | 30 ++++++++++-- src/app/app.component.html | 8 +++- src/app/app.component.ts | 20 ++------ src/app/app.module.ts | 36 +++++++++----- src/app/category-list.pipe.ts | 16 +++++++ src/app/favorite.directive.ts | 22 +++++++++ src/app/media-item-form.component.css | 52 ++++++++++++++++++++ src/app/media-item-form.component.html | 42 ++++++++++++++++ src/app/media-item-form.component.ts | 46 ++++++++++++++++++ src/app/media-item-list.component.css | 37 +++++++++++++++ src/app/media-item-list.component.html | 10 ++++ src/app/media-item-list.component.ts | 55 +++++++++++++++++++++ src/app/media-item.component.css | 66 ++++++++++++++++++++++++++ src/app/media-item.component.html | 28 +++++++---- src/app/media-item.component.ts | 20 ++++---- src/index.html | 2 +- src/main.ts | 2 +- 18 files changed, 457 insertions(+), 77 deletions(-) create mode 100644 src/app/category-list.pipe.ts create mode 100644 src/app/favorite.directive.ts create mode 100644 src/app/media-item-form.component.css create mode 100644 src/app/media-item-form.component.html create mode 100644 src/app/media-item-form.component.ts create mode 100644 src/app/media-item-list.component.css create mode 100644 src/app/media-item-list.component.html create mode 100644 src/app/media-item-list.component.ts diff --git a/README.md b/README.md index ed221c269..a7599342f 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,24 @@ This is the repository for my course **Angular Essential Training** The full course is available at [LinkedIn Learning](https://www.linkedin.com/learning) and [lynda.com](https://lynda.com). -[LinkedIn Learning subscribers: watch here](https://www.linkedin.com/learning/angular-essential-training-2) -[Lynda.com subscribers: watch here](https://www.lynda.com/Angular-tutorials/Angular-Essential-Training/5034181-2.html) +[LinkedIn Learning subscribers: watch here](https://www.linkedin.com/learning/angular-2-essential-training-2) +[Lynda.com subscribers: watch here](https://www.lynda.com/AngularJS-tutorials/Angular-2-Essential-Training/540347-2.html) ## Course Description -Angular was designed by Google to address challenges programmers face building complex, single-page applications. This JavaScript platform provides a solid core of web functionality, letting you take care of the design and implementation details. In this course, Justin Schwartzenberger introduces you to the essentials of this "superheroic" platform, including powerful features such as two-way data binding, comprehensive routing, and dependency injection. Justin steps through the platform one feature at a time, focusing on the component-based architecture of Angular. Learn what Angular is and what it can do, as Justin builds a full-featured web app from start to finish. After mastering the essentials, you can tackle the other project-based courses in our library and create your own Angular app. +JavaScript frameworks help you code more quickly, by providing special functionality for developing specific types of web projects. Angular was designed by Google to address challenges programmers face building single-page applications. This course introduces you to the essentials of this "superheroic" framework, including declarative templates, two-way data binding, and dependency injection. Justin Schwartzenberger steps through the framework one feature at a time, focusing on the new component-based architecture of Angular. After completing this training, you'll be able to tackle the other project-based courses in our library and create your own Angular app. Topics include: - What is Angular? -- Working with components -- Binding events and properties -- Getting data to components -- Using directives and pipes -- Creating Angular forms -- Validating form data -- How Angular does dependency injection -- Making HTTP calls -- Routing -- Styling components +- Setting up an Angular template +- Creating a component +- Displaying data +- Working with events +- Using two-way data binding +- Creating a subcomponent +- Using the built-in HTTP module +- Using the built-in router module ## Instructions @@ -36,10 +34,6 @@ Topics include: 3. CD to the folder `cd angular-essential-training` - - and then fetch all of the remote branches for the repository - - `git fetch --all` 4. Run the following to install the project dependencies: @@ -53,7 +47,7 @@ Topics include: `http://localhost:4200/` -The repository has a branch for each video starting point. For example, the branch **02-01b** is used as the starting code for the video *02-01 NgModule and the root module*. You can checkout branches using `git checkout ` and not have to re-run `npm install` each time since you will remain in the same root folder. +The repository has a branch for each video starting point. For example, the branch **02-01b** is used as the starting code for the video *02-01 NgModule and the root module*. You can checkout branches using `git checkout -b ` and not have to re-run `npm install` each time since you will remain in the same root folder. ## Angular CLI @@ -62,6 +56,18 @@ This project was generated with [Angular CLI](https://github.com/angular/angular To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). +## FAQ +If you are getting a list of errors on `npm install` that look like `Cannot find name 'Promise'`, check your `package.json` file and see if the following DevDependencies have a caret in front of the version number (the ^ symbol): +```json +"devDependencies": { + + "@types/core-js": "0.9.34", + "@types/node": "6.0.41" + +} +``` +If the caret is there (would look like `"@types/core-js": "^0.9.34"`) then remove it (or copy the contents of the [package.json](https://github.com/coursefiles/angular2-essential-training/blob/master/package.json) file on the origin repository) and run `npm install` again. + ## More Stuff Check out some of my [other courses on LinkedIn Learning](https://www.linkedin.com/learning/instructors/justin-schwartzenberger?u=2125562). You can also [follow me on twitter](https://twitter.com/schwarty). \ No newline at end of file diff --git a/src/app/app.component.css b/src/app/app.component.css index 288225aed..7e7d3d759 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -1,7 +1,27 @@ -p { - color: #ffffff; +:host { + display: flex; + min-height: 100%; } -.description { - font-style: italic; - color: green; +nav { + width: 68px; + background-color: #53ace4; } +nav .icon { + width: 48px; + height: 48px; + margin: 10px; +} +section { + width: 100%; + background-color: #32435b; +} +section > header { + color: #ffffff; + padding: 10px; +} +section > header > h1 { + font-size: 2em; +} +section > header .description { + font-style: italic; +} \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index aaf5946e0..25177819b 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,8 @@
-

ce zici bai

- +
+

Media Watch List

+

Keeping track of the media I want to watch.

+
+ +
\ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fe95c4700..542df5092 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,20 +1,8 @@ import { Component } from '@angular/core'; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] + selector: 'mw-app', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] }) -export class AppComponent { - firstMediaItem = { - id: 1, - name: 'Firebug', - medium: 'Series', - category: 'SF', - year: 2021, - watchedOn: 123123, - isFavorite: false - } - - onMediaItemDelete(mediaItem) { } -} \ No newline at end of file +export class AppComponent {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a3385e687..a9713c0a1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,18 +1,28 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; -import { AppComponent } from './app.component' +import { ReactiveFormsModule } from '@angular/forms'; +import { AppComponent } from './app.component'; import { MediaItemComponent } from './media-item.component'; +import { MediaItemListComponent } from './media-item-list.component'; +import { FavoriteDirective } from './favorite.directive'; +import { CategoryListPipe } from './category-list.pipe'; +import { MediaItemFormComponent } from './media-item-form.component'; + @NgModule({ - imports: [ - BrowserModule - ], - declarations: [ - AppComponent, - MediaItemComponent - ], - bootstrap: [ - AppComponent - ] + imports: [ + BrowserModule, + ReactiveFormsModule + ], + declarations: [ + AppComponent, + MediaItemComponent, + MediaItemListComponent, + FavoriteDirective, + CategoryListPipe, + MediaItemFormComponent + ], + bootstrap: [ + AppComponent + ] }) - -export class AppModule { } +export class AppModule {} diff --git a/src/app/category-list.pipe.ts b/src/app/category-list.pipe.ts new file mode 100644 index 000000000..7e03e0237 --- /dev/null +++ b/src/app/category-list.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'categoryList' +}) +export class CategoryListPipe implements PipeTransform { + transform(mediaItems) { + const categories = []; + mediaItems.forEach(mediaItem => { + if (categories.indexOf(mediaItem.category) <= -1) { + categories.push(mediaItem.category); + } + }); + return categories.join(', '); + } +} diff --git a/src/app/favorite.directive.ts b/src/app/favorite.directive.ts new file mode 100644 index 000000000..33fd4d424 --- /dev/null +++ b/src/app/favorite.directive.ts @@ -0,0 +1,22 @@ +import { Directive, HostBinding, HostListener, Input } from '@angular/core'; + +@Directive({ + selector: '[mwFavorite]' +}) +export class FavoriteDirective { + @HostBinding('class.is-favorite') isFavorite = true; + + @HostBinding('class.is-favorite-hovering') hovering = false; + + @HostListener('mouseenter') onMouseEnter() { + this.hovering = true; + } + + @HostListener('mouseleave') onMouseLeave() { + this.hovering = false; + } + + @Input() set mwFavorite(value) { + this.isFavorite = value; + } +} diff --git a/src/app/media-item-form.component.css b/src/app/media-item-form.component.css new file mode 100644 index 000000000..15b57f9ef --- /dev/null +++ b/src/app/media-item-form.component.css @@ -0,0 +1,52 @@ +:host { + display: block; + padding: 10px; +} +ul { + list-style-type: none; +} +ul li { + margin: 10px 0; +} +header, label { + color: #53ace4; +} +input, select { + background-color: #29394b; + color: #c6c5c3; + border-radius: 3px; + border: none; + box-shadow: 0 1px 2px rgba(0,0,0,0.2) inset, 0 -1px 0 rgba(0,0,0,0.05) inset; + border-color: #53ace4; + padding: 6px; +} +.ng-invalid:not(.ng-pristine):not(.required-invalid) { + border: 1px solid #d93a3e; +} +input[required].ng-invalid { + border-right: 5px solid #d93a3e; +} +input[required]:not(.required-invalid), +input[required].ng-invalid:not(.required-invalid) { + border-right: 5px solid #37ad79; +} +.error { + color: #d93a3e; +} +#year { + width: 50px; +} +button[type=submit] { + background-color: #45bf94; + border: 0; + padding: 10px; + font-size: 1em; + border-radius: 4px; + color: #ffffff; + cursor: pointer; +} +button[type=submit]:disabled { + background-color: #333; + color: #666; + cursor: default; +} \ No newline at end of file diff --git a/src/app/media-item-form.component.html b/src/app/media-item-form.component.html new file mode 100644 index 000000000..1333140af --- /dev/null +++ b/src/app/media-item-form.component.html @@ -0,0 +1,42 @@ +
+

Add Media to Watch

+
+
+
    +
  • + + +
  • +
  • + + +
    + Name has invalid characters +
    +
  • +
  • + + +
  • +
  • + + +
    + must be beetween {{ yearErrors.year.min}} - {{ yearErrors.year.max }} +
    +
  • +
+ +
\ No newline at end of file diff --git a/src/app/media-item-form.component.ts b/src/app/media-item-form.component.ts new file mode 100644 index 000000000..14665d034 --- /dev/null +++ b/src/app/media-item-form.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit} from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +@Component({ + selector: 'mw-media-item-form', + templateUrl: './media-item-form.component.html', + styleUrls: ['./media-item-form.component.css'] +}) +export class MediaItemFormComponent implements OnInit { + form: FormGroup; + + ngOnInit() { + this.form = new FormGroup({ + medium: new FormControl('Movies'), + name: new FormControl('', Validators.compose([ + Validators.required, + Validators.pattern('[\\w\\-\\s\\/]+') + ])), + category: new FormControl(''), + year: new FormControl('', this.yearValidator), + }); + } + + yearValidator(control: FormControl) { + if (control.value.trim().length === 0) { + return null; + } + const year = parseInt(control.value, 10); + const minYear = 1800; + const maxYear = 2500; + if (year >= minYear && year < maxYear) { + return null; + } else { + return { + year: { + min: minYear, + max: maxYear + } + }; + } + } + + onSubmit(mediaItem) { + console.log(mediaItem); + } +} diff --git a/src/app/media-item-list.component.css b/src/app/media-item-list.component.css new file mode 100644 index 000000000..c6f46b3a2 --- /dev/null +++ b/src/app/media-item-list.component.css @@ -0,0 +1,37 @@ +:host { + display: flex; + height: calc(100% - 95px); + flex-direction: column; + padding: 10px; +} +header { + color: #c6c5c3; +} +header.medium-movies { + color: #53ace4; +} +header.medium-series { + color: #45bf94; +} +header > h2 { + font-size: 1.4em; +} +header > h2.error { + color: #d93a3e; +} +section { + flex: 1; + display: flex; + flex-flow: row wrap; + align-content: flex-start; +} +section > media-item { + margin: 10px; +} +footer { + text-align: right; +} +footer .icon { + width: 64px; + height: 64px; +} \ No newline at end of file diff --git a/src/app/media-item-list.component.html b/src/app/media-item-list.component.html new file mode 100644 index 000000000..ca9979e6d --- /dev/null +++ b/src/app/media-item-list.component.html @@ -0,0 +1,10 @@ +
+
{{ mediaItems | categoryList }}
+
+
+ +
\ No newline at end of file diff --git a/src/app/media-item-list.component.ts b/src/app/media-item-list.component.ts new file mode 100644 index 000000000..7ad917a68 --- /dev/null +++ b/src/app/media-item-list.component.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'mw-media-item-list', + templateUrl: './media-item-list.component.html', + styleUrls: ['./media-item-list.component.css'] +}) +export class MediaItemListComponent { + mediaItems = [ + { + id: 1, + name: 'Firebug', + medium: 'Series', + category: 'Science Fiction', + year: 2010, + watchedOn: 1294166565384, + isFavorite: false + }, + { + id: 2, + name: 'The Small Tall', + medium: 'Movies', + category: 'Comedy', + year: 2015, + watchedOn: null, + isFavorite: true + }, { + id: 3, + name: 'The Redemption', + medium: 'Movies', + category: 'Action', + year: 2016, + watchedOn: null, + isFavorite: false + }, { + id: 4, + name: 'Hoopers', + medium: 'Series', + category: 'Drama', + year: null, + watchedOn: null, + isFavorite: true + }, { + id: 5, + name: 'Happy Joe: Cheery Road', + medium: 'Movies', + category: 'Action', + year: 2015, + watchedOn: 1457166565384, + isFavorite: false + } + ]; + + onMediaItemDelete(mediaItem) { } +} diff --git a/src/app/media-item.component.css b/src/app/media-item.component.css index e69de29bb..d3da443aa 100644 --- a/src/app/media-item.component.css +++ b/src/app/media-item.component.css @@ -0,0 +1,66 @@ +:host { + display: flex; + flex-direction: column; + width: 140px; + height: 200px; + border: 2px solid; + background-color: #29394b; + padding: 10px; + color: #bdc2c5; + margin: 0 12px 12px 0; +} +h2 { + font-size: 1.6em; + flex: 1; +} +:host(.medium-movies) { + border-color: #53ace4; +} +:host(.medium-movies) > h2 { + color: #53ace4; +} +:host(.medium-series) { + border-color: #45bf94; +} +:host(.medium-series) > h2 { + color: #45bf94; +} +.tools { + margin-top: 8px; + display: flex; + flex-wrap: nowrap; + justify-content: space-between; +} +.favorite { + width: 24px; + height: 24px; + fill: #bdc2c5; + cursor: pointer; +} +.favorite.is-favorite { + fill: #37ad79; +} +.favorite.is-favorite-hovering { + fill: #45bf94; +} +.favorite.is-favorite.is-favorite-hovering { + fill: #ec4342; +} +.delete { + display: block; + background-color: #ec4342; + padding: 4px; + font-size: .8em; + border-radius: 4px; + color: #ffffff; + cursor: pointer; +} +.details { + display: block; + background-color: #37ad79; + padding: 4px; + font-size: .8em; + border-radius: 4px; + color: #ffffff; + text-decoration: none; +} \ No newline at end of file diff --git a/src/app/media-item.component.html b/src/app/media-item.component.html index 1060f4349..38ceb2b5a 100644 --- a/src/app/media-item.component.html +++ b/src/app/media-item.component.html @@ -1,12 +1,20 @@ -

The REdemptino

-
true
-
Action
-
2021
+

{{ mediaItem.name }}

+ +
Watched on {{ mediaItem.watchedOn | date: 'shortDate' }}
+
+
{{ mediaItem.category }}
+
{{ mediaItem.year }}
\ No newline at end of file diff --git a/src/app/media-item.component.ts b/src/app/media-item.component.ts index 7277516de..22ce942b4 100644 --- a/src/app/media-item.component.ts +++ b/src/app/media-item.component.ts @@ -1,17 +1,15 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ - selector: 'mw-media-item', - templateUrl: './media-item.component.html', - styleUrls: ['./media-item.component.css'] + selector: 'mw-media-item', + templateUrl: './media-item.component.html', + styleUrls: ['./media-item.component.css'] }) - export class MediaItemComponent { - @Input() mediaItem; - @Output() delete = new EventEmitter(); + @Input() mediaItem; + @Output() delete = new EventEmitter(); - onDelete() { - console.log('deleted'); - this.delete.emit(this.mediaItem); - } -} \ No newline at end of file + onDelete() { + this.delete.emit(this.mediaItem); + } +} diff --git a/src/index.html b/src/index.html index 01c08e3c4..da9d46ee2 100644 --- a/src/index.html +++ b/src/index.html @@ -9,6 +9,6 @@ - + diff --git a/src/main.ts b/src/main.ts index e60eb8198..f22933ba8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; -platformBrowserDynamic().bootstrapModule(AppModule); \ No newline at end of file +platformBrowserDynamic().bootstrapModule(AppModule); From b5dd2ba927bc3a88e3e2b358351b9094bd939151 Mon Sep 17 00:00:00 2001 From: Jipa Dragos Date: Thu, 12 Aug 2021 22:02:02 +0300 Subject: [PATCH 3/4] dependency injection --- src/app/app.module.ts | 4 ++ src/app/media-item-form.component.html | 3 +- src/app/media-item-form.component.ts | 20 +++++--- src/app/media-item-list.component.ts | 58 +++++----------------- src/app/media-item.service.ts | 66 ++++++++++++++++++++++++++ src/app/providers.ts | 8 ++++ 6 files changed, 103 insertions(+), 56 deletions(-) create mode 100644 src/app/media-item.service.ts create mode 100644 src/app/providers.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a9713c0a1..ded641151 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -7,6 +7,7 @@ import { MediaItemListComponent } from './media-item-list.component'; import { FavoriteDirective } from './favorite.directive'; import { CategoryListPipe } from './category-list.pipe'; import { MediaItemFormComponent } from './media-item-form.component'; +import { lookupLists, lookupListsToken } from './providers'; @NgModule({ imports: [ @@ -21,6 +22,9 @@ import { MediaItemFormComponent } from './media-item-form.component'; CategoryListPipe, MediaItemFormComponent ], + providers: [ + { provide: lookupListsToken, useValue: lookupLists } + ], bootstrap: [ AppComponent ] diff --git a/src/app/media-item-form.component.html b/src/app/media-item-form.component.html index 1333140af..b596ad192 100644 --- a/src/app/media-item-form.component.html +++ b/src/app/media-item-form.component.html @@ -8,8 +8,7 @@

Add Media to Watch

  • diff --git a/src/app/media-item-form.component.ts b/src/app/media-item-form.component.ts index 14665d034..f96c09775 100644 --- a/src/app/media-item-form.component.ts +++ b/src/app/media-item-form.component.ts @@ -1,5 +1,7 @@ -import { Component, OnInit} from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { Component, Inject, OnInit} from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { MediaItemService } from './media-item.service'; +import { lookupListsToken } from './providers'; @Component({ selector: 'mw-media-item-form', @@ -9,15 +11,18 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; export class MediaItemFormComponent implements OnInit { form: FormGroup; + constructor(private formBuilder: FormBuilder, private mediaItemService: MediaItemService, + @Inject(lookupListsToken) public lookupLists) {} + ngOnInit() { - this.form = new FormGroup({ - medium: new FormControl('Movies'), - name: new FormControl('', Validators.compose([ + this.form = this.formBuilder.group({ + medium: this.formBuilder.control('Movies'), + name: this.formBuilder.control('', Validators.compose([ Validators.required, Validators.pattern('[\\w\\-\\s\\/]+') ])), - category: new FormControl(''), - year: new FormControl('', this.yearValidator), + category: this.formBuilder.control(''), + year: this.formBuilder.control('', this.yearValidator), }); } @@ -41,6 +46,7 @@ export class MediaItemFormComponent implements OnInit { } onSubmit(mediaItem) { + this.mediaItemService.add(mediaItem); console.log(mediaItem); } } diff --git a/src/app/media-item-list.component.ts b/src/app/media-item-list.component.ts index 7ad917a68..f4c6ee9a7 100644 --- a/src/app/media-item-list.component.ts +++ b/src/app/media-item-list.component.ts @@ -1,55 +1,19 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { MediaItemService } from './media-item.service'; @Component({ selector: 'mw-media-item-list', templateUrl: './media-item-list.component.html', styleUrls: ['./media-item-list.component.css'] }) -export class MediaItemListComponent { - mediaItems = [ - { - id: 1, - name: 'Firebug', - medium: 'Series', - category: 'Science Fiction', - year: 2010, - watchedOn: 1294166565384, - isFavorite: false - }, - { - id: 2, - name: 'The Small Tall', - medium: 'Movies', - category: 'Comedy', - year: 2015, - watchedOn: null, - isFavorite: true - }, { - id: 3, - name: 'The Redemption', - medium: 'Movies', - category: 'Action', - year: 2016, - watchedOn: null, - isFavorite: false - }, { - id: 4, - name: 'Hoopers', - medium: 'Series', - category: 'Drama', - year: null, - watchedOn: null, - isFavorite: true - }, { - id: 5, - name: 'Happy Joe: Cheery Road', - medium: 'Movies', - category: 'Action', - year: 2015, - watchedOn: 1457166565384, - isFavorite: false - } - ]; +export class MediaItemListComponent implements OnInit { + mediaItems; + constructor(private mediaItemService: MediaItemService) {} - onMediaItemDelete(mediaItem) { } + ngOnInit() { + this.mediaItems = this.mediaItemService.get(); + } + onMediaItemDelete(mediaItem) { + this.mediaItemService.delete(mediaItem); + } } diff --git a/src/app/media-item.service.ts b/src/app/media-item.service.ts new file mode 100644 index 000000000..7f9b93cd3 --- /dev/null +++ b/src/app/media-item.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class MediaItemService { + mediaItems = [ + { + id: 1, + name: 'Firebug', + medium: 'Series', + category: 'Science Fiction', + year: 2010, + watchedOn: 1294166565384, + isFavorite: false + }, + { + id: 2, + name: 'The Small Tall', + medium: 'Movies', + category: 'Comedy', + year: 2015, + watchedOn: null, + isFavorite: true + }, { + id: 3, + name: 'The Redemption', + medium: 'Movies', + category: 'Action', + year: 2016, + watchedOn: null, + isFavorite: false + }, { + id: 4, + name: 'Hoopers', + medium: 'Series', + category: 'Drama', + year: null, + watchedOn: null, + isFavorite: true + }, { + id: 5, + name: 'Happy Joe: Cheery Road', + medium: 'Movies', + category: 'Action', + year: 2015, + watchedOn: 1457166565384, + isFavorite: false + } + ]; + + get() { + return this.mediaItems; + } + + add(mediaItem) { + this.mediaItems.push(mediaItem); + } + + delete(mediaItem) { + const index = this.mediaItems.indexOf(mediaItem); + if (index >= 0){ + this.mediaItems.splice(index, 1); + } + } +} \ No newline at end of file diff --git a/src/app/providers.ts b/src/app/providers.ts new file mode 100644 index 000000000..a424fb7ba --- /dev/null +++ b/src/app/providers.ts @@ -0,0 +1,8 @@ +import { InjectionToken } from '@angular/core' + +export const lookupListsToken = new InjectionToken('lookupListsToken'); + +export const lookupLists = { + mediums: ['Movies', 'Series'] +}; + \ No newline at end of file From 5dd222f149ed4f2d835cd18c9d87ff556f9719b8 Mon Sep 17 00:00:00 2001 From: Jipa Dragos Date: Thu, 12 Aug 2021 23:13:26 +0300 Subject: [PATCH 4/4] HTTP requests --- src/app/app.module.ts | 10 ++- src/app/media-item-form.component.html | 15 ++-- src/app/media-item-form.component.ts | 24 ++--- src/app/media-item-list.component.css | 3 + src/app/media-item-list.component.html | 21 ++++- src/app/media-item-list.component.ts | 24 +++-- src/app/media-item.service.ts | 103 ++++++++++------------ src/app/mock-xhr-backend.ts | 116 +++++++++++++++++++++++++ src/app/providers.ts | 7 +- tsconfig.json | 2 +- 10 files changed, 233 insertions(+), 92 deletions(-) create mode 100644 src/app/mock-xhr-backend.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ded641151..034f93658 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,18 +1,21 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClientModule, HttpXhrBackend } from '@angular/common/http'; import { AppComponent } from './app.component'; import { MediaItemComponent } from './media-item.component'; import { MediaItemListComponent } from './media-item-list.component'; import { FavoriteDirective } from './favorite.directive'; import { CategoryListPipe } from './category-list.pipe'; import { MediaItemFormComponent } from './media-item-form.component'; -import { lookupLists, lookupListsToken } from './providers'; +import { lookupListToken, lookupLists } from './providers'; +import { MockXHRBackend } from './mock-xhr-backend'; @NgModule({ imports: [ BrowserModule, - ReactiveFormsModule + ReactiveFormsModule, + HttpClientModule ], declarations: [ AppComponent, @@ -23,7 +26,8 @@ import { lookupLists, lookupListsToken } from './providers'; MediaItemFormComponent ], providers: [ - { provide: lookupListsToken, useValue: lookupLists } + { provide: lookupListToken, useValue: lookupLists }, + { provide: HttpXhrBackend, useClass: MockXHRBackend } ], bootstrap: [ AppComponent diff --git a/src/app/media-item-form.component.html b/src/app/media-item-form.component.html index b596ad192..4d169642b 100644 --- a/src/app/media-item-form.component.html +++ b/src/app/media-item-form.component.html @@ -7,20 +7,20 @@

    Add Media to Watch

    • - +
    • - +
      Name has invalid characters
    • - @@ -31,9 +31,12 @@

      Add Media to Watch

    • - +
      - must be beetween {{ yearErrors.year.min}} - {{ yearErrors.year.max }} + Must be between + {{yearErrors.year.min}} + and + {{yearErrors.year.max}}
    diff --git a/src/app/media-item-form.component.ts b/src/app/media-item-form.component.ts index f96c09775..6579159f4 100644 --- a/src/app/media-item-form.component.ts +++ b/src/app/media-item-form.component.ts @@ -1,7 +1,7 @@ -import { Component, Inject, OnInit} from '@angular/core'; -import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Component, OnInit, Inject } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms'; import { MediaItemService } from './media-item.service'; -import { lookupListsToken } from './providers'; +import { lookupListToken } from './providers'; @Component({ selector: 'mw-media-item-form', @@ -11,8 +11,10 @@ import { lookupListsToken } from './providers'; export class MediaItemFormComponent implements OnInit { form: FormGroup; - constructor(private formBuilder: FormBuilder, private mediaItemService: MediaItemService, - @Inject(lookupListsToken) public lookupLists) {} + constructor( + private formBuilder: FormBuilder, + private mediaItemService: MediaItemService, + @Inject(lookupListToken) public lookupLists) {} ngOnInit() { this.form = this.formBuilder.group({ @@ -31,12 +33,12 @@ export class MediaItemFormComponent implements OnInit { return null; } const year = parseInt(control.value, 10); - const minYear = 1800; - const maxYear = 2500; - if (year >= minYear && year < maxYear) { + const minYear = 1900; + const maxYear = 2100; + if (year >= minYear && year <= maxYear) { return null; } else { - return { + return { year: { min: minYear, max: maxYear @@ -46,7 +48,7 @@ export class MediaItemFormComponent implements OnInit { } onSubmit(mediaItem) { - this.mediaItemService.add(mediaItem); - console.log(mediaItem); + this.mediaItemService.add(mediaItem) + .subscribe(); } } diff --git a/src/app/media-item-list.component.css b/src/app/media-item-list.component.css index c6f46b3a2..047f6b215 100644 --- a/src/app/media-item-list.component.css +++ b/src/app/media-item-list.component.css @@ -4,6 +4,9 @@ flex-direction: column; padding: 10px; } +nav a { + cursor: pointer; +} header { color: #c6c5c3; } diff --git a/src/app/media-item-list.component.html b/src/app/media-item-list.component.html index ca9979e6d..fa6b17984 100644 --- a/src/app/media-item-list.component.html +++ b/src/app/media-item-list.component.html @@ -1,9 +1,22 @@ -
    -
    {{ mediaItems | categoryList }}
    + +
    +

    {{medium}}

    +
    {{ mediaItems | categoryList }}
    - diff --git a/src/app/media-item-list.component.ts b/src/app/media-item-list.component.ts index f4c6ee9a7..4e5c85e5a 100644 --- a/src/app/media-item-list.component.ts +++ b/src/app/media-item-list.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { MediaItemService } from './media-item.service'; +import { MediaItemService, MediaItem } from './media-item.service'; @Component({ selector: 'mw-media-item-list', @@ -7,13 +7,27 @@ import { MediaItemService } from './media-item.service'; styleUrls: ['./media-item-list.component.css'] }) export class MediaItemListComponent implements OnInit { - mediaItems; + medium = ''; + mediaItems: MediaItem[]; + constructor(private mediaItemService: MediaItemService) {} ngOnInit() { - this.mediaItems = this.mediaItemService.get(); + this.getMediaItems(this.medium); + } + + onMediaItemDelete(mediaItem: MediaItem) { + this.mediaItemService.delete(mediaItem) + .subscribe(() => { + this.getMediaItems(this.medium); + }); } - onMediaItemDelete(mediaItem) { - this.mediaItemService.delete(mediaItem); + + getMediaItems(medium: string) { + this.medium = medium; + this.mediaItemService.get(medium) + .subscribe(mediaItems => { + this.mediaItems = mediaItems; + }); } } diff --git a/src/app/media-item.service.ts b/src/app/media-item.service.ts index 7f9b93cd3..207b1a19f 100644 --- a/src/app/media-item.service.ts +++ b/src/app/media-item.service.ts @@ -1,66 +1,53 @@ import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { map, catchError } from 'rxjs/operators'; +import { throwError } from 'rxjs'; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class MediaItemService { - mediaItems = [ - { - id: 1, - name: 'Firebug', - medium: 'Series', - category: 'Science Fiction', - year: 2010, - watchedOn: 1294166565384, - isFavorite: false - }, - { - id: 2, - name: 'The Small Tall', - medium: 'Movies', - category: 'Comedy', - year: 2015, - watchedOn: null, - isFavorite: true - }, { - id: 3, - name: 'The Redemption', - medium: 'Movies', - category: 'Action', - year: 2016, - watchedOn: null, - isFavorite: false - }, { - id: 4, - name: 'Hoopers', - medium: 'Series', - category: 'Drama', - year: null, - watchedOn: null, - isFavorite: true - }, { - id: 5, - name: 'Happy Joe: Cheery Road', - medium: 'Movies', - category: 'Action', - year: 2015, - watchedOn: 1457166565384, - isFavorite: false - } - ]; + constructor(private http: HttpClient) { } - get() { - return this.mediaItems; - } + get(medium: string) { + const getOptions = { + params: { medium } + }; + return this.http.get('mediaitems', getOptions) + .pipe( + map((response: MediaItemsResponse) => { + return response.mediaItems; + }), + catchError(this.handleError) + ); + } - add(mediaItem) { - this.mediaItems.push(mediaItem); - } + add(mediaItem: MediaItem) { + return this.http.post('mediaitems', mediaItem) + .pipe(catchError(this.handleError)); + } - delete(mediaItem) { - const index = this.mediaItems.indexOf(mediaItem); - if (index >= 0){ - this.mediaItems.splice(index, 1); - } - } -} \ No newline at end of file + delete(mediaItem: MediaItem) { + return this.http.delete(`mediaitems/${mediaItem.id}`) + .pipe(catchError(this.handleError)); + } + + private handleError(error: HttpErrorResponse) { + console.log(error.message); + return throwError('A data error occurred!'); + } +} + +interface MediaItemsResponse { + mediaItems: MediaItem[]; +} + +export interface MediaItem { + id: number; + name: string; + medium: string; + category: string; + year: number; + watchedOn: number; + isFavorite: boolean; +} diff --git a/src/app/mock-xhr-backend.ts b/src/app/mock-xhr-backend.ts new file mode 100644 index 000000000..c7ebd54a2 --- /dev/null +++ b/src/app/mock-xhr-backend.ts @@ -0,0 +1,116 @@ +import { HttpEvent, HttpRequest, HttpResponse, HttpBackend } from '@angular/common/http'; +import { Observable, Observer } from 'rxjs'; + +export class MockXHRBackend implements HttpBackend { + private mediaItems = [ + { + id: 1, + name: 'Firebug', + medium: 'Series', + category: 'Science Fiction', + year: 2010, + watchedOn: 1294166565384, + isFavorite: false + }, + { + id: 2, + name: 'The Small Tall', + medium: 'Movies', + category: 'Comedy', + year: 2015, + watchedOn: null, + isFavorite: true + }, { + id: 3, + name: 'The Redemption', + medium: 'Movies', + category: 'Action', + year: 2016, + watchedOn: null, + isFavorite: false + }, { + id: 4, + name: 'Hoopers', + medium: 'Series', + category: 'Drama', + year: null, + watchedOn: null, + isFavorite: true + }, { + id: 5, + name: 'Happy Joe: Cheery Road', + medium: 'Movies', + category: 'Action', + year: 2015, + watchedOn: 1457166565384, + isFavorite: false + } + ]; + + handle(request: HttpRequest): Observable> { + return new Observable((responseObserver: Observer>) => { + let responseOptions; + switch (request.method) { + case 'GET': + if (request.urlWithParams.indexOf('mediaitems?medium=') >= 0 || request.url === 'mediaitems') { + let medium; + if (request.urlWithParams.indexOf('?') >= 0) { + medium = request.urlWithParams.split('=')[1]; + if (medium === 'undefined') { medium = ''; } + } + let mediaItems; + if (medium) { + mediaItems = this.mediaItems.filter(i => i.medium === medium); + } else { + mediaItems = this.mediaItems; + } + responseOptions = { + body: {mediaItems: JSON.parse(JSON.stringify(mediaItems))}, + status: 200 + }; + } else { + let mediaItems; + const idToFind = parseInt(request.url.split('/')[1], 10); + mediaItems = this.mediaItems.filter(i => i.id === idToFind); + responseOptions = { + body: JSON.parse(JSON.stringify(mediaItems[0])), + status: 200 + }; + } + break; + case 'POST': + const mediaItem = request.body; + mediaItem.id = this._getNewId(); + this.mediaItems.push(mediaItem); + responseOptions = {status: 201}; + break; + case 'DELETE': + const id = parseInt(request.url.split('/')[1], 10); + this._deleteMediaItem(id); + responseOptions = {status: 200}; + } + + const responseObject = new HttpResponse(responseOptions); + responseObserver.next(responseObject); + responseObserver.complete(); + return () => { + }; + }); + } + + _deleteMediaItem(id) { + const mediaItem = this.mediaItems.find(i => i.id === id); + const index = this.mediaItems.indexOf(mediaItem); + if (index >= 0) { + this.mediaItems.splice(index, 1); + } + } + + _getNewId() { + if (this.mediaItems.length > 0) { + return Math.max.apply(Math, this.mediaItems.map(mediaItem => mediaItem.id)) + 1; + } else { + return 1; + } + } +} diff --git a/src/app/providers.ts b/src/app/providers.ts index a424fb7ba..2dd00660b 100644 --- a/src/app/providers.ts +++ b/src/app/providers.ts @@ -1,8 +1,7 @@ -import { InjectionToken } from '@angular/core' +import { InjectionToken } from '@angular/core'; -export const lookupListsToken = new InjectionToken('lookupListsToken'); +export const lookupListToken = new InjectionToken('lookupListToken'); export const lookupLists = { - mediums: ['Movies', 'Series'] + mediums: ['Movies', 'Series'] }; - \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 6ec9ceb17..856a8c193 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,4 +19,4 @@ "dom" ] } -} +} \ No newline at end of file