مطالب
تنظیمات شیرپوینت برای فعال سازی Session در هنگام کد نویسی

اگر بخواهید در کد وب پارت خود از ویژگی Session استفاده کنید ، ممکن است SharePoint این اجازه را به شما ندهد... راه حل چیست؟

اگر کد خود را بعد از پیاده سازی Session اجرا کنید چنین پیغامی را مشاهده خواهید کرد: 

Server Error in '/' Application.
 
Session state can only be used when enableSessionState is set to true, either in a configuration file or in the Page directive. Please also make sure that System.Web.SessionStateModule or a custom session state module is included in the <configuration>\<system.web>\<httpModules> section in the application configuration. 

این پیغام نشان می دهد که تنظیمات Session در فایل web.config فعال نیست .یکی از راه حل های این مشکل چک کردن ویژگی enableSessionState="true" در هدر صفحه و در تگ Page است 

اگر این ویژگی True باشد و همچنان همان پیغام فوق را مشاهده کردید به فایلweb.config وبسایت مورد نظر مراجعه کرده و به روش زیر عمل کنید : 

// 1 - Find This Node
<modules runAllManagedModulesForAllRequests="true">
// 2 - Add These Nodes
<remove name="Session" />
<add name="Session" type="System.Web.SessionState.SessionStateModule" preCondition="" /> 

اکنون امکان استفاده از Session برای شما فراهم شده است . 

در غیر این صورت باید تغییراتی در Permission Policy های شیرپوینت که در فایل های wss_minimaltrust.config و wss_mediumtrust.config و ... انجام دهید و یک Permission Set به آن اضافه کنید 

این فایل ها در زیر مجموعه 14 و در پوشه CONFIG قرار دارند . 

موفق باشید

نظرات مطالب
تزریق وابستگی‌ها به صورت پویا در فروشگاه‌ساز Nop Commerce
موضوع هزینه Resolve کردن اینترفیس‌ها نیست. هر بار یک کلاس را درخواست میکنیم، علاوه بر سازنده(Constructor) خود کلاس اصلی، سازنده همه کلاس‌های وابسته  نیز فراخوانی میشوند(از همه آنها instance ایجاد میشود در حافظه) در نتیجه مثلا برای فراخوانی یک متد ساده ممکنه است دهها وهله(instance) از کلاس‌های مختلف ایجاد شود بدون اینکه نیازی به آنها داشته باشیم.
موضوع اینکه در تعریف و رجیستر کردن وابستگی‌های یک کلاس سعی کنیم با سختگیری یک کلاس را با حداقل وابستگی‌ها تزریق کنیم. 
نظرات مطالب
بررسی برخی تغییرات در Angular 8
TypeScript 3.4.x Support
انگیولار 8، از (3.4) typescript و نگارش‌های بالاتر پشتیبانی می‌کند. اگر می‌خواهیم از انگیولار 8 برای App ‌های جدید استفاده کنیم، نیاز است typescript را به نگارش 3.4 و یا بالاتر ارتقاء دهیم.

Ivy Rendering Engine
یکی از مهمترین و مورد انتظارترین ویژگی‌های انگیولار 8، موتور IVY می‌باشد. IVY یک Angular Compiler جدید می‌باشد و هم چنین یک ابزار که به عنوان یک rendering pipeline جدید عمل می‌کند. مزیت Ivy این است که به طور قابل توجهی bundle‌های کوچکی را تولید می‌کند (سایز bundle‌ها را کاهش میدهد)  و همچنین به آسانی می‌تواند کامپایل سریعی را انجام دهد. بنابراین Ivy، اساس نوآوری در دنیای انگیولار می‌باشد. Ivy در انگیولار 8 به صورت پیش نمایشی می‌باشد. هدف اصلی این نسخه این است که بازخورد‌ها را از جامعه توسعه دهندگان انگیولار، مرتبط با Ivy دریافت کند. پیشنهاد شده است که در این روزها از Ivy برای حالت ارائه‌ی نهایی (Production) استفاده نشود.


در ngconf  سال  2019، (Brad Green)، هدایت کننده فنی تیم انگیولار گفت که در صورت استفاده از Ivy، از مزایای زیر برخوردار هستیم: 

  • کامپایل سریعتری را فراهم می‌کند (انتشار در انگیولار  9) 
  • بررسی type  در قالب‌ها، خیلی بیشتر بهبود یافته است؛ به‌گونه‌ای که می‌توان خطاهای بیشتری را در زمان build گرفت که باعث می‌شود کاربران در زمان runtime به آن خطاها برخورد نکنند (انتشار در انگیولار 9). 
  • bundle‌های با سایز کوچکتری در مقایسه با سایز bundle‌های کامپایل شده‌ی جاری 
  • کد‌های تولید شده توسط  Angular compiler، بسیار آسان‌تر، برای خواندن و درک انسان است. 
  • آخرین و مهمترین ویژگی مورد علاقه من این است که می‌توان قالب‌ها (templates) را debug کرد. من یقین دارم که این ویژگی توسط تعداد زیادی از توسعه دهندگان مورد توجه قرار خواهد گرفت .
همانطور که در متن بالا گفته شده است اگر بخواهید در یک پروژه‌ی انگیولار، Ivy  را شامل کنید، علاوه بر حالت گفته شده‌ی در متن‌، می‌توانید به صورت دستی تنظیم بالا را به پروژه‌ی انگیولار اضافه کنید (بعد از ارتقاء به انگیولار 8). پیشنهاد شده‌است که اگر می‌خواهیم از Ivy  در Application ‌ها استفاده کنیم، Application را در حالت debug، همراه با AOT compilation اجرا کنید:
ng serve --aot

Bye Bye @angular/http
از نگارش 8 انگیولار، پشتیبانی از angular/http@ متوقف می‌شود. تا نگارش 7 انگیولار، امکان استفاده‌ی از angular/http@ برای ما فراهم بود؛ اما استفاده‌ی از angular/http@ منسوخ شده بود و در نگارش 4 انگیولار یک فراخوانی امن و کارآمد HTTP را با استفاده از  angular/common/http@  فراهم کردند. 

PNPM Support
در نگارش 8 انگیولار، پشتیبانی از یک package manager جدید به نام PNPM وجود دارد که شامل NPM و Yarn می‌باشد.

Support for New Builders/Architect API
نگارش جدید Angular CLI  این اجازه را به ما می‌دهد که از نسخه‌ی جدید Builders که به عنوان Architect API شناخته می‌شود، استفاده کنیم. انگیولار از Builders API برای اجرای  عملیاتی مثل server, build, test, lint و e2e استفاده می‌کند. در ضمن می‌توانیم از builders در فایل angular.json استفاده کنیم: 
"projects": {  
  "app-name": {  
    "architect": {  
      "build": {  
        "builder": "@angular-devkit/build-angular:browser",  
      },  
      "serve": {  
        "builder": "@angular-devkit/build-angular:dev-server",  
      },  
      "test": {  
        "builder": "@angular-devkit/build-angular:karma",  
      },  
      "lint": {  
        "builder": "@angular-devkit/build-angular:tslint",  
      },  
      "e2e": {  
        "builder": "@angular-devkit/build-angular:protractor",  
      }  
    }  
  }  
}
مطالب
HTML5 Web Component - قسمت دوم - Custom Elements
Custom Elements، دارای یک چرخه حیات می‌باشند. در طی این چرخه حیات، می‌توان تعدادی متد خاص را به المان سفارشی خود اضافه کرد که به صورت خودکار توسط مرورگر فراخوانی می‌شوند. به این متدها Life-cycle Callbacks یا Custom Element Reactions نیز می‌گویند. برای درک بهتر چرخه حیات مذکور، به تکه کد زیر توجه نمائید:
customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
        console.log('constructed!');
    }

    connectedCallback() {
        console.log('connected!');
    }

    disconnectedCallback() {
        console.log('disconnected!');
    }

    adoptedCallback() {
        console.log('adopted!');
    }

    attributeChangedCallback(name, oldValue, newValue) {
        console.log('attirbuteChanged!', name, oldValue, newValue);
    }

    static get observedAttributes() {
        return ['checked','demo','label'];
    }
});

Element Upgrades
به صورت پیش‌فرض، المان‌های موجود در DOM که مبتنی‌بر استانداردهای HTML تعریف نشده‌ا‌ند، توسط مرورگر به عنوان HTMLUnknownElement تجزیه و تحلیل خواهند شد. ولی این موضوع برای المان‌های سفارشی که نام معتبری دارند (وجود «-»)، صدق نمی‌کند. برای مثال دو خط کد زیر را در کنسول مربوط به Developer tools مرورگر خود اجرا کنید:
// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true //true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true //true
در این صورت، امکان استفاده از المان سفارشی، قبل از معرفی و ثبت آن توسط متد customElements.define نیز وجود خواهد داشت. یعنی اگر در DOM شما تعدادی المان سفارشی وجود داشته باشند که به هر دلیل نیاز است پس از گذشت یک بازه زمانی کوتاهی معرفی و ثبت شوند (مثال: lazy load اسکریپت‌های متناظر با المان‌های سفارشی در Angular)، این المان‌ها معتبر هستند. فرآیند فراخوانی متد define و استفاده از کلاس معرفی شده برای ارتقاء المان سفارشی موجود در DOM، اصطلاحا Element Upgrades نامیده می‌شود. همچنین با استفاده از متد customElements.whenDefined که یک Promise را بازگشت می‌دهد، می‌توان از معرفی و ثبت شدن المان خاصی آگاه شد:
customElements.whenDefined('x-component').then(() => {
  console.log('x-component defined');
});
یا حتی امکان استفاده از سلکتور «‎:defined‏» نیز به شکل زیر وجود دارد:
<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>

// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map(socialButton => {
  return customElements.whenDefined(socialButton.localName);
));

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});
در اینجا ابتدا تمام المان‌های تعریف نشده، کوئری شده‌اند و با استفاده از متد map و اجرای متد whenDefined، به لیستی از Promiseها رسیده‌ایم و در نهایت با استفاده از Promise.all منتظر اتمام مرحله upgrade المان‌های مذکور هستیم.
سازنده مرتبط با کلاس المان سفارشی x-component، در هنگام وهله‌سازی یا فرآیند upgrades فراخوانی می‌شود و می‌تواند برای مقداردهی اولیه خواص وهله جاری، تنظیم رخدادگردان‌ها (Event Listeners) و یا ایجاد و اتصال ShadowDOM با استفاده از متد attachShadow، محل مناسبی باشد. طبق مستندات مرتبط، فراخوانی ()super بدون ارسال هیچ آرگومانی باید در اولین خط این سازنده انجام شود.
برای وهله‌سازی المان سفارشی نیز می‌توان از متد customeElements.get به شکل زیر استفاده کرد:
customeElements.define('x-component',...)
let XComponent = customElements.get('x-component');
document.body.appendChild(new XComponent())
متد get، ارجاعی به سازنده کلاس x-component را بازگشت خواهد داد.

connectedCallback 
 اولین متد بعد از فراخوانی سازنده، connectedCallback نام دارد و زمانی رخ می‌دهد که وهله‌ای از یک المان سفارشی به Light DOM افزوده شده‌است و یا توسط Parser مرورگر، در DOM شناسایی شود. این متد، محل پیشنهاد شده برای اجرای کدهای زمان راه‌اندازی مانند دریافت منابع از سرور و یا رندر کردن محتوایی خاص، می‌باشد. 
نکته: این متد ممکن است بیش از یکبار نیز فراخوانی شود! هنگامیکه یک المان موجود در DOM از طریق کد از DOM جداشده و سپس اضافه شود:
const el = document.createElement('x-component');
document.body.appendChild(el);
// connectedCallback() called

el.remove();
// disconnectedCallback()

document.body.appendChild(el);
// connectedCallback() called again


disconnectedCallback
این متد نیز هر وقت المانی از DOM حذف شود، اجرا خواهد شد و مانند متد connectedCallback ممکن است چندین بار فراخوانی شود. همچنین محل مناسبی برای آزادسازی منابع استفاده شده در پیاده‌سازی المان سفارشی، می‌باشد. مانند: استفاده از متد clearInterval برای پاکسازی یک تایمر که با متد setInterval ایجاد شده‌است.  
نکته: هیچ تعهدی به اجرای متد disconnectedCallback در تمام حالاتی که یک المان از DOM حذف می‌شود، وجود ندارد. به عنوان مثال هنگامیکه یک برگه‌ی مرورگر بسته شود، این متد فراخوانی نخواهد شد.

‌attributeChangedCallback
 این متد هنگامی فراخوانی خواهد شد که خصوصیات مشخص شده از طریق observedAttributes به عنوان یک static getter، اضافه، حذف، ویرایش و یا جایگزین شوند. همچنین در مرحله Upgrades برای مقادیر اولیه خصوصیات که توسط استفاده کننده از المان سفارشی، مشخص شده‌است نیز فراخوانی می‌شود.
adoptedCallback
متدهای قبلی بیشترین استفاده را دارند؛ این متد خاص نیز زمانیکه یک المان سفارشی به یک DOM دیگری منتقل می‌شود، اجرا خواهد شد. برای مثال:
const iframe = document.querySelector('iframe');
const iframeImages = iframe.contentDocument.querySelectorAll('img');
const newParent = document.getElementById('images');

iframeImages.forEach(function(imgEl) {
  newParent.appendChild(document.adoptNode(imgEl));
});
در تکه کد بالا، لیست المان‌های img موجود در داخل iframe کوئری شده و سپس با پیمایش بر روی لیست بدست آمده و در زمان فراخوانی متد adoptNode که کار تغییر ownerDocument مرتبط با یک المان را انجام می‌دهد، متد adoptedCallback ما نیز اجرا خواهد شد.

Methods

اگر المان‌های بومی و استاندارد موجود را بررسی کنید، همه آنها دارای یکسری متد، پراپرتی و صفات مشخصی هستند. در اینجا نیز تعریف رفتاری برای یک المان سفارشی و کپسوله، نکته خاصی ندارد و به شکل زیر قابل تعریف و استفاده می‌باشد:
class XComponent extends HTMLElement {
  constructor() {
    super();
  }
  
  doSomething(){
    console.log('doSomething');
  }
}

let component = document.querySelector('x-component');
component.doSomething();
مشخص است که امکان تعریف انواع و اقسام متدها با پارامترها و خروجی‌های مختلفی و حتی نسخه‌های همزمان یا ناهمزمان آن‌ها وجود دارد.

Attributes & Properties

در HTML خیلی رایج است که مقادیر پراپرتی‌های یک المان در قالب یکسری صفات، نمودی در DOM هم داشته باشند. برای مثال:
div.id = 'id-value';
div.hidden = true;
انتظار چنین خروجی داریم:
<div id="id-value" hidden>
این موضوع، تحت عنوان «Reflecting Properties to Attributes» مطرح می‌باشد. در این صورت علاوه بر اینکه با یک نگاه به DOM، از مقادیر خصوصیات یک المان آگاه خواهیم بود، امکان استفاده از این صفات به عنوان سلکتورهایی در زمان استایل‌دهی نیز وجود دارد. حال از این مکانیزم برای اعمال یکسری صفات دسترسی‌پذیری مانند صفات ARIA به المان سفارشی خود نیز می‌توان استفاده کرد. برای مثال:
class XComponent extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this._render();
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }

    _render() {
        //... 
    }
}
ایده کار خیلی ساده است؛ پراپرتی‌های یک المان‌سفارشی را از طریق متدهای getter و setter تعریف کرده و در بدنه پیاده‌سازی آنها، صفات HTML ای المان جاری را تغییر داده و یا از مقادیر آنها استفاده کنیم.
اینبار با مقداردهی پراپرتی disabled برروی وهله‌ای از المان سفارشی ما، این مقادیر نمودی در DOM هم خواهند داشت. با استفاده از متدهای setAttribute یا removeAttribute کار همگام‌سازی پراپرتی‌ها با صفات را انجام داده‌ایم.
همچین با استفاده از متد attributeChangedCallback نیز می‌توان برای اعمال صفات ARIA که اشاره شد، به شکل زیر استفاده کرد:
attributeChangedCallback(name, oldValue, newValue) {
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-disabled', !!newValue);
      break;
    ...
  }
یا حتی به شکل زیر:
attributeChangedCallback(name, oldValue, newValue) {
    // When the component is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the other attribute changing.
  }
در اینجا از همان getter که طبق پیاده‌سازی ما، در پشت صحنه از همان مقدار صفت disabled استفاده می‌کند، برای تنظیم یکسری صفات دیگر استفاده کرده‌ایم. به عنوان مثال اگر المان ما غیرفعال شده بود، صفت tabindex آن را با «‎-1‏» مقداردهی می‌کنیم تا از توالی پیمایش مبتنی‌بر Tab خارج شود.
نکته: پیشنهاد می‌شود از مکانیزم همگام‌سازی پراپرتی‌ها و صفات، برای انواع داده‌ای اولیه (رشته، عدد و ...) استفاده شود و برای دریافت مقادیری مانند objects و یا arrays، از متدها یا پراپرتی‌ها استفاده کنید. در غیر این صورت نیاز خواهد بود که این مقادیر را سریالایز و در داخل المان سفارشی، عملیات معکوس آن را انجام دهید که می‌تواند هزینه‌ی زیادی داشته باشد. عملیات سریالایز نیز خود باعث از دست دادن ارجاعات به آن مقادیر خواهد شد. به صورت کلی هیچکدام از المان‌های بومی موجود، چنین اطلاعاتی را دریافت نمی‌کند. برای مثال:
constructor() {
    super();

    this._data = [];
}

get data() {
    return _data;
}

set data(value) {
    if (this_data === value) return;
    this._data = value;
    this._render();
}

Lazy Properties

همانطور که اشاره شد حتی قبل از مرحله upgrades مربوط به المان‌های سفارشی استفاده شده در سند HTML برنامه شما، به عنوان المان‌های معتبری هستند که امکان کوئری کردن و مقداردهی اولیه خصوصیات آنها از طریق کد نیز ممکن است. این موضوع زمانیکه از فریم‌ورکی مثل Angular استفاده می‌شود، المان موردنظر به صورت خودکار توسط فریم‌ورک لود و به صفحه اضافه شده و در انتهای عملیات، binding پراپرتی‌های آن به خصوصیات موجود در کامپوننت Angular ای انجام خواهد شد. به مثال زیر توجه کنید:
<x-component [disabled]="model.disabled"></x-component>
در این صورت اگر لود اسکریپت، معرفی و ثبت این المان سفارشی به صورت Lazy انجام شود، امکان آن وجود دارد که فریم‌ورک، عملیات binding را قبل از مرحله upgrades انجام دهد. خوب... این موضوع چه مشکلی را ایجاد می‌کند؟ در این صورت چون مرحله upgrades تمام نشده است، پیاده‌سازی بدنه متد setter متناظر با خصوصیات المان سفارشی، توسط پراپرتی جدیدی که توسط فریم‌ورک برروی وهله موجود تعریف می‌شود، بی‌استفاده خواهد ماند. برای مثال، اگر سعی کنیم قبل از مرحله upgrades خصوصیت disabled المان x-component را مقداردهی کنیم، عملیات مکانیزم همگام‌سازی مدنظر ما اجرا نخواهد شد:
let el = document.querySelector('x-component');
el.disabled = true;

customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }
});

با این خروجی مواجه خواهیم شد که هیچ اثری از صفت disabled دیده نمی‌شود:


