Refresh the resources in the Perspective Component Registry

I have a module that needs to execute some JS on the startup of every perspective page, because its pulling in some additional CSS that isn't located in the themes directory.

I managed to copy some strategies in @bmusson's embr modules

Where essentially on startup of the module, I iterate through all of the components in the component registry, add my resource as a dependency, and then when I shut down I remove that reference as well. This forces the page to always load my content, without my component having to be on the page (Because I dont have one!)

I recognize this is a bit hacky, and side note, it would be ideal for module developers to have a way to load in their own browser resources into Perspective overall...

Regardless, I noticed that when I do this, it only actually adds the resource IF I add a component as well. Seems that maybe this is triggering some type of "refresh" on the actual registry that causes it to show up? If I dont, my resource wont show up in the client until after a successful project scan happens when a view is modified, or if I manually restart the Perspective module.

I really hate this solution of making a fake component just for the sake of triggering this "refresh". Is there any less hacky ways to do what I am trying to do here?

 public static final Set<BrowserResource> BROWSER_RESOURCES = Set.of(
            new BrowserResource(
                    "example-bootstrap",
                    "/res/browser-resource-example/bootstrap.js",
                    BrowserResource.ResourceType.JS));

@Override
public void startup(LicenseState activationState) {

    PerspectiveContext perspectiveContext = PerspectiveContext.get(context);
    this.componentRegistry = perspectiveContext.getComponentRegistry();

    this.modifiedComponents = new ArrayList<>();
    ComponentRegistryUtils.addResourcesTo(this.componentRegistry, BROWSER_RESOURCES,
            component -> {
                boolean matches = component.moduleId().equals(PerspectiveModule.MODULE_ID);
                if (matches) {
                    modifiedComponents.add(component);
                }
                return matches;
            });
    
    // This is cursed....
    ComponentDescriptor fakeComponent = ComponentDescriptorImpl.ComponentBuilder.newBuilder()
            .setId("resource.fake.component")
            .setResources(BrowserResourceGatewayHook.BROWSER_RESOURCES)
            .build();
        
    this.componentRegistry.registerComponent(fakeComponent);
    this.componentRegistry.removeComponent(fakeComponent.id());
}

Here is a link to the repo with this example in it: An Example module for showing loading a browser resource in perspective without components

EDIT: I realized I should have also thrown in what addResourcesTo is really doing, here is what it ends up calling for each descriptor and resource

private static void addBrowserResource(ComponentDescriptor descriptor, BrowserResource resource) {
    try {
        Set<BrowserResource> browserResources = new HashSet<>(descriptor.browserResources());
        browserResources.add(resource);
        Field field = ComponentDescriptorImpl.class.getDeclaredField("browserResources");
        field.setAccessible(true);
        field.set(descriptor, browserResources);
    } catch (NoSuchFieldException | IllegalAccessException e) {
        logger.error("Failed to add browser resource {} to descriptor {}", resource.name, descriptor.id(), e);
        throw new RuntimeException("Failed to add browser resource", e);
    }
}
3 Likes

+1

The stuff I have to do in Embr to ensure my JavaScript is always loaded is way more difficult than it should be.

1 Like

Y'all need to make a compelling presentation to our sales engineering department so they can push stuff like this in front of the right people.

I've long held that Webdev should have some better tie-in to Perspective - if we expose a hook that Webdev can use we're probably going to (implicitly or explicitly) also open a path for other modules to use.

I can make this happen, and I’ll also take it as a “well, there’s no way to do it right now” lol

3 Likes

The backbone of Ignition's module system are its hooks. We have hooks into the gateway, the designer, and Vision client, but we don't have any hooks into the Perspective client. We have the ability to add components to Perspective, but that's not a substitute for a "real" hook.

I'd like a way to specify a set of BrowserResource's to be loaded by the Perspective client on startup.

My suggestion is the introduction of a ResourceRegistry avaiable through PerspectiveContext.getResourceRegistry(), which would provide a method for registering BrowserResources that are unconditionally loaded by the client after PerspectiveClient.js and PerspectiveComponents.js.

When these resources load, it would give us a place to run code in Perspective that has side-effects on the session as a whole. For examples on session side-effects, see Periscope:

  1. Periscope registers a client-wide connection handler for receiving runJavaScript messages. These messages are not tied to a single component, but instead the whole page.
  2. Periscope adds a new React root on top of Perspective for displaying Toasts. This allows Toasts to be always be available and on top of the Perspective session (again, this is a page feature, not a component feature).

These features are "possible" today, but only using hacks which force BrowserResources to load regardless of what components are used in the project.

This suggestion allows for a bridge between WebDev and Perspective, by providing a method for registering WebDev resources with Perspective's ResourceRegistry.

For example, a WebDev resource that provides a CSS file could be added to the ResourceRegistry. When the Perspective client starts, the resource would be added to the head of the document and the WebDev endpoint would be queried, loading the CSS into the session.

5 Likes