בפוסט הזה נראה איך לייצר כפתור לבחירת קובץ מהמחשב, הצגה של התמונה על המסך לפני שמעלים לשרת, בדיקה ואימות שאכן מדובר בקובץ תמונה ולא משהו אחר והכנה שלו לשליחה לצד שרת/בסיס נתונים.
נוסיף את HttpClientModule ואת ReactiveFormsModule ל- app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http' import { ReactiveFormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { ImgUploaderComponent } from './components/img-uploader/img-uploader.component'; @NgModule({ declarations: [ AppComponent, ImgUploaderComponent, ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, ReactiveFormsModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
ניצור קומפוננטה חדשה בשם ImgUploaderComponent
ונאכלס אותה בקוד הבא:
<form [formGroup]="form" (submit)="onSavePost()"> <div> <button class="file-select" type="button" (click)="filePicker.click()">Pick Image</button> <input type="file" #filePicker (change)="onImagePicked($event)"> </div> <div class="image-preview" *ngIf="imagePreview!=='' && imagePreview && form.get('image')!.valid"> <img [src]="imagePreview" [alt]="form.value.title"> </div> <button color="primary" type="submit">Save File</button> </form>
import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { mimeType } from './mime-type.validator'; import { ImgService } from './img.service'; @Component({ selector: 'app-img-uploader', templateUrl: './img-uploader.component.html', styleUrls: ['./img-uploader.component.scss'] }) export class ImgUploaderComponent implements OnInit { form: FormGroup = {} as FormGroup; imagePreview: string | ArrayBuffer = ''; constructor(public imgService: ImgService) { } ngOnInit(): void { this.form = new FormGroup({ 'image': new FormControl(null, { validators: [Validators.required], asyncValidators: [mimeType] }), }); } onImagePicked(event: any) { const file = event.target!.files[0]; this.form.patchValue({ image: file }); this.form.get('image')!.updateValueAndValidity(); const reader = new FileReader(); reader.onload = () => { this.imagePreview = reader.result!; }; reader.readAsDataURL(file); } onSavePost() { if (this.form.invalid) { return; } this.imgService.addImg(this.form.value.image); this.form.reset(); } }
input[type="file"] { visibility: hidden; } .image-preview { height: 5rem; margin: 1rem 0; } .image-preview img { height: 100%; } .file-select { background: transparent; padding: 10px 14px; margin: 10px; border: 1px solid steelblue; border-radius: 20px; cursor: pointer; &:hover { background: tan; } } [type="submit"] { background: transparent; padding: 10px 14px; margin: 10px; border: 1px solid green; border-radius: 20px; cursor: pointer; &:hover { background: lightseagreen; } }
מיד נוסיף גם את הבדיקה של סוג הקובץ mimeType ואת הסרויס לשליחה לשרת ImgService.
לפני זה, נראה מה קורה במה שכתבנו עד עכשיו,
אם נסתכל על ה-HTML , נראה טופס עם הגדרות רגילות שמאפיינות טפסים בנוסף ניתן לראות את input מסוג file.
בגלל שלא ממש ניתן לעצב אותו כמו שרוצים, העלמנו אותו בעזרת ה-CSS ע"י visibility: hidden .
הצמדנו לא כפתור שאותו נעצב איך שאנחנו רוצים ובלחיצה עליו נבצע לחיצה על הכפתור שהסתרנו, כך זה יראה שלחצו על הכפתור שעצבנו, והוא פתח את סייר הקבצים שלנו.
שימו לב שקשרנו בין הכפתורים על ידי כך שהוספנו משתנה לכפתור המוסתר filePicker עם # לפני.
לבסוף, הוספנו כפתור מסוג submit כדי לשלוח את הטופס.
בצד ה-TS:
יצרנו משתנה מסוג טופס בשם form , אתחלנו אותו והוספנו לו את image.
ל-image יש 2 בדיקות תקינות:
- בדיקת חובה – בדיקה סינכרונית.
- בדיקת סוג הקובץ – בדיקה לא סינכרונית, בגלל שלוקח זמן לקרוא את הקובץ ואנחנו לא רוצים לתקוע את הדף למשתמש.
בשורה 22 אנחנו מושכים את הקובץ במקום 0 מהסיבה שתמיד חוזר לנו מערך למקרה ונרצה לתת למשתמש את האפשרות לבחירה מרובה של קבצים.
במקרה שלנו יש בחירה יחידה ולכן אנחנו תמיד נמשוך את התא הראשון.
סימון הקריאה (!) נמצא שם מהסיבה ש-event.target יכול להחזיר NULL והקומפיילר מתריע מפני זה,
על ידי הוספת סימן הקריאה, אנחנו אומרים לקומפיילר שאנחנו יודעים ודואגים לזה שלא יהיה שם NULL (מה שנקרה… "האחריות עלינו").
בשורה 23 – אנחנו שומרים את המידע באובייקט פורם שלנו.
בשורה 24 – אנחנו מבקשים לבצע עידכן ובדיקה מחדש למצב הפורם ובדיקות האימות שלו.
כדי שנוכל להציג על הדף את התמונה שבחרנו אנחנו צריכים להפוך את המידע הזה ל- "URL" ולשלוח אותו כ-SRC לתגית IMG.
שורות 26 – 29 מטפלות בזה והופכות את המידע שיש בקובץ ל-DATA שהדף יכול להבין ולהראות.
ולבסוף, כששולחים את הטופס, שורה 35 מבצעת את השליחה לסרויס שבתורו ישלח את המידע לשרת.
עכשיו אנחנו יכולים להמשיך ולהוסיף את בדיקת סוג הקובץ.
השיטה הפשוטה היא לבדוק את הסיומת של הקובץ😂, אני סתם צוחק,
הרי את הסיומת המשתמש יכול לשנות ואז להעביר לנו קובץ מאיזה סוג שהוא רוצה.
אנחנו בודקים את המידע בתוך הקובץ, את זה יותר קשה לשנות ונעשה את זה כך
import { AbstractControl } from "@angular/forms"; import { Observable, Observer, of } from "rxjs"; export const mimeType = (control: AbstractControl): Promise<{ [key: string]: any }> | Observable<{ [key: string]: any }> | Observable<null> => { if (typeof (control.value) === 'string') { return of(null) } const file = control.value as File; const fileReader = new FileReader(); const frObs = new Observable((observer: Observer<{ [key: string]: any }>) => { fileReader.addEventListener("loadend", () => { const arr = new Uint8Array(fileReader.result as ArrayBuffer).subarray(0, 4); let isValid: boolean = false; let header = ""; for (let i = 0; i < arr.length; i++) { header += arr[i].toString(16); } switch (header) { case '89504e47': isValid = true; break; case "ffd8ffe0": case "ffd8ffe1": case "ffd8ffe2": case "ffd8ffe3": case "ffd8ffe8": isValid = true; break; default: isValid = false; break; } if (isValid) { observer.next(null); } else { observer.next({ invalidMineType: true }); } observer.complete(); }); fileReader.readAsArrayBuffer(file); }); return frObs; };
אם הבדיקה יוצאת תקינה, אנחנו מחזירים NULL, כל דבר אחר שנחזיר נחשב לכישלון בבדיקה.
הבדיקה שאנחנו עושים היא לתחילת התוכן של הקובץ, אם הוא מתאים למקרה הראשון אז מדובר ב-png ואם הוא מתאים למקרה השני ועד השישי אז מדובר ב-JPG/JPEG .
אפשר לחפש עוד סוגי קבצים באינטרנט לסיומות שונות לבדיקה.
עכשיו נוסיף את החלק האחרון שהוא הסרויס
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http' import { Router } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class ImgService { constructor(private http: HttpClient, private router: Router) { } apiFile: string = '' //'http://localhost:3000/api/imgFiles' addImg(image: File) { const imgData = new FormData; imgData.append("image", image, 'newImgTitle'); console.log(imgData); this.http.post<{ message: string }>( this.apiFile, imgData ).subscribe((respData) => { this.router.navigate(["/"]); }) } }
השירות די פשוט,
ב-apiFile נאחסן את המיקום שאליו נשמור/נשלח את התמונה.
נבצע POST ונצהיר שאנחנו מצפים לקבל חזרה הודעה מהשרת שתעדכן אותנו מה קרה (לא חייבים).
נוסיף לשליחה את המיקום לשליחה ואת המידע של הקובץ.
אחרי שהתהליך יסתיים ונקבל תשובה, נבצע ניתוב לדף הבית.