یکی از روش‌های پیشنهاد شده برای حل این مشکل، مقداردهی مجدد پراپرتی‌ها بعد از مرحله upgrades و پس از اینکه متد setter تعریف شده‌است، می‌باشد:
let el = document.querySelector('x-component');
el.disabled = true;

customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this._upgradeProp('disabled');
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }

    _upgradeProp(prop) {
        if (this.hasOwnProperty(prop)) {
            let value = this[prop];
            delete this[prop]; //delete instance property
            this[prop] = value; // set prototype property
        }
    }
});
ایده کار به این صورت است که مقدار پراپرتی مورد نظر را که قبل از مرحله upgrades برروی وهله جاری (instance property) تنظیم شده‌است، در متغییری نگهداری کرده و آن پراپرتی را حذف و سپس پراپرتی تعریف شده در کلاس (prototype property) را برای وهله جاری مقداردهی کنیم.
نکته: به یاد داشته باشید که قبل از اینکه یکسری صفات خاص را مقدار دهی کنید، بررسی شود که استفاده کننده از المان سفارشی، مقداری را تنظیم نکرده باشد. برای مثال اگر قصد دارید المان سفارشی شما قابلیت focus را داشته باشد، نیاز است شما حداقل tabindex=-1 را تنظیم کنید؛ حتی اگر استفاده کننده، آن را مقداردهی نکرده باشد:
connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', -1); //element is not reachable via sequential keyboard navigation, but could be focused
}

مطالب
Angular Material 6x - قسمت هفتم - کار با انواع قالب‌ها
در این قسمت می‌خواهیم روش تغییر رنگ‌های قالب‌های پیش‌فرض Angular Material را به همراه تغییر پویای آن‌ها در زمان اجرا، بررسی کنیم. همچنین Angular Material از راست به چپ نیز به خوبی پشتیبانی می‌کند که مثالی از آن‌را در ادامه بررسی خواهیم کرد.


بررسی ساختار یک قالب Angular Material

قالب، مجموعه‌ای از رنگ‌ها است که به کامپوننت‌های Angular Material اعمال می‌شود. هر قالب از چندین جعبه‌رنگ یا palette تشکیل می‌شود:
- primary palette: به صورت گسترده‌ای در تمام کامپوننت‌ها مورد استفاده‌است.
- accent palette: به المان‌های تعاملی انتساب داده می‌شود.
- warn palette: برای نمایش خطاها و اخطارها بکار می‌رود.
- foreground palette: برای متون و آیکن‌ها استفاده می‌شود.
- background palette: برای پس‌زمینه‌ی المان‌ها بکار می‌رود.

روش انتخاب این جعبه رنگ‌ها نیز به صورت زیر است:
<mat-card>
  Main Theme:
  <button mat-raised-button color="primary">
    Primary
  </button>
  <button mat-raised-button color="accent">
    Accent
  </button>
  <button mat-raised-button color="warn">
    Warning
  </button>
</mat-card>
در Angular Material تمام قالب‌ها استاتیک بوده و در زمان کامپایل برنامه به صورت خودکار به آن اضافه می‌شوند. به همین جهت برنامه نیازی به تشکیل این اجزا و کامپایل یک قالب را در زمان آغاز آن ندارد.
همانطور که در قسمت اول این سری نیز بررسی کردیم، بسته‌ی Angular Material به همراه چندین قالب از پیش طراحی شده‌است (قالب‌های از پیش آماده‌ی متریال را در پوشه‌ی node_modules\@angular\material\prebuilt-themes می‌توانید مشاهده کنید) و در حین اجرای برنامه تنها یکی از آن‌ها که در فایل styles.css ذکر شده‌است، مورد استفاده قرار می‌گیرد.
اگر نیاز به سفارشی سازی بیشتری وجود داشته باشد، می‌توان قالب‌های ویژه‌ی خود را نیز طراحی کرد. این قالب جدید باید mat-core() sass mixin را import کند که حاوی تمام شیوه‌نامه‌های مشترک بین کامپوننت‌ها است. این مورد باید تنها یکبار به کل برنامه الحاق شود تا حجم آن‌را بیش از اندازه زیاد نکند. سپس این قالب سفارشی، جعبه رنگ‌های خاص خودش را معرفی می‌کند. در ادامه این جعبه رنگ‌ها توسط توابع mat-light-theme و یا mat-dark-theme ترکیب شده و مورد استفاده قرار می‌گیرند. سپس این قالب را include خواهیم کرد. به این ترتیب یک قالب سفارشی Angular Material، چنین طرحی را دارد:
@import '~@angular/material/theming';
@include mat-core();

$candy-app-primary: mat-palette($mat-indigo);
$candy-app-accent: mat-palette($mat-pink, A200, A100, A400);
$candy-app-warn: mat-palette($mat-red);

$candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn);

@include angular-material-theme($candy-app-theme);
اعداد و ارقامی را که در اینجا ملاحظه می‌کنید، در سیستم رنگ‌های طراحی متریال به darker hue و lighter hue تفسیر می‌شوند. همچنین امکان سفارشی سازی تایپوگرافی آن نیز وجود دارد.
ذکر جعبه رنگ اخطار در اینجا اختیاری است و اگر ذکر نشود به قرمز تنظیم خواهد شد.

ایجاد یک قالب سفارشی جدید Angular Material

برای ایجاد یک قالب سفارشی نیاز است از فایل‌های sass استفاده کرد. بنابراین بهترین روش ایجاد برنامه‌های Angular Material در ابتدای کار، ذکر صریح نوع style مورد استفاده به sass است:
 ng new MyProjectName --style=sass
اگر اینکار را انجام نداده‌ایم و حالت پیش‌فرض پروژه همان css است، مهم نیست. می‌توان فایل قالب سفارشی را در یک فایل با پسوند custom.theme.scss نیز در پوشه‌ی src قرار داد و سپس آن‌را در فایل angular.json مشخص کرد تا به صورت css کامپایل شده و مورد استفاده قرار گیرد:
"styles": [
   "node_modules/material-design-icons/iconfont/material-icons.css",
   "src/styles.css",
   "src/custom.theme.scss"
],
بدیهی است اگر از ابتدا style=sass را تنظیم کرده بودیم، نیازی به ایجاد این فایل اضافی نبود و همان styles.scss اصلی را می‌شد ویرایش کرد و در این حالت فایل angular.json بدون تغییر باقی می‌ماند.
پس از افزودن و تنظیم فایل custom.theme.scss، به فایل styles.css مراجعه کرده و قالب فعلی را به صورت comment در می‌آوریم:
/* @import "~@angular/material/prebuilt-themes/indigo-pink.css"; */

body {
  margin: 0;
}
سپس فایل src\custom.theme.scss را به صورت زیر تکمیل می‌کنیم:
@import '~@angular/material/theming';
@include mat-core();

$my-app-primary: mat-palette($mat-blue-grey);
$my-app-accent:  mat-palette($mat-pink, 500, 900, A100);
$my-app-warn:    mat-palette($mat-deep-orange);

$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);

@include angular-material-theme($my-app-theme);

.alternate-theme {
  $alternate-primary: mat-palette($mat-light-blue);
  $alternate-accent:  mat-palette($mat-yellow, 400);

  $alternate-theme: mat-light-theme($alternate-primary, $alternate-accent);

  @include angular-material-theme($alternate-theme);
}
که در اینجا شامل مراحل import فایل‌های پایه‌ی Angular Material، تعریف جعبه رنگ جدید، ترکیب آن‌ها و در نهایت include آن‌ها می‌باشد.

در اینجا روش تعریف یک قالب دوم (alternate-theme) را نیز مشاهده می‌کنید. علت تعریف قالب دوم در همین فایل جاری، کاهش حجم نهایی برنامه است. از این جهت که اگر alternate-themeها را در فایل‌های scss دیگری قرار دهیم، مجبور به import تعاریف اولیه‌ی قالب‌های Angular Material در هرکدام به صورت جداگانه‌ای خواهیم بود که حجم قابل ملاحظه‌ای را به خود اختصاص می‌دهند. به همین جهت قالب‌های دیگر را نیز در همینجا به صورت کلاس‌های ثانویه تعریف خواهیم کرد.
 در این حالت روش استفاده‌ی از این قالب ثانویه به صورت زیر می‌باشد:
<mat-card class="alternate-theme">
  Alternate Theme:
  <button mat-raised-button color="primary">
    Primary
  </button>
  <button mat-raised-button color="accent">
    Accent
  </button>
  <button mat-raised-button color="warn">
    Warning
  </button>
</mat-card>

پس از افزودن فایل src\custom.theme.scss به برنامه، اگر آن‌را اجرا کنیم به خروجی زیر خواهیم رسید:




افزودن امکان انتخاب پویای قالب‌ها به برنامه

قصد داریم به منوی برنامه که اکنون گزینه‌ی new contact را به همراه دارد، گزینه‌ی toggle theme را هم جهت تغییر پویای قالب اصلی برنامه اضافه کنیم. به همین جهت فایل toolbar.component.html را گشوده و به صورت زیر تغییر می‌دهیم:
  <mat-menu #menu="matMenu">
    <button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
    <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button>
  </mat-menu>
در اینجا دکمه‌ای به منو اضافه شده‌است که سبب صدور رخدادی به والد آن یا همان sidenav خواهد شد. علت اینجا است که تغییر قالب را در sidenav، که در برگیرنده‌ی router-outlet است، می‌توان به کل برنامه اعمال کرد.
بنابراین جهت تبادل اطلاعات بین toolbar و sidenav از یک رخ‌داد استفاده خواهیم کرد. برای این منظور فایل toolbar.component.ts را گشوده و این رخ‌داد را به آن اضافه می‌کنیم:
export class ToolbarComponent implements OnInit {

