در اینجا توسط کامپوننت sidenav، کار نمایش لیست تماسها صورت میگیرد و نمایش این کامپوننت واکنشگرا است. به این معنا که در اندازههای صفحات نمایشی بزرگ، نمایان است و در صفحات نمایشی کوچک، مخفی خواهد شد. در بالای صفحه یک Toolbar قرار دارد که همیشه نمایان است و از آن برای نمایش گزینههای منوی برنامه استفاده میکنیم. همچنین ناحیهی main content را هم مشاهده میکنید که با انتخاب هر شخص از لیست تماسها، جزئیات او در این قسمت نمایش داده خواهد شد.
ایجاد ماژول مدیریت تماسها
در قسمت اول، برنامه را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد کردیم که نتیجهی آن تولید فایل src\app\app-routing.module.ts میباشد:
ng new MaterialAngularClient --routing
ng g m ContactManager -m app.module --routing
این دستور ماژول جدید contact-manager را به همراه تنظیمات ابتدایی مسیریابی و همچنین به روز رسانی app.module، برای درج آن، ایجاد میکند. البته در این حالت نیاز است به app.module.ts مراجعه کرد و محل درج آنرا تغییر داد:
import { ContactManagerModule } from "./contact-manager/contact-manager.module"; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, CoreModule, SharedModule.forRoot(), ContactManagerModule, AppRoutingModule ], }) export class AppModule { }
سپس دستور زیر را اجرا میکنیم تا کامپوننت contact-manager-app در ماژول contact-manager ایجاد شود:
ng g c contact-manager/ContactManagerApp --no-spec
CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.html (38 bytes) CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.ts (319 bytes) CREATE src/app/contact-manager/contact-manager-app/contact-manager-app.component.css (0 bytes) UPDATE src/app/contact-manager/contact-manager.module.ts (436 bytes)
این کامپوننت به عنوان میزبان سایر کامپوننتهایی که در مقدمهی بحث عنوان شدند، عمل میکند. این کامپوننتها را به صورت زیر در پوشهی components ایجاد میکنیم:
ng g c contact-manager/components/toolbar --no-spec ng g c contact-manager/components/main-content --no-spec ng g c contact-manager/components/sidenav --no-spec
تنظیمات مسیریابی برنامه
در ادامه به src\app\app-routing.module.ts مراجعه کرده و این ماژول جدید را به صورت lazy load معرفی میکنیم:
import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; const routes: Routes = [ { path: "contactmanager", loadChildren: "./contact-manager/contact-manager.module#ContactManagerModule" }, { path: "", redirectTo: "contactmanager", pathMatch: "full" }, { path: "**", redirectTo: "contactmanager" } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
سپس تنظیمات مسیریابی ماژول مدیریت تماسها را در فایل src\app\contact-manager\contact-manager-routing.module.ts به صورت زیر تغییر میدهیم:
import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { MainContentComponent } from "./components/main-content/main-content.component"; import { ContactManagerAppComponent } from "./contact-manager-app/contact-manager-app.component"; const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class ContactManagerRoutingModule { }
کامپوننت ContactManagerApp که کار هاست سایر کامپوننتهای این ماژول را بر عهده دارد، دارای router-outlet خاص خود خواهد بود. به همین جهت برای آن children تعریف شدهاست که مسیر پیشفرض آن، بارگذاری کامپوننت MainContent است.
بنابراین نیاز است به فایل contact-manager-app\contact-manager-app.component.html مراجعه و ابتدا منوی کنار صفحه را به آن افزود:
<app-sidenav></app-sidenav>
سپس در قالب sidenav\sidenav.component.html، کار تعریف toolbar و همچنین router-outlet را انجام میدهیم:
<app-toolbar></app-toolbar> <router-outlet></router-outlet>
معرفی Angular Material به ماژول جدید مدیریت تماسها
در قسمت اول، یک فایل material.module.ts را ایجاد کردیم که به همراه تعریف تمامی کامپوننتهای Angular Material بود. سپس آنرا به shared.module.ts افزودیم که حاوی تعریف ماژول فرمها و همچنین Flex Layout نیز هست. به همین جهت برای معرفی اینها به این ماژول جدید تنها کافی است در فایل src\app\contact-manager\contact-manager.module.ts در قسمت imports، کار معرفی SharedModule صورت گیرد:
import { SharedModule } from "../shared/shared.module"; @NgModule({ imports: [ CommonModule, SharedModule, ContactManagerRoutingModule ] }) export class ContactManagerModule { }
پس از اجرای برنامه مشاهده میکنید که ابتدا ماژول مدیریت تماسها بارگذاری شدهاست و سپس contact-manager-app عمل و sidenav را بارگذاری کرده و آن نیز سبب نمایش کامپوننت toolbar و سپس main-content شدهاست.
تنظیم طرحبندی برنامه توسط کامپوننتهای Angular Material و همچنین Flex Layout
پس از این تنظیمات اکنون نوبت به تنظیم طرحبندی برنامهاست و آنرا با قراردادن کامپوننت Sidenav بستهی Angular Material شروع میکنیم:
<mat-sidenav-container *ngIf="shouldRun"> <mat-sidenav mode="side" opened> Sidenav content </mat-sidenav> Primary content </mat-sidenav-container>
- Over: قسمت Sidenav content بر روی Primary content قرار میگیرد.
- Push: قسمت Sidenav content قسمت Primary content را از سر راه خود بر میدارد.
- Side: قسمت Sidenav content در کنار Primary content قرار میگیرد.
در اینجا از حالت Side، در صفحات نمایشی بزرگ (اولین تصویر این قسمت) و از حالت Over، در صفحات نمایشی موبایل (مانند تصویر زیر) استفاده خواهیم کرد.
در ابتدا کدهای کامل هر سه کامپوننت و سپس توضیحات آنها را مشاهده خواهید کرد:
تنظیم margin در CSS اصلی برنامه
زمانیکه sidenav و toolbar را بر روی صفحه قرار میدهیم، فاصلهای بین آنها و لبههای صفحه مشاهده میشود. برای اینکه این فاصله را به صفر برسانیم، به فایل src\styles.css مراجعه کرده و margin بدنهی صفحه را به صفر تنظیم میکنیم:
@import "~@angular/material/prebuilt-themes/indigo-pink.css"; body { margin: 0; }
طراحی قالب main content
<mat-card> <h1>Main content</h1> </mat-card>
sidenav\sidenav.component.css | sidenav\sidenav.component.html |
.app-sidenav-container { position: fixed; } .app-sidenav { width: 240px; } .wrapper { margin: 50px; } | <mat-sidenav-container fxLayout="row" fxFill> <mat-sidenav #sidenav fxFlex="1 1 100%" [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'"> <mat-toolbar color="primary"> Contacts </mat-toolbar> <mat-list> <mat-list-item>Item 1</mat-list-item> <mat-list-item>Item 2</mat-list-item> <mat-list-item>Item 3</mat-list-item> </mat-list> </mat-sidenav> <mat-sidenav-content fxLayout="column" fxFlex="1 1 100%" fxFill> <app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar> <div> <router-outlet></router-outlet> </div> </mat-sidenav-content> </mat-sidenav-container> |
- نمای کلی صفحه در این قسمت طراحی شدهاست. sidenav-container که در برگیرندهی اصلی است، به fxLayout از نوع row تنظیم شدهاست. یعنی mat-sidenav و mat-sidenav-content دو ستون آنرا از چپ به راست تشکیل میدهند و درون یک ردیف، سیلان خواهند یافت. همچنین میخواهیم این container کل صفحه را پر کند، به همین جهت از fxFill استفاده شدهاست. این fxFill اعمال شدهی به container، زمانی عمل خواهد کرد که position آن در css، به fixed تنظیم شود که اینکار در css این قالب و در کلاس app-sidenav-container آن انجام شدهاست.
- سپس toolbar و همچنین router-outlet که main content را نمایش میدهند، داخل sidenav-content قرار گرفتهاند و هر دو با هم، ستون دوم این طرحبندی را تشکیل میدهند. به همین جهت fxLayout آن به column تنظیم شدهاست (ستون اول آن، لیست تماسها است و ستون دوم آن، از دو ردیف toolbar و main-content تشکیل میشود).
- اگر دقت کنید یک template reference variable به نام sidenav# به container اعمال شدهاست. از آن، جهت باز و بسته کردن sidenav استفاده میشود:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
- mat-sidenav از دو قسمت تشکیل شدهاست. بالای آن توسط mat-toolbar صرفا کلمهی Contacts نمایش داده میشود و سپس ذیل آن، لیست فرضی تماسها توسط کامپوننت mat-list قرار گرفتهاند (تا فعلا خالی نباشد. در قسمتهای بعدی آنرا پویا خواهیم کرد). رنگ تولبار آنرا ("color="primary) نیز به primary تنظیم کردهایم تا خاکستری پیشفرض آن نباشد.
- کار کلاس mat-elevation-z10 این است که بین sidenav و main-content یک سایهی سه بعدی را ایجاد کند که آنرا در تصاویر مشاهده میکنید. عددی که پس از z قرار میگیرد، میزان عمق سایه را مشخص میکند.
- این قسمت از sidenav به همراه دو خاصیت opened و همچنین mode است که به مقدار isScreenSmall عکس العمل نشان میدهند:
<mat-sidenav [opened]="!isScreenSmall" [mode]="isScreenSmall ? 'over' : 'side'">
محتویات فایل sidenav\sidenav.component.ts:
import { Component, OnDestroy, OnInit } from "@angular/core"; import { MediaChange, ObservableMedia } from "@angular/flex-layout"; import { Subscription } from "rxjs"; @Component({ selector: "app-sidenav", templateUrl: "./sidenav.component.html", styleUrls: ["./sidenav.component.css"] }) export class SidenavComponent implements OnInit, OnDestroy { isScreenSmall = false; watcher: Subscription; constructor(private media: ObservableMedia) { this.watcher = media.subscribe((change: MediaChange) => { this.isScreenSmall = change.mqAlias === "xs"; }); } ngOnInit() { } ngOnDestroy() { this.watcher.unsubscribe(); } }
تاثیر خاصیت isScreenSmall بر روی دو خاصیت opened و mode کامپوننت sidenav را در دو تصویر زیر مشاهده میکنید. اگر اندازهی صفحه کوچک شود، ابتدا sidenav مخفی میشود. اگر کاربر بر روی دکمهی منوی همبرگری کلیک کند، سبب نمایش مجدد sidenav، اینبار با حالت over و بر روی محتوای زیرین آن خواهد شد:
طراحی نوار ابزار واکنشگرا
کدهای قالب و css تولبار (ستون دوم طرحبندی کلی صفحه) را در ادامه مشاهده میکنید:
toolbar\toolbar.component.css | toolbar\toolbar.component.html |
.sidenav-toggle { padding: 0; margin: 8px; min-width:56px; } | <mat-toolbar color="primary"> <button mat-button fxHide fxHide.xs="false" class="sidenav-toggle" (click)="toggleSidenav.emit()"> <mat-icon>menu</mat-icon> </button> <span>Contact Manager</span> </mat-toolbar> |
با توجه به استفادهی از fxHide، یعنی دکمهی نمایش منوی همبرگری در تمام حالات مخفی خواهد بود. برای لغو آن و نمایش آن در حالت موبایل، از حالت واکنشگرای آن یعنی fxHide.xs استفاده میکنیم (قسمت «کار با API واکنشگرای Angular Flex layout» در مطلب قبلی این سری). به این ترتیب زمانیکه کاربر اندازهی صفحه را کوچک میکند و یا اندازهی واقعی صفحهی نمایش او کوچک است، این دکمه نمایان خواهد شد.
همچنین در sidenav یک چنین تعریفی را داریم:
<app-toolbar (toggleSidenav)="sidenav.toggle()"></app-toolbar>
محتویات فایل toolbar\toolbar.component.ts:
import { Component, EventEmitter, OnInit, Output } from "@angular/core"; @Component({ selector: "app-toolbar", templateUrl: "./toolbar.component.html", styleUrls: ["./toolbar.component.css"] }) export class ToolbarComponent implements OnInit { @Output() toggleSidenav = new EventEmitter<void>(); constructor() { } ngOnInit() { } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-02.zip
برای اجرای آن نیز ابتدا فایل restore.bat و سپس فایل ng-serve.bat را اجرا کنید. پس از اجرای برنامه، یکبار آنرا در حالت تمام صفحه و بار دیگر با کوچکتر کردن اندازهی مرورگر آزمایش کنید. در حالتیکه به اندازهی موبایل رسیدید، بر روی دکمهی همبرگری نمایان شده کلیک کنید تا عکس العمل آن و نمایش مجدد sidenav را در حالت over، مشاهده نمائید.
سایتهای بسیاری هستند که سرویسهایی را برای بدست آوردن مشخصات کشور، از روی IP ارائه میدهند؛ ولی اکثر آنها برای این سرویسی که ارائه میدهند هزینه دریافت میکنند. سایتی که من در این مقاله معرفی خواهم کرد این سرویس را به رایگان ارائه میهد، به شرط اینکه درخواستهای شما در هر ساعت بیشتر از 10000 نباشد. اگر این اتفاق روی دهد، یعنی درخواستهای شما به بیش از 10000 در ساعت برسد، درخواستهای شما با خطای HTTP 403, forbidden مواجه خواهد شد و تا زمانیکه محدودیت شما به پایان برسد، باید منتظر بمانید.
سرویسی که این سایت ارائه میدهد، سورس باز و رایگان میباشد. اگر شما با این محدودیت 10000 درخواست در ساعت مشکلی داشتید، میتوانید این
سرویس را از اینجا دریافت و آنرا در سرور
خودتان اجرا کنید. سورس سرویس هم بر روی github موجود هست.
freegeoip.net/{format}/{IP_or_hostname}
freegeoip.net/xml/4.2.2.2 : XML freegeoip.net/csv/8.8.8.8 :CSV freegeoip.net/json/github.com :JSON
http://freegeoip.net/xml/162.158.88.214
<Response> <IP>162.158.88.214</IP> <CountryCode>DE</CountryCode> <CountryName>Germany</CountryName> <RegionCode>HE</RegionCode> <RegionName>Hesse</RegionName> <City>Frankfurt am Main</City> <ZipCode>60438</ZipCode> <TimeZone>Europe/Berlin</TimeZone> <Latitude>50.1167</Latitude> <Longitude>8.6833</Longitude> <MetroCode>0</MetroCode> </Response>
اگر آدرس IP را مشخص نکنید مشخصات مربوط به IP آدرس خودتان را دریافت خواهید کرد.
در ادامه از یک برنامه console application برای دریافت این پاسخ ارسال شده از سرویس و استخراج دادهها از آن استفاده خواهیم کرد.
البته این روش برای برنامههای تحت وب هم به همین صورت خواهد بود و تفاوتی نمیکند.
از این روش میتوانید برای بلاک کردن کشورهایی که نمیخواهید به برنامهی شما دسترسی پیدا کنند استفاده کنید و یا اینکه بدانید ip هایی که از سایت شما بازدید میکنند از کدام کشورهای جهان هستند و یا کار خلاقانهای که میتوانید انجام دهید، نمایش مکانهای بازدید کنندهها بر روی نقشه گوگل، با استفاده از طول و عرض جغرافیایی که این سرویس در اختیارتان میگذارد، میباشدpublic static void ReadXmlElements(string ipAddress) { XDocument xdoc=XDocument.Load("http://www.freegeoip.net/xml/" + ipAddress); var country = xdoc.Descendants("Response").Select(c => new { IpAddress = c.Element("IP")?.Value, CountryCode = c.Element("CountryCode")?.Value, CountryName = c.Element("CountryName")?.Value, RegionCode = c.Element("RegionCode")?.Value, RegionName = c.Element("RegionName")?.Value, City = c.Element("City")?.Value, ZipCode = c.Element("ZipCode")?.Value, TimeZone = c.Element("TimeZone")?.Value, Latitude = c.Element("Latitude")?.Value, Longitude = c.Element("Longitude")?.Value, MetroCode = c.Element("MetroCode")?.Value, }); var countryData = country.First(); Console.WriteLine("CountryName :" + countryData.CountryName +Environment.NewLine + "CountryCode :"+countryData.CountryCode + Environment.NewLine + "RegionCode :"+countryData.RegionCode + Environment.NewLine + "RegionName :" + countryData.RegionName + Environment.NewLine + "City :" + countryData.City + Environment.NewLine + "ZipCode :" + countryData.ZipCode + Environment.NewLine + "TimeZone :" + countryData.TimeZone + Environment.NewLine + "Latitude :" + countryData.Latitude + Environment.NewLine + "Longitude :" + countryData.Longitude + Environment.NewLine + "MetroCode :" + countryData.MetroCode ); Console.ReadKey(); }
در متد main برنامه، متد ReadXmlElements را فراخوانی کرده و آدرس آی
پی مورد نظر را پاس میدهیم:
static void Main(string[] args) { ReadXmlElements("162.158.88.214"); }
یک نکته : برای مقایسه کد کشور بدست آمده، باید لیست کاملی از کشورهای جهان را در اختیار داشته باشید، تا بتوانید تصمیم بگیرید که آیا کشور درخواست کننده، مد نظر شما هست یا خیر. برای اینکار، کلاسی را آماده کردهام که شامل کل کشورهای جهان میباشد و نام و کد کشورها، در آن وجود دارد؛ به صورت زیر:
public class WorldCountries { public string CountryCode { get; set; } public string CountryName { get; set; } }
public static class GenerateCountries { public static IList<WorldCountries> CreateCountries() { return new[] { new WorldCountries { CountryCode = "AF", CountryName = "Afghanistan"}, new WorldCountries { CountryCode = "AX", CountryName = "Åland Islands"}, new WorldCountries { CountryCode = "AL", CountryName = "Albania"}, new WorldCountries { CountryCode = "DZ", CountryName = "Algeria"}, new WorldCountries { CountryCode = "AS", CountryName = "American Samoa"}, new WorldCountries { CountryCode = "AD", CountryName = "Andorra"}, new WorldCountries { CountryCode = "AO", CountryName = "Angola"}, new WorldCountries { CountryCode = "AI", CountryName = "Anguilla"}, new WorldCountries { CountryCode = "AQ", CountryName = "Antarctica"}, new WorldCountries { CountryCode = "AG", CountryName = "Antigua and Barbuda"}, new WorldCountries { CountryCode = "AR", CountryName = "Argentina"}, new WorldCountries { CountryCode = "AM", CountryName = "Armenia"}, new WorldCountries { CountryCode = "AW", CountryName = "Aruba"}, new WorldCountries { CountryCode = "AU", CountryName = "Australia"}, new WorldCountries { CountryCode = "AT", CountryName = "Austria"}, new WorldCountries { CountryCode = "AZ", CountryName = "Azerbaijan"}, new WorldCountries { CountryCode = "BS", CountryName = "Bahamas"}, new WorldCountries { CountryCode = "BH", CountryName = "Bahrain"}, new WorldCountries { CountryCode = "BD", CountryName = "Bangladesh"}, new WorldCountries { CountryCode = "BB", CountryName = "Barbados"}, new WorldCountries { CountryCode = "BY", CountryName = "Belarus"}, new WorldCountries { CountryCode = "BE", CountryName = "Belgium"}, new WorldCountries { CountryCode = "BZ", CountryName = "Belize"}, new WorldCountries { CountryCode = "BJ", CountryName = "Benin"}, new WorldCountries { CountryCode = "BM", CountryName = "Bermuda"}, new WorldCountries { CountryCode = "BT", CountryName = "Bhutan"}, new WorldCountries { CountryCode = "BA", CountryName = "Bosnia and Herzegovina"}, new WorldCountries { CountryCode = "BW", CountryName = "Botswana"}, new WorldCountries { CountryCode = "BV", CountryName = "Bouvet Island"}, new WorldCountries { CountryCode = "BR", CountryName = "Brazil"}, new WorldCountries { CountryCode = "IO", CountryName = "British Indian Ocean Territory"}, new WorldCountries { CountryCode = "BN", CountryName = "Brunei Darussalam"}, new WorldCountries { CountryCode = "BG", CountryName = "Bulgaria"}, new WorldCountries { CountryCode = "BF", CountryName = "Burkina Faso"}, new WorldCountries { CountryCode = "BI", CountryName = "Burundi"}, new WorldCountries { CountryCode = "KH", CountryName = "Cambodia"}, new WorldCountries { CountryCode = "CM", CountryName = "Cameroon"}, new WorldCountries { CountryCode = "CA", CountryName = "Canada"}, new WorldCountries { CountryCode = "CV", CountryName = "Cape Verde"}, new WorldCountries { CountryCode = "KY", CountryName = "Cayman Islands"}, new WorldCountries { CountryCode = "CF", CountryName = "Central African Republic"}, new WorldCountries { CountryCode = "TD", CountryName = "Chad"}, new WorldCountries { CountryCode = "CL", CountryName = "Chile"}, new WorldCountries { CountryCode = "CN", CountryName = "China"}, new WorldCountries { CountryCode = "CX", CountryName = "Christmas Island"}, new WorldCountries { CountryCode = "CC", CountryName = "Cocos (Keeling) Islands"}, new WorldCountries { CountryCode = "CO", CountryName = "Colombia"}, new WorldCountries { CountryCode = "KM", CountryName = "Comoros"}, new WorldCountries { CountryCode = "CG", CountryName = "Congo"}, new WorldCountries { CountryCode = "CK", CountryName = "Cook Islands"}, new WorldCountries { CountryCode = "CR", CountryName = "Costa Rica"}, new WorldCountries { CountryCode = "HR", CountryName = "Croatia"}, new WorldCountries { CountryCode = "CU", CountryName = "Cuba"}, new WorldCountries { CountryCode = "CW", CountryName = "Curaçao"}, new WorldCountries { CountryCode = "CY", CountryName = "Cyprus"}, new WorldCountries { CountryCode = "CZ", CountryName = "Czech Republic"}, new WorldCountries { CountryCode = "DK", CountryName = "Denmark"}, new WorldCountries { CountryCode = "DJ", CountryName = "Djibouti"}, new WorldCountries { CountryCode = "DM", CountryName = "Dominica"}, new WorldCountries { CountryCode = "DO", CountryName = "Dominican Republic"}, new WorldCountries { CountryCode = "EC", CountryName = "Ecuador"}, new WorldCountries { CountryCode = "EG", CountryName = "Egypt"}, new WorldCountries { CountryCode = "SV", CountryName = "El Salvador"}, new WorldCountries { CountryCode = "GQ", CountryName = "Equatorial Guinea"}, new WorldCountries { CountryCode = "ER", CountryName = "Eritrea"}, new WorldCountries { CountryCode = "EE", CountryName = "Estonia"}, new WorldCountries { CountryCode = "ET", CountryName = "Ethiopia"}, new WorldCountries { CountryCode = "FK", CountryName = "Falkland Islands (Malvinas)"}, new WorldCountries { CountryCode = "FO", CountryName = "Faroe Islands"}, new WorldCountries { CountryCode = "FJ", CountryName = "Fiji"}, new WorldCountries { CountryCode = "FI", CountryName = "Finland"}, new WorldCountries { CountryCode = "FR", CountryName = "France"}, new WorldCountries { CountryCode = "GF", CountryName = "French Guiana"}, new WorldCountries { CountryCode = "PF", CountryName = "French Polynesia"}, new WorldCountries { CountryCode = "TF", CountryName = "French Southern Territories"}, new WorldCountries { CountryCode = "GA", CountryName = "Gabon"}, new WorldCountries { CountryCode = "GM", CountryName = "Gambia"}, new WorldCountries { CountryCode = "GE", CountryName = "Georgia"}, new WorldCountries { CountryCode = "DE", CountryName = "Germany"}, new WorldCountries { CountryCode = "GH", CountryName = "Ghana"}, new WorldCountries { CountryCode = "GI", CountryName = "Gibraltar"}, new WorldCountries { CountryCode = "GR", CountryName = "Greece"}, new WorldCountries { CountryCode = "GL", CountryName = "Greenland"}, new WorldCountries { CountryCode = "GD", CountryName = "Grenada"}, new WorldCountries { CountryCode = "GP", CountryName = "Guadeloupe"}, new WorldCountries { CountryCode = "GU", CountryName = "Guam"}, new WorldCountries { CountryCode = "GT", CountryName = "Guatemala"}, new WorldCountries { CountryCode = "GG", CountryName = "Guernsey"}, new WorldCountries { CountryCode = "GN", CountryName = "Guinea"}, new WorldCountries { CountryCode = "GW", CountryName = "Guinea-Bissau"}, new WorldCountries { CountryCode = "GY", CountryName = "Guyana"}, new WorldCountries { CountryCode = "HT", CountryName = "Haiti"}, new WorldCountries { CountryCode = "HM", CountryName = "Heard Island and McDonald Islands"}, new WorldCountries { CountryCode = "VA", CountryName = "Holy See (Vatican City State)"}, new WorldCountries { CountryCode = "HN", CountryName = "Honduras"}, new WorldCountries { CountryCode = "HK", CountryName = "Hong Kong"}, new WorldCountries { CountryCode = "HU", CountryName = "Hungary"}, new WorldCountries { CountryCode = "IS", CountryName = "Iceland"}, new WorldCountries { CountryCode = "IN", CountryName = "India"}, new WorldCountries { CountryCode = "ID", CountryName = "Indonesia"}, new WorldCountries { CountryCode = "IR", CountryName = "Iran"}, new WorldCountries { CountryCode = "IQ", CountryName = "Iraq"}, new WorldCountries { CountryCode = "IE", CountryName = "Ireland"}, new WorldCountries { CountryCode = "IM", CountryName = "Isle of Man"}, new WorldCountries { CountryCode = "IL", CountryName = "Israel"}, new WorldCountries { CountryCode = "IT", CountryName = "Italy"}, new WorldCountries { CountryCode = "JM", CountryName = "Jamaica"}, new WorldCountries { CountryCode = "JP", CountryName = "Japan"}, new WorldCountries { CountryCode = "JE", CountryName = "Jersey"}, new WorldCountries { CountryCode = "JO", CountryName = "Jordan"}, new WorldCountries { CountryCode = "KZ", CountryName = "Kazakhstan"}, new WorldCountries { CountryCode = "KE", CountryName = "Kenya"}, new WorldCountries { CountryCode = "KI", CountryName = "Kiribati"}, new WorldCountries { CountryCode = "KP", CountryName = "Korea"}, new WorldCountries { CountryCode = "KW", CountryName = "Kuwait"}, new WorldCountries { CountryCode = "KG", CountryName = "Kyrgyzstan"}, new WorldCountries { CountryCode = "LV", CountryName = "Latvia"}, new WorldCountries { CountryCode = "LB", CountryName = "Lebanon"}, new WorldCountries { CountryCode = "LS", CountryName = "Lesotho"}, new WorldCountries { CountryCode = "LR", CountryName = "Liberia"}, new WorldCountries { CountryCode = "LY", CountryName = "Libya"}, new WorldCountries { CountryCode = "LI", CountryName = "Liechtenstein"}, new WorldCountries { CountryCode = "LT", CountryName = "Lithuania"}, new WorldCountries { CountryCode = "LU", CountryName = "Luxembourg"}, new WorldCountries { CountryCode = "MO", CountryName = "Macao"}, new WorldCountries { CountryCode = "MK", CountryName = "Macedonia"}, new WorldCountries { CountryCode = "MG", CountryName = "Madagascar"}, new WorldCountries { CountryCode = "MW", CountryName = "Malawi"}, new WorldCountries { CountryCode = "MY", CountryName = "Malaysia"}, new WorldCountries { CountryCode = "MV", CountryName = "Maldives"}, new WorldCountries { CountryCode = "ML", CountryName = "Mali"}, new WorldCountries { CountryCode = "MT", CountryName = "Malta"}, new WorldCountries { CountryCode = "MH", CountryName = "Marshall Islands"}, new WorldCountries { CountryCode = "MQ", CountryName = "Martinique"}, new WorldCountries { CountryCode = "MR", CountryName = "Mauritania"}, new WorldCountries { CountryCode = "MU", CountryName = "Mauritius"}, new WorldCountries { CountryCode = "YT", CountryName = "Mayotte"}, new WorldCountries { CountryCode = "MX", CountryName = "Mexico"}, new WorldCountries { CountryCode = "FM", CountryName = "Micronesia"}, new WorldCountries { CountryCode = "MD", CountryName = "Moldova"}, new WorldCountries { CountryCode = "MC", CountryName = "Monaco"}, new WorldCountries { CountryCode = "MN", CountryName = "Mongolia"}, new WorldCountries { CountryCode = "ME", CountryName = "Montenegro"}, new WorldCountries { CountryCode = "MS", CountryName = "Montserrat"}, new WorldCountries { CountryCode = "MA", CountryName = "Morocco"}, new WorldCountries { CountryCode = "MZ", CountryName = "Mozambique"}, new WorldCountries { CountryCode = "MM", CountryName = "Myanmar"}, new WorldCountries { CountryCode = "NA", CountryName = "Namibia"}, new WorldCountries { CountryCode = "NR", CountryName = "Nauru"}, new WorldCountries { CountryCode = "NP", CountryName = "Nepal"}, new WorldCountries { CountryCode = "NL", CountryName = "Netherlands"}, new WorldCountries { CountryCode = "NC", CountryName = "New Caledonia"}, new WorldCountries { CountryCode = "NZ", CountryName = "New Zealand"}, new WorldCountries { CountryCode = "NI", CountryName = "Nicaragua"}, new WorldCountries { CountryCode = "NE", CountryName = "Niger"}, new WorldCountries { CountryCode = "NG", CountryName = "Nigeria"}, new WorldCountries { CountryCode = "NU", CountryName = "Niue"}, new WorldCountries { CountryCode = "NF", CountryName = "Norfolk Island"}, new WorldCountries { CountryCode = "MP", CountryName = "Northern Mariana Islands"}, new WorldCountries { CountryCode = "NO", CountryName = "Norway"}, new WorldCountries { CountryCode = "OM", CountryName = "Oman"}, new WorldCountries { CountryCode = "PK", CountryName = "Pakistan"}, new WorldCountries { CountryCode = "PW", CountryName = "Palau"}, new WorldCountries { CountryCode = "PS", CountryName = "Palestine"}, new WorldCountries { CountryCode = "PA", CountryName = "Panama"}, new WorldCountries { CountryCode = "PG", CountryName = "Papua New Guinea"}, new WorldCountries { CountryCode = "PY", CountryName = "Paraguay"}, new WorldCountries { CountryCode = "PE", CountryName = "Peru"}, new WorldCountries { CountryCode = "PH", CountryName = "Philippines"}, new WorldCountries { CountryCode = "PN", CountryName = "Pitcairn"}, new WorldCountries { CountryCode = "PL", CountryName = "Poland"}, new WorldCountries { CountryCode = "PT", CountryName = "Portugal"}, new WorldCountries { CountryCode = "PR", CountryName = "Puerto Rico"}, new WorldCountries { CountryCode = "QA", CountryName = "Qatar"}, new WorldCountries { CountryCode = "RE", CountryName = "Réunion"}, new WorldCountries { CountryCode = "RO", CountryName = "Romania"}, new WorldCountries { CountryCode = "RU", CountryName = "Russian Federation"}, new WorldCountries { CountryCode = "RW", CountryName = "Rwanda"}, new WorldCountries { CountryCode = "BL", CountryName = "Saint Barthélemy"}, new WorldCountries { CountryCode = "KN", CountryName = "Saint Kitts and Nevis"}, new WorldCountries { CountryCode = "LC", CountryName = "Saint Lucia"}, new WorldCountries { CountryCode = "MF", CountryName = "Saint Martin (French part)"}, new WorldCountries { CountryCode = "PM", CountryName = "Saint Pierre and Miquelon"}, new WorldCountries { CountryCode = "VC", CountryName = "Saint Vincent and the Grenadines"}, new WorldCountries { CountryCode = "WS", CountryName = "Samoa"}, new WorldCountries { CountryCode = "SM", CountryName = "San Marino"}, new WorldCountries { CountryCode = "ST", CountryName = "Sao Tome and Principe"}, new WorldCountries { CountryCode = "SA", CountryName = "Saudi Arabia"}, new WorldCountries { CountryCode = "SN", CountryName = "Senegal"}, new WorldCountries { CountryCode = "RS", CountryName = "Serbia"}, new WorldCountries { CountryCode = "SC", CountryName = "Seychelles"}, new WorldCountries { CountryCode = "SL", CountryName = "Sierra Leone"}, new WorldCountries { CountryCode = "SG", CountryName = "Singapore"}, new WorldCountries { CountryCode = "SX", CountryName = "Sint Maarten (Dutch part)"}, new WorldCountries { CountryCode = "SK", CountryName = "Slovakia"}, new WorldCountries { CountryCode = "SI", CountryName = "Slovenia"}, new WorldCountries { CountryCode = "SB", CountryName = "Solomon Islands"}, new WorldCountries { CountryCode = "SO", CountryName = "Somalia"}, new WorldCountries { CountryCode = "ZA", CountryName = "South Africa"}, new WorldCountries { CountryCode = "GS", CountryName = "South Georgia and the South Sandwich Islands"}, new WorldCountries { CountryCode = "SS", CountryName = "South Sudan"}, new WorldCountries { CountryCode = "ES", CountryName = "Spain"}, new WorldCountries { CountryCode = "LK", CountryName = "Sri Lanka"}, new WorldCountries { CountryCode = "SD", CountryName = "Sudan"}, new WorldCountries { CountryCode = "SR", CountryName = "Suriname"}, new WorldCountries { CountryCode = "SJ", CountryName = "Svalbard and Jan Mayen"}, new WorldCountries { CountryCode = "SZ", CountryName = "Swaziland"}, new WorldCountries { CountryCode = "SE", CountryName = "Sweden"}, new WorldCountries { CountryCode = "CH", CountryName = "Switzerland"}, new WorldCountries { CountryCode = "SY", CountryName = "Syrian Arab Republic"}, new WorldCountries { CountryCode = "TW", CountryName = "Taiwan"}, new WorldCountries { CountryCode = "TJ", CountryName = "Tajikistan"}, new WorldCountries { CountryCode = "TZ", CountryName = "Tanzania"}, new WorldCountries { CountryCode = "TH", CountryName = "Thailand"}, new WorldCountries { CountryCode = "TL", CountryName = "Timor-Leste"}, new WorldCountries { CountryCode = "TG", CountryName = "Togo"}, new WorldCountries { CountryCode = "TK", CountryName = "Tokelau"}, new WorldCountries { CountryCode = "TO", CountryName = "Tonga"}, new WorldCountries { CountryCode = "TT", CountryName = "Trinidad and Tobago"}, new WorldCountries { CountryCode = "TN", CountryName = "Tunisia"}, new WorldCountries { CountryCode = "TR", CountryName = "Turkey"}, new WorldCountries { CountryCode = "TM", CountryName = "Turkmenistan"}, new WorldCountries { CountryCode = "TC", CountryName = "Turks and Caicos Islands"}, new WorldCountries { CountryCode = "TV", CountryName = "Tuvalu"}, new WorldCountries { CountryCode = "UG", CountryName = "Uganda"}, new WorldCountries { CountryCode = "UA", CountryName = "Ukraine"}, new WorldCountries { CountryCode = "AE", CountryName = "United Arab Emirates"}, new WorldCountries { CountryCode = "GB", CountryName = "United Kingdom"}, new WorldCountries { CountryCode = "US", CountryName = "United States"}, new WorldCountries { CountryCode = "UM", CountryName = "United States Minor Outlying Islands"}, new WorldCountries { CountryCode = "UY", CountryName = "Uruguay"}, new WorldCountries { CountryCode = "UZ", CountryName = "Uzbekistan"}, new WorldCountries { CountryCode = "VU", CountryName = "Vanuatu"}, new WorldCountries { CountryCode = "VE", CountryName = "Venezuela"}, new WorldCountries { CountryCode = "VN", CountryName = "Viet Nam"}, new WorldCountries { CountryCode = "VG", CountryName = "British Virgin Islands"}, new WorldCountries { CountryCode = "VI", CountryName = "US Virgin Islands"}, new WorldCountries { CountryCode = "WF", CountryName = "Wallis and Futuna"}, new WorldCountries { CountryCode = "EH", CountryName = "Western Sahara"}, new WorldCountries { CountryCode = "YE", CountryName = "Yemen"}, new WorldCountries { CountryCode = "ZM", CountryName = "Zambia"}, new WorldCountries { CountryCode = "ZW", CountryName = "Zimbabwe"}, }; } }
دریافت کد کامل این پروژه
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0"> <edmx:DataServices> <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="OwinAspNetCore.Models"> <EntityType Name="Product"> <Key> <PropertyRef Name="Id"/> </Key> <Property Name="Id" Type="Edm.Int32" Nullable="false"/> <Property Name="Name" Type="Edm.String"/> <Property Name="Price" Type="Edm.Decimal" Nullable="false"/> </EntityType> </Schema> <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default"> <Function Name="TestFunction" IsBound="true"> <Parameter Name="bindingParameter" Type="Collection(OwinAspNetCore.Models.Product)"/> <Parameter Name="Val" Type="Edm.Int32" Nullable="false"/> <Parameter Name="Name" Type="Edm.String"/> <ReturnType Type="Edm.Int32" Nullable="false"/> </Function> <EntityContainer Name="Container"> <EntitySet Name="Products" EntityType="OwinAspNetCore.Models.Product"/> </EntityContainer> </Schema> </edmx:DataServices> </edmx:Edmx>
string postUrl = "http://localhost:port/...."; HttpClient client = new HttpClient(); var response = client.PostAsync(postUrl, new StringContent(JsonConvert.SerializeObject(new { Rating = 5 }), Encoding.UTF8, "application/json")).Result;
آن را نصب نموده و بعد از تکمیل شدن، visual studio را restart کنید.
پروژهی console خود را باز کرده و از طریق Add -> new item، آیتم OData client را جستجو کرده و با نام ProductClient.tt آن را تولید نمایید (نام آن اختیاری است):
فایل ProductClient.tt را که یک T4 code generator میباشد، باز کرده و مقدار ثابت MetadataDocumentUri را به آدرس سرویس odata خود تغییر دهید:
public const string MetadataDocumentUri = "http://localhost:port/odata/";
روی این آیتم کلیک راست و گزینهی Run Custom tool را انتخاب نمایید. این تمام کاری است که نیاز به انجام دادن دارید.
حال فایل Program.cs را باز کرده و آنرا اینگونه تغییر دهید:
using ConsoleApplication1.OwinAspNetCore.Models; using System; using System.Linq; namespace ConsoleApplication1 { public class Program { static void Main(string[] args) { Uri uri = new Uri("http://localhost:24977/odata"); //var context = new Default.Container(uri); var context = new TestNameSpace.TestNameSpace(uri); //get var products = context.Products.Where(pr => pr.Name.Contains("a")) .Take(1).Select(pr => new { Firstname = pr.Name, PriceValue = pr.Price }).ToList(); //add context.AddToProducts(new Product() { Name = "Name1", Price = 123 }); //update Product p = context.Products.First(); p.Name = "changed"; context.UpdateObject(p); //delete context.DeleteObject(context.Products.Last()); //commit context.SaveChanges(); } } }
مشاهده میفرمایید که همهی عملیاتهای لازم برای CRUD، به شرط اینکه در سمت سرور طراحی شده باشند، به راحتی از سمت کلاینت قابل فراخوانی خواهند بود.
از این ویژگی فوق العاده میتوان حتی در کلاینتها جاوااسکریپتی نیز استفاده کرد. فرض کنید نرم افزار تحت وبی را با استفاده از jquery یا angularjs طراحی کردهاید. قاعدتا فراخوانی درخواستهای شما به سمت سرور، چیزی شبیه به این خواهد بود:
//angularjs $http.get("/products/get", {Name: "Test", Company: "Test"}) .then(function(response) { console.log(response.data); }); //jquery $.get("/products/get", {Name: "Test", Company: "Test"}, function(data, status){ console.log("Data: " + data); });
با استفاده از odata و typescript و یک library مربوط به odata client در سمت کلاینت، نرم افزار شما بجای موارد، بالا چیزی شبیه به مثال زیر خواهد بود (با همراه داشتن strongly typed و intellisense کامل)
let product1 = await context.products.filter(c => c.Name.contains("Ali")).toArray(); let product2 = await context.products.getSomeFunction(1, 'Test'); context.product.add({Name: 'Test'} as Product); await context.saveChanges()
در مقالههای آتی به ویژگیهای بیشتری از Odata خواهیم پرداخت.
کتابی در مورد Angular و Firebase
What you will learn in this eBook?
We will cover the following Angular concepts in this book:
- Using Cloud Firestore with an Angular application
- Angular Material and Bootstrap
- Template-driven forms
- Form validation
- Custom pipes
- Auth-guards in Angular
- Authentication and Authorization
- Login with Google using Firebase
- Social share option using ngx-share
- Client-Side pagination using ngx-pagination
- Deploying an Angular app in Firebase
انقیاد به خواص یا property binding
قابلیت property binding این امکان را فراهم میکند که یکی از خواص المانهای HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
<img [src]='producr.imageUrl'>
در حین تعریف property binding، مقصد اتصال، داخل براکتها قرار میگیرد و خاصیت مدنظر المان را مشخص میکند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار میگیرد.
اگر اینکار را بخواهیم با interpolation معرفی شدهی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
<img src={{producr.imageUrl}}>
خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیهی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
<img src='http://www.mysite.com/images/{{producr.imageUrl}}'>
تکمیل قالب کامپوننت لیست محصولات
اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شدهی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع دادهی آن متصل میکنیم:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName'> </td> <td>{{ product.productName }}</td> <td>{{ product.productCode }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price }}</td> <td>{{ product.starRating }}</td> </tr> </tbody>
هرچند اینبار تصاویر محصولات نمایش داده شدهاند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آنرا تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; products: any[] = [ // as before... ]; }
پس از تعریف این خواص، امکان دسترسی به آنها در قالب کامپوننت وجود خواهد داشت:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
همچنین در اینجا نحوهی style binding را نیز مشاهده میکنید. مقصد اتصال همیشه با [] مشخص میشود و سپس کار با ذکر .style شروع شده و پس از آن نام خاصیت مدنظر عنوان خواهد شد. اگر نیاز به ذکر واحدی وجود داشت، پس از درج نام خاصیت، قید خواهد شد. برای مثال [style.fontSize.em] و یا [%.style.fontSize]
یک نکته:
اگر مثال را قدم به قدم دنبال کرده باشید، با افزودن style binding و بارگذاری مجدد صفحه، احتمالا تغییراتی را مشاهده نخواهید کرد. این مورد به علت کش شدن قالب قبلی و یا فایل جاوا اسکریپتی متناظر با آن است (فایلی که خواص عرض و حاشیهی تصویر به آن اضافه شدهاند).
یک روش سادهی حذف کش آن، بازکردن آدرس http://localhost:2222/app/products/product-list.component.js در مرورگر به صورت مجزا و سپس فشردن دکمههای ctrl+f5 بر روی آن است.
پاسخ دادن به رخدادها و یا event binding
تا اینجا تمام data bindingهای تعریف شدهی ما یک طرفه بودند؛ از خواص کلاس کامپوننت به اجزای قالب متناظر با آن. اما گاهی از اوقات نیاز است تا با کلیک کاربر بر روی دکمهای، عملی خاص صورت گیرد و در این حالت، جهت ارسال اطلاعات، از قالب کامپوننت، به متدها و خواص کلاس متناظر با آن خواهند بود. کامپوننت به اعمال کاربر از طریق event binding گوش فرا میدهد:
<button (click)='toggleImage()'>
در این حالت اگر کاربر روی دکمهی تعریف شده کلیک کند، متد toggleImage موجود در کلاس متناظر، فراخوانی خواهد شد.
چه رخدادهایی را در اینجا میتوان ذکر کرد؟ پاسخ آنرا در آدرس ذیل میتوانید مشاهده کنید:
https://developer.mozilla.org/en-US/docs/Web/Events
این syntax جدید AngularJS 2.0 سطح API آنرا کاهش داده است. دیگر در اینجا نیازی نیست تا به ازای هر رخداد ویژهای، یک دایرکتیو و یا syntax خاص آنرا در مستندات آن
جستجو کرد. فقط کافی است syntax جدید (نام رخداد) را مدنظر داشته باشید.
تکمیل مثال نمایش لیست محصولات با فعال سازی دکمهی Show Image آن
در اینجا قصد داریم با کلیک بر روی دکمهی Show image، تصاویر موجود در ستون تصاویر، مخفی و یا نمایان شوند. برای این منظور خاصیت جدیدی را به نام showImage به کلاس ProductListComponent اضافه میکنیم. بنابراین فایل product-list.component.ts را گشوده و تغییر ذیل را به آن اعمال کنید:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false;
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه میکنیم:
export class ProductListComponent { // as before ... toggleImage(): void { this.showImage = !this.showImage; } }
پس از این تغییرات، اکنون میتوان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> Show Image </button>
خوب، تا اینجا اگر کاربر بر روی دکمهی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحلهی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شدهاست. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المانهای DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه میکنیم:
<tr *ngFor='#product of products'> <td> <img *ngIf='showImage' [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمیشود. پس از کلیک بر روی دکمهی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.
یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> {{showImage ? 'Hide' : 'Show'}} Image </button>
بررسی انقیاد دو طرفه یا two-way binding
تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیفهای جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده میشود:
<input [(ngModel)]='listFilter' >
سپس () تعریف شدهاست تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.
در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را دادهاند یا «موز درون جعبه»! موز همان event binding است که داخل جعبهی property binding قرار میگیرد!
خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشتهای listFilter را به آن اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false; listFilter: string = 'cart';
<div class='panel-body'> <div class='row'> <div class='col-md-2'>Filter by:</div> <div class='col-md-4'> <input type='text' [(ngModel)]='listFilter' /> </div> </div> <div class='row'> <div class='col-md-6'> <h3>Filtered by: {{listFilter}}</h3> </div> </div>
پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیهی cart را خواهد داشت و اگر آنرا تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by میتوان مشاهده کرد. به این رخداد two-way binding میگویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمیشود که آنرا در قسمت بعد تکمیل خواهیم کرد.
فرمت کردن اطلاعات نمایش داده شدهی در جدول با استفاده از Pipes
تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده میشود و کار آنها تغییر دادهها، پیش از نمایش آنها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شدهاست.
در اینجا یک مثال سادهی pipes را مشاهده میکنید:
{{ product.productCode | lowercase }}
از pipes در property binding هم میتوان استفاده کرد:
[title]='product.productName | uppercase'
و یا میتوان pipes را به صورت زنجیرهای نیز تعریف کرد:
{{ product.price | currency | lowercase }}
بعضی از pipes، پارامتر هم قبول میکنند:
{{ product.price | currency:'USD':true:'1.2-2' }}
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.
در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
<td>{{ product.productName }}</td> <td>{{ product.productCode | lowercase }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price | currency:'USD':true:'1.2-2'}}</td> <td>{{ product.starRating }}</td>
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part4.zip
خلاصهی بحث
data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن میشود. در AngularJS 2.0، چهار نوع binding وجود دارند:
interpolation، عبارت رشتهای محاسبه شده را در بین المانهای DOM درج میکند و یا میتواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM میشود.
event binding به رخدادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری میشود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافتهی توسط کاربر را به کلاس انجام میدهد.
اطلاعات نمایش داده شدهی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده میشود.
AngularJS #2
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[myIf]', inputs:["myIf"] }) export class UnlessDirective { constructor(private _templateRef:TemplateRef<any>,private _viewContainerRef:ViewContainerRef) { } set myIf(condition:true) { if(!condition) { this._viewContainerRef.clear(); } else{ this._viewContainerRef.createEmbeddedView(this._templateRef); } } }
سپس ورودی را به شکل متد نوشته و عبارت set را جلوی آن مینویسیم. عبارت set به این معناست که وقتی مقداری به این ورودی set شد، متد myIf را اجرا کن. اگر نتیجه شرط ورودی false شده باشد، محتوای ViewContainer پاک خواهد شد و اگر محتوای true داشته باشد templateRef به آن اضافه خواهد شد.
برای استفادهی از دایرکتیو نوشته شده، از تکه کد زیر استفاده میکنیم:
<input type="text" #x (keyup)="0"/> <div *myIf="x.value=='yes'"> it means we are on condition </div>