Nithin Prasad
Nithin Prasad

Reputation: 99

Using promise or async/await inside a lit-element web component

I have 2 components - product-overview and product-overview-item. The product-overview-item is an individual item that gets rendered inside of product-overview component every time the built-in render() method loads the HTML template on the screen.

I have an object (say, agreement) with the HATEOAS links array as shown below.

{
    "id": "1"
    "type": "INVEST"
    "group": "INVEST"
    "role": "HOLD"
    "accountId": "1",
    "commercialId": {
        "value": "DEFAULT",
        "type": "3000",
    },
    "productName": "Self Invest",
    "displayAlias": "Superman",
    "visible": true,
    "status": "ACTIVE"
    "balance": {
        "currency": "EUR",
        "value": "34.00",
    },
    "_links": [
        {
            "rel": "details",
            "href": "/nl/agree/192712812/investbalance",
        },
        {
            "rel": "portfolio",
            "href": "/dv/agreesecurities/981261892y9aksjakcnm",
        },
        {
            "rel": "edit",
            "href": "/mb/port/921nclaskofknsscsdwd",
        }
    ]
}

I need to make an API call to the href value inside the HATEOAS links array that starts with /nl/agreement and ends with /investmentbalance. The response from the investment balance is very similar to the above response. From that, I need to fetch the balance object and pass it down to the child component (product-overview-item).

The balance object is similar to the one shown above. Here is what I have tried so far.

1. product-overview.js (Parent component)

_getUrl(agreement) {
    const { _links } = agreement;
    const nlAgreements = _links?.filter(link => link?.rel && link?.rel === 'details' && link?.href && link?.href.startsWith('/nl/agreements'));
    if (nlAgreements === undefined || nlAgreements.length === 0) return;
    return nlAgreements[0]?.href;
}
 
async _fetchMigratedInvestmentBalance(agreement) {
    await ajax.get(this._getUrl(agreement))
      .then(response => {
        if (!response) throw new Error('Failed to fetch balance');
        else return response;
      })
      .then(response => this.investmentBalance = response?.data?.balance)
      .catch(error => {
        throw new Error('Failed to fetch balance due to: ', error);
      });
}
 
_getProducts(status, role, tab) {
    ...
    ...
    if (this.agreementsList && this.agreementsList.length > 0) {
      let groupedAgreements = [];
      const filteredAgreements = this._filterAgreements(this.agreementsList, status, role);
      groupedAgreements = this._getGroupedAgreements(filteredAgreements);
      tab.count = filteredAgreements.length;
 
      if (groupedAgreements && groupedAgreements.length > 0) {
        const noBalanceAlert = this.isAPA ? html`
          <div class="alert"><uic-alert type="error">
          ${localize.msg('product-overview:NO_BALANCE')}
          </uic-alert></div>
        ` : '';
        return html`
        ${noBalanceAlert}
        ${groupedAgreements.map(item => html`
          <uic-expandable-item>
            ...
            <article slot="details" class="item-details productDetails">
              <ul class="list-unstyled">
                ${item.agreements.map((agreement, index) => html`
                <li class="productDetailsItem">
                  ${when(BeProductOverviewServices._isClickable((agreement.type).toUpperCase()), () => html` <a @click='${e => this._clickedToProductDetailsPage(e, item.agreements)}'
                    data-group="${item.agreementType}" data-index="${index}"
                    data-url="product-details/be/${item.agreementType}/${agreement.commercialId.value}">
                    <product-overview-item .agreement="${agreement}" @connected="${this._fetchMigratedInvestmentBalance(agreement)}" .balance="${this.investmentBalance}" .role="${role}">
                    </product-overview-item></a>`, () => html`<product-overview-item
                    .agreement="${agreement}" @connected="${this._fetchMigratedInvestmentBalance(agreement)}" .balance="${this.investmentBalance}" .role="${role}"
                    .disabled="${this.disabled}"></product-overview-item>
                  `)}
                <li>
                `)}
              </ul>
            </article>
          </uic-expandable-item>`)}`;
      `)}`;
      }
    }
    return html``;
  }

The _getProducts() function gets called in the render() function.

2. product-overview-item.js (child component) In the child component, I am dispatching an event each time the item loads in connectedCallback() function.

class beProductOverviewItem extends LitElement {
  static get properties() {
    return {
      agreement: { type: Object },
      balance: { type: Object, attribute: 'investment-balance', reflect: true },
      role: { type: String },
      disabled: { type: Boolean },
    };
  }
 
  static get styles() {
    return beProductOverviewStyle;
  }
 
  connectedCallback() {
    super.connectedCallback();
    this.dispatchEvent(new CustomEvent('connected'));
  }
 
  render() {
    return html`<uic-item ?disabled="${this.disabled}">
      <uic-item-header>
      <span class="bold">${this.agreement?.productName}</span>
      <span class="bold" slot="header-addon">
      ${(this.agreement?.status === 'CLOSED') ? html`
      ${localize.msg('product-overview:CLOSED')}` : (this.agreement?.type !== 'INVESTMENT' && this.agreement?.balance) ? html`
    ${formatAmountHtml(this.agreement?.balance?.value, {
    locale: localize.locale,
    style: 'currency',
    currencyDisplay: 'symbol',
    currency: this.agreement?.balance?.currency,
  })}
` : (this.agreement?.type === 'INVESTMENT' && this.balance) ? html`
  ${formatAmountHtml(this.balance?.value, {
    locale: localize.locale,
    style: 'currency',
    currencyDisplay: 'symbol',
    currency: this.balance?.currency,
  })}
  ` : ''}
 
            </uic-item>`;
  }
}

The promise resolves without undefined/pending but this time it is going in to an infinite loop loading the balance continuously. Is there something we are missing out?

NOTE: Now, I'll be the first to state I don't have enough experience to understand what is going on with these async calls. How do I incorporate async/await logic into my component?

Upvotes: 1

Views: 5145

Answers (2)

Nithin Prasad
Nithin Prasad

Reputation: 99

I am answering my own question because the solution to this is a bit different than what has been suggested above.

I idea of calling the API in the child component and keeping the balance object ready in connectedCallback() did the trick!

class ProductOverviewItem extends LitElement {
    static get properties() {
        return {
          balance: { state: true },
          loading: { type: Boolean, reflect: true },
        }
    }

    _getUrl() {
        const { _links } = this.agreement;
        const nlAgree = _links?.filter(link => link?.rel && link?.rel === 'details' && link?.href && link?.href.startsWith('/nl/agree'));
        if (nlAgree === undefined && nlAgree.length === 0) return;
        return nlAgree[0].href;
    }

    async _fetchInvestBalance() {
        const url = this._getUrl();
        const investmentBalance = await ajax.get(this._getUrl());
        this.balance = investmentBalance.data.balance;
    }
    
    connectedCallback() {
       super.connectedCallback();
       until(this._fetchInvestBalance(), 'Loading...');
    }

    render() {
      // displaying the balance from the property value set here.
    }
}

Upvotes: 0

Benny Powers
Benny Powers

Reputation: 5836

try something like this:

class ProductOverviewItem extends LitElement {
    static get properties() {
        return {
          balance: { state: true },
          loading: { type: Boolean, reflect: true },
        }
    }

    _getUrl() {
        const { _links } = this.agreement;
        const nlAgree = _links?.filter(link => link?.rel && link?.rel === 'details' && link?.href && link?.href.startsWith('/nl/agree'));
        if (nlAgree === undefined && nlAgree.length === 0) return;
        return nlAgree[0].href;
    }

    async _fetchInvestBalance() {
        await fetch(this._getUrl())
          .then(x => {
            if (!x.ok)
              throw new Error(x.status)
            else 
              return x;
          })
          .then(x => x.json())
          .then(x => this.balance = x.data?.data?.data?.balance)
          .catch(error => {
              this.error = error;
          });
    }

    render() {
      return html`
        <product-overview-item
            ?hidden="${this.loading}"
            @connected="${this._fetchInvestBalance()}"
            .agree="${agree}"
            .balance="${this.balance}"></product-overview-item>
      `;
    }
}

class ProduceOverviewItem extends LitElement {
  // ...
  connectedCallback() {
    super.connectedCallback();
    this.dispatchEvent(new CustomEvent('connected'));
  }
}

Where you fetch the data using the in-build fetch API, and set it as state on the parent component, to pass it down to the child.

Upvotes: 1

Related Questions