  @Output() toggleTheme = new EventEmitter<void>();
پس از این تعریف، به sidenav.component.html مراجعه کرده و به این رخ‌داد گوش فرا می‌دهیم:
<app-toolbar (toggleTheme)="toggleTheme()" (toggleSidenav)="sidenav.toggle()"></app-toolbar>
متد toggleTheme را نیز به صورت زیر به sidenav.component.ts اضافه می‌کنیم:
export class SidenavComponent {

  isAlternateTheme = false;

  toggleTheme() {
    this.isAlternateTheme = !this.isAlternateTheme;
  }
}
در اینجا یک خاصیت عمومی boolean را با کلیک بر روی گزینه‌ی منوی Toggle theme، به true و یا false تنظیم می‌کنیم. اکنون از این مقدار جهت تغییر css قالب sidenav استفاده خواهیم کرد:
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill 
   [class.alternate-theme]="isAlternateTheme">
به این ترتیب اگر مقدار isAlternateTheme مساوی true باشد، کلاس alternate-theme به قالب sidenav به صورت پویا اعمال خواهد شد و برعکس. در تصویر زیر نمونه‌ای از تغییر پویای قالب برنامه را مشاهده می‌کنید:




افزودن پشتیبانی از راست به چپ به قالب برنامه

اگر به mat-sidenav-container ویژگی dir=rtl را اضافه کنیم، قالب برنامه راست به چپ خواهد شد. در ادامه می‌خواهیم شبیه به حالت تغییر پویای قالب سایت، گزینه‌ای را به منوی برنامه جهت تغییر جهت برنامه نیز اضافه کنیم. برای این منظور به قالب toolbar.component.html مراجعه کرده و گزینه‌ی Toggle dir را به آن اضافه می‌کنیم:
  <mat-menu #menu="matMenu">
    <button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
    <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button>
    <button mat-menu-item (click)="toggleDir.emit()">Toggle dir</button>
  </mat-menu>
سپس این رخ‌داد را که قرار است در نهایت به sidenav منتقل شود، به صورت زیر به toolbar.component.ts اضافه می‌کنیم:
export class ToolbarComponent implements OnInit {

  @Output() toggleDir = new EventEmitter<void>();
اکنون در sidenav.component.html به این ر‌خ‌داد گوش فرا خواهیم داد:
<app-toolbar (toggleDir)="toggleDir()" 
(toggleTheme)="toggleTheme()" 
(toggleSidenav)="sidenav.toggle()"></app-toolbar>
متد toggleDir در sidenav.component.ts به صورت زیر پیاده سازی می‌شود:
export class SidenavComponent implements OnInit, OnDestroy {

  dir = "ltr";

  toggleDir() {
    this.dir = this.dir === "ltr" ? "rtl" : "ltr";
  }
}
و در نهایت این جهت را به mat-sidenav-container در فایل sidenav.component.html اعمال می‌کنیم:
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill [dir]="dir"
  [class.alternate-theme]="isAlternateTheme">
در تصویر زیر نمونه‌ای از تغییر پویای جهت برنامه را مشاهده می‌کنید:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-06.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت چهارم - data binding
در قسمت قبل، نگاهی مقدماتی داشتیم به مبحث data binding. در ادامه، این مبحث را به همراه pipes، جهت اعمال تغییرات بر روی اطلاعات، پیگیری خواهیم کرد.


انقیاد به خواص یا property binding

قابلیت property binding این امکان را فراهم می‌کند که یکی از خواص المان‌های HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
 <img [src]='producr.imageUrl'>
در این مثال، خاصیت src المان تصویر، به آدرس تصویر یک محصول متصل شده‌است.
در حین تعریف 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...
    ];
}
البته همانطور که پیشتر نیز ذکر شد، چون مقادیر پیش فرض این خواص عددی هستند، نیازی به ذکر صریح نوع number در اینجا وجود ندارد (type inference).
پس از تعریف این خواص، امکان دسترسی به آن‌ها در قالب کامپوننت وجود خواهد داشت:
<tbody>
    <tr *ngFor='#product of products'>
        <td>
            <img [src]='product.imageUrl'
                 [title]='product.productName'
                 [style.width.px]='imageWidth'
                 [style.margin.px]='imageMargin'>
        </td>
همانطور که مشاهده می‌کنید، چون خاصیت‌های جدید تعریف شده، جزئی از خواص اصلی کلاس هستند و نه خواص اشیاء لیست محصولات، دیگر به همراه .product ذکر نشده‌اند.
همچنین در اینجا نحوه‌ی 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()'>
syntax آن بسیار شبیه است به حالت property binding و در اینجا بجای [] از () جهت مشخص سازی رخدادی خاص از المان مدنظر استفاده می‌شود. سمت راست این انتساب، متدی است که داخل "" قرار می‌گیرد و این متد متناظر است با متدی مشخص در کلاس متناظر با کامپوننت جاری.
در این حالت اگر کاربر روی دکمه‌ی تعریف شده کلیک کند، متد 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;
در اینجا خاصیت Boolean جدیدی به نام showImage با مقدار اولیه‌ی false تعریف شده‌است. به این ترتیب تصاویر، در زمان اولین بارگذاری صفحه نمایش داده نخواهند شد.
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه می‌کنیم:
export class ProductListComponent {
    // as before ...
 
    toggleImage(): void {
        this.showImage = !this.showImage;
    }
}
در کلاس‌های TypeScript نیازی به ذکر صریح واژه‌ی کلیدی function برای تعریف متدی وجود ندارد. این متد، خروجی هم ندارد، بنابراین نوع خروجی آن void مشخص شده‌است. در بدنه‌ی این متد، وضعیت خاصیت نمایش تصویر معکوس می‌شود.
پس از این تغییرات، اکنون می‌توان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
<button class='btn btn-primary'
        (click)='toggleImage()'>
    Show Image
</button>
در اینجا click به عنوان رخداد مقصد، مشخص شده‌است. سپس آن‌را به متد toggleImage کلاس ProductListComponent متصل می‌کنیم.
خوب، تا اینجا اگر کاربر بر روی دکمه‌ی 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>
با توجه به ngIf* تعریف شده، المان تصویر تنها زمانی به DOM اضافه خواهد شد که مقدار خاصیت showImage مساوی true باشد.

اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمی‌شود. پس از کلیک بر روی دکمه‌ی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.

یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
<button class='btn btn-primary'
        (click)='toggleImage()'>
    {{showImage ? 'Hide' : 'Show'}} Image
</button>
در اینجا اگر مقدار خاصیت showImage مساوی true باشد، مقدار رشته‌ای Hide و اگر false باشد، مقدار رشته‌ای show بجای {{}} درج خواهد شد.



بررسی انقیاد دو طرفه یا two-way binding

تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیف‌های جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده می‌شود:
 <input [(ngModel)]='listFilter' >
ابتدا [] ذکر می‌شود تا مشخص شود که این عملیات در اصل یک property binding است؛ از خاصیت عمومی به نام listFilter به المان textbox تعریف شده.
سپس () تعریف شده‌است تا 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';
سپس فایل قالب product-list.component.html را گشوده و انقیاد دو طرفه را به آن اعمال می‌کنیم:
<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>
در اینجا انقیاد دو طرفه، توسط ngModel، به خاصیت listFilter کلاس، در المان input تعریف شده، صورت گرفته است‌. سپس توسط interpolation مقدار این تغییر را در قسمت Filtered by به صورت یک برچسب نمایش می‌دهیم.


پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیه‌ی cart را خواهد داشت و اگر آن‌را تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by می‌توان مشاهده کرد. به این رخداد two-way binding می‌گویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمی‌شود که آن‌را در قسمت بعد تکمیل خواهیم کرد.


فرمت کردن اطلاعات نمایش داده شده‌ی در جدول با استفاده از Pipes

تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده می‌شود و کار آن‌ها تغییر داده‌ها، پیش از نمایش آن‌ها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شده‌است.
در اینجا یک مثال ساده‌ی pipes را مشاهده می‌کنید:
 {{ product.productCode | lowercase }}
پس از قید نام خاصیتی که قرار است نمایش داده شود، حرف pipe یا | قرار گرفته و سپس نوع pipe ذکر می‌شود. به این ترتیب کد محصول، پیش از نمایش، ابتدا به حروف کوچک تبدیل شده و سپس نمایش داده می‌شود.

از pipes در property binding هم می‌توان استفاده کرد:
 [title]='product.productName | uppercase'
در اینجا برای مثال عنوان تصویر با حروف بزرگ نمایش داده خواهد شد.

و یا می‌توان pipes را به صورت زنجیره‌ای نیز تعریف کرد:
 {{ product.price | currency | lowercase }}
در اینجا pipe توکار currency سبب نمایش سه حرف اول واحد پولی، با حروف بزرگ می‌شود. اگر علاقمند بودیم که آن‌را با حروف کوچک نمایش دهیم می‌توان یک pipe دیگر را در انتهای این زنجیره قید کرد.

بعضی از pipes، پارامتر هم قبول می‌کنند:
 {{ product.price | currency:'USD':true:'1.2-2' }}
در اینجا هر پارامتر با یک : مشخص می‌شود. برای مثال pipe واحد پولی، سه پارامتر را دریافت می‌کند: یک کد دلخواه، نمایش یا عدم نمایش علامت پولی، بجای کد دلخواه و مشخصات ارقام نمایش داده شده. برای مثال 2-1.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>
در اینجا با استفاده از pipes، شماره محصول با حروف کوچک و قیمت آن تا حداکثر دو رقم اعشار، فرمت خواهند شد.
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part4.zip


خلاصه‌ی بحث

data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن می‌شود. در AngularJS 2.0، چهار نوع binding وجود دارند:


