0

I am attempting to set up a set of checkboxes, with a condition if the parent checkbox is checked, then all children checkboxes will also be checked. Conversely, if ALL child checkboxes are checked, the parent checkbox will become checked.

I have the first part of this logic and it's working correctly. However, while working on the 2nd part of this, I am able to get the value to update in the parent component AND the component re-renders with the new value (according to the renderedCallback()). However, the parent checkbox remains unchanged.

Is anyone able to help me understand why this is?

Reproduction:

  1. Check the checkbox next to "Unique" and see that the child component is checked.
  2. Uncheck "Unique" (otherwise behavior gets super fun due to the problem described above)
  3. Check the checkbox under "Unique", see that "Unique is NOT checked but in the console you can see that it is rendered with the value of true.

Code: webcomponents.dev

3
  • Without your code, it's going to be pretty hard to help troubleshoot - if you could share the relevant HTML and JS that would be helpful. Mar 16, 2022 at 17:26
  • That's what the webcomponents.dev link is! Mar 16, 2022 at 17:59
  • D'oh! My bad. Used to seeing the codeblocks in the question itself. Mar 16, 2022 at 18:04

2 Answers 2

2

I was able to resolve this by hoisting the state logic to the grandparent and removing the getters/setters from the child. I don't know why the parent checkbox component won't re-render when the value is updated from within itself, but this was my workaround.

app.html

<template>
    <div class="app slds-p-around_x-large">
        <h1 class="slds-text-heading_large">{title}</h1>
        <template for:each={areas} for:item='area' for:index='index'>
            <lightning-layout-item key={area.Id} flexibility="auto" padding="around-small" onchange={handleAreaChange}>
                <c-area index={index} id={area.Id} name={area.Name} questions={area.CSC_Questions__r} value={area.value}
                    onchange={handleAreaChange}></c-area>
            </lightning-layout-item>
        </template>
    </div>
</template>

app.js

import { LightningElement, track } from "lwc";

export default class App extends LightningElement {
  title = "Welcome to Lightning Web Components!";

  showFeatures = true;

  /**
   * Getter for the features property
   */
  get features() {
    return [
      {
        label: "Learn in the browser.",
        icon: "utility:edit",
      },
      {
        label: "View changes to code instantly with Live Compilation.",
        icon: "utility:refresh",
      },
      {
        label: "Style your components with SLDS.",
        icon: "utility:brush",
      },
    ];
  }

  @track areas = [
    {
        Name: "Unique",
        Order__c: 1,
        CSC_Version__c: "a058A000005LedXQAS",
        Id: "a038A000006PxPAQA0",
        Org__c: "a048A000003mW3AQAU",
        value: false,
        CSC_Questions__r: [
            {
                CSC_Area__c: "a038A000006PxPAQA0",
                Id: "a028A000005Bsj8QAC",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Searching with the requestor&#39;s context reveals that this article is not a duplicate (Duplicate: No other article with the same content/context exists). </span></p><p><br></p><p><span style=\"color: rgb(0, 0, 0);\"><span class=\"ql-cursor\"></span>For more background on how duplicates are defined: The Search for Common Issues &amp; Technique 4.2: Flag It or Fix It &gt; Duplicates</span></p>",
                Order__c: 1
            }
        ],
        CSC_Version__r: {
            Name: "01",
            Id: "a058A000005LedXQAS"
        }
    },
    {
        Name: "Foundation",
        Order__c: 2,
        CSC_Version__c: "a058A000005LedXQAS",
        Id: "a038A000006PxPZQA0",
        Org__c: "a048A000003mW3AQAU",
        value: false,
        CSC_Questions__r: [
            {
                CSC_Area__c: "a038A000006PxPZQA0",
                Id: "a028A000005BsjEQAS",
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Was the template applied?</span></p>",
                value: false,
                Order__c: 1
            },
            {
                CSC_Area__c: "a038A000006PxPZQA0",
                Id: "a028A000005BsjIQAS",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Have all the relevant fields in the template been filled out?</span></p>",
                Order__c: 2
            },
            {
                CSC_Area__c: "a038A000006PxPZQA0",
                Id: "a028A000005BsjNQAS",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Is the Content Type set correctly?</span></p>",
                Order__c: 3
            }
        ],
        CSC_Version__r: {
            Name: "01",
            Id: "a058A000005LedXQAS"
        }
    },
    {
        Name: "Factors",
        Order__c: 3,
        CSC_Version__c: "a058A000005LedXQAS",
        Id: "a038A000006PxPGQA0",
        Org__c: "a048A000003mW3AQAU",
        value: false,
        CSC_Questions__r: [
            {
                CSC_Area__c: "a038A000006PxPGQA0",
                Id: "a028A000005BsjSQAS",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Are the applicable environmental factors captured?</span></p>",
                Order__c: 1
            },
            {
                CSC_Area__c: "a038A000006PxPGQA0",
                Id: "a028A000005BsjXQAS",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Is there an applicable product and version captured?</span></p>",
                Order__c: 2
            }
        ],
        CSC_Version__r: {
            Name: "01",
            Id: "a058A000005LedXQAS"
        }
    },
    {
        Name: "Context",
        Order__c: 4,
        CSC_Version__c: "a058A000005LedXQAS",
        Id: "a038A000006PxPeQAK",
        Org__c: "a048A000003mW3AQAU",
        value: false,
        CSC_Questions__r: [
            {
                CSC_Area__c: "a038A000006PxPeQAK",
                Id: "a028A000005BsjcQAC",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Does the title appropriately reflect the requestor&#39;s context?</span></p>",
                Order__c: 1
            },
            {
                CSC_Area__c: "a038A000006PxPeQAK",
                Id: "a028A000005BsjhQAC",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Does the description expand on the original requestor&#39;s context?</span></p>",
                Order__c: 2
            }
        ],
        CSC_Version__r: {
            Name: "01",
            Id: "a058A000005LedXQAS"
        }
    },
    {
        Name: "Consumable",
        Order__c: 5,
        CSC_Version__c: "a058A000005LedXQAS",
        Id: "a038A000006PxOSQA0",
        Org__c: "a048A000003mW3AQAU",
        value: false,
        CSC_Questions__r: [
            {
                CSC_Area__c: "a038A000006PxOSQA0",
                Id: "a028A000005BsjDQAS",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Has all customer data/info been sanitized from the content that will be visible publicly?</span></p>",
                Order__c: 1
            }
        ],
        CSC_Version__r: {
            Name: "01",
            Id: "a058A000005LedXQAS"
        }
    },
    {
        Name: "Actionable",
        Order__c: 6,
        CSC_Version__c: "a058A000005LedXQAS",
        Id: "a038A000006PxPjQAK",
        Org__c: "a048A000003mW3AQAU",
        value: false,
        CSC_Questions__r: [
            {
                CSC_Area__c: "a038A000006PxPjQAK",
                Id: "a028A000005BsjmQAC",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">Do we tell the reader what needs to be done, then provide instruction on how to do it?</span></p>",
                Order__c: 1
            },
            {
                CSC_Area__c: "a038A000006PxPjQAK",
                Id: "a028A000005BsjYQAS",
                value: false,
                Question__c: "<p><span style=\"color: rgb(0, 0, 0);\">The article (attempts to be) written in complete thoughts rather than complete sentences.</span></p>",
                Order__c: 2
            }
        ],
        CSC_Version__r: {
            Name: "01",
            Id: "a058A000005LedXQAS"
        }
    }
    ];

  
  
  
  handleAreaChange({detail: {index, value, childIndex, childValue}}) {
      console.log('Area change fn called!')
      if(!index && index !== 0) return
    console.log(index, value, childIndex, childValue)
    let temp = this.areas;
      if(typeof value !== 'undefined'){ //updating area
        console.log('Updating Area!')
        temp[index] = {
            ...temp[index],
            value,
            CSC_Questions__r: temp[index].CSC_Questions__r.map((q) => {
               return {
                    ...q,
                    value,
                }
            })
        }
        this.areas = temp;

      } else { //updating question
            console.log('updating question')
          const area = temp[index];
            console.log('Area: ', area)
            console.log('ChildIndex: ', childIndex)
          area.CSC_Questions__r[childIndex].value = childValue;
          console.log('Questions:', area.CSC_Questions__r)
          if(!childValue){
              area.value = false;
          }
          console.log()
          if(area.CSC_Questions__r.every((q) => q.value)){
              console.log('All true!')
              area.value = true;
          }
          temp[index] = area;
          this.areas = temp;
      }
          this.checkValue()
  }

  checkValue(){
      console.log('VALUES:', this.areas)
  }
}

area.html

<template>
  <lightning-input onchange={handleChange} type="checkbox" label={name} id={id} name={name} checked={value}>
  </lightning-input>
  <div class="questionsContainer" style="margin-left: 1.5em">
    <template for:each={questions} for:item='question' for:index="qIndex">
      <c-question value={question.value} key={question.Id} id={question.Id} ontoggle={handleToggle}
        question={question.Question__c} parent-index={index} index={qIndex}>
      </c-question>
    </template>
  </div>
</template>

area.js

import { LightningElement, api, track } from "lwc";


export default class Area extends LightningElement {
  @api name;
  @api questions;
  @api value;
  @api index;
  @api id;
  @track renderInputs = true;
  
    renderedCallback() {
    console.log('Area ' + this.name + ' is (re)rendered with ' + this.value + '!')
  }

  
  // @api
  // get questions() {
  //   return this._questions
  // }
  // set questions(val) {
  //   console.log('Questions setter hit!', val)
  //   if(val.every((q) => q.value)){
  //     this.dispatchEvent(new CustomEvent('change', {
  //       detail: {
  //         index: this.index,
  //         value: val
  //       }
  //     }));  
  //   }
  //   this._questions = val;
  // }

  
 //  @api
 //  get value() {
 //    return this._value
 //  }
 //  set value(val){
 //    console.log('Setter triggered for change to ', val, ' for ' + this.name)
 //    const temp = this._questions;
 //    this._questions = temp.map((q) => {
 //      return {
 //        ...q,
 //        value: val,
 //      }
 //    })
 //    this._value = val;
 //    this.checkValue();
 //  }

  checkValue(){
    console.log('Value check for ' + this.name +':' + this.value);
  }

  handleChange({target: {checked}}) {
    console.log('Handling change!')
    console.log('Changing to ', checked)
    const x = this.index;
    this.dispatchEvent(new CustomEvent('change', {
     detail: {
          index: x,
          value: checked
        }
    }));
  }

  handleToggle({detail: { index: qIndex, value: qValue}}) {
    console.log('HandleToggle!', this.index, qIndex, qValue)
    this.dispatchEvent(new CustomEvent('change', {
        detail: {
          index: this.index,
          childIndex: qIndex,
          childValue: qValue
        }
      }));  
    
  }
}

question.html

<template>
  <div class="slds-grid">
    <lightning-input label={id} data-parent={parentIndex} class="slds-col slds-size_1-of-20" variant="label-hidden"
      data-id={id} onchange={handleToggle} type="checkbox" checked={value}>
    </lightning-input>
    <label class="slds-col slds-size_11-of-12 slds-wrap" for={id}>
            <lightning-formatted-rich-text value={question}></lightning-formatted-rich-text>
        </label>
  </div>

</template>

question.js

import { LightningElement, api } from "lwc";

export default class Question extends LightningElement {
  @api id;
  @api question;
  @api parentIndex;
  @api value
  @api index;
  
  renderedCallback() {
    console.log('Questions are rerendered!')
  }

  handleToggle() {
    this.dispatchEvent(new CustomEvent('toggle', {
      detail: {
        index: this.index,
        value: !this.value,
      }
    }));
  }
}
1
  • Welcome to the Salesforce side of SE! Thanks for coming back and adding your solution as answer - that helps future searchers.
    – Moonpie
    Mar 16, 2022 at 18:55
1

The easiest way, in my opinion, would be to set up two flows using the Flow Builder for this (through Setup -> Flows set New Flow as 'Record Triggered Flow).

You would have one flow based on creation/edit to the parent object with no conditions. Then, outcome 1 would have the condition of 'when the checkbox is true' with the result of 'update all child checkboxes to true'. Outcome 2 would have the condition of 'when the checkbox is false' with the result of 'update all child checkboxes to false'.

Your second flow would be basically the same logic, except basing the flow on the creation/edit of the child object.

An example below:

  • Start Flow 1 on Parent Object for created/updated with no entry condtions.

enter image description here

  • Click the + to add a decision: add the entry condition to when the parent checkbox is set to 'true'

enter image description here

  • Add an Outcome to that decision to 'Update Record': I have a field on my child records that links back to the parent, so I was able to establish what record to access by saying 'in every record of the specified object type (my child object) where this field references the id of this other object (my parent object)...'. Then, I set the flow to update the related checkbox on those records to true.

enter image description here

  • Next, you would add another decision directly under the one we just added, with the entry condition of checkbox being 'false' on the parent. You'd grab the child records the same way, and then update their checkboxes to false.
  • To set the flow from child to parent, you can essentially create a duplicate of the same flow, but just swap around the objects. To access the parent, you can have it grab the record listed in the specified field (in my example, I'd use the 'Opportunity' field from my child record to choose what to update).

To access multiple children and check if all checkboxes are true before updating, you might be able to do something like this:

  • Get records of child object where a certain field references the parent object. Select store all records and choose the specific field (checkbox field)

enter image description here enter image description here

  • Then, add a loop through each record.
  • Then, add a decision option, where the true condition is the checkbox on the current record in the loop being 'true'. When that is true, the flow will loop back and through the next record in the list. You can also set a 'Stop Record' element when the decision renders 'false'. This should stop the flow if any of the items you're looping through have a 'false' checkbox.
  • Then 'after last item in loop', you should be able to set an update record component to change the parent checkbox to 'true'. The flow should never reach that unless all children have a true checkbox.

An overview of my example looks something like this:

enter image description here

I hope this was helpful! If you find this useful, please mark the answer as accepted so it can help others too. Happy SalesForcing!

2
  • I appreciate the work you put in to this! However, this relies on the issue being corrected server side and not client side which would result in an awful user experience. I'm looking for a way to have the LWC work via JS methods. I know I can manually manipulate the DOM onchange, but I should be able to update the value and have it rerendered like a stateful component in React. Mar 16, 2022 at 18:03
  • @technicallynick - Gotcha! Thanks for explaining further, and sorry I wasn't able to help resolve this issue! Mar 16, 2022 at 18:11

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .