praktikant 2 tuần trước cách đây
mục cha
commit
5189ee0fb5

+ 2 - 2
src-angl/is/src/app/app.config.ts

@@ -1,11 +1,11 @@
 import { ApplicationConfig, inject, Injectable, provideBrowserGlobalErrorListeners } from '@angular/core';
-import { provideRouter } from '@angular/router';
+import { provideRouter, withComponentInputBinding, withRouterConfig } from '@angular/router';
 
 import { routes } from './app.routes';
 
 export const appConfig: ApplicationConfig = {
   providers: [
     provideBrowserGlobalErrorListeners(),
-    provideRouter(routes),
+    provideRouter(routes, withRouterConfig({onSameUrlNavigation: 'reload'}),withComponentInputBinding()),
   ]
 };

+ 5 - 3
src-angl/is/src/app/app.routes.ts

@@ -1,8 +1,10 @@
 import { Routes } from '@angular/router';
 import { Articles } from './articles/articles';
-import { ArticleForm } from './article-form/article-form';
+import { ArticleFormEdit } from './article-form-edit/article-form-edit';
+import { ArticleFormNew } from './article-form-new/article-form-new';
 
 export const routes: Routes = [
-	{ path: '', component: Articles },
-	{ path: 'new', component: ArticleForm }
+	{path: '', component: Articles},
+	{path: 'new', component: ArticleFormNew},
+	{path: 'edit/:id', component: ArticleFormEdit},
 ];

+ 7 - 0
src-angl/is/src/app/article-delete-button/article-delete-button.html

@@ -0,0 +1,7 @@
+<button [disabled]="busy" (click)="delete()">
+	Artikel löschen
+
+	@if (busy) {
+		processing...
+	}
+</button>

+ 28 - 0
src-angl/is/src/app/article-delete-button/article-delete-button.ts

@@ -0,0 +1,28 @@
+import { Component, EventEmitter, inject, Input, Output } from '@angular/core';
+import { Article } from '../bo/article';
+import { DataService } from '../data-service';
+
+@Component({
+	selector: 'is-article-delete-button',
+	imports: [],
+	templateUrl: './article-delete-button.html',
+})
+export class ArticleDeleteButton {
+	busy = false;
+
+	dataService = inject(DataService);
+
+	@Input({ required: true })
+	article!: Article;
+
+	@Output()
+	deleted = new EventEmitter();
+
+	delete(){
+		this.busy = true;
+		this.dataService.deleteArticle(this.article.id).subscribe(() => {
+			this.busy = false;
+			this.deleted.emit();
+		});
+	}
+}

+ 6 - 0
src-angl/is/src/app/article-form-edit/article-form-edit.html

@@ -0,0 +1,6 @@
+@if (!articleInputSignal()) {
+	loading...
+} @else {
+	<is-article-form [title]="'Edit'" [submitButtonName]="'Update Article'" [articleInput]="articleInputSignal()!"
+			(articleInputChange)="handleSubmit($event)"></is-article-form>
+}

+ 79 - 0
src-angl/is/src/app/article-form-edit/article-form-edit.ts

@@ -0,0 +1,79 @@
+import { Component, inject, OnInit, signal } from '@angular/core';
+import {  FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { firstValueFrom, Observable } from 'rxjs';
+import { ArticleGroup } from '../bo/article-group';
+import { Article } from '../bo/article';
+import { DataService } from '../data-service';
+import { ActivatedRoute, Router } from '@angular/router';
+import { ArticleForm, ArticleInput } from '../article-form/article-form';
+
+@Component({
+	selector: 'is-article-form-edit',
+	templateUrl: './article-form-edit.html',
+	imports: [
+		FormsModule,
+		ReactiveFormsModule,
+		ArticleForm
+	],
+})
+export class ArticleFormEdit implements OnInit {
+	// @Input()
+	// set id(arg: string) {
+	// 	const id = Number(arg);
+	// 	if (isNaN(id)) {
+	// 		this.cancel();
+	// 		return;
+	// 	}
+	//
+	// 	this.update(id)
+	// }
+
+	articleToEditId!: number;
+	articleToEdit!: Article;
+	articleToEditArticleGroup!: ArticleGroup | null;
+
+	articleInputSignal = signal<ArticleInput|undefined>(undefined) ;
+
+	constructor(private route: ActivatedRoute, private router: Router) {
+
+	}
+
+	private dataService = inject(DataService);
+
+	ngOnInit() {
+		this.route.paramMap.subscribe((paramMap) => {
+			const id = Number(paramMap.get('id'));
+			if (isNaN(id)) {
+				this.cancel();
+			}
+
+			this.articleToEditId = id;
+			this.update(id);
+		});
+	}
+
+	private update(id: number) {
+		this.dataService.getArticleById(id).subscribe((article: Article) => {
+			this.articleToEdit = article;
+
+			this.articleInputSignal.set({
+				title: this.articleToEdit.title,
+				text: this.articleToEdit.text,
+				categoryName: this.articleToEdit.categoryName,
+				articleGroupId: this.articleToEdit.articleGroupId,
+			});
+		});
+	}
+
+
+	handleSubmit(articleInput: ArticleInput) {
+		this.dataService.putArticle(this.articleToEditId, articleInput).subscribe(() => {
+			this.cancel();
+		});
+	}
+
+	cancel(): void {
+		this.router.navigateByUrl('/');
+	}
+
+}

+ 3 - 0
src-angl/is/src/app/article-form-new/article-form-new.html

@@ -0,0 +1,3 @@
+
+<is-article-form [title]="'Create'" [submitButtonName]="'Create Article'"
+		(articleInputChange)="handleSubmit($event)"></is-article-form>

+ 30 - 0
src-angl/is/src/app/article-form-new/article-form-new.ts

@@ -0,0 +1,30 @@
+import { Component, inject, OnInit, signal } from '@angular/core';
+import { DataService } from '../data-service';
+import { Article } from '../bo/article';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { ArticleForm, ArticleInput } from '../article-form/article-form';
+
+@Component({
+	selector: 'is-article-form-new',
+	templateUrl: './article-form-new.html',
+	imports: [
+		FormsModule,
+		ReactiveFormsModule,
+		ArticleForm
+	]
+})
+export class ArticleFormNew{
+	// title = input.required<string>();
+	private dataService = inject(DataService)
+
+	constructor(private router: Router ) {
+	}
+
+	handleSubmit(articleInput: ArticleInput){
+		this.dataService.postArticle(articleInput).subscribe((article: Article) => {
+			this.router.navigateByUrl('/');
+		});
+	}
+
+}

+ 6 - 6
src-angl/is/src/app/article-form/article-form.html

@@ -6,7 +6,7 @@
 </head>
 <body>
 <form [formGroup]="articleForm" (ngSubmit)="handleSubmit()">
-	<h2 class="form-header">Create Article</h2>
+	<h2 class="form-header">{{ title }} Article</h2>
 	<label>Title</label>
 	<input type="text" formControlName="title" required/>
 	<span class="input-error">
@@ -32,7 +32,7 @@
 	</span>
 	<label>Category</label>
 	<select id="categoryNames" formControlName="categoryName" required>
-		<option value="" disabled>--Select a category--</option>
+		<option [value]="null" disabled>--Select a category--</option>
 		@for (cn of categoryNames; track cn) {
 			<option [value]="cn">{{ cn }}</option>
 		}
@@ -43,18 +43,18 @@
 	}
 	</span>
 	<label>Article Group</label>
-	<select id="articleGroups" formControlName="articleGroup">
-		<option value=null disabled>--Select a Article Group--</option>
+	<select id="articleGroups" formControlName="articleGroupId">
+		<option [value]="null" disabled>--Select a Article Group--</option>
 		@for (ag of articleGroups$ | async; track ag.id) {
 			<option [value]="ag.id">{{ ag.name }}</option>
 		}
 	</select>
 	<span class="input-error">
-	@if (formSubmitted && articleForm.controls.articleGroup.errors?.['required']) {
+	@if (formSubmitted && articleForm.controls.articleGroupId.errors?.['required']) {
 		Please select a Article Group
 	}
 	</span>
-	<input type="submit" value="Create Article">
+	<input type="submit" value='{{ submitButtonName }}'>
 </form>
 </body>
 </html>

+ 49 - 20
src-angl/is/src/app/article-form/article-form.ts

@@ -1,4 +1,4 @@
-import { Component, inject, OnInit } from '@angular/core';
+import { Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core';
 import { AsyncPipe } from '@angular/common';
 import { DataService } from '../data-service';
 import { Observable } from 'rxjs';
@@ -10,7 +10,7 @@ import { Router } from '@angular/router';
 @Component({
 	selector: 'is-article-form',
 	templateUrl: './article-form.html',
-	styleUrl: 'article-form.css',
+	styleUrl: './article-form.css',
 	imports: [
 		AsyncPipe,
 		FormsModule,
@@ -18,15 +18,27 @@ import { Router } from '@angular/router';
 	]
 })
 export class ArticleForm implements OnInit {
-	formSubmitted = false;
-	articleForm = new FormGroup({
-		title: new FormControl('', Validators.required),
-		text: new FormControl('', Validators.required),
-		categoryName: new FormControl('', Validators.required),
-		articleGroup: new FormControl(null, Validators.required),
-	})
 	private dataService = inject(DataService)
 
+	@Input() title?: string;
+	@Input() submitButtonName?: string;
+
+	@Input()
+	articleInput: ArticleInput = { title: null, text: null, categoryName: null, articleGroupId: null };
+	@Output()
+	articleInputChange = new EventEmitter<ArticleInput>();
+
+	// title = input.required<string>();
+	formSubmitted = false;
+	articleForm = new FormGroup({
+		title: new FormControl<string | null>('', Validators.required),
+		text: new FormControl<string | null>('', Validators.required),
+		categoryName: new FormControl<CategoryName|null>(
+				CategoryName.sport,
+				Validators.required
+		),
+		articleGroupId: new FormControl<number|null>(null, Validators.required),
+	});
 
 	articleGroups$?: Observable<ArticleGroup[]>;
 	categoryNames: CategoryName[] = Object.values(CategoryName);
@@ -35,6 +47,12 @@ export class ArticleForm implements OnInit {
 	}
 
 	ngOnInit() {
+		this.articleForm.patchValue({
+			title: this.articleInput.title,
+			text: this.articleInput.text,
+			categoryName: this.articleInput.categoryName,
+			articleGroupId: this.articleInput.articleGroupId,
+		});
 		this.articleGroups$ = this.dataService.getArticleGroups();
 		this.listenToInputChange("title");
 		this.listenToInputChange("text");
@@ -45,33 +63,37 @@ export class ArticleForm implements OnInit {
 
 		this.articleForm.updateValueAndValidity();
 		console.log(this.articleForm.valid);
-		console.log(this.articleForm.controls.articleGroup.value);
+		console.log(this.articleForm.controls.articleGroupId.value);
 		console.log(this.articleForm.controls.title.errors);
 		console.log(this.articleForm.controls.text.errors);
 		console.log(this.articleForm.controls.categoryName.errors);
-		console.log(this.articleForm.controls.articleGroup.errors);
+		console.log(this.articleForm.controls.articleGroupId.errors);
 		if (!this.articleForm.valid) {
 			return;
 		}
 
-		let article: Article = {
-			id: 0,
+		const articleInput: ArticleInput = {
 			title: this.articleForm.value.title!,
 			text: this.articleForm.value.text!,
-			categoryName: this.articleForm.value.categoryName! as CategoryName,
-			articleGroupId: this.articleForm.value.articleGroup!
+			categoryName: this.articleForm.value.categoryName!,
+			articleGroupId: this.articleForm.value.articleGroupId!
 		};
 
-		this.dataService.postArticle(article).subscribe((article: Article) => {
-			console.log(article)
-			this.router.navigateByUrl('/');
-		});
+		this.articleInputChange.emit(articleInput);
+
+
+
+		// this.dataService.postArticle(article).subscribe((article: Article) => {
+		// 	console.log(article)
+		// 	this.router.navigateByUrl('/');
+		// });
 	}
 
-	listenToInputChange(controlName: keyof typeof this.articleForm.controls){
+	listenToInputChange(controlName: 'title' | 'text'){
 		console.log(this.articleForm.controls)
 		const control = this.articleForm.controls[controlName];
 		control.valueChanges.subscribe(value => {
+			if (typeof value !== 'string') return;
 			const regExp = new RegExp('^([A-Za-z]|\\s)*$')
 			const errors = { ...control.errors };
 
@@ -85,4 +107,11 @@ export class ArticleForm implements OnInit {
 		});
 	}
 
+}
+
+export interface ArticleInput {
+	title: string|null;
+	text: string|null;
+	categoryName: CategoryName|null;
+	articleGroupId: number|null;
 }

+ 29 - 3
src-angl/is/src/app/articles/articles.css

@@ -1,7 +1,33 @@
 table {
-    border-collapse: collapse
+    border-collapse: collapse;
+    width: 100%;
+    text-align: left;
 }
 
-th, td {
-    border: 1px solid black
+th {
+    border-bottom: 1px solid DarkGray;
+    padding: 0.5rem 0.5rem 1rem 0.5rem;
+    border-right: 1px solid DarkGray;
+}
+
+th:last-child{
+    border-right: none;
+}
+
+td {
+    border-bottom: 1px solid DarkGray;
+    padding: 0.5rem;
+}
+
+.modify-buttons{
+    display: flex;
+    column-gap: 1rem;
+}
+
+.modify-buttons>td:first-child{
+    background-color: green;
+}
+
+.modify-buttons>td:last-child{
+    background-color: red;
 }

+ 29 - 21
src-angl/is/src/app/articles/articles.html

@@ -1,30 +1,38 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-  <meta charset="UTF-8">
-  <title>Title</title>
+	<meta charset="UTF-8">
+	<title>Title</title>
 </head>
 <body>
 <div>
-  <table>
-    <tr>
-      <th>Id</th>
-      <th>Title</th>
-      <th>Text</th>
-      <th>Category Name</th>
-      <th>Article Group</th>
-    </tr>
-    @for (article of articles$ | async; track article.id) {
-      <tr>
-        <td>{{ article.id }}</td>
-        <td>{{ article.title }}</td>
-        <td>{{ article.text }}</td>
-        <td>{{ article.categoryName }}</td>
-        <td>{{ article.articleGroupId === null ? "None" : article.articleGroupId}}</td>
-      </tr>
-    }
-  </table>
-	<is-app-floatButton />
+	<table>
+		<tr>
+			<th>Id</th>
+			<th>Title</th>
+			<th>Text</th>
+			<th>Category Name</th>
+			<th>Article Group</th>
+			<th>Modify</th>
+		</tr>
+		@for (article of articlesSignal(); track article.id) {
+			<tr>
+				<td>{{ article.id }}</td>
+				<td>{{ article.title }}</td>
+				<td>{{ article.text }}</td>
+				<td>{{ article.categoryName }}</td>
+				<td>{{ article.articleGroupId === null ? "None" : article.articleGroupId }}</td>
+				<td>
+					<div class="modify-buttons">
+						<span routerLink="edit/{{ article.id }}">Edit</span>
+						<is-article-delete-button [article]="article" (deleted)="articleDeleted(article)">
+						</is-article-delete-button>
+					</div>
+				</td>
+			</tr>
+		}
+	</table>
+	<is-app-floatButton/>
 </div>
 </body>
 </html>

+ 24 - 5
src-angl/is/src/app/articles/articles.ts

@@ -1,25 +1,44 @@
-import {Component, inject, OnInit} from '@angular/core';
-import { AsyncPipe } from '@angular/common';
+import { ChangeDetectorRef, Component, inject, OnInit, signal } from '@angular/core';
 import {DataService} from '../data-service';
 import { Observable } from 'rxjs';
 import { Article } from '../bo/article';
 import { FloatButton } from '../float-button/float-button';
+import { RouterLink } from '@angular/router';
+import { ArticleDeleteButton } from '../article-delete-button/article-delete-button';
 @Component({
 	selector: 'is-app-articles',
 	templateUrl: 'articles.html',
 	styleUrl: 'articles.css',
-	imports: [AsyncPipe, FloatButton],
+	imports: [FloatButton, RouterLink, ArticleDeleteButton],
 })
 
 export class Articles implements OnInit {
     private dataService = inject(DataService);
 
-	articles$?: Observable<Article[]>;
+	articlesSignal = signal<Article[]|undefined>(undefined)
+
+	constructor(private cdr: ChangeDetectorRef) {
+	}
 
     ngOnInit() {
-      	this.articles$ = this.dataService.getArticles();
+      	this.dataService.getArticles().subscribe((articles) => {
+		  	this.articlesSignal.set(articles);
+		});
     }
 
+	 articleDeleted(article: Article){
+		const articles = this.articlesSignal();
+		if (!articles) {
+			return;
+		}
+
+		const index = articles.findIndex((a) => a.id === article.id);
+		if (index !== -1) {
+			articles.splice(index, 1);
+			this.articlesSignal.set([...articles]);
+		}
+	}
+
 }
 
 

+ 19 - 1
src-angl/is/src/app/data-service.ts

@@ -3,12 +3,14 @@ import {HttpClient} from '@angular/common/http';
 import {Observable} from 'rxjs';
 import { Article } from './bo/article';
 import { ArticleGroup } from './bo/article-group';
+import { ArticleInput } from './article-form/article-form';
 
 @Injectable({
   providedIn: 'root'
 })
 export class DataService {
 	url = 'http://localhost/internship-playground/src-php/public/api/';
+
 	constructor(private http: HttpClient) {
 
 	}
@@ -17,11 +19,27 @@ export class DataService {
 		return this.http.get<Article[]>(this.url + 'articles');
 	}
 
+	getArticleById(id: number): Observable<Article>{
+		return this.http.get<Article>(this.url + 'article/' + id)
+	}
+
 	getArticleGroups(): Observable<ArticleGroup[]>{
 		return this.http.get<ArticleGroup[]>(this.url + 'group/articlegroups');
 	}
 
-	postArticle(article: Article): Observable<Article>{
+	getArticleGroupById(id: number): Observable<ArticleGroup>{
+		return this.http.get<ArticleGroup>(this.url + 'group/articlegroup/' + id)
+	}
+
+	postArticle(article: ArticleInput): Observable<Article>{
 		return this.http.post<Article>(this.url + 'article/', article);
 	}
+
+	putArticle(id: number, article: ArticleInput): Observable<Article>{
+		return this.http.put<Article>(this.url + 'article/' + id, article)
+	}
+
+	deleteArticle(id: number): Observable<Article>{
+		return this.http.delete<Article>(this.url + 'article/' + id);
+	}
 }