interpolation، عبارت رشته‌ای محاسبه شده را در بین المان‌های DOM درج می‌کند و یا می‌تواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM می‌شود.
event binding به رخ‌دادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری می‌شود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافته‌ی توسط کاربر را به کلاس انجام می‌دهد.
اطلاعات نمایش داده شده‌ی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده می‌شود.
مطالب
ساخت یک سایت ساده‌‌ی نمایش لیست فیلم با استفاده از Vue.js - قسمت دوم
در قسمت قبلی نحوه کانفیگ اولیه برنامه را به همراه نصب پلاگین‌های مورد نیاز، بررسی نمودیم؛ در ادامه قصد داریم تا چندین کامپوننت , ^ را برای نمایش لیست فیلمها، جزییات فیلم و جستجو، به برنامه اضافه کنیم و به هر کدام یک route را نیز انتساب دهیم. از کامپوننت‌ها برای بخش بندی قسمتهای مختلف سایت استفاده میکنیم. هر بخش برای دریافت و نمایش اطلاعاتی خاص مورد استفاده قرار میگیرد. بر خلاف Angular که به‌راحتی با دستور زیر میتوان برای آن یک کامپوننت ایجاد نمود و هر بخشی (css,js,ts,html) را در یک فایل جداگانه قرار داد:
ng generate component [name]
یا
ng g c [name]
در Vue.js هنوز امکان اینکه بتوان از طریق cli  یک کامپوننت را ایجاد کرد، فراهم نشده‌است. البته پکیج‌هایی برای اینکار تدارک دیده شده‌اند، ولی در این مقاله به‌صورت دستی اینکار انجام میشود و از Single File Component استفاده میکنیم. بصورت پیش فرض برنامه ایجاد شده vue.js دارای یک کامپوننت با نام HelloWorld.vue  در پوشه components  می‌باشد ( چیزی شبیه Hello Dolly در Wordpress)؛ آن را حذف میکنیم و محتویات فایل App.vue را مطابق زیر تغییر میدهیم ( قسمت import کردن کامپوننت HelloWorld.vue را حذف میکنیم) 
<template>
  <v-app>
    <v-toolbar app>
      <v-toolbar-title>
        <span>Vuetify</span>
        <span>MATERIAL DESIGN</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn
        flat
        href="https://github.com/vuetifyjs/vuetify/releases/latest"
        target="_blank"
      >
        <span>Latest Release</span>
      </v-btn>
    </v-toolbar>

    <v-content>
      <HelloWorld/>
    </v-content>
  </v-app>
</template>

<script>


export default {
  name: 'App',
  components: {
    
  },
  data () {
    return {
      //
    }
  }
}
</script>
 در پوشه components، سه کامپوننت را با نام‌های LatestMovie.vue ، Movie.vue و SearchMovie.vue ایجاد کنید.
محتویات LatestMovie.vue
<template>

  <v-container v-if="loading">
    <div>
      <v-progress-circular
        indeterminate
        :size="150"
        :width="8"
        color="green">
      </v-progress-circular>
    </div>
  </v-container>

  <v-container v-else grid-list-xl>
    <v-layout wrap>
      <v-flex xs4
        v-for="(item, index) in wholeResponse"
        :key="index"
        mb-2>
        <v-card>
          <v-img
            :src="item.Poster"
            aspect-ratio="1"
          ></v-img>

          <v-card-title primary-title>
            <div>
              <h2>{{item.Title}}</h2>
              <div>Year: {{item.Year}}</div>
              <div>Type: {{item.Type}}</div>
              <div>IMDB-id: {{item.imdbID}}</div>
            </div>
          </v-card-title>

          <v-card-actions>
            <v-btn flat
              color="green"
              @click="singleMovie(item.imdbID)"
              >View</v-btn>
          </v-card-actions>

        </v-card>
      </v-flex>
  </v-layout>
  </v-container>
</template>

<script>
import movieApi from '@/services/MovieApi'

export default {
  data () {
    return {
      wholeResponse: [],
      loading: true
    }
  },
  mounted () {
    movieApi.fetchMovieCollection('indiana')
      .then(response => {
        this.wholeResponse = response.Search
        this.loading = false
      })
      .catch(error => {
        console.log(error)
      })
  },
  methods: {
    singleMovie (id) {
      this.$router.push('/movie/' + id)
    }
  }
}
</script>

<style scoped>
  .v-progress-circular
    margin: 1rem
</style>

محتویات Movie.vue
<template>

  <v-container v-if="loading">
    <div>
        <v-progress-circular
          indeterminate
          :size="150"
          :width="8"
          color="green">
        </v-progress-circular>
      </div>
  </v-container>

  <v-container v-else>
    <v-layout wrap>
      <v-flex xs12 mr-1 ml-1>
        <v-card>
          <v-img
            :src="singleMovie.Poster"
            aspect-ratio="2"
          ></v-img>
          <v-card-title primary-title>
            <div>
              <h2>{{singleMovie.Title}}-{{singleMovie.Year}}</h2>
              <p>{{ singleMovie.Plot}} </p>
              <h3>Actors:</h3>{{singleMovie.Actors}}
               <h4>Awards:</h4> {{singleMovie.Awards}}
               <p>Genre: {{singleMovie.Genre}}</p>
            </div>
          </v-card-title>
          <v-card-actions>
            <v-btn flat color="green" @click="back">back</v-btn>
          </v-card-actions>
        </v-card>
      </v-flex>
    </v-layout>

    <v-layout row wrap>
      <v-flex xs12>
        <div>
        <v-dialog
          v-model="dialog"
          width="500">
          <v-btn
            slot="activator"
            color="green"
            dark>
            View Ratings
          </v-btn>
          <v-card>
            <v-card-title
             
              primary-title
            >
              Ratings
            </v-card-title>
            <v-card-text>
              <table style="width:100%" border="1" >
                <tr>
                  <th>Source</th>
                  <th>Ratings</th>
                </tr>
                <tr v-for="(rating,index) in this.ratings" :key="index">
                  <td align="center">{{ratings[index].Source}}</td>
                  <td align="center"><v-rating :half-increments="true" :value="ratings[index].Value"></v-rating></td>
                </tr>
              </table>
            </v-card-text>
            <v-divider></v-divider>
            <v-card-actions>
              <v-spacer></v-spacer>
              <v-btn
                color="primary"
                flat
                @click="dialog = false"
              >
                OK
              </v-btn>
            </v-card-actions>
          </v-card>
        </v-dialog>
      </div>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import movieApi from '@/services/MovieApi'
export default {
  props: ['id'],

  data () {
    return {
      singleMovie: '',
      dialog: false,
      loading: true,
      ratings: ''
    }
  },

  mounted () {
    movieApi.fetchSingleMovie(this.id)
      .then(response => {
        this.singleMovie = response
        this.ratings = this.singleMovie.Ratings
        this.ratings.forEach(function (element) {
          element.Value = parseFloat(element.Value.split(/\/|%/)[0])
          element.Value = element.Value <= 10 ? element.Value / 2 : element.Value / 20
        }
        )
        this.loading = false
      })
      .catch(error => {
        console.log(error)
      })
  },
  methods: {
    back () {
      this.$router.push('/')
    }
  }
}

</script>

<style scoped>
  .v-progress-circular
    margin: 1rem
</style>

محتویات SearchMovie.vue 
<template>

  <v-container v-if="loading">
    <div>
      <v-progress-circular
        indeterminate
        :size="150"
        :width="8"
        color="green">
      </v-progress-circular>
    </div>
  </v-container>

  <v-container v-else-if="noData">
    <div>
    <h2>No Movie in API with {{this.name}}</h2>
    </div>
  </v-container>

  <v-container v-else grid-list-xl>
    <v-layout wrap>
      <v-flex xs4
        v-for="(item, index) in movieResponse"
        :key="index"
        mb-2>
        <v-card>
          <v-img
            :src="item.Poster"
            aspect-ratio="1"
          ></v-img>

          <v-card-title primary-title>
            <div>
              <h2>{{item.Title}}</h2>
              <div>Year: {{item.Year}}</div>
              <div>Type: {{item.Type}}</div>
              <div>IMDB-id: {{item.imdbID}}</div>
            </div>
          </v-card-title>

          <v-card-actions>
            <v-btn flat
              color="green"
              @click="singleMovie(item.imdbID)"
              >View</v-btn>
          </v-card-actions>

        </v-card>
      </v-flex>
  </v-layout>
  </v-container>
</template>

<script>
// در همه کامپوننتها جهت واکشی اطلاعات ایمپورت میشود
import movieApi from '@/services/MovieApi'

export default {
  // route پارامتر مورد استفاده در 
  props: ['name'],
  data () {
    return {
      // آرایه ای برای دریافت فیلمها
      movieResponse: [],
      // جهت نمایش لودینگ در زمان بارگذاری اطلاعات
      loading: true,
      // مشخص کردن آیا اطللاعاتی با سرچ انجام شده پیدا شده یا خیر
      noData: false
    }
  },
  // تعریف متدهایی که در برنامه استفاده میکنیم
  methods: {
    // این تابع باعث میشود که 
    // route
    // تعریف شده با نام
    // Movie
    // فراخوانی شود و آدرس بار هم تغییر میکنید به آدرسی شبیه زیر
    // my-site/movie/tt4715356 
    singleMovie (id) 
      this.$router.push('/movie/' + id)
    },

    fetchResult (value) {
      movieApi.fetchMovieCollection(value)
        .then(response => {
          if (response.Response === 'True') {
            this.movieResponse = response.Search
            this.loading = false
            this.noData = false
          } else {
            this.noData = true
            this.loading = false
          }
        })
        .catch(error => {
          console.log(error)
        })
    }
  },
  // جز توابع
  // life cycle
  // vue.js
  // میباشد و زمانی که تمپلیت رندر شد اجرا میشود
  // همچنین با هر بار تغییر در 
  // virtual dom
  // این تابع اجرا میشود
  mounted () {
    this.fetchResult(this.name)
  },
  // watch‌ها // کار ردیابی تغییرات را انجام میدهند و به محض تغییر مقدار  پراپرتی 
  // name
  // کد مورد نظر در بلاک زیر انجام میشود
  watch: {
    name (value) {
      this.fetchResult(value)
    }
  }
}
</script>

<style scoped>
  .v-progress-circular
    margin: 1rem
</style>

توضیحی درباره کدهای بالا
برای درخواستهای ا‌‌‌‌‌‌‌‌ی‌جکس از axios استفاده میکنیم و با توجه به اینکه در این برنامه سه کامپوننت داریم، باید در هر کامپوننت axios را import کنیم:
import axios from 'axios'
لذا (DRY) یک فولدر را بنام service در پوشه src  ایجاد میکنیم. یک فایل جاوااسکریپتی را نیز با نام دلخواهی در آن ایجاد و فقط یکبار axios را در آن  import میکنیم و توابع مورد نیاز را در آنجا مینویسیم (هر چند راه‌های بهتر دیگری هم برای کار با axios هست که در حیطه مقاله جاری نیست).

محتویات فایل MovieApi.js در پوشه service
import axios from 'axios'

export default {

  fetchMovieCollection (name) {
    return axios.get('&s=' + name)
      .then(response => {
        return response.data
      })
  },

  fetchSingleMovie (id) {
    return axios.get('&i=' + id)
      .then(response => {
        return response.data
      })
  }
}
فایل main.js برنامه را بشکل زیر تغییر میدهیم و با استفاده از تنظیماتی که برای axios وجود دارد، آدرس baseURL آن را به ازای نمونه وهله سازی شده‌ی vue برنامه، تنظیم میکنیم.
axios.defaults.baseURL = 'http://www.omdbapi.com/?apikey=b76b385c&page=1&type=movie&Content-Type=application/json'

فایل  index.js درون پوشه router را باز میکنیم و محتویات آن را بشکل زیر تغییر می‌دهیم:
import Vue from 'vue'
import VueRouter from 'vue-router'
// برای رجیستر کردن کامپوننت‌ها در بخش روتر، آنها را ایمپورت میکنیم
import LatestMovie from '@/components/LatestMovie'
import Movie from '@/components/Movie'
import SearchMovie from '@/components/SearchMovie'

Vue.use(VueRouter)

export default new VueRouter({
  routes: [
    {
      // مسیری هست که برای این کامپوننت در نظر گرفته شده(صفحه اصلی)بدون پارامتر
      path: '/',
      // نام روت
      name: 'LatestMovie',
      // نام کامپوننت مورد نظر
      component: LatestMovie
    },
    {
      // پارامتری هست که به این کامپوننت ارسال میشه id
      // برای دستیابی به این کامپوننت نیاز هست با آدرسی شبیه زیر اقدام کرد
      // my-site/movie/tt4715356
      path: '/movie/:id',
      name: 'Movie',
      // در کامپوننت جاری یک پراپرتی وجود دارد 
      //id که میتوان با نام 
      // به آن دسترسی پیدا کرد
      props: true,
      component: Movie
    },
    {
      path: '/search/:name',
      name: 'SearchMovie',
      props: true,
      component: SearchMovie
    }
  ],
  // achieve URL navigation without a page reload
  // When using history mode, the URL will look "normal," e.g. http://oursite.com/user/id. Beautiful!
  // در آدرس # قرار نمیگیرد
  mode: 'history'
})

در برنامه ما سه کامپوننت وجود دارد. ما برای هر کدام یک مسیر و نام را برای route آنها تعریف میکنیم، تا بتوانیم با آدرس مستقیم، آنها را فراخوانی کنیم و با دکمه‌های back و forward مرورگر کار کنیم.


نکته:  برای اجرای برنامه و دریافت پکیج‌های مورد استفاده در مثال جاری، نیاز است دستور زیر را اجرا کنید: 
npm install
مطالب
پشتیبانی از کشیدن و رها کردن در Angular 7
یکی از قابلیت‌هایی که به Angular 7 اضافه شده‌است، پشتیبانی از Drag and Drop می‌باشد. برای استفاده از این قابلیت، در ابتدا لازم است Angular CLI را بروز رسانی کنیم و بعد از آن پکیج @angular/cdk  را نصب و ماژول DragDropModule را در فایل app.module.ts، بخش import  اضافه کنیم:
npm install @angular/cdk

app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { DragDropModule } from '@angular/cdk/drag-drop';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [ AppComponent ],
  imports: [
    BrowserModule,
    FormsModule,
    DragDropModule
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

cdkDrag
با استفاده از دایرکتیو cdkDrag، یک آیتم قابلیت جابجایی را پیدا می‌کند و به صورت پیش فرض این دایرکتیو اجازه جابجایی آیتم را در تمامی جهات می‌دهد:
<div cdkDrag>
  I'm Draggable
</div>

در صورتیکه قصد داشته باشید جابجایی را بر اساس محور x یا y، محدود کنید، از دایرکتیو cdkDragLockAxis استفاده می‌شود که می‌تواند مقدار x یا y را نیز بپذیرد:
<div cdkDrag cdkDragLockAxis="x">
  I'm Draggable
</div>

در ادامه قصد پیاده سازی مثال زیر را داریم :

در ابتدا یک مدل را ایجاد می‌کنیم: 

export interface Todo {
    title: string;
    type?: string;
}


فایل app.component.ts 

export class AppComponent implements OnInit {

  public title = 'Darg and drop';
  public model: Todo;

  public todo: Todo[];
  public done: Todo[];
  public cancelled: Todo[];


  ngOnInit(): void {
    this.setDefalutValue();
  }

  addItem(form, $event: Event) {
    $event.preventDefault();
    if (form.valid) {
      if (this.model.type === 'todo') {
        this.todo.push({ title: this.model.title });
      } else {
        this.done.push({ title: this.model.title });
      }
    } else {
      alert('فرم معتبر نمی‌باشد . عنوان را وارد نمایید');
    }
  }

  drop(event: CdkDragDrop<Todo[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else {
      transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex);
    }
  }



  private setDefalutValue() {
    this.todo = [
      { title: 'خرید مواد غذایی' },
      { title: 'رفتن به خانه' },
      { title: 'خوابیدن' }
    ];

    this.done = [
      { title: 'بیدار شدن' },
      { title: 'مسواک زدن' },
      { title: 'دوش گرفتن' },
      { title: 'چک کردن ایمیل' }
    ];

    this.cancelled = [];

    this.model = {
      title: null,
      type: 'todo'
    };

  }

}


در اینجا 3 آرایه یکی برای to-do  و یکی برای Done  و دیگری برای Cancelled  ایجاد می‌کنیم و به هر کدام تعدادی آیتم را اضافه می‌کنیم.

addItem : زمانیکه فرم را submit می‌کنیم اجرا می‌شود . 


drop : زمانی اجرا می‌شود که یک آیتم را Drag and Drop کنیم. در ایجا شرط event.previousContainer === event.container  زمانی درست است که جابجایی در درون یک لیست باشد و هدف در این صورت، مرتب سازی است .

moveItemInArray : ایندکس آیتم‌ها را در همان لیست تغیر می‌دهد (مرتب سازی).

transferArrayItem: آیتم را از یک لیست حذف و به لیست دیگری اضافه می‌کند.


فایل app.component.html

  <div>
    <!-- فرم -->
    <div>
      <fieldset>
        <legend>
          اضافه کردن آیتم جدید
        </legend>
        <form #form="ngForm" (submit)="addItem(form,$event)">

          <label></label>
          <input type="text" required name="title" #name="ngModel" [(ngModel)]="this.model.title">
          <label></label>

          <select required name="type" #type="ngModel" [(ngModel)]="this.model.type">
            <option value="todo">
              انجام دادن
            </option>
            <option value="done">
              انجام شده
            </option>
          </select>

          <input type="submit" value="ذخیره">
        </form>
      </fieldset>
    </div>

    <!-- آیتم‌ها -->
    <div>
      <fieldset>
        <legend>
          لیست آیتم‌ها </legend>

        <div>
          <!-- انجام دادن -->
          <div>
            <p>
              انجام دادن
            </p>
            <div cdkDropList #todoList="cdkDropList" [cdkDropListData]="todo" [cdkDropListConnectedTo]="[doneList, cancelledList]"
              (cdkDropListDropped)="drop($event)">
              <div *ngFor="let item of todo" cdkDrag>
                <p> {{ item.title | titlecase }} </p>
              </div>
            </div>
          </div>

          <!-- انجام شده -->
          <div>
            <p>
              انجام شده
            </p>

            <div cdkDropList #doneList="cdkDropList" [cdkDropListData]="done" [cdkDropListConnectedTo]="[todoList, cancelledList]"
              (cdkDropListDropped)="drop($event)">
              <div *ngFor="let item of done" cdkDrag>
                <p> {{ item.title | titlecase }} </p>
              </div>
            </div>

          </div>

          <!-- انجام نشده -->
          <div>
            <p>
              انجام نشده
            </p>
            <div cdkDropList #cancelledList="cdkDropList" [cdkDropListData]="cancelled" [cdkDropListConnectedTo]="[todoList, doneList]"
              (cdkDropListDropped)="drop($event)">
              <div *ngFor="let item of cancelled" cdkDrag>
                <p> {{ item.title | titlecase }} </p>
              </div>
            </div>
          </div>

        </div>

      </fieldset>
    </div>
  </div>


در ابتدا یک فرم داریم که در اینجا همه چیز مشخص است ( فرم‌های مبتنی بر قالب‌ها در Angular )

در ادامه 3 container را ایجاد می‌کنیم یکی برای to-do  و یکی برای Done  و در آخر یکی برای Cancelled

container  ایجاد شده برای  to-do

<div cdkDropList #todoList="cdkDropList" [cdkDropListData]="todo" [cdkDropListConnectedTo]="[doneList, cancelledList]"
     (cdkDropListDropped)="drop($event)">
   <div *ngFor="let item of todo" cdkDrag>
      <p> {{ item.title | titlecase }} </p>
   </div>
</div>


توضیحات: 

cdkDropList یک container می‌باشد، برای آیتم‌هایی که قرار است Drag and Drop شوند. 

todoList #:

    id مربوط به container را مشخص می‌کند.

cdkDropListConnectedTo:

 id مربوط به container های دیگری که می‌تواند آیتم های container جاری را بپذیرد.

cdkDropListData مشخص کنند منبع داده است.

cdkDropListDropped:  این رویداد زمانی اجرا می‌شود که Drag and Drop برای یک آیتم انجام شود.

cdkDrag:  برای اینکه آیتم‌های درون یک container قابلیت Drag and Drop را داشته باشند، این دایرکتیو را  اضافه می‌کنیم.


تمام ! 


DEMO 

مطالب
ارتباط بین کامپوننت‌ها در Vue.js - قسمت اول ارتباط بین Parent و Child

برنامه‌های Vue.jsای از چندین کامپوننت برای بخش بندی هر قسمت تشکیل میشوند و این بخش بندی برای مدیریت بهتر تغییرات، خطایابی، نگهداری و استفاده مجدد (reusable) می‌باشد. فرض کنید تعدادی کامپوننت در برنامه داریم و اطلاعات این کامپوننت‌ها بهم وابسته می‌باشند؛ بطور مثال یک کامپوننت انتخاب دسته بندی را داریم و به محض تغییر این مقدار، میخواهیم لیستی از محصولات پیشنهادی یا پرفروشِ آن دسته بندی، در کامپوننت پایین صفحه نمایش داده شود و یا خرید یک محصول را در نظر بگیرید که بلافاصله محتوای نمایش سبد خرید، بروزرسانی شود. در این مقاله ارتباط از نوع Parent و Child بین کامپوننت‌ها بررسی میشود.

 نکته: برای ارسال اطلاعات از کامپوننتِ Parent به Child، از  Props استفاده میشود و برای ارتباط از Child به Parent، از emit$ استفاده میشود.


یک برنامه Vue.js با نام vue-communication-part-1 ایجاد نمایید. سپس دو کامپوننت را با نام‌های Parent و Child، در پوشه components ایجاد کنید:



در کامپوننت Parent، یک تابع با نام increase وجود دارد که مقدار متغیر parentCounter را افزایش میدهد. چون قصد داریم مقدار متغیر parentCounter در کامپوننت Child نیز بروزرسانی شود، آن را به کامپوننت Child پاس میدهیم:

<Child :childCounter="parentCounter"/>

محتوای کامپوننت Parent:

<template>
  <div>
    <div>
      <h2>Parent Component</h2>
      <!-- را نمایش میدهید parentCounter مقدار -->
      <h1>{{ parentCounter }}</h1>
      <button @click="increase">Increase Parent</button>
    </div>
    <div>
      <!-- پاس میدهید Child در کامپوننت childCounter را به پراپرتی parentCounter مقدار -->
      <!--از طریق  decreaseParent سبب اتصال و فراخوانی تابع  @callDecreaseParent به  decreaseParent با انتساب -->
      <!-- میشود Child  در کامپوننت  callDecreaseMethodInParent تابع   -->
      <Child :childCounter="parentCounter" @callDecreaseParent="decreaseParent"/>
    </div>
  </div>
</template>

<script>
//برای استفاده در کامپوننت جاری Child ایمپورت کردن کامپوننت
import Child from "./Child.vue";

export default {
  // در این بخش متغیرهای مورد نیاز کامپوننت را تعریف میکنیم
  data() {
    return {
      parentCounter: 0
    };
  },
  components: {
    // میتوان آرایه ای از کامپوننت‌ها را در یک کامپوننت استفاده نمود
    // در این مثال فقط از یک کامپوننت استفاده شده
    Child
  },
  methods: {
    //را یک واحد افزایش میدهد parentCounter این متد مقدار
    increase() {
      this.parentCounter++;
    },
    decreaseParent() {
      this.parentCounter--;
    }
  }
};
</script>

<style>
.parent-block,
.child {
  text-align: center;
  margin: 20px;
  padding: 20px;
  border: 2px gray solid;
}
</style>


در کامپوننت Child  قصد دریافت مقدار پراپرتیِ childCounter را داریم که از طریق کامپوننت Parent، مقدارش تنظیم و بروزرسانی میشود. به این منظور در قسمت props  یک متغیر بنام childCounter را ایجاد میکنیم. 

Data is the private memory of each component where you can store any variables you need. Props are how you pass this data from a parent component down to a child component

محتوای کامپوننت Child

<template>
  <div>
    <h3>Child Component</h3>
    <!-- را نمایش میدهید childCounter مقدار -->
    <h3>{{ childCounter }}</h3>
    <button @click="increase">Increase Me</button>
    <button @click="callDecreaseMethodInParent">Call Decrease Method In Parent</button>
  </div>
</template>

<script>
export default {
  // استفاده میشود Child به  Parent برای ارتباط بین کامپوننت  props از
  props: {
    childCounter: Number
  },
  data() {
    return {};
  },
  methods: {
    //را یک واحد افزایش میدهد childCounter این متد مقدار
    increase() {
      this.childCounter++;
    },
    // فراخوانی میکند Parent را در کامپوننت decreaseParent تابع
    callDecreaseMethodInParent() {
      this.$emit("callDecreaseParent");
    }
  }
};
</script>


محتوای کامپوننت اصلی برنامه  App.vue:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <parent></parent>
  </div>
</template>

<script>
import Parent from "./components/Parent.vue";

export default {
  name: "app",
  components: {
    parent: Parent
  }
};
</script>
<style>
#app {
  width: 50%;
  margin: 0 auto;
  text-align: center;
}
</style>

اکنون برنامه را با دستور زیر اجرا کنید:

npm run serve

بعد از اجرای دستور فوق، روی گزینه زیر ctrl+click میکنیم تا نتیجه کار در مرورگر قابل رویت باشد: 

نمایش صفحه زیر نشان دهنده‌ی درستی انجام کار تا اینجا است:

اکنون روی دکمه‌ی Increase Parent کلیک میکنیم. همزمان مقدار شمارشگر، در هر دو کامپوننت Parent و Child افزایش می‌یابد و این بدین معناست که با استفاده از Props میتوانیم داده‌های دلخواهی را در کامپوننت Child بروز رسانی کنیم. هر زمانی روی دکمه‌ی Increase Me در کامپوننت Child کلیک کنیم، فقط به مقدار شمارشگر درون خودش اضافه میشود و تاثیری را بر شمارشگر Parent ندارد. در واقع یک کپی از مقدار شمارشگر Parent را درون خود دارد.

در ادامه قصد داریم بروزرسانی داده را از Child به Parent انجام دهیم. برای انجام اینکار از emit$ استفاده میکنیم. در دیکشنری Cambridge Dictionary معنی emit به ارسال یک سیگنال ترجمه شده‌است. در واقع بااستفاده از emit میتوانیم یک تابع را در کامپوننت Parent فراخوانی کنیم و در آن تابع، کد دلخواهی را برای دستکاری داده‌ها مینویسیم.

در تابع callDecreaseMethodInParent در کامپوننت Child، کد زیر را قرار میدهیم:

 this.$emit("callDecreaseParent");

هر زمان‌که این تابع اجرا شود، یک سیگنال از طریق کد زیر برای کامپوننت Parent ارسال میشود:

      <Child  @callDecreaseParent="decreaseParent"/>

در کد فوق مشخص شده که با ارسال سیگنال callDecreaseParent، تابع decreaseParent در کامپوننت Parent فراخوانی شود.


کد کامل مثال بالا 

نکته:  برای اجرای برنامه و دریافت پکیج‌های مورد استفاده در مثال جاری، نیاز است دستور زیر را اجرا کنید: 

npm install



چند نکته

this.$emit 
//dispatches an event to its parent component

کد فوق سبب اجرای یک تابع در کامپوننتِ Parent خودش میشود.

this.$parent
// gives you a reference to the parent component

ارجاعی به کامپوننت Parent خودش را فراهم میکند:

this.$root
// gives you a reference to the root component

زمانیکه چندین کامپوننت تو در تو را داریم یا به اصطلاح  nested component، سبب ارجاعی به بالاترین کامپوننت Parent میگردد.

this.$parent.$emit
// will make the parent dispatch the event to its parent

سبب اجرای تابعِ Parent کامپوننتِ Parent جاری میشود. به بیان ساده اگر این کد در کامپوننت فرزند فراخوانی شود، سبب اجرای تابعی در کامپوننت پدربزرگِ خود میشود.

this.$root.$emit
// will make the root dispatch the event to itself

سبب اجرای تابعی در کامپوننت root میشود (بالاترین کامپوننتِ پدرِ کامپوننت جاری).


تابع emit$ دارای آگومان‌های دیگری برای پاس دادن اطلاعات از کامپوننت Child به Parent می‌باشد؛ مثل زمانیکه قصد دارید اطلاعاتی در مورد محصول خریداری شده را به سبد خرید پاس دهید. در مثال دیگری که در ادامه قرار میگیرد نحوه کارکرد ارتباط کامپوننت Parent و Child را در یک برنامه بهتر تجربه میکنیم.

پیاده سازی یک سبد خرید ساده با روش مقاله‌ی جاری 

نکته:  برای اجرای برنامه و دریافت پکیج‌های مورد استفاده در مثال جاری، نیاز است دستور زیر را اجرا کنید:  

npm install

همچنین نیاز هست تا پکیچ node-sass را با دستور زیر برای این مثال نصب کنید.

npm install node-sass

مطالب
چگونه پروژه‌های Angular ی سبکی داشته باشیم - قسمت دوم
همانطور که در مطلب قبل دیدیم، با اضافه کردن style ها  و فایل‌های Javascript ای حجم صفحات خروجی رو به افزایش بودند. اولین راه بهینه سازی، استفاده از feature module است. می‌خواهیم هر زمان که به ماژولی نیاز داریم، آن را import و استفاده کنیم.
در ادامه دو فایل زیر را برای استفاده از ماژول‌های Angular Material و Kendo Angular UI در مسیر app\modules تعریف می‌کنیم.
// angular-material-feature.module.ts

import { NgModule } from '@angular/core';

// Import angular kendo angular
import {
  MatFormFieldModule, MatInputModule,
  MatButtonModule, MatButtonToggleModule,
  MatDialogModule, MatIconModule,
  MatSelectModule, MatToolbarModule,
  MatDatepickerModule,
  DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatTableModule, MatCheckboxModule, MatRadioModule, MatCardModule, fadeInContent,
  MatListModule, MatProgressBarModule, MatTabsModule
} from '@angular/material';

import {
  MatSidenavModule,
  MatSlideToggleModule,
} from '@angular/material';

import { MatMenuModule } from '@angular/material/menu';

@NgModule({
  declarations: [
   // KendoGridPaginationComponent
  ],
  imports: [

    MatSidenavModule,
    MatSlideToggleModule,
    MatInputModule,
    MatFormFieldModule,
    MatButtonModule, MatButtonToggleModule,
    MatDialogModule, MatIconModule,
    MatSelectModule, MatToolbarModule,
    MatDatepickerModule,
    MatCheckboxModule,
    MatRadioModule,
    MatCardModule,
    MatMenuModule,
    MatListModule,
    MatProgressBarModule,
    MatTabsModule
  ],


  exports: [


    MatSidenavModule,
    MatSlideToggleModule,
    MatInputModule,
    MatFormFieldModule,
    MatButtonModule, MatButtonToggleModule,
    MatDialogModule, MatIconModule,
    MatSelectModule, MatToolbarModule,
    MatDatepickerModule,
    MatCheckboxModule,
    MatRadioModule,
    MatCardModule,
    MatMenuModule,
    MatListModule,
    MatProgressBarModule,
    MatTabsModule


  ],
  providers: [


  ]

})
export class AngularMaterialFeatureModule {
}
و 
// kendo-feature.module.ts

import { NgModule } from '@angular/core';

// Import kendo angular ui
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { GridModule, ExcelModule } from '@progress/kendo-angular-grid';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
import { DialogsModule } from '@progress/kendo-angular-dialog';
import { RTL } from '@progress/kendo-angular-l10n';
import { LayoutModule } from '@progress/kendo-angular-layout';

import { WindowService, WindowRef, WindowCloseResult, DialogService, DialogRef, DialogCloseResult } from '@progress/kendo-angular-dialog';
import { SnotifyModule, SnotifyService, SnotifyPosition, SnotifyToastConfig, ToastDefaults } from 'ng-snotify';


@NgModule({
  declarations: [
   
  ],
  imports: [


    ButtonsModule,
    GridModule,
    ExcelModule,
    DropDownsModule,
    InputsModule,
    DateInputsModule,
    DialogsModule,
    LayoutModule,

    SnotifyModule,
    


  ],
  exports: [

    ButtonsModule,
    GridModule,
    ExcelModule,
    DropDownsModule,
    InputsModule,
    DateInputsModule,
    DialogsModule,
    LayoutModule,

  ],
  providers: [

    
    { provide: 'SnotifyToastConfig', useValue: ToastDefaults },
    SnotifyService,


    // Enable Right-to-Left mode for Kendo UI components
    { provide: RTL, useValue: true },
  ]

})
export class KendoFeatureModule {
}

 از این پس، هر زمانیکه به ماژولی نیاز داریم، feature module آن را import می‌کنیم.

در ادامه‌ی بهینه سازی، از قابلیت Lazy-Loading استفاده می‌کنیم و فایل ‌های زیر را جهت پیاده سازی lazy-loading تغییر می‌دهیم.
محتویات فایل styles.scss را پاک می‌کنیم. 
فایل app.module.ts : 
// app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserAnimationsModule,
    RouterModule,
    AppRoutingModule,

  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
 و فایل app.component.ts : 
// app.component.ts

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  encapsulation: ViewEncapsulation.None,
})
export class AppComponent {
  title = 'HowToKeepAngularDeploymentSizeSmall';

  constructor() {

  }

}
 و  محتویات فایل app.component.scss  را پاک می‌کنیم. 
و فایل app.component.html : 
// app.component.html

<router-outlet></router-outlet>
و فایل app-routing.module.ts 
// app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [

  { path: '', loadChildren: './projects/home/home.module#HomeModule' } 

];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

نکته 1: سعی کنید کامپوننت‌های کمتری را به app.module.ts اضافه کنید.
نکته 2: stylesheet‌ها را در ماژول‌هایی که به آنها نیاز دارند import کنید.

تا به اینجای کار، ماژول اصلی پروژه را سبک کردیم. حال می‌خواهیم صفحه‌ی اول پروژه را به module ای به نام home، در مسیر app\projects\home مسیردهی کنیم. 
home.module.ts :
// home.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import {AngularMaterialFeatureModule , KendoFeatureModule} from '../../modules/index';
import {HomeRoutingModule } from './home.routing';

import { HomeComponent } from './home.component';
import { IndexComponent } from './pages/index/index.component';

import {LoginComponent } from './pages/login/login.component';

@NgModule({

  declarations: [
    HomeComponent,
     IndexComponent,
    LoginComponent
    ],
  imports: [
    CommonModule,
    HomeRoutingModule,
    AngularMaterialFeatureModule
  ],
  providers:[

  ],
  entryComponents:[
    LoginComponent

  ]

})
export class HomeModule { }
home.routing.ts :
// home.routing.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home.component';
import { IndexComponent } from './pages/index/index.component';

const routes: Routes = [
  {
    path: "",
    component: HomeComponent,
    children: [
      {
        path: "",
        component: IndexComponent,
        data: {
          breadcrumb: " Index "
        }
      },
      {
        path: "about",
        loadChildren: "./pages/aboutus/aboutus.module#AboutusModule"
      },
      {
        path: "blog",
        loadChildren: "./pages/blog/blog.module#BlogModule"
      },
      {
        path: "blog-detail",
        loadChildren: "./pages/blogdetail/blogdetail.module#BlogdetailModule"
      },
      {
        path: "pricing",
        loadChildren: "./pages/pricing/pricing.module#PricingModule"
      },
      {
        path: "contact",
        loadChildren: "./pages/contact/contact.module#ContactModule"
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class HomeRoutingModule  {
}

و فایل home.component.ts :
// home.component.ts

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';

import { LoginComponent } from './pages/login/login.component';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class HomeComponent implements OnInit {

  constructor(
    private dialog: MatDialog) { }

  ngOnInit() {

  }


  public toggleModal(){
    let dialogRef = this.dialog.open(LoginComponent, {
    });
  }

}
و فایل  home.component.scss : 
// home.component.scss

@import '../../../assets/home/css/themify-icons.css';
@import '../../../assets/home/css/font-awesome.min.css';
@import '../../../assets/home/css/set1.css';

@import '../../../assets/home/css/bootstrap.min.css';
@import '../../../assets/home/css/style.css';
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
و فایل  home.component.html :
// home.component.html

<!--============================= HEADER =============================-->
<div>
  <div>
    <div>
      <div>
        <div>
          <nav>
            <a href="index.html"><img src="../../../assets/home/images/logo.png" alt="logo"></a>
            <button type="button" data-toggle="collapse" data-target="#navbarNavDropdown"
              aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
              <span></span>
            </button>
            <div id="navbarNavDropdown">
              <ul>

                <li>
                  <a  target="_blank" [routerLink]="['/dashboard']">Dashboard</a>
                </li>


                <li>
                  <a [routerLink]="['/about']">About</a>
                </li>

                <li>
                  <a [routerLink]="['/contact']">Contact</a>
                </li>

                <li>
                  <a [routerLink]="['/pricing']">Pricing</a>
                </li>

                <li>
                  <a href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    Blog
                    <span></span>
                  </a>
                  <div>
                    <a [routerLink]="['/blog']">Blog Listing</a>
                    <a [routerLink]="['/blog-detail']">Blog Detail</a>
                  </div>
                </li>
                <li>
                  <a (click)='toggleModal()' style="cursor: pointer;">Login</a>
                </li>
                <li><a href="add-listing.html"><span></span> Add
                    Listing</a></li>
              </ul>
            </div>
          </nav>
        </div>
      </div>
    </div>
  </div>
</div>

<!--//END HEADER -->

<router-outlet></router-outlet>


<!--============================= FOOTER =============================-->
<footer>
  <div>
    <div>
      <div>
        <div>
          <i aria-hidden="true"></i>
          <p>503 Sylvan Ave, Mountain View<br> CA 94041, United States</p>
        </div>
      </div>
      <div>
        <div>
          <img src="../../../assets/home/images/footer-logo.png" alt="#">
        </div>
      </div>
      <div>
        <ul>
          <li><a href="#"><i aria-hidden="true"></i></a></li>
          <li><a href="#"><i aria-hidden="true"></i></a></li>
          <li><a href="#"><i aria-hidden="true"></i></a></li>
          <li><a href="#"><i aria-hidden="true"></i></a></li>
          <li><a href="#"><i aria-hidden="true"></i></a></li>
        </ul>
      </div>
    </div>
    <div>
      <div>
        <div>
          <p>Copyright © 2017 Listed Inc. All rights reserved</p>
          <a href="#">Privacy</a>
          <a href="#">Terms</a>
        </div>
      </div>
    </div>
  </div>
</footer>
<!--//END FOOTER -->

پروژه را با دستور ng serve --open --prod  اجرا کرده و خروجی را بررسی می‌کنیم. حجم خروجی صفحه اول را همراه با قالبی از پیش طراحی شده در تصویر زیر می‌بینیم:


سورس کامل پروژه در HowToKeepAngularDeploymentSizeSmall قرار دارد.