Real-time preview for props changes of JS components

Created on 23 March 2025, 6 months ago

Overview

As a content editor building a page using (JavaScript) Code Components, when I update prop values in the right sidebar, I want to see the preview update immediately, without waiting to get a response from the server.

It would be great to have this when using SDCs as well, but that's harder, so isn't part of this issue's scope. That would need its own child issue of ๐Ÿ“Œ Implement endpoint for realtime preview Active .

Proposed resolution

  • We need a way to get a prop value from the widgets (form input elements) in the right sidebar. ๐Ÿ“Œ Move clientside assumptions about prop form data shape into a series of prop specific transforms Active made that possible for at least some widgets. This issue's scope is only for the cases where we have that. For widgets that require a round trip to the server in order to map to field value and prop value, we don't get the real-time preview update.
  • Assuming we can get the updated prop value client-side, we can update the preview by simply targeting the <astro-island> element for this component instance, and updating its props attribute. The Astro island will then automatically re-render itself client-side based on the new props.
  • We should still send the request to the server, in order to auto-save our changes. However, we could potentially experiment with different debounce thresholds. For example, update the preview with a <1 second (maybe <0.1s?) debounce and auto-save to the server with a >1 second (maybe >5s?) debounce.

User interface changes

โœจ Feature request
Status

Active

Version

0.0

Component

Page builder

Created by

๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @effulgentsia
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
    • Assuming we can get the updated prop value client-side, we can update the preview by simply targeting the <astro-island> element for this component instance, and updating its props attribute. The Astro island will then automatically re-render itself client-side based on the new props.

    I forget the details of how our undo/redo functionality is implemented exactly (and I see we're missing docs for it other than a one-liner ๐Ÿซฃ), but โ€ฆ AFAICT this would then have to add Component Source-specific logic to perform undo+redo. It'd need to check every affected component instance when undoing/redoing, check if it's provided by the js ComponentSource plugin, and then perform this alternative.
    And if and only if there's zero other component instances affected, then that's all that's needed. But if there's >=1 component instance from another component source that needs undoing/redoing, we still need to talk to the server, because the server can only update the entire preview, not a subset.

    (And I bet there's more complications.)

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    Is there a reason we don't include the preview HTML in the undo stack? Thereby allowing all undo operations (regardless of what kinds of components are on the page) to optimistically render before the server response?

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Fair question. I suspect because "memory usage will go through the roof".

  • Assigned to jessebaker
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    Tagging this as a beta blocker, because we want early adopters of the beta able to run XB in production, including under potentially heavy server load.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    I think @effulgentsia implies with "heavy server load" that he wants Content Creators using XB to have a good experience, which implies a low-latency experience, even when latency is high (either high client โ†’ server network latency or simply the server response latency being high, aka high server load).

    If so, can we start with implementing this while accepting that not every prop's resolved value (see: EvaluatedComponentModel

    My concrete proposal:

    1. for many prop shapes, we already have client-side transforms for the used field widget
    2. we have JSON Schema information for each such prop, which allows client-side validation of the resolved value against the JSON Schema
    3. restrict scope of this issue to only those prop shapes where #1 (a client-side transforms) exists, and which meets #2 (transforms to a valid resolved value per the JSON Schema for that prop)
    4. leave EVERYTHING ELSE to follow-up issues: A) client-side transform exists but does not pass client-side JSON Schema validation, B) client-side transform does not yet exist but is possible, C) client-side transform to resolved value is impossible (for example: media library widget), but we could do some client-side caching โ€” A+B+C can then be follow-ups that improve the state this issue would put us in.

    That way, we can start implementation any time (even today), and learn what the most valuable missing pieces would be.

  • ๐Ÿ‡ซ๐Ÿ‡ฎFinland lauriii Finland

    restrict scope of this issue to only those prop shapes where #1 (a client-side transforms) exists, and which meets #2 (transforms to a valid resolved value per the JSON Schema for that prop)

    Where can I find a list of prop types that meet this criteria?

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    Postponing this on โœจ Unwrap astro-islands and astro-slots Active , because that issue will make this one more complicated, because once we remove the astro wrappers, it's less clear how to then target the component instance and re-render it with updated props. It's definitely solvable, just trickier.

    Related, it doesn't strictly need to block a beta despite #5. Though I still stand by #5 as a strong nice-to-have, so instead of removing the "beta blocker" tag entirely, this is now our first issue that I'm tagging as a "beta target". And now that we have our first one, we'll likely start using this tag for some other issues as well :)

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    Starting on this on top of โœจ Unwrap astro-islands and astro-slots Active which has an MR now.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    we can update the preview by simply targeting the element for this component instance, and updating its props attribute

    In my testing (Firefox) this updates the HTML inside the iframe's contentDocument but doesn't trigger re-rendering.

    I wonder if that's because we're making use of the srcdoc attribute

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    I wonder if that's because we're making use of the srcdoc attribute

    Ah I think this might be that I'm updating the wrong iframe, I forget we did the swapping

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    Ah I think this might be that I'm updating the wrong iframe, I forget we did the swapping

    No joy, I'm definitely updating the write iframe/component

    The screenshot shows I've changed the text prop to the value 'hotdog' but as you can see the <p> still contains the original value 'Some text'

    I've confirmed that the attributeChangedCallback is calling hydrate and that is seeing the new prop value.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    Ah, the preact client bails early if there's no ssr attribute and the first hydrate call removes it

    And if we setAttribute('ssr') that triggers a hydrate call which then removes itself again. So I think we might need something different to <astro-island> like we did in โœจ Unwrap astro-islands and astro-slots Active but that ignores the ssr attribute.

    Seeing what other options we have.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    Ok, got something along those lines going with this code in the console and a custom element <xb-island>

    ai = Array.from(document.querySelectorAll('iframe')).reduce((c, i) => c.concat(...Array.from(i.contentDocument.querySelectorAll('xb-island'))), []).forEach(e => {delete (e.__k);e.setAttribute('props', JSON.stringify({...JSON.parse(e.getAttribute('props')), text: ['raw', 'hotdog']}))})
    
    

    the delete (e.__k) is to remove the node preact adds for diffing. With that present it always tries to update existing elements but they don't exist because the innerHTML has been blown away by the hydration process

    Where <xb-island> is

    const AstroIsland = customElements.get("astro-island");
    class XbIsland extends AstroIsland {
        constructor() {
          super();
          this.addEventListener("astro:hydrate", () => {
            // Add the ssr attribute back so we can re-hydrate when props change.
            this.setAttribute('ssr', '');
          });
        }
      }
      customElements.define("xb-island", XbIsland);
    

    Will do some more digging to see if I can find a way to make use of the existing __children node preact retains

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    screeny showing the preview updated ๐ŸŒญ

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    Current train of thought on this

    * When we're in preview, have the unwrapped-astro-island add a template element with its props and children on it during unwrap. Place another template element after the last child so we have markers for where the replaced (unwrapped) HTML goes to and from.
    * In XB UI emit a custom event on the active iframe preview in the inputBehaviorsComponent input form when a component prop changes _and_ it has passed validation _and_ we know it doesn't need a server side trip (can probably assume this will be fine for any scalar prop. If the event from that
    * When we're in preview have unwrapped-astro-island and xb-island listen for these events and update their markup - in the case of xb-island by something similar to #14 but hopefully without needing to know the internals of preact. In the case of the unwrapped island by doing something with the stashed template elements

    Will continue on monday

  • Merge request !1320Issue #3514910: Real time updates โ†’ (Merged) created by larowlan
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    Now that โœจ Unwrap astro-islands and astro-slots Active is postponed, we don't need to postpone this on that.

    Got a working version of this in the branch

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    This is green and ready for review w/ test coverage

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    larowlan โ†’ changed the visibility of the branch 3514910-pp-1-real-time-preview to hidden.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10
  • Pipeline finished with Failed
    about 1 month ago
    #561939
  • Pipeline finished with Success
    about 1 month ago
    #566644
  • Assigned to larowlan
  • Status changed to Needs work 29 days ago
  • ๐Ÿ‡ช๐Ÿ‡ธSpain penyaskito Seville ๐Ÿ’ƒ, Spain ๐Ÿ‡ช๐Ÿ‡ธ, UTC+2 ๐Ÿ‡ช๐Ÿ‡บ

    For feedback. But hopefully this will get FED review before reaching you.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10
  • Pipeline finished with Failed
    29 days ago
    #572489
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States bnjmnm Ann Arbor, MI
  • ๐Ÿ‡ช๐Ÿ‡ธSpain penyaskito Seville ๐Ÿ’ƒ, Spain ๐Ÿ‡ช๐Ÿ‡ธ, UTC+2 ๐Ÿ‡ช๐Ÿ‡บ

    Rebased this without any issue. The functionality still works as expected (see recording at #18)

    Ben approved, I reviewed too, and Lee provided the requested feedback. What's next here?

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
    1. The screencast looks great!
    2. Zero remarks on the BE changes (thanks, @penyaskito, for reviewing and getting it to that point!)
    3. The FE has already been approved by @bnjmnm.

    ๐Ÿš€

    Understanding

    This ended up not relying on client-side transforms as much as I expected (see my write-up at #6), and instead relies on a much simpler heuristic:

    
      // Scalar prop-types might be able to perform real-time updates.
      const isScalarProp = ['number', 'integer', 'string', 'boolean'].includes(
        component?.propSources?.[propName]?.jsonSchema?.type as string,
      );
      // We don't debounce updates for code components where the prop is scalar -
      // but all other components/props should be debounced to avoid thrashing the
      // server with multiple PATCH requests.
      const shouldDebounce =
        !isScalarProp ||
        components?.[selectedComponentType]?.source !== 'Code component';
    

    But โ€ฆ I'm not sure that's sufficient? ๐Ÿ˜…

    Manual testing

    For example: type: string, format: uri-reference. (Install xb_test_code_components and then instantiate the xb_test_code_components_with_link_prop aka code component.)

    1. ๐ŸŸก The above logic would NOT debounce (i.e. perform a real-time update of the preview) it because it's scalar and a code component. Is that intentional?
    2. ๐Ÿ”ด In fact, AFAICT this does not run client-side transforms. Confirmed: auto-complete works, but results in <a href="entity:node/2"> being rendered on the client side, which should've been transformed to /node/2 by the link transformer (see transforms.ts).
    3. ๐Ÿ”ด It doesn't validate the value against JSON schema, even though the client side has this.
    4. ๐ŸŸข You can recover from the faulty/imperfect preview state by reloading the browser; that will trigger the server side to render the preview.

    Given that

    1. probably the majority of props uses simple scalars (without additional constraints)
    2. the most evident way to run into this (see above) actually doesn't cause a bad visual experience in most cases (a link you can't even click, so you can't really notice its target doesn't make sense)
    3. there's a way to recover (reload)
    4. this is a MASSIVE UX improvement

    Conclusion

    I'm going ahead and merging the current MR, but am marking it and , because this really should be fixed. I'm sure there will be cases in the future where the preview being updated with invalid values will cause regressions.

    I think 1+2 should be fixed here.

    3 can be a follow-up.

  • Pipeline finished with Skipped
    15 days ago
    #583309
  • I am able to reproduce the mentioned issue in the #27 comment -

    Steps to reproduce :

    1. Install xb_test_code_components module.
    2. Add the โ€œMy Code Component Linkโ€ component to a page in Experience Builder.
    3. In the link prop, use the autocomplete to pick a node/article (e.g., select an article by title).
    4. The input fills with a value like entity:node/2.
    5. Inspect This is my test link on the canvas component in browser DevTools.

      Value will be: <a href="entity:node/2">This is my test link</a> being rendered on the client side,

      which should've been transformed to /node/2 by the link transformer.
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia
Production build 0.71.5 2024