Lightning Web Components Basics

Всім привіт! Цього місяця ми починаємо серію статей, присвячених LWC. Разом ми розберемось у структурі та принципах розробки LWC компонентів, базових принципах розмітки, комунікації між компонентами та звязку компонентів з Apex.

Отже давайте почнемо наше знайомство з Lightning Web Components або LWC. LWC — це заснована на сучасних стандартах веб програмування модель для створення веб-компонентів, яка працює в Salesforce. Зараз ми розглянемо основи LWC, зокрема, що це таке, його структуру та теорію, рекомендації Salesforce щодо створення компонентів, найкращі практики використання LWC тощо. Давайте розпочнемо!

Що таке LWC?
Lightning Web Components або LWC — це легкий у використанні фреймворк. Модель програмування якого використовує сучасні веб-стандарти, такі як веб-компоненти, користувацькі елементи, тіньовий DOM і модулі ECMAScript, для створення багаторазово використовуваних компонентів інтерфейсу користувача. LWC підтримується Salesforce, що означає, що ви можете створювати, тестувати та розгортати компоненти LWC безпосередньо на платформі Salesforce.

LWC використовує компонентну архітектуру, яка складається з файлів HTML, CSS і JavaScript. LWC також використовує ієрархічну структуру, яка дозволяє компонентам складатися з інших компонентів. Це дозволяє розробникам створювати складні інтерфейси користувача шляхом поєднання простих компонентів, які можна багаторазово використовувати.
У LWC кожен компонент визначається як клас JavaScript, який розширює базовий клас LightningElement. Цей клас містить властивості, методи та хуки життєвого циклу, які визначають поведінку компонента.

LWC надає набір базових інструментів, які допомагають вам керувати станом і життєвим циклом ваших компонентів. Деякі з ключових інструментів включають:

  • ConnectedCallback() — це хук життєвого циклу, який викликається, коли компонент вставляється в DOM. Це означає, що компонент завершив рендеринг і готовий до відображення. Ви можете використовувати цей хук для виконання будь-яких завдань, які вимагають доступу до DOM, наприклад, налаштування прослуховувачів подій або отримання даних із сервера.
connectedCallback() {
    // Add a window resize event listener
    window.addEventListener('resize', this.handleWindowResize);
}

handleWindowResize(event) {
    // Your logic for handling the window resize event goes here
    // You can access event properties like event.target, event.clientX, event.clientY, etc.
    // For example, you can update component attributes or call methods.
    console.log('Window resized:', event.target.innerWidth, event.target.innerHeight);
}
  • DisconnectedCallback() — це ще один хук життєвого циклу, який викликається, коли компонент видаляється з DOM. Це означає, що компонент більше не видно, та він знищується. Ви можете використовувати цей хук для виконання будь-яких завдань очищення, таких як видалення слухачів подій або скасування будь-яких запитів, що очікують на розгляд.

disconnectedCallback() {
    // Remove the window resize event listener when the component is disconnected
    window.removeEventListener('resize', this.handleWindowResize);
}
  • RenderedCallback() — це хук життєвого циклу, який викликається після кожного циклу візуалізації компонента. Це означає, що він викликається кожного разу, коли компонент відображається, незалежно від того, чи відбувається це вперше, чи наступні рази. Ви можете використовувати цей хук для виконання будь-яких завдань, які вимагають доступу до візуалізованої DOM, наприклад, маніпулювання DOM або оновлення стану компонента. У прикладі ви можете побачити, що renderedCallback() додає клас стилю до елемента кнопки кожного разу, коли компонент відображається. Це дає вам змогу змінювати DOM після того, як він був відтворений, що може бути корисним у певних сценаріях. Важливо відзначити, що renderedCallback() може бути викликаним кілька разів протягом життєвого циклу компонента, тому слід бути обережним, щоб не виконувати довгі та ресурсно витратні операції або робити занадто багато викликів API у цьому хуку.
@api buttonStyle;
    
 renderedCallback() {
     // When we finished Render we adding some spec  ific style
     // class to our button which was passed from parrent LWC
     const button = this.template.querySelector('button'); 
     button.classList.add(this.buttonStyle);
 }
  • errorCallback() - це хук життєвого циклу, який викликається в разі виникнення помилки під час візуалізації компонента у LWC. Він надає можливість обробити й відловити помилку та вжити необхідних заходів для відновлення стану компонента або відображення відповідного повідомлення про помилку. Цей хук викликається після виникнення помилки та дозволяє виконувати додаткові дії, такі як запис помилки в журнал, сповіщення користувача або інші маніпуляції з DOM. Використовуючи errorCallback(), ви можете керувати відображенням та реакцією компонента на помилки, що допомагає забезпечити більш стабільну та надійну роботу вашого LWC компонента.
errorCallback(error, stack) {
    // We catch errors in error callback and can work with them as we need
    console.error('Error message:', error);
    console.error('Error stack trace:', stack);
    this.doSomething();
}


doSomething() {
    // Perform some actions
}

Тепер давайте поговоримо про основу використання змінних у LWC.
У LWC є два основних рівні змінних: змінні рівня класу та змінні рівня функції.

Змінні рівня класу оголошуються поза будь-якими функціями в класі JavaScript компонента та доступні для всіх функцій у класі. Зазвичай вони використовуються для зберігання даних, до яких потрібно отримати доступ з HTML або утримувати дані для маніпуляції компонентом протягом його життєвого циклу.

Наприклад, ви можете оголосити змінну рівня класу під назвою count, яка відстежує кількість натискань кнопки:

count = 0; 
maxLimit = 10;

handleClick() { 
  if (this.count < this.maxLimit) {
      this.count++; 
  }
}

У цьому прикладі змінну count оголошено поза функцією handleClick, тому функція може отримати до неї доступ і оновити її лише посилаючись через this. . Кожного разу, коли викликається функція handleClick (наприклад, коли натискається кнопка), змінна лічильника збільшується. Та перевіряється чи ми не досягли вказаного нами ліміту зі збільшення числа.

З іншого боку, змінні функціонального рівня оголошуються всередині функції й доступні лише в цій функції. Зазвичай вони використовуються для зберігання тимчасових або проміжних даних, які потрібні лише в контексті цієї функції. Наприклад, ви можете оголосити змінну функціонального рівня під назвою isValid, яка перевіряє, чи дійсний введений користувачем код:

У цьому прикладі змінна isValid оголошена всередині функції handleInputChange і використовується для перевірки дійсності введених користувачем даних. Змінна на рівні функції тут потрібна щоб виконати перевірку лише один раз та потім скористатись результатом двічі для IF кондиції та статусу помилки.

showError = false;

handleInputChange(event) {
    let userInput = event.target.value;
    let isValueInvalid = userInput.length > 10;

    if (isValueInvalid ) {
         console.log('You hit limit');
    }
    if (userInput.length === 0 || isValueInvalid) {
         this.showError = true;
    }
}

Також невід’ємною частиною роботи з LWC є різні анотації. Анотації є важливими елементами у розробці LWC в Salesforce. Вони використовуються для додавання метаданих до класів та методів компонентів, що допомагає забезпечити їхню правильну роботу.

У цій статті ми познайомимось лише з однієї з них – @track. Наступні анотації – такі як @api та @wire – ми розберемо у майбутніх статтях на тему LWC.

@track: Ця анотація використовується для відстежування змін у властивостях компонента. Вона дозволяє автоматично оновлювати розмітку компонента при зміні значень властивостей. Варто розуміти, що ви можете не використовувати анотацію @track для простих типів даних, таких як рядки, числа та булеві значення, оскільки вони вже автоматично відстежуються LWC. Анотація @track потрібна лише для відстежування змін в об’єктах та масивах, тому що зміни в цих типах даних не відстежуються автоматично.

У цьому прикладі ми додаємо елементи до масиву і для того, щоб наш компонент міг коректно відстежувати зміни у ньому ми додали анотацію трек.

Ось кілька порад для використання LWC:

  • Зробіть ваші компоненти простими, використовуйте модульну конструкцію, розділяйте компоненти на менші частини, які можна багаторазово використовувати, та об’єднувати, щоб створити складніші інтерфейси.
  • Хорошою практикою є компоненти, які виконують одну функцію і роблять це добре. Уникайте створення монолітних компонентів, які роблять занадто багато.
  • Використовуйте шаблони дизайну, такі як PubSub, Decorator і Facade, щоб упорядкувати свій код і покращити можливість повторного використання коду.
  • Використовуйте систему Salesforce Lightning Design System (SLDS), щоб переконатися, що ваші компоненти відповідають користувальницькому досвіду Salesforce й узгоджені з іншими компонентами Salesforce.
  • Напишіть модульні тести для ваших компонентів, щоб переконатися, що вони поводяться належним чином, і виявити помилки на ранніх етапах циклу розробки.

Зараз давайте розпочнемо створення нашого першого LWC компонента та розглянемо на практиці усе те, про що говорили вище. Для цього розробимо просту форму для додавання простих чисел.

<template>
    <section class="main">
        <h1>Number sum form</h1>


        <div class="twoColumnStyle">
            <fieldset>
                <div class="threeColumnStyle">
                    <lightning-input
                        value={firstNumber}
                        label="First Number"
                        onchange={handleInput}
                        data-name="firstNumber">
                    </lightning-input>
                    <lightning-input
                        value={secondNumber}
                        label="Second Number"
                        onchange={handleInput}
                        data-name="secondNumber">
                    </lightning-input>
                    <lightning-button
                        label="Sum"
                        onclick={handleButtonClick}>
                    </lightning-button>
                </div>
                <div class="oneColumnStyle">
                    <lightning-input
                        value={result}
                        label="Result">
                    </lightning-input>
                </div>
            </fieldset>
           
            <section>
                <template lwc:if={isHistoryExist}>
                    <h2>History of operations</h2>
                    <template for:each={operationsHistory} for:item="historyRecord">
                        <div key={historyRecord.value} class="borderStyle">
                            {historyRecord.firstNumber} + {historyRecord.secondNumber} = {historyRecord.value}
                        </div>
                    </template>
                </template>
            </section>
        </div>
    </section>
</template>

Для початку давайте створимо розмітку для нашого компонента.
На ній ми можемо побачити 2 поля введення чисел, кнопку запуску калькуляції, поле для відображення результатів калькуляції та блок з історією операцій, які вже відбулись.
Блок з історією ітерується по масиву об’єктів, у яких ми зберігаємо потрібні для нас дані (більш детально про ітерацію у LWC ми поговоримо в наступних статтях про LWC).
Також там ми можемо побачити LWC:IF, який не покаже цей блок, якщо історія є пустою.
З моментів, на які варто звернути увагу – це додавання дата нейму для полів введення, щоб надалі використати їх у JS для отримання даних.

import { LightningElement, track } from 'lwc';

export default class SimpleNumberSumForm extends LightningElement {
    @track operationsHistory = [];
    firstNumber;
    secondNumber;
    result;


    connectedCallback() {
        // One of common uses of connectedCallback is calling Apex method to retrieve data from SF
        // We will show examples of this usage in a future video
    }


    errorCallback(error, stack) {
        console.log(error, stack);
    }


    handleInput(event) {
        this[event.target.dataset.name] = event.target.value;
    }


    handleButtonClick() {
        this.result = parseInt(this.firstNumber) + parseInt(this.secondNumber);


        if (!Number.isNaN(this.result)) {
            let history = JSON.parse(JSON.stringify(this.operationsHistory));
            history.push({firstNumber: this.firstNumber, secondNumber: this.secondNumber, value: this.result});
            this.operationsHistory = history;
        }
    }


    get isHistoryExist() {
        return this.operationsHistory.length >= 1;
    }
}

Тепер перейдемо до нашого JS класу. Для початку ми імпортували анотацію трек. Оголосили масив на рівні класу з цією анотацією для зберігання історії. В цьому випадку анотація потрібна, щоб наш LWC компонент реагував на зміни у масиві та дозволяв ітерації відпрацьовувати з урахуванням останніх змін у ньому.

Далі ми оголосили дві змінні для зберігання введення користувача та змінну для збереження результатів калькуляцій.

Далі на черзі - connectedCallback. Для прикладу у цьому компоненті ми можемо використати цей хук для ініціалізації історії попередніх операцій з Salesforce за допомогою Apex. Ми не будемо зараз зупинятись на цьому, оскільки у майбутніх статтях серії, присвячених LWC, ми будемо розбирати можливі способи комунікації LWC з Apex та покажемо цей випадок на прикладі.

Далі ми бачимо errorCallback для відображення помилок та їхнього stack trace у консолі.

Також ми додали 2 функції: перша handleInput викликається коли користувач вводить або змінює дані у полі. В цій функції ми отримуємо івент, у якому міститься значення, яке ввів користувач, та ім’я поля, яке саме надіслало цей івент. Оскільки попередньо ми додали дата нейм теги до наших інпут полів на html, ця конструкція дозволить нам використовувати одну функцію для присвоєння для усіх наших інпутів, тому що у дата нейм тег ми присвоїли те ж ім’я, що й у змінній на рівні класу, яка зберігає ці дані. Це динамічне звернення можна також замінити на звичайну конструкцію this крапка та змінна, до якої ми звертаємось.

Далі в нас є функція, яка спрацьовує на натискання кнопки. Вона бере перше та друге число, додає їх та зберігає у результаті. Далі, якщо це число є коректним, ми додаємо його в історію операцій, яка буде показана на сторінці.

Останнім ми бачимо геттер, який перевіряє чи довжина масиву історії операції не пуста для нашої кондиції LWC:IF, яка буде відображати історію.

Отже, Lightning Web Components - це сучасна, стандартна модель програмування для створення багаторазових компонентів інтерфейсу користувача, які працюють нативно в Salesforce. Дотримуючись рекомендацій та кращих практик Salesforce, ви можете створювати компоненти високої якості, масштабовані, безпечні та легкі у підтримці. Завдяки потужним базовим інструментом, LWC дозволяє легко керувати станом та життєвим циклом компонентів, а його модульна архітектура дозволяє створювати складні інтерфейси, комбінуючи прості, багаторазові компоненти. У наступній статті ми поговоримо про базові принципи розмітки у LWC. Розглянемо які можливості та особливості нам надає використання LWC для розробки наших додатків для Salesforce.

1 Вподобання

It would be better if you use onclick in the template markup instead of calling addEventListener because this listener automatically removes on component destroy
Also, please be cautious when passing your methods as callbacks because they can lose the this binding to your code. For callbacks consider using arrow functions instead of methods.
Here is the error causing example:

class Demo extends LightningElement {
  response;
  connectedCallback() {
    getApexData().then(this.handleResponse);
  }
  handleResponse(response) {
    this.response = response;
}

If you use this code, the response will not save and depending on the LWC setup you may receive a visible error message “cannot find response property on null” because the method changed this binding

2 Вподобання

From Salesforce LWC docs,

connectedCallback()
Called when the element is inserted into a document. This hook flows from parent to child. You can’t access child elements because they don’t exist yet.

So it is not possible to query a button from the template at this stage

Here is the example of modified lightning-button component example to count the buttons:
HTML
Screenshot 2023-09-06 at 15.51.47

And JS:
Screenshot 2023-09-06 at 15.51.55

The selector doesn’t find any button. They can be queried only on renderedCallback

Also, you can check this chart in the documentation that shows the lifecycle steps for parent and child components

2 Вподобання

Totally agree with your suggestion. This code was just set up as an example of usage and there are some cases where this behavior can be preferred. Onclick is more known and used so the decision was to also show the possibility of such usage as event listeners.

1 Вподобання

Thanks that very good point. We can`t query select the child elements in the moment of Connectedcalback hook execution. I changed the example to one that is working and also better and at same time easily represents usage of ConnectedCallback.