Consider adding "in_preview" var to components

Created on 7 December 2024, 5 months ago

Overview

Some interactive components, such as accordions, have default states that prevent successful usage within Experience Builder.

For example, if an accordion component is collapsed by default, meaning that it's slot is effectively inaccessible on render, then the drag/drop UX doesn't work to add things to it.

Proposed resolution

Layout builder and block content templates have an "in_preview" variable that can be used to customize how rendering happens depending on whether the layout or block is being edited, or if it is being rendered otherwise.

We use this feature in our accordion componentry and also to display placeholder information on the layout builder UI for blocks that may be conditionally visible based on external conditions, like time of day.

Would it be feasible to add an equivalent variable to SDC components so that they can react in the same fashion? Or perhaps another mechanism already that needs some docs written?

User interface changes

None (until extension authors capitalize on the new feature, then 🥳🥳🥳)

Feature request
Status

Active

Version

0.0

Component

Page builder

Created by

🇺🇸United States luke.leber Pennsylvania

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

Comments & Activities

  • Issue created by @luke.leber
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    AFAICT this is in direct conflict with @lauriii's product requirement 3. Real-time page preview:

    […] Custom component code should not have to be aware that it may be rendered in the page builder.

  • 🇺🇸United States luke.leber Pennsylvania

    Attached screenshot with more context.

  • 🇺🇸United States luke.leber Pennsylvania
  • 🇺🇸United States luke.leber Pennsylvania

    I suppose that an alternative approach might be to allow certain rendered elements receive input events, but that seems like it'd be quite the technical lift to accomplish.

  • 🇺🇸United States effulgentsia

    allow certain rendered elements receive input events

    That should already work with Initial support for allowing interaction with the preview Active . Not sure if state is retained when exiting preview though, or if the whole canvas gets re-rendered and therefore the accordion reverts to collapsed.

    Even when the accordion is collapsed, are you able to move a component into a hidden slot via the Layers panel (left sidebar)? That's certainly not enough of a solution, but I'm curious if that at least works in the meantime.

  • 🇺🇸United States luke.leber Pennsylvania

    Even when the accordion is collapsed, are you able to move a component into a hidden slot via the Layers panel (left sidebar)?

    Yep. That works.

    That should already work with #3486785: Initial support for allowing interaction with the preview. Not sure if state is retained when exiting preview though, or if the whole canvas gets re-rendered and therefore the accordion reverts to collapsed.

    Yep, the preview mode works perfectly -- things are able to be interacted with just fine in there. It's just the non-preview mode where it's impossible to use the mouse and/or keyboard to expand/collapse interactive things.

    While this can be worked around for accordions (through adding an "expand by default" prop and temporarily adjust that while adding content), I don't think it'll be quite as easy for other interactive UI patterns, like multiple tabs. For completeness, this is how we've set up our Tabs implementation in Layout Builder (dragging a block over a tab activator will actually activate the tab in the LB editor):

  • 🇺🇸United States effulgentsia

    It's just the non-preview mode where it's impossible to use the mouse and/or keyboard to expand/collapse interactive things.

    But why do you need to? What's wrong with whenever you want to interact with the component to go into preview mode, interact, then exit preview mode? Is the problem that when you exit preview mode the canvas is re-rendered in a way that resets any interaction that you did while in preview mode? If so, I think that's an issue that we should open and fix.

  • 🇺🇸United States luke.leber Pennsylvania

    Here's a gif of the behavior (described in previous comment).

  • 🇬🇧United Kingdom jessebaker

    I actually snuck into 0.x a very work-in-progress/hacky (and totally untested by anyone except me) feature where you can press and hold the 'V' key and it will hide the overlay UI and allow you to interact with the page inside the iFrame. *

    I use it for debugging and I have some vague intentions to build it out into a full feature in the fullness of time, but I'm curious if it is enough to work around your problem (and as such would be a good use case for us to consider more fully fleshing out that feature).

    As for your request, when I was building Visual Page Builder for Site Studio we had to add in a variable/property to let Site Studio interactive components know they were being rendered in the VPB for very similar reasons to the ones you are facing. I think one of the main reasons was a gallery slider that would auto-transition to the next slide every few seconds which was very undesirable when trying edit the slides!

    * if you get stuck in the Visual mode, make sure you focus outside the iFrame and press 'v' again a few times and it will unstick you. The issue is the keyup event when you let go of V sometimes gets swallowed by the iFrame. Like I said, very work-in-progress!

  • 🇺🇸United States luke.leber Pennsylvania

    That's pretty neat, @jessebaker. It definitely *helps*, but the editing UX still seems off as the accordion seems to re-flip back to closed every time it's re-rendered.

    Strangely enough, the newly added component seems to be selected by default even after re-paint and it's hidden.

  • 🇬🇧United Kingdom jessebaker

    Hmm, that's a really difficult challenge.

    There are actually 2 iframe elements one on top of the other. Each time any data changes, the hidden iframe is updated with the new preview and once it has loaded, the iframes are swapped so you can see the changes.

    Because the page is re-rendered server side whenever there are any changes, the iframe swapping has to be done to hide the document loading and stop any FOUC issues/flickering.

    As a result of that implementation though, no front end state on the page is maintained between data updates.

    Off the top of my head I don't have an immediate thought of how to even begin addressing that.

  • 🇺🇸United States luke.leber Pennsylvania

    It's actually pretty simple to resolve if the component knows if it's being observed in Experience Builder.

    A custom twig extension...

    class XBExtension extends AbstractExtension  {
      public function getFunctions() {
        return parent::getFunctions() + [
          new TwigFunction('is_xb_editing', [
            $this,
            'isXbEditing',
          ]),
        ];
      }
    
      public function isXbEditing() {
        return \Drupal::routeMatch()->getRouteName() === 'experience_builder.api.preview';
      }
    }
    

    ...and using it in the SDC template:

    {% set expanded = is_xb_editing() %} {# <-- "in_preview" variable-like-thing #}
    
    {% include '@my-upstream-components/accordion.twig' %}
    

    That said, I wouldn't recommend writing this off as a niche problem. I can't think of any interactive / progressive disclosure elements from *any* design systems that wouldn't need a mechanism like this. I think it's more along the lines of not having any example components available in XB to have stepped on this yet. It'd be really unfortunate for each design system to have to roll their own "in_preview"-like feature.

  • 🇺🇸United States luke.leber Pennsylvania

    Added copy/paste example to I.S. w/o any external dependencies :-).

  • 🇺🇸United States luke.leber Pennsylvania
  • 🇫🇮Finland lauriii Finland

    I agree with the premise from #2 that the proposal to add in_preview variable is in conflict with the idea that the components shouldn't have to be aware that they are being rendered in XB. That said, think we will eventually have to add in_preview as a tool to help working around the limitations of the editor in the complex use cases.

    There's an alternative approach to this, which is to add a new property that allows toggling the accordion. This would essentially allow setting the default value for the open/closed state, but would also allow opening the accordion item in XB.

  • 🇬🇧United Kingdom jessebaker

    I'm not sure your alternative approach in #16 @lauriii would be enough to handle the scenario of disabling an auto-playing image slider for instance.

    Maybe it's just semantics but "Custom component code should not have to be aware..." - suggests that components must work without being aware they are in the editor but doesn't say that they can't be told they are in the editor if that allows for someone to improve the DX when building them out.

    I think we have work arounds planned to mean that any/all components will work and be useable (e.g. falling back to using the layers panel) which fulfils that requirement but we shouldn't intentionally hamper more advanced use cases just to meet that requirement.

  • 🇫🇮Finland lauriii Finland

    To clarify my comment from #16, I'm +1 to adding the in_preview (or similar variable). At the same time, I think that we should continue improving our tools to better facilitate an seamless integration of components for common use cases, even if they come with some interaction.

  • Assigned to lauriii
  • Status changed to Needs review 2 months ago
  • 🇬🇧United Kingdom jessebaker

    Adding label and related issue link.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    It's just the non-preview mode where it's impossible to use the mouse and/or keyboard to expand/collapse interactive things.

    This is intentional, but you know that already. To allow interacting with elements, XB has a preview mode, introduced in Initial support for allowing interaction with the preview Active . I wrote about it in https://wimleers.com/xb-week-28.
    EDIT: I see @effulgentsia pointed to it in #6 already, but just didn't link it!

    @lauriii expressed his approval of this in #18, so unassigning.

    I see @jessebaker expressed confirmation of this in #17 too.

    I personally am still very wary of this, I'd rather leave this "explicitly inform the component it's in a preview through an explicit component input" box unopened 😇 It IMHO breaks/pollutes the abstraction unnecessarily.

    • For dynamic/interactive things like #17's auto-playing image slider, there inevitably is JS. Why not have its JS detect that it's in an iframe[data-xb-iframe] and then behave differently? (Quite like detecting that you're being embedded into an origin you don't want your content to be played on.)
    • That does not yet solve the no-JS (pure HTML + CSS) accordion. For those, I think @luke.leber's solution in #13 is very much preferable: it means no new input to the component (no in_preview SDC prop).
      Note that the Twig extension would look slightly different, because ever since 📌 Code Components should render with their auto-saved state(if any) when rendered in the XB UI Active , the routes rendering a preview of XB explicitly inform the (component instance) rendering process of that 👍

    I support @luke.leber's #13 proposal, but that will only work for server-side-rendered components (SDCs, blocks …). To do this purely client-side, I propose that such a component adds a few lines of code to be XB iframe-aware.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    IMHO this is a stable blocker, for the reasons @luke.leber stated:

    I can't think of any interactive / progressive disclosure elements from *any* design systems that wouldn't need a mechanism like this. I think it's more along the lines of not having any example components available in XB to have stepped on this yet. It'd be really unfortunate for each design system to have to roll their own "in_preview"-like feature.

    Tagged and bumped priority. Updated issue summary to reflect full proposal, for all known XB Component Source plugins. #20 didn't cover blocks-as-components sufficiently. Now got that covered.

    Asking @jessebaker for feedback.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Bump, thoughts, @jessebaker?

  • 🇬🇧United Kingdom jessebaker

    "code components — or any component that loads JS - XB preview iframe-awareness by checking it's loaded in a iframe[data-xb-iframe]"

    I don't think it's possible from inside the iFrame to check properties of the parent iFrame (E.g. a data-xb-iframe attribute). So it would be possible to determine that the code is running in an iFrame but not possible to detect that it's specifically running in XB's iFrame.

    But, we could inject a variable from the parent or set a value in localstorage or a cookie that the JS in the iFrame could check for? We would need to do this both for the main preview but also the xb-code-editor-preview iFrame shown in the top right when editing Code Components.

    This does, however, "explicitly inform the component it's in a preview" which @wim leers I know you were wary of doing.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    we could inject a variable from the parent or set a value in localstorage or a cookie that the JS in the iFrame could check for?

    Sounds reasonable.

    @jesse.baker: Not just mine, but @lauriii's — see #2. In #20 I already indicated which direction I'm okay with. I'd still very much rather not, but if there's no other way … there's no other way 😬

    @jesse.baker Can you update the code components bit in the issue summary with the details of your proposal for those ? 🙏

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    As the lead on the whole code components infrastructure, @balintbrews, WDYT about @jessebaker's proposal he just added to the issue summary? 😊🙏

  • 🇺🇸United States luke.leber Pennsylvania

    I would like to draw some attention back to the gif at the end of #7. While a naive "in_preview" works for simple accordion-like controls with Boolean states, things like tabs, which could have multiple mutually exclusive states remains a question mark.

    To accomplish this in layout builder, our team currently injects additional JS on top of what normally runs on the front-end component to keep track of state.

    Admittedly it is a bit clunky as there are massive layout shifts on re-paint though...so I wouldn't read too much into how we "solved" it with layout builder.

  • 🇬🇧United Kingdom jessebaker

    @luke.leber I agree that there are two parts to this

    1) the technical side of how to make a component's JS aware it's in the preview so that it can behave differently (as coded by the component creator)
    2) the UX side of how we expect a site builder user to work with/build/preview interactive components in XB.

    I'll escalate this with the relevant people on XB team.

  • 🇬🇧United Kingdom jessebaker

    Based on conversation with @balintbrews and @callumharrod

    1) In this initial approach (which is not sufficient for a high quality DX) we should aim to insert a hardcoded script tag at the top of the preview iFrame that sets a property on the window object to identify that the page is being loaded in XB (window.isInsideExperienceBuilder).
    2) For now the expectation is that more complex use cases (like tabs or sliders) will have to be coded for by the component developer making use of that window.isInsideExperienceBuilder. Unlike Layout Builder, XB users will have access to the Layers Panel which will hopefully allow them to, at least, "muddle through" these use cases.
    3) In the future a more expansive API will likely need to be developed (or a different solution entirely that we have yet to think of) that allows the preview to maintain data between re-renders. This might be something the component developer will need to manually do (set a state when a change happens and get/check for a state on initialising their JS).

    Unfortunately I don't think it's feasible for us to implement 3 right now as we push towards 1.0 even though I agree with @luke.leber that ultimately that is desirable.

  • 🇺🇸United States luke.leber Pennsylvania

    2) For now the expectation is that more complex use cases (like tabs or sliders) will have to be coded for by the component developer making use of that window.isInsideExperienceBuilder

    I think this could work for the most part if there was sufficient render-time context to conditionally render additional JS. From the limited understanding I have of the semi-decoupled theme engine, that seems possible, so 🥳.

    I apologize that I no longer have any officially sponsored time (9-5) to delegate to this effort, but it was a tenuous arrangement from the start. I'll endeavor to respond to issues I'm already involved in until closed though on my own time.

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    With 🐛 Auto-saved changes to component are not loaded in preview canvas when component is inside a slot Active in, we now have test coverage to prove behavior in either case for any component source. \Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\JsComponent already behaves differently when previewing as of that issue, but not for the code components themselves, but for rendering the auto-saved (non-live-code) alternative when previewing.

  • Issue was unassigned.
  • Status changed to Postponed about 9 hours ago
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    I think this is less urgent.

Production build 0.71.5 2024