I'm creating a set of LWR components that can be added to a page declaratively in Experience Builder. Since these components aren't hard-coded to the top-level component, based on a recommendation from this thread, I'm using a shared import approach to communicate and share data and logic between the components. When components are rendered and when changes occur, the shared object is updated. However, DOM elements that I would like to be re-rendered based on changes to the shared object don't reflect those changes.
This example illustrates what I'm doing and the issue I'm having:
appModel.js
class Model {
inputs = {};
}
export const model = new Model(); // This is the shared object variable
app.html
<template>
<x-input label="First Name" required></x-input>
<x-input label="Last Name" required></x-input>
<button disabled={isSubmitDisabled} onclick={handleSubmit}>Submit</button>
</template>
app.js
import { LightningElement } from 'lwc';
import { model } from 'x/appModel'; // The shared variable is imported here
export default class App extends LightningElement {
// This evaluates to false initially and never re-evaluates after model.inputs changes
get isSubmitDisabled() {
return Object.values(model.inputs).some((input) => input.isValid === false);
}
handleSubmit = () => {
console.log(JSON.parse(JSON.stringify(model.inputs)));
// Here I can see that the inputs object was updated as expected:
// {
// "First Name": { value: "", isValid: false },
// "Last Name": { value: "", isValid: false }
// }
}
}
input.html
<template>
<label for={label}>
{label}
<span lwc:if={required}>*</span>
</label>
<input required={required} onchange={updateModel} />
</template>
input.js
import { LightningElement, api } from 'lwc';
import { model } from 'x/appModel'; // The shared variable is imported here too
export default class Input extends LightningElement {
@api label;
@api required = false;
hasBeenRenderedOnce = false;
renderedCallback() {
if (!this.hasBeenRenderedOnce) this.updateModel();
this.hasBeenRenderedOnce = true;
}
updateModel = () => {
const input = this.template.querySelector('input');
model.inputs[this.label] = {
value: input.value,
isValid: input.checkValidity(),
}; // Shared variable is updated on render and when input values change
};
}
Although a console log shows that the inputs object has 2 values where isValid === false, get isSubmitDisabled() does not re-evaluate after the custom <x-input> components execute their updateModel logic. The <button> element remains enabled.
Even if I track the inputs object like this, I still have the same problem -- the Submit button remains enabled.
app.js
import { LightningElement, track } from 'lwc';
import { model } from 'x/appModel';
export default class App extends LightningElement {
@track inputs = model.inputs; // Tracking the object here
// This still evaluates to false initially and never re-evaluates
get isSubmitDisabled() {
return Object.values(this.inputs).some((input) => input.isValid === false);
}
...
}
I like this shared import approach overall. It's just the re-render that's lacking. How can I work around this problem?
@apiattribute which will mean changes should be trackedquerySelectorAllfor all its descendents.