Inter-Feature Communication with suggestFeatureInfo

August 22, 2026 · View on GitHub

When multiple custom element features need to coordinate — one feature producing configuration that another feature consumes — suggestFeatureInfo and getFeatureInfoSuggestions provide a formal, version-safe mechanism.

The Problem

Feature A (e.g., form association) knows things that Feature B (e.g., reactive binding) needs:

  • Which properties should be observed and forwarded
  • Which attributes should be parsed
  • How property changes should propagate

Without a formal mechanism, features would need to hard-code references to each other's classes (brittle across versions) or rely on ad-hoc conventions.

The Solution

Each feature publishes a stable Symbol identifier. Features communicate by posting and reading "suggestions" keyed by that Symbol, scoped per custom element class.

API

import { suggestFeatureInfo, getFeatureInfoSuggestions } from 'assign-gingerly/assignFeatures.js';

// Feature A posts a suggestion to Feature B
suggestFeatureInfo(FeatureAClass, TARGET_FEATURE_SYMBOL, {
    customData: { /* config fragments to merge */ },
    withAttrs: { /* attribute patterns to merge */ }
}, targetClass);

// Feature B reads suggestions
const suggestions = getFeatureInfoSuggestions(MY_FEATURE_SYMBOL, targetClass);

Timing

Features are processed in declaration order within assignFeatures. Each feature's static onAssigned is awaited sequentially, so suggestions posted by earlier features are guaranteed to be available when later features read them:

await customElements.assignFeatures(MyElement, {
    faceUp: { spawn: FaceUp },           // onAssigned runs first, posts suggestions
    roundabout: { spawn: RoundaboutFeature }  // onAssigned runs second, reads suggestions
});

Real-World Example: face-up → roundabout

The face-up feature (form association) suggests configuration to roundabout (reactive binding). This tells roundabout how to forward form-related properties and which attributes to observe.

Source: integrateWithRoundabout.js

import { suggestFeatureInfo } from 'assign-gingerly/assignFeatures.js';

export async function integrateWithRoundabout(FaceUpClass, key, ctr) {
    const { id } = await import('roundabout-lib/roundaboutFeature.js');

    suggestFeatureInfo(FaceUpClass, id, {
        customData: {
            // Use += to append to roundabout's merges array
            '?.raConfig?.merges +=': [
                {
                    ifKeyIn: ['value'],
                    assign: { [`?.${key}?.value`]: '?.value' }
                },
                {
                    ifKeyIn: ['disabled'],
                    assign: { [`?.${key}?.disabled`]: '?.disabled' }
                },
                {
                    ifKeyIn: ['required'],
                    assign: { [`?.${key}?.required`]: '?.required' }
                },
                {
                    ifKeyIn: ['validationMessage'],
                    assign: { [`?.${key}?.validationMessage`]: '?.validationMessage' }
                },
            ]
        },
        withAttrs: {
            value: 'value',
            _value: { sourceOfTruth: true, valIfNull: null },
            disabled: 'disabled',
            _disabled: { sourceOfTruth: true, instanceOf: Boolean, valIfNull: false },
            required: 'required',
            _required: { sourceOfTruth: true, instanceOf: Boolean, valIfNull: false },
            validationMessage: 'validation-message',
            _validationMessage: { sourceOfTruth: true, valIfNull: '' },
        }
    }, ctr);
}

The sourceOfTruth: true entries mark attributes whose DOM value should mirror the host property. See docs/withAttrs.md for details.

What's happening here

  1. customData with += array append — The suggestion uses assignGingerly's += command syntax in the key ('?.raConfig?.merges +='). When roundabout merges this into its config via assignGingerly, the merge rules are appended to the existing array rather than replacing it. This is the array concatenation feature in action.

  2. withAttrs patterns — face-up suggests additional attributes that roundabout should parse (value, disabled, required, validationMessage). These get merged into roundabout's withAttrs configuration.

  3. Dynamic key reference — The feature key (e.g., 'faceUp') is used in the merge paths (?.${key}?.value), so roundabout knows which feature property to forward values to.

  4. Version-safe — face-up imports roundabout's Symbol (id) dynamically. If roundabout isn't being used, the import fails gracefully. Different versions of roundabout share the same Symbol (via Symbol.for).

How roundabout consumes suggestions

// In roundabout's onAssigned:
import { getFeatureInfoSuggestions } from 'assign-gingerly/assignFeatures.js';
import { id } from './roundaboutFeature.js'; // Symbol.for('bahrus/roundabout')

class RoundaboutFeature {
    static onAssigned(ctr, featureConfig, key) {
        const suggestions = getFeatureInfoSuggestions(id, ctr);
        
        for (const suggestion of suggestions) {
            if (suggestion.customData) {
                assignGingerly(featureConfig, suggestion.customData);
            }
            if (suggestion.withAttrs) {
                featureConfig.withAttrs = {
                    ...featureConfig.withAttrs,
                    ...suggestion.withAttrs
                };
            }
        }
    }
}

Key Design Decisions

DecisionRationale
Symbol-based targetingStable across versions and mock substitutions
Scoped per target classPrevents leaking between different custom elements
Sequential onAssignedGuarantees suggestions are available when consumed
assignGingerly for mergingLeverages += array append, nested paths, etc.
Dynamic import of target IDGraceful failure if target feature isn't used

Guidelines

  1. Declare suggesting features before consuming features in assignFeatures.
  2. Export your feature's Symbol from a stable module (e.g., symbols.js).
  3. Use Symbol.for('org/package-name') for the identifier — survives across package versions.
  4. Use assignGingerly syntax in customData keys (like +=) for additive merging.
  5. Always pass ctr as the targetClass parameter to scope suggestions correctly.