سازماندهی بهتر کامپوننتهای ngx-bootstrap
پس از نصب بستهی npm کتابخانهی ngx-bootstrap و تنظیم فایل angular-cli.json. که در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانههای ثالث» بررسی شدند، برای کار با کامپوننتهای این کتابخانه باید متدهای BsDropdownModule.forRoot، TooltipModule.forRoot، ModalModule.forRoot و ... را به قسمت imports فایل app.module.ts اضافه کرد. با انجام اینکار پس از مدتی به یک فایل بسیار شلوغ app.module.ts خواهیم رسید. برای مدیریت بهتر آن میتوان شبیه به مطلب «سازماندهی برنامههای Angular توسط ماژولها» در پوشهی Shared برنامه، ماژول ذیل را تدارک دید. برای اینکار ابتدا فایل جدید src\app\shared\shared.bootstrap.module.ts را ایجاد نمائید. سپس کامپوننتهای این کتابخانه را به صورت ذیل در این تک ماژول اختصاصی قرار دهید:
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { BsDropdownModule } from "ngx-bootstrap/dropdown"; import { TooltipModule } from "ngx-bootstrap/tooltip"; import { ModalModule } from "ngx-bootstrap/modal"; @NgModule({ imports: [ CommonModule, BsDropdownModule.forRoot(), TooltipModule.forRoot(), ModalModule.forRoot() ], exports: [ BsDropdownModule, TooltipModule, ModalModule ] }) export class SharedBootstrapModule { }
اکنون برای استفادهی از SharedBootstrapModule اختصاصی فوق، میتوان دو روش را بکار برد:
الف) import مستقیم آن در فایل app.module.ts
import { SharedBootstrapModule } from './shared/shared.bootstrap.module'; @NgModule({ imports: [BrowserModule, SharedBootstrapModule], // ... }) export class AppModule {}
و یا اگر فایل src\app\shared\shared.module.ts را مطابق مطلب «سازماندهی برنامههای Angular توسط ماژولها» ایجاد کردهاید، این ماژول به صورت ذیل، در دو قسمت imports و exports آن اضافه خواهد شد:
import { SharedBootstrapModule } from "./shared.bootstrap.module"; @NgModule({ imports: [ CommonModule, SharedBootstrapModule ], exports: [ CommonModule, SharedBootstrapModule ] })
نمایش یک modal dialog توسط کامپوننت Modal
پس از تعریف ModalModule.forRoot، اکنون میتوان به کامپوننت Modal این ماژول دسترسی یافت. برای این منظور کامپوننتی که قرار است یک Modal را نمایش دهد، چنین ساختاری را پیدا میکند:
import { Component, OnInit, TemplateRef } from "@angular/core"; import { BsModalRef, BsModalService } from "ngx-bootstrap"; @Component({ selector: "app-modal-dialog-test", templateUrl: "./modal-dialog-test.component.html", styleUrls: ["./modal-dialog-test.component.css"] }) export class ModalDialogTestComponent implements OnInit { modalRef: BsModalRef; constructor(private modalService: BsModalService) { } openModal(template: TemplateRef<any>) { this.modalRef = this.modalService.show(template, { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false }); } closeModal() { this.modalRef.hide(); } }
سپس با فراخوانی متد this.modalService.show میتوان این قالب را نمایش داد. خروجی این متد ارجاعی را به این modal بازگشت میدهد. از این ارجاع میتوان در جهت بستن آن استفاده کرد (مانند متد closeModal).
بنابراین در ادامه، قالب کامپوننت مثال این قسمت، یک چنین شکلی را پیدا میکند:
<h1>Displaying modal bootstrap dialogs</h1> <button type="button" class="btn btn-info" (click)="openModal(template1)">Create template modal</button> <ng-template #template1> <div class="modal-header"> <h4 class="modal-title pull-left">Modal</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="closeModal()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> This is a modal. </div> </ng-template>
طراحی یک کامپوننت عمومی مودال جهت دریافت تائید انجام عملیات
در ادامه میخواهیم توسط یک modal dialog، کار دریافت تائید و یا لغو انجام یک عملیات را انجام دهیم. چون این کامپوننت عمومی قرار است در بیش از یک ماژول استفاده شود، بنابراین نیاز است آنرا در Shared Module ثبت کرد. به همین جهت این کامپوننت را به نحو ذیل در پوشهی Shared ایجاد میکنیم:
ng g c Shared/ConfirmModal --skip-import
import { ConfirmModalComponent } from "./confirm-modal/confirm-modal.component"; @NgModule({ imports: [ ], entryComponents: [ ConfirmModalComponent ], declarations: [ ConfirmModalComponent ] }) export class SharedModule {}
این کامپوننت دریافت تائید کاربر به صورت ذیل تعریف میشود:
import { Component } from "@angular/core"; @Component({ selector: "app-confirm-modal", templateUrl: "./confirm-modal.component.html", styleUrls: ["./confirm-modal.component.css"] }) export class ConfirmModalComponent { args: { title: string; message: string; }; close: (val?: any) => void; }
قالب این کامپوننت نیز بدون استفاده از ng-template تعریف میشود:
<div class="modal-header"> <h4 class="modal-title pull-left">{{ args?.title }}</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="close()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <p>{{ args?.message }}</p> </div> <div class="modal-footer"> <button class="btn btn-danger" (click)="close(true)">Yes</button> <button class="btn btn-primary" (click)="close()">Cancel</button> </div>
تا اینجا یک کامپوننت نمایش دریافت تائید انجام عملیات را تهیه کردیم. در ادامه نیاز است یک سرویس را جهت بارگذاری پویای اینگونه کامپوننتهای مودال طراحی کنیم. این سرویس عمومی در پوشهی Core و CoreModule ثبت خواهد شد:
>ng g s Core/Modal
import { Injectable } from "@angular/core"; import { BsModalService } from "ngx-bootstrap"; @Injectable() export class ModalService { constructor(private bsModalService: BsModalService) { } show(component: any, args?: any, options?: any): Promise<any> { return new Promise(resolve => { options = options || {}; const modal = this.bsModalService.show(component, options); let result: any; const sub = this.bsModalService.onHidden.subscribe(() => { sub.unsubscribe(); resolve(result); }); modal.content.args = args; modal.content.close = (val?: any) => { result = val; modal.hide(); }; }); } }
یک مودال در سه حالت ممکن است بسته شود:
الف) کلیک بر روی دکمهی close و یا cancel
ب) کلیک بر روی علامت ضربدر درج شدهی در یک سمت عنوان آن
ج) کلیک بر روی قسمتی از صفحه، خارج از مودال
در حالات ب و ج، رخداد this.bsModalService.onHidden فراخوانی میشود. در حالت الف، همان متد close درج شدهی در کامپوننت فراخوانی میشود.
برای اینکه بتوان نتیجهی عملیات را از طرف یک سرویس به کامپوننت فراخوان آن گزارش دهیم، یکی از روشها، استفاده از Promiseها است که مشاهده میکنید. با فراخوانی resolve(result)، کار ارسال نتیجهی فراخوانی متدهای close(true) و ()close صورت میگیرد (یا true و یا undefined).
خاصیت modal.content امکان دسترسی به خواص عمومی کامپوننت در حال استفاده را میسر میکند (content به کامپوننت بارگذاری شده اشاره میکند). اینجا است که میتوان برای مثال به خاصیت args یک کامپوننت، مقادیری را نسبت داد و یا به متد close آن دسترسی یافت.
پس از افزودن این سرویس، محل تعریف آن در قسمت providers مربوط به CoreModule است تا در تمام برنامه قابل دسترسی شود:
import { ModalService } from "./modal.service"; @NgModule({ providers: [ ModalService ] }) export class CoreModule {}
<button type="button" class="btn btn-danger" (click)="deleteRecord()">Delete record</button> <div *ngIf="confirmResult" class="alert alert-info">{{confirmResult}}</div>
import { ModalService } from "./../../core/modal.service"; import { ConfirmModalComponent } from "./../../shared/confirm-modal/confirm-modal.component"; export class ModalDialogTestComponent implements OnInit { confirmResult: string; constructor(private modalService: ModalService) { } deleteRecord() { this.confirmResult = ""; this.modalService.show( ConfirmModalComponent, { title: "Confirm", message: "Do you want to delete this record?" }, { animated: true, keyboard: true, backdrop: true, ignoreBackdropClick: false }).then(confirmed => { if (confirmed) { this.confirmResult = "Deleted!"; } else { this.confirmResult = "Canceled!"; } }); } }
توسط this.modalService.show میتوان انواع و اقسام کامپوننتهای مودال را به صورت پویا بارگذاری کرد و نمایش داد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
- تامین اطلاعات از طریق کامپوننت پدر و انتساب اطلاعات به کامپوننتهای فرزند جهت نمایش
- تامین اطلاعات به صورت توکار و توسط خود کامپوننت فرزند
- انتقال اطلاعات از کامپوننت پدر به فرزند از طریق متادیتا Input@
- بهجریان انداختن رخداد از کامپوننت فرزند و گرفتن آن از طریق کامپوننت پدر
- تعامل کامپوننت پدر و فرزند از طریق template reference variable
- فراخوانی کامپوننت فرزند از کامپوننت پدر به کمک ViewChild@
- ارتباط کامپوننت پدر و فرزند از طریق سرویس
انتقال اطلاعات از کامپوننت پدر به فرزند از طریق متادیتای Input@
import { Component, OnInit, Input} from '@angular/core'; @Component({ selector: 'app-customer-info', templateUrl: './customer-info.component.html', styleUrls: ['./customer-info.component.css'] }) export class CustomerInfoComponent implements OnInit { @Input() FormIsReadOnly: boolean; constructor() { } ngOnInit() { } }
<app-customer-info FormIsReadOnly="{{true}}"></app-customer-info>
در صورت استفاده به شکل زیر:
<app-customer-info FormIsReadOnly="true"></app-customer-info>
<app-customer-info [FormIsReadOnly]="booleanVariable"></app-customer-info>
پیغام نامناسب در زمان انتخاب بانک ملت
import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl } from '@angular/forms'; @Component({ selector: 'validation-message', template: ` <ng-container *ngIf="control.invalid && control.touched"> {{ message }} </ng-container> ` }) export class ValidationMessageComponent implements OnInit { @Input() control: AbstractControl; @Input() fieldDisplayName: string; @Input() rules: { [key: string]: string }; get message(): string { return this.control.hasError('required') ? `${this.fieldDisplayName} را وارد نمائید.` : this.control.hasError('pattern') ? `${this.fieldDisplayName} را به شکل صحیح وارد نمائید.` : this.control.hasError('email') ? `${this.fieldDisplayName} را به شکل صحیح وارد نمائید.` : this.control.hasError('minlength') ? `${this.fieldDisplayName} باید بیشتر از ${ this.control.errors.minlength.requiredLength } کاراکتر باشد.` : this.control.hasError('maxlength') ? `${this.fieldDisplayName} باید کمتر از ${ this.control.errors.maxlength.requiredLength } کاراکتر باشد.` : this.control.hasError('min') ? `${this.fieldDisplayName} باید بیشتر از ${ this.control.errors.min.requiredLength } باشد.` : this.control.hasError('max') ? `${this.fieldDisplayName} باید کمتر از ${ this.control.errors.max.requiredLength } باشد.` : this.hasRule() ? this.findRule() : this.control.hasError('model') ? `${this.control.errors.model.messages[0]}` : ''; } constructor() {} private hasRule() { return ( this.rules && Object.keys(this.control.errors).some(ruleKey => this.rules[ruleKey] ? true : false ) ); } private findRule(): string { let message = ''; Object.keys(this.control.errors).forEach(ruleKey => { if (this.rules[ruleKey]) { message += `${this.rules[ruleKey]} `; } }); return message; } ngOnInit(): void {} }
<mat-error *ngIf="form.controls['userName'].invalid && form.controls['userName'].touched" class="mat-text-warn"> <validation-message [control]="form.controls['userName']" fieldDisplayName="نام کاربری" [rules]="{rule1:'پیغام متناظر با rule1'}"> </validation-message> </mat-error>
this.form = this.formBuilder.group({ userName: [ '', [Validators.required, UserNameValidators.rule1)] ], password: ['', Validators.required], rememberMe: [false] }); export class UserNameValidators{ static rule1(control: AbstractControl) { if (control.value.indexOf(' ') >= 0) { return { rule1: true }; } return null; } }
بنابراین برای ایجاد یک کامپوننت میتوانیم به اینصورت عمل کنیم:
var app = angular.module("dntModule", []); app.component("pmApp", { template: `Hello this is a simple component` });
همانطور که مشاهده میکنید تابع component دو پارامتر را از ورودی دریافت خواهد کرد؛ نام کامپوننت و یک شیء برای تعیین تنظیمات کامپوننت. نام کامپوننت در اینجا به صورت camel case تعریف شده است؛ که در واقع یک convention برای Angular است. در اینحالت برای استفادهی از کامپوننت باید به اینصورت عمل کنیم:
<pm-app></pm-app>
در قسمت تنظیمات کامپوننت، در سادهترین حالت یک template تعیین شدهاست که بیانگر نحوهی رندر شدن یک کامپوننت میباشد. در اینحالت وقتی انگیولار به تگ فوق برسد، یک کامپوننت با نام pmApp را بارگذاری خواهد کرد.
ایجاد یک کامپوننت ساده
در ادامه میخواهیم یک کامپوننت ساده را جهت نمایش یکسری URL درون صفحه طراحی کنیم. ساختار صفحه index.html به صورت زیر خواهد بود:
<html ng-app="DNT"> <head> <meta charset="UTF-8"> <title>Using Angular Component</title> <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css"> <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css"> </head> <body> <div class="container"> <div class="row"> <div class="col-md-3"> <dnt-widget></dnt-widget> </div> </div> </div> <script src="bower_components/angular/angular.js"></script> <script src="scripts/app.js"></script> <script src="scripts/components/dnt-widget.component.js"></script> </body> </html>
در اینجا ابتدا توسط دایرکتیو ng-app، به Angular، ماژولمان را معرفی کردهایم. سپس مداخل بوتاسترپ و کتابخانهی font-awesome را مشاهده میکنید. در ادامه، کتابخانهی Angular و همچنین فایل app.js جهت معرفی ماژول برنامه معرفی شدهاست. در نهایت نیز یک فایل در مسیر ذکر شده برای قرار دادن کدهای کامپوننت در مسیر scripts/components اضافه شدهاست.
همانطور که ملاحظه میکنید، کامپوننتمان به صورت یک تگ سفارشی، درون صفحه قرار گرفته است:
<dnt-archive></dnt-archive>
در ادامه باید به Angular، نحوهی تعریف این کامپوننت را اعلام کنیم. بنابراین یک فایل جاوا اسکریپتی را با نام dnt-widget.component، با محتویات زیر ایجاد کنید:
(function () { "use strict"; var app = angular.module("DNT"); function DntArchiveController() { var model = this; model.panel = { title: "Panel Title", items: [ { title: "Dotnettips", url: "https://www.dntips.ir" }, { title: "Google", url: "http://www.google.con" }, { title: "Yahoo", url: "http://www.yahoo.con" } ] }; }; app.component("dntWidget", { templateUrl: '/scripts/components/dnt-widget.component.html', controllerAs: "model", controller: DntArchiveController }); } ());
توضیح کدهای فوق:
همانطور که مشاهده میکنید، برای پارمتر دوم کامپوننت، سه پراپرتی را تعیین کردهایم:
templateUrl: به کمک این پراپرتی به Angular گفتهایم که محتوای قالب این کامپوننت، درون یک فایل HTML مجزا قرار دارد و به صورت linked template میباشد.
controllerAs: یکی از مزایای استفاده از کامپوننتها، استفاده از controller as syntax میباشد. لازم به ذکر است اگر این پراپرتی را مقداردهی نکنیم، به صورت پیشفرض مقدار ctrl$ در نظر گرفته خواهد شد.
controller: مزیت دیگر کامپوننتها، استفاده از کنترلرها است. با استفاده از این پراپرتی، یک کنترلر را برای کامپوننتمان رجیستر کردهایم. در نتیجه زمانیکهی Angular میخواهد کامپوننتمان را نمایش دهد، تابع تعریف شده برای این پراپرتی، جهت ایجاد یک controller instance فراخوانی خواهد شد. بنابراین هر پراپرتی یا تابعی که برای این controller instance تعریف کنیم، به راحتی درون ویوی آن جهت اعمال بایندینگ در دسترس خواهد بود (در نتیجه نیازی به scope$ نخواهد بود).
درون کنترلر نیز برای راحتی کار و همچنین به عنوان یک best practice، مقدار this را توسط یک متغیر با نام model، کپچر کردهایم. در اینجا یک شیء را با نام panel نیز به مدل اضافه کردهایم.
محتویات تمپلیت:
<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> <span class="fa fa-archive"></span> {{ model.panel.title}} </h3> </div> <ul class="list-group"> <li class="list-group-item" ng-repeat="item in model.panel.items"> <span class="fa fa-industry"></span> <a href="{{ item.url }}">{{ item.title }}</a> </li> </ul> </div>
ویوی کامپوننت پیچیدگی خاصی ندارد. همانطور که مشاهده میکنید یک پنل بوتاسترپی را ایجاد کردهایم که مقدار عنوان آن و همچنین آیتمهای آن، از شیء اتچ شده به مدل دریافت خواهند شد. بنابراین اکنون اگر برنامه را اجرا کنید، خروجی کامپوننت را به اینصورت مشاهده خواهید کرد:
همانطور که مشاهده میکنید استفاده از کامپوننتها در Angular 1.5 در مقایسه با ایجاد دایرکتیوها و کنترلرها خیلی سادهتر است. در واقع امکانات این API جدید تنها به مثال فوق ختم نمیشود؛ بلکه این API یک سیستم مسیریابی جدید را نیز معرفی کرده است که در قسمتهای بعدی به آن نیز خواهیم پرداخت.
جهت تکمیل بحث نیز یک تقویم شمسی ساده را در اینجا قرار دادهام. میتوانید جهت مرور بحث جاری به کدهای آن مراجعه کنید. البته هدف از تعریف این پروژه تنها یک مثال ساده برای معرفی کامپوننتها بود و طبیعتاً باگهای زیادی دارد. اگر مایل بودید میتوانید در توسعهی آن مشارکت نمائید.
کدهای این قسمت را نیز از اینجا میتوانید دریافت کنید.
معرفی کتابخانهی DNTCaptcha.Core