Closed thrbnhrtmnn closed 4 months ago
The radio-group component was adjusted to use the slot instead of the native input element as radio buttons. The changes can be found in here https://github.com/deven-org/boiler/pull/1079/commits/6a5c68ab0b90ff7bed906c102666030ccd9522cf.
The current changes depend on the click event (The wrapper of the slot in radio-group component listens to the event bubbled by the slotted element[radio-button]). Therefore, it is limited in terms of functionality i.e. changes made from outside of click event does not work, a point in case is while changing the checked status from the Chrome DevTool console.
The potential solution was explored while resolving this PR https://github.com/deven-org/boiler/pull/1079 where the steps taken and major key observations were as follows:
1. Slotting Implementation
The blr-radio elements were slotted into the blr-radio-group like this:
`
`
2. Attribute Change Issues Observed that changing the checked attribute from the developer console was not detected by the Lit component, breaking sync between the radio buttons.
3. Ineffective Methods Methods like slot-change and MutationObserver did not observe property changes, proving ineffective.
4. Optimal Synchronization Achieved optimal sync by looping through slotted elements (blr-radio) inside the blr-radio-group and customizing their getter and setter methods. This ensured that any change of property/attribute, regardless of source, was observed and the component could maintain a single checked state. The also ease up the class modification of the slotted element based on the attribute change eg: checked/unchecked.
5. Uncontrolled Component Approach
Using the parent component (<div class="blr-radio-group ${classes}"><slot></slot></div>
) as an uncontrolled component, similar to React, proved effective. As shown in the following example form component where state was used to set and hold the value through event handlers rather than getting the value directly from the element.
import { LitElement, html } from 'lit'; import { TAG_NAME } from './renderFunction'; import { LitElementCustom } from '../../utils/lit-element-custom'; import { state } from 'lit/decorators.js';
export class BlrForm extends LitElementCustom { @state() formValue = { selectedOption: '', checkboxChecked: false, description: '', selectedValue: '' };
protected handleSubmit = () => { console.log('submitted with ', this.formValue); };
handleInputChange(e) { const inputName = e.target.localName; if (inputName === 'blr-radio') { this.formValue.selectedOption = e.detail.selectedValue; } else if (inputName === 'blr-checkbox') { this.formValue.checkboxChecked = e.detail.checkedState; } else if (inputName === 'blr-input-field-text') { this.formValue.description = e.detail.originalEvent.target.value; } else if (inputName === 'blr-select') { this.formValue.selectedValue = e.detail.selectedValue; } } protected render() { return html`
`; } }if (!customElements.get(TAG_NAME)) { customElements.define(TAG_NAME, BlrForm); }
export type BlrFormType = Omit<BlrForm, keyof LitElement>;
Context and Problem Statement
In this research task we should evaluate various methods of mutating the state of shared element instances via their public API and how these mutations are signalled to instance owners. This is especially relevant for scenarios where we use slots for component composition but it also has a general impact on how we design stateful components. Many aspects of this topic are similar to the design philosophy of controlled vs. uncontrolled components in react. The research should focus on evaluating different approaches to state manipulation and data flow in behavioural components, document the pros and cons, report the findings and initiate a team decision.
We can evaluate these different methods in the context of our
radio-group
component.The following are some very rough ideas that come to mind and could be expanded on:
Direct State Manipulation vs. Signal Only vs. Mixed Mode
Direct State Manipulation
This approach assumes that the element instances which are slotted into our components by ui-library users can be manipulated by our components directly via their public API and only notify consuming parties after we made changes to an elements state. This is the default behaviour for many HTML input elements.
In this case we need to ensure that both: changes done through an element's public API and changes done to an element's state by an end user (eg: by clicking on it) can be traced in userland and vice versa so that no synchronisation issues occur.
Example of a synchronisation issue introduced into component land through userland manipulation:
To tackle this issue we need the ability to track changes made through an element's public API (similar to our ui events for end user interactions). This also needs to work the other way around.
Example of a synchronisation issue introduced into userland by the controlling component:
When settling for this approach we pose the risk of making changes to a component's state during async operations which could lead to race conditions.
Signal Only
This approach assumes that the state of slotted element instances as well as any other state of a component is never to be modified directly. Instead, when a controlling component wants to make a change, it signals it's intent to listeners further up the hierarchy. The owning party is then responsible to react to that intent and either mutate the state of any related components or ignore it - making our components effectively stateless.
Mixed Mode
This method would modify the process of change signalling proposed in Direct State Manipulation to such a degree that controlling components such as
radio-group
wouldn't just make changes to the state of shared element instances without consent.Example:
A
control-mode
option on each controlling component could dictate the default behaviour of state change requests (assume-control
,forfeit-control
, ...?).Where
assume-control
means that userland code would get notified before the controlling component makes changes to shared element state and can choose toapply
ordiscard
those changes.forfeit-control
means userland code would get notified of changes after the controlling component already made them. Changes are effectivelyauto-applied
.With this approach we could have the best of both worlds and have self-sufficient components where reactive data flow is less of a concern (with
forfeit-control
mode) and circumvent the risk of race conditions in scenarios where reactive data flow and state management is a more of a concern (withassume-control
mode).Acceptance Criteria
radio-group
> done via #507select
> we decided to implement a workaround for the current select, but we will revisit this in the future once we have a fully custom selecttab-bar
Code of Conduct