Skip to content

Conditionally injecting a symbol/glyph into a node (pseudo-elements aren't available inline) #358

Description

@CarlosNZ

Problem

A common ask is to conditionally inject a small symbol or glyph into a node's display — e.g. append a "⚠️" after a value when some condition holds (validation error, a flagged field, a "modified since save" marker, etc.). The instinctive approach is a ::before/::after pseudo-element with a content string, toggled from a style function.

That doesn't work directly: theme/style functions return React.CSSProperties (src/types.ts:677) and the result is applied straight to style={...} (ValueNodeWrapper.tsx:548, CollectionNode.tsx:393). Inline styles cannot express pseudo-elements, so there's no inline-only way to add content: "⚠️".

This issue collects the viable approaches so we can pick one (or document a recommended recipe, and/or ship a preset/helper). It generalises the narrower validationStyles ⚠️-indicator question raised in #357.

For reference, the stable class names on value text are .jer-value-string, .jer-value-number, .jer-value-boolean, .jer-value-null (set in ValueNodes.tsx).

Potential implementations

1. Custom node component wrapping originalNode (recommended)

Custom components receive the fully-rendered built-in node as originalNode (and the key as originalNodeKey) when the definition opts in with passOriginalNode: true (src/types.ts:544, src/types.ts:585). So the component is a tiny wrapper that re-emits the original rendering and adds the glyph:

const FlaggedNode = ({ originalNode, nodeData }: CustomComponentProps) => (
  <span>
    {originalNode}
    {isFlagged(nodeData) && <span> ⚠️</span>}
  </span>
)

const flagDefinition: CustomNodeDefinition = {
  condition: (nd) => typeof nd.value !== 'object', // which nodes are eligible
  component: FlaggedNode,
  passOriginalNode: true,
}

Because originalNode is the standard rendering, the wrapper inherits its theme styles, value-type formatting, and edit affordances — it adds nothing but the glyph, and works for every value type (and keys, via originalNodeKey) rather than being string-specific. Real DOM means tooltips / click handlers / arbitrary layout are all available.

Notes:

  • passOriginalNode is opt-in (default false) because it makes the editor build the original node's JSX up front — wasted work for nodes that fully replace rendering, but exactly what a wrap-and-augment node wants (src/types.ts:582).
  • Most flexible, and the lowest-code option for a real text glyph.

2. CSS-variable bridge — real ::after, driven from a style function

React inline styles pass CSS custom properties through (and current @types/react types them), so a style function can toggle a variable that a real stylesheet rule consumes in a pseudo-element:

// style function (theme):
string: (nd) => (isFlagged(nd) ? { '--jer-flag': '"⚠️"' } : null)
/* one rule the consumer ships once: */
.jer-value-string::after { content: var(--jer-flag, ''); margin-left: 0.3em; }

The condition lives in the (inline) style function; the pseudo-element lives in static CSS. Keeps everything in the theme channel with no customNodeDefinitions.

Notes:

  • Requires the consumer to ship that one stylesheet rule (could be packaged as a preset CSS file).
  • CSS custom properties inherit — reset on descendants if that bleeds.

3. data-URI SVG background-image — self-contained, no stylesheet

Stays entirely inside the inline style object, so it needs no consumer CSS:

string: (nd) =>
  isFlagged(nd)
    ? {
        backgroundImage: 'url("data:image/svg+xml,…⚠️…")',
        backgroundRepeat: 'no-repeat',
        backgroundPosition: 'right center',
        paddingRight: '1.2em',
      }
    : null

Notes:

  • Fully self-contained → good for a shippable theme preset.
  • It's an image, not a text glyph; positioning is fiddly and emoji must be embedded as SVG <text>.

Cross-cutting caveat (staleness)

Whichever channel is used, the condition is a render-time function of NodeData. If the symbol depends on cross-branch data, it only re-evaluates when that node re-renders — the same staleness documented in #357. The piercing channel is per-prop: theme identity for styles (Options 2/3), or memoizing the customNodeDefinitions array (Option 1). See #357 for the useStableValue-based discipline.

Suggested next step

Pick a recommended recipe to document (Option 1 reads as the default for a real glyph; Option 2 for staying theme-only; Option 3 for a zero-stylesheet preset), and decide whether any of these warrants a packaged helper/preset in @json-edit-react/utils or @json-edit-react/components.

Related: #357, #197.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions