مدیریت تغییرات ذخیره نشده در انگولار — از صفر تا صد

همه ما با مواردی مواجه شدهایم که قبل از ذخیره کردن برخی تغییرات در یک فرم، به اشتباه دکمه خروج را زدهایم. در این موارد بهترین کار نمایش یک کادر پیام PopUp و پرسش در مورد ضرورت ذخیره تغییرات ذخیره نشده است. در این مقاله در مورد روش مدیریت تغییرات ذخیره نشده در انگولار با استفاده از یک چنین کادری صحبت خواهیم کرد.
برای پیادهسازی چنین کادر پیام بازشوندهای کافی است <wm-can-leave> را در همه صفحههایی که میخواهیم مورد حفاظت قرار گیرند، بگنجانیم. در ادامه شیوه پیادهسازی یک کادر پیام هشداردهنده در مورد تغییرات ذخیره نشده را با هم مرور میکنیم.
هر چه سادهتر، بهتر
در این مقاله از کتابخانه MatDialogModule (+) از Angular Material (+) به عنوان مبنای کار استفاده میکنیم. اگر با MatDialogModule آشنایی داشته باشید، بخش عمدهای از کد مثال زیر را متوجه میشوید:
... <!-- CanLeave Popup Dialog (based on MatDialog) --> <wm-can-leave [dontLeave]="needSave" #canLeave> <h2 mat-dialog-title><b>Unsaved changes</b></h2> <mat-dialog-content> You're about to leave the page without saving the latest changes.<br>Please confirm. </mat-dialog-content> <mat-dialog-actions align="end"> <button mat-button color="primary" (click)="canLeave.close(false)" cdkFocusInitial>Stay</button> <button mat-button color="warn" (click)="canLeave.close(true)">Leave anyhow</button> </mat-dialog-actions> </wm-can-leave> ...
تصور کنید کد فوق بخشی از قالب صفحه است که در مورد تغییرات ذخیره نشده، حفاظتی ایجاد میکند. بنابراین به محض این که بخواهید از صفحه خارج شوید، به شرط تعیین شدن متغیر needSave به صورت true، یک کادر محاورهای بازشونده نمایش مییابد که از سوی آن چه درون عنصر <wm-can-leave> قرار دارد، توصیف شده است.
این میتواند یک راهحل عالی برای تضمین کنترل کامل روی محتوای پیام بازشونده باشد، در حالی که کاربرد ساده و شگفتانگیزی نیز دارد. کافی است <wm-can-leave> را در همه صفحههایی که میخواهید مورد حفاظت قرار گیرند، قرار دهید.
پشت صحنه
سلکتور <wm-can-leave> به CanLeaveComponent تعلق دارد که ظرفیتهای یک DialogComponent را ترکیب میکند و بر اساس نشانگرهای یک حفاظت مسیر CanDeactivate نمایش مییابد:
/** Extends the DialogComponent to popup during CanDeactivate*/ @Component({ selector: 'wm-can-leave', template: '<ng-template><ng-content></ng-content></ng-template>' }) export class CanLeaveComponent extends DialogComponent<boolean> { constructor(private canLeave: CanLeaveService, dialog: MatDialog) { super(dialog); // Hooks on the allowDeactivation observer this.canLeave.allowDeactivation( this.canLeave$ ); } /** When true, pops-up a dialog asking for user's consent to leave*/ @Input() dontLeave: boolean = false; // CanLeave Observavble private get canLeave$(): Observable<boolean> { // Builds an observable to evaluate donLeave at subscription time return defer( () => this.dontLeave ? this.open().afterClosed() : of(true) ) // Makes sure all the following requests will be true once the first has been granted .pipe( tap( allow => this.dontLeave = !allow ) ); } }
برای تعریف قلابی روی متد حفاظتی CanDeactivate که از سوی روتر انگولار ارائه شده است، کامپوننت متد ()allowDeactivation را از طریق CanLeaveService تزریق شده فراخوانی میکند.
همچنان که در ادامه با بررسی کد سرویس خواهیم دید، ()allowDeactivation یک Observable میپذیرد که در زمان حفاظت از صفحه resolve میشود و با بازگشت دادن مقدار true امکان غیرفعال شدن صفحه را میدهد. در حالی که بازگشت مقدار false مانع این کار خواهد شد.
Observable به نام canLeave$ به این منظور با به تأخیر انداختن ارزیابی ورودی dontLeave امکان نمایش پیام به کاربر را به جای غیرفعال کردن مستقیم فراهم میسازد.
عملگر ()tap به صورت pipe شده تضمین میکند که وقتی غیرفعال شدن انجام یابد، درخواستها در صورت وجود، مورد بررسی قرار میگیرند. این وضعیت به منظور جلوگیری از نمایش چندباره پیام به دلیل نیاز روتر به ریدایرکت کردن کاربر در زمان ترک صفحه طراحی شده است.
حفاظت تعویقی
CanLeaveService حفاظت CanDeactivate را پیادهسازی میکند که بر Observable ارائه شده از سوی CanLeaveComponent تکیه دارد:
export type CanLeaveType = boolean|Promise<boolean>|Observable<boolean>; @Injectable() export class CanLeaveService implements CanDeactivate<any> { private observer$ = new BehaviorSubject<CanLeaveType>(true); /** Pushes a quanding value into the guard observer to resolve when leaving the page */ public allowDeactivation(guard: CanLeaveType) { this.observer$.next(guard); } // Implements the CanDeactivate interface to conditionally prevent leaving the page canDeactivate(): Observable<boolean> { // Returns an observable resolving into a suitable guarding value return this.observer$.pipe( // Flatten the observer to a lower order one flatMap( canLeave => typeof(canLeave) === 'boolean' ? of(canLeave) : canLeave ), // Makes sure the observable always resolves first() ); } }
پیادهسازی فوق عمومی است و میتوان به روشهای متفاوتی نیز از آن استفاده کرد. مقدار ارسالی همراه با فراخوانی ()allowDeactivation به صورت یک مقدار حفاظتی مناسب از سوی دستگیره ()canDeactivate در زمان مسیریابی مورد استفاده قرار میگیرد.
در این مثال، از مزیت این پیادهسازی برای به تأخیر انداختن نمایش پیام کاربر تا زمان رخداد مسیریابی واقعی استفاده میکنیم.
کادر اعلانی
اگر کنجکاو هستید که بدانید آیا نسخه اعلانی (Declarative) کادر محاورهای که از سوی انگولار ارائه شده را از دست دادهاید، باید بگوییم که نگران نباشید چنین چیزی اصلاً وجود ندارد. کادر ارائه شده از سوی انگولار متریال سرویسی است که باید به صورت دستوری (imperative) استفاده شود. البته این وضعیت موجب میشود همواره فکر کنیم چیزی را از دست دادهایم.
بدین ترتیب برای پر کردن این شکاف میتوان از چیزی مانند زیر استفاده کرد:
/** Dialog ref */ export type DialogRef<R> = MatDialogRef<any, R>; /** * Component implementing a declarative version of the Angular Material Dialog */ @Component({ selector: 'wm-dialog', template: '<ng-template><ng-content></ng-content></ng-template>' }) export class DialogComponent<R=any> implements MatDialogConfig<any> { @ViewChild(TemplateRef, { static: true }) template: TemplateRef<any>; /** The dialog reference, when openend */ public ref: DialogRef<R>; constructor(readonly dialog: MatDialog/*, readonly viewContainerRef: ViewContainerRef*/) {} // -- Start of MatDialogConfig implementaiton -- /** ID for the dialog. If omitted, a unique one will be generated. */ @Input() id: string; /** The ARIA role of the dialog element. */ @Input() role: DialogRole = 'dialog'; /** Custom class for the overlay pane. */ @Input() panelClass: string | string[] = '' /** Whether the dialog has a backdrop. */ @Input() hasBackdrop: boolean = true; /** Custom class for the backdrop. */ @Input() backdropClass: string = ''; /** Whether the user can use escape or clicking on the backdrop to close the modal. */ @Input() disableClose: boolean = false; /** Width of the dialog. */ @Input() width: string = ''; /** Height of the dialog. */ @Input() height: string = ''; /** Min-width of the dialog. If a number is provided, assumes pixel units. */ @Input() minWidth: number | string; /** Min-height of the dialog. If a number is provided, assumes pixel units. */ @Input() minHeight?: number | string; /** Max-width of the dialog. If a number is provided, assumes pixel units. Defaults to 80vw. */ @Input() maxWidt: number | string = '80vw'; /** Max-height of the dialog. If a number is provided, assumes pixel units. */ @Input() maxHeight: number | string; /** Position overrides. */ @Input() position: DialogPosition; /** Layout direction for the dialog's content. */ @Input() direction: Direction; /** ID of the element that describes the dialog. */ @Input() ariaDescribedBy: string | null = null; /** ID of the element that labels the dialog. */ @Input() ariaLabelledBy: string | null = null; /** Aria label to assign to the dialog element. */ @Input() ariaLabel: string | null = null; /** Whether the dialog should focus the first focusable element on open. */ @Input() autoFocus: boolean = true; /** Whether the dialog should restore focus to the previously-focused element, after it's closed. */ @Input() restoreFocus: boolean = false; /** Scroll strategy to be used for the dialog. */ @Input() scrollStrategy: ScrollStrategy; /** Whether the dialog should close when the user goes backwards/forwards in history. */ @Input() closeOnNavigation: boolean = true; //public data: any = this; // -- End of MatDialogConfig implementaiton -- /** Opens the dialog when the passed condition is true */ @Input() set opened(open: boolean) { if(coerceBooleanProperty(open)) { this.open(); } } /** Reports the open status */ @Output() openedChange = new EventEmitter<boolean>(); /** Forces the dialog closing with the given value */ @Input() set closed(value: R) { this.close(value); } /** Reports the value the dialog as been closed with */ @Output() closedChange = new EventEmitter<R>(); /** Opens the dialog returning the reference */ public open(): DialogRef<R> { // Prevents multiple opening if(!!this.ref) { return this.ref; } // Opens the dialog with the given configuration this.ref = this.dialog.open<any,any,R>(this.template, this); // Emits the dialog has been opened this.ref.afterOpened().subscribe( () => this.openedChange.emit(true) ); // Emist the dialog is closing this.ref.beforeClosed().subscribe( () => this.openedChange.emit(false) ); this.ref.afterClosed().subscribe( value => { // Emits the dialog closed with value this.closedChange.emit(value); // Makes sure the reference goes backundefined when closed this.ref = undefined; }); // Returns the reference for further use return this.ref; } /** Closes the dialog passing along the output value */ public close(value: R): void { !!this.ref && this.ref.close(value); } }
ponent شامل یک قالب فرزند تودرتو است که در آن محتوا با استفاده از <ng-content> از والد دریافت میشود. این بدان معنی است که وقتی درون صفحه اعلان شود، قالب دیالوگ کامپایل خواهد شد، اما تا زمانی که بعداً به وسیله منطق MatDialog در زمان فراخوانی ()open رندر نشود، نمایش پیدا نخواهد کرد.
گزینههای زیادی وجود دارند که میتوان برای سفارشیسازی رفتار کادر محاورهای مورد استفاده قرار دارد و همه آنها به صورت یک سری ورودیها روی کامپوننت تعریف شدهاند که بازتابی از مشخصههای MatDialogConfig (+) هستند. کامپوننت یک متد ()open و یک متد ()close ارائه میکند که دومی مقداری دریافت کرده و وضعیت بسته شدن را گزارش میکند.
این کامپوننت همچنین جفتهای ورودی/خروجی opened/openedChange و closed/closedChange را عرضه کرده است که امکان اتصال داده دوطرفه را برای هر دو عملیات باز کردن و بستن کادر محاورهای فراهم میسازد.
محدودیتهای شناخته شده
کامپوننت Dialog محتوای خود را از کانتینر والد میگیرد و از این رو روشی برای عملیاتی کردن دایرکتیو mat-dialog-close وجود ندارد. دلیل این امر آن است که قالبی که باید نمایش یابد، خیلی پیشتر از باز شدن دیالوگ کامپایل شده است و از این رو دایرکتیو ها وهلهسازی شدهاند. یعنی امکان دریافت یک ارجاع دیالوگ معتبر جهت فراخوانی ()close روی آن وجود ندارد.
به طور عکس، قالب دیالوگ درون چارچوب کانتینر والد اجرا میشود و از این رو با استفاده از یک متغیر قالبی به نام ()close میتوانیم به <wm-dilaog> ارجاع دهیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش مقدماتی AngularJS برای ساخت اپلیکیشن های تک صفحه ای
- Lazy Loading در انگولار — به زبان ساده
- ساخت اپلیکیشن انگولار با امکان Drag and Drop — از صفر تا صد
==