Custom element bind() lifecycle`


#1

A have a complex form with many custom elements. When the main form is loaded it queries the database and when the data is returned sets the property public data: IDataClass = data-from-db.

The main form then binds data to the various custom elements via
my-custom-element-data.bind='data'

where each custom element has a @bindable public property called my-custom-element-data

So far so good.

The main form is bound to the data, as are all of the custom elements.

However, the problem is that I need to access the data in the custom elements as soon as the binding has occurred.

The logical place to do this in the custom element’s bind() method. However, it seems that the custom elements bind() method is called before the main form’s bind() method, so the data in the custom element is always undefined. I’ve also tried attached().

The life-cycle seems to be something like:

  1. customElement.bind() - the bindings for data are undefined
  2. main form.bind() - the bindings for data are set correctly
  3. custom element objects are now bound to data.

I would have thought it more logical that the bindings in the custom element would occur after the binding in their container.

How do I get at data in step 2) from the custom element?

I would like to avoid EventAggregator if that’s possible.


#2

From what you described, it seems you want to delay bind() of form elements to a point where data has been fetched.

There are few ways to resolve this

  • you can either put the form in an if, only show when the form model value has returned from database query, this probably requires moving form model out of the form
  • you can put an if on form elements, only show when the form model value has returned from database query. This similar yo the above but requires an if for every field
  • you can change form element into more reactive/resilient to undefined value, so the initial bind wont affect it, and move the main logic into change hanfler of those form elements
  • if the form is a component used by <router-view/>, then you can return a promise in activate lifecycle, where you delay the rendering by a call to database to query necessary data

#3

Oh man - you have no idea how your comments have helped!

If you’re interested please take a look at the original answer I wrote to your suggestions. As soon as I had finished writing it I realized that the easiest thing to do was to create a property (uiInput) on the data, and that simplified the code a million-fold.

So the html I ended up with in the custom element went from

 <span if.bind="(row.sector == 'Borrowings (+)' || row.sector == 'Repayments (-)') && selectedPeriod.period == 'Months'">
<span class="border p-1" contenteditable textcontent.bind="row.months[(selectedYear-1)*12+$index] | number" input.delegate="callback()"></span>
            </span>

<span if.bind="bindingNames().indexOf(row.sector) > -1">
              ${item | numberFormat: "0,0 | null" }
            </span>

to

<span if.bind="row.uiInput && selectedPeriod.period == 'Months'">
    <span class="border p-1" contenteditable textcontent.bind="row.months[(selectedYear-1)*12+$index] | number" input.delegate="callback()"></span>
</span>

<span else>
   ${item | numberFormat: "0,0 | null" }
 </span>

Again a million thanks for your help.
All the best
Jeremy

Original answer to your comments

Your comments made me look at the code again.

In the main form where costingService.calculateAllValus() can take a long time

  protected calculateValues() {
    if (this.costing) {
      const { productionList, shellingCapacityMt, productionCosts } = this.costingsService.calculateAllValues(this.costing, this.selectedYear);

      this.productionList = productionList;
      this.shellingCapacityMt = shellingCapacityMt;
      this.productionCostsList = productionCosts;
    }
  }

  protected selectedYearChanged(year: number) {
    this.calculateValues();
  }

  protected selectedPeriodChanged() {
    this.calculateValues();
  }

  private async loadCostingByCompanyId(companyId: string) {
    this.costing = await this.costingsService.loadByCompanyId(companyId);

    this.costingsService.fillDefaultCosts(this.costing, this.dictionary);
    this.calculateValues();
  }

Main form html where the custom elements are all bound to this.costing and each utilize the bound values of selectedYear and selectedPeriod.

<div class="card-body">
      <fieldset>
        <legend>Select production year</legend>
        <select-production-year title="Purchases and Production" selected-year.two-way="selectedYear"
                                selected-period.two-way="selectedPeriod" periods-id="sp1" periods.bind="periods"
                                callback.call="calculateValues()"></select-production-year>
      </fieldset>

      <purchases id="purchases" costing.bind="costing" selected-year.bind="selectedYear" callback.call="calculateValues()">
      </purchases>

      <production id="production" production-list.bind="productionList" costing.bind="costing" selected-year.bind="selectedYear"
                  selected-period.bind="selectedPeriod" callback.call="calculateValues()"></production>

      <production-costs id="profit-and-loss" root.bind="productionCostsList" , selected-year.two-way="selectedYear"
                        selected-period.two-way="selectedPeriod" periods.bind="periods" costing.bind="costing"
                        callback.call="calculateValues()"></production-costs>

      <cashflow id="cashflow" root.bind="productionCostsList" , selected-year.two-way="selectedYear" 
                selected-period.two-way="selectedPeriod" periods.bind="periods" costing.bind="costing" callback.call="calculateValues()"></cashflow>

      <kpis id="kpis" root.bind="productionCostsList" , selected-year.two-way="selectedYear" selected-period.two-way="selectedPeriod" 
            periods.bind="periods" costing.bind="costing" callback.call="calculateValues()"></kpis>
...

and finally one of the custom elements cashflow-custom-element.ts

@autoinject
export class CashflowCustomElement {
  @bindable public selectedYear: number;
  @bindable public selectedPeriod: IPeriod;
  @bindable public root: IProductionCostsResults;
  @bindable public costing: ICosting;
  @bindable public callback: () => void;
  @bindable public periods: IPeriod[];

  constructor(private readonly productionCostsService: ProductionCostsService) { }

  public bindingNames() {
    return [
      "B/F",
      "Interest (-)",
      "Capital purchases machines (-)",
      "Civil engineering (-)",
      "P&L (+)",
      "Balance"
    ];
  }

  public bindingNamesFromCosting = () => this.costing.variables.productionCostsResults.cashflow.map(cf => cf.sector);

  public getValues(row: IExpenseItem) {
    if (row && this.selectedPeriod) {
      const startDate = this.costing ? this.costing.variables.dates.civilEngineeringEndDate : moment().format("YYYY-MM-DD");

      const values = this.productionCostsService.getRowValues(row, this.selectedYear, { period: this.selectedPeriod.period }, startDate);

      return values;
    }
  }
}

cashflow-custom-element

<template>

  <require from="./filter-value-converter"></require>
  <require from="./select-production-year-element"></require>
  <require from="./quick-links-element"></require>

  <fieldset>
    <quick-links legend="Cashflow"></quick-links>

    <select-production-year selected-year.two-way="selectedYear" selected-period.two-way="selectedPeriod" periods-id="spCashflow"
                            periods.bind="periods" show-periods="true" callback.call="callback()"></select-production-year>

    <table class="table table-sm table-borderless ${selectedPeriod.period == 'Quarters' ? 'quarter-highlight last-col-bold' : ''}">
      <thead>
        <tr class="small">
          <th style="width:20%"> </th>
          <th repeat.for="date of getValues(root.quantities[0]).dates" class="text-right">
            ${date | dateFormat: "MMM YY"}
          </th>
        </tr>
      </thead>

      <tbody>
        <tr repeat.for="row of root.cashflow" class="small ${row.name=='Header' ? 'font-weight-bold text-primary' : ''} ${row.sector=='Balance' ? 'font-weight-bold' : ''}">
          <td>${row.sector}</td>

          <td repeat.for="item of getValues(row).values" class="text-right" class.bind="item | 'posNegColor'">

            <span if.bind="(row.sector == 'Borrowings (+)' || row.sector == 'Repayments (-)') && selectedPeriod.period == 'Months'">
              <span class="border p-1" contenteditable textcontent.bind="row.months[(selectedYear-1)*12+$index] | number" input.delegate="callback()"></span>
            </span>

            <span if.bind="bindingNames().indexOf(row.sector) > -1">
              ${item | numberFormat: "0,0 | null" }
            </span>
          </td>
        </tr>
      </tbody>
    </table>
</template>

+++++++++++++++++++++++++++++++++

So the problem I am trying to solve is that the bindNames() function returns hard wired names for the binding rules used in the table.

What it should be doing is using the bindingNamesFromCosting () function, but this doesn’t work because costing is not yet bound to the custom element at the time that its called…


#4

Glad to see that, I believe you will simplify everything even more, as you go along :smile:


#5

I just wanted to report back on what in the end was a really obvious solution.

The original problem was that the property I needed to access in the custom element was being bound after the data in the main page.

custom-element-ts

{
  @bindable public costing: ICosting
  public myProp: string;

  public myMethod(){
    if(this.costing){
       this.myProp = this.costing.variables.importantData;
    }
  }
}

The solution was simply to watch for the change on costing and instead of trying to call myMethod() in attached or bind, binding directly to the myProp property.

public costingChanged(costing: ICosting){
     this.myProp = this.costing.variables.importantData;
}

You were 100% correct when you said that I would simplify things as I go along :smile:


#6

The more you explore Aurelia, the more code you delete :smile: