Musson Industrial's Embr-Periscope Module

I'm guessing it's a classloader thing; only the gateway enforces classloader separation for modules. We probably never thought about/tested third party modules trying to use our schemas.

You might be able to, in some semi-hacky way, set the thread context classloader to Perspective's, but I'm not actually sure where you would do that and where you might want to undo it.

1 Like

:man_mage:

More testing required, but this seems to make the gateway happy (no more errors in the logs).

    override fun startup(activationState: LicenseState) {
        ...

        logger.debug("Registering components...")
        val originalClassLoader = Thread.currentThread().contextClassLoader
        Thread.currentThread().contextClassLoader = context.perspectiveContext.javaClass.classLoader

        ...
        componentRegistry.registerComponent(...)
        modelDelegateRegistry.register(...)
        ...

        Thread.currentThread().contextClassLoader = originalClassLoader
    }

Edit: The current hack. This has the benefit of also resolving my own reusable schemas.

class DelegatedClassLoader(parent: ClassLoader, private vararg val delegates: ClassLoader) :
    ClassLoader(parent) {

    override fun findClass(name: String): Class<*> {
        for (delegate in delegates) {
            try {
                return delegate.loadClass(name)
            } catch (_: ClassNotFoundException) {}
        }
        throw ClassNotFoundException(name)
    }

    override fun findResource(name: String): URL? =
        delegates.firstNotNullOfOrNull { it.getResource(name) }

    override fun findResources(name: String): Enumeration<URL> =
        Collections.enumeration(delegates.flatMap { it.getResources(name).toList() })
}

fun withContextClassLoaders(vararg delegates: ClassLoader, block: () -> Unit) {
    val originalContextClassLoader = Thread.currentThread().contextClassLoader
    Thread.currentThread().contextClassLoader =
        DelegatedClassLoader(originalContextClassLoader, *delegates)

    block()

    Thread.currentThread().contextClassLoader = originalContextClassLoader
}
    // GatewayHook
    override fun startup(activationState: LicenseState) {
        ...

        logger.debug("Registering components...")
        withContextClassLoaders(
            this.javaClass.classLoader,
            context.perspectiveContext.javaClass.classLoader,
        ) {
            componentRegistry.registerComponent(...)
            modelDelegateRegistry.register(...)
        }

        ...
    }

This fix is now included in 0.7.2.

2 Likes

Hey @bmusson, annoying js question again :grimacing:

Toastify: how would I be able to put words on new lines in the content? Usually I have to set the css white-space to pre*, but I have no idea where to start with this one..

Option 1: You can add classes to the toast.

system.perspective.runJavaScriptAsync('''() => {
	periscope.toast('My\\nContent\\nOn\\nDifferent\\nLines', {
		className: 'psc-MultiLineToast'
	})
}''')

Option 2: You can use inline styles.

system.perspective.runJavaScriptAsync('''() => {
	periscope.toast('My\\nContent\\nOn\\nDifferent\\nLines', {
		style: {
			whiteSpace: 'pre',
			pointerEvents: 'all'
		}
	})
}''')

Edit: If using inline styles, you have to add pointerEvents: 'all' for any interaction to work. This is because I'm using inline styles on the ToastContainer for the positioning and disabling interaction. This could be improved in a future version.

1 Like

Question regarding Portal:

Patch Changes

  • 3ea4d36: (FlexRepeaterPlus) Add instance key as an implicit parameter to the instance view.
  • 1bd2a1a: (FlexRepeaterPlus) Improve ViewModel caching.
    • Move ViewModel caching from the instance level to the component level, allowing the ViewModel reference to be retained for the lifetime of the component.
    • Previously, a ViewModel instance was only cached for the lifetime of its associated InstancePropsHandler, and not much care was taken to remember InstancePropsHandlers.This resolves a bug that would occur when simultaneously (in a single update to props.instances):
    1. Moving existing instances.
    2. Adding new instances.
    3. Changing the final size of the instances array.
  • 3b53009: (Toasts) Move pointerEvents setting from inline styles to CSS.
    • This makes it easier for users to use the style property of the toast function.
    • Users no longer need to add pointerEvents: 'all' to every inline style definition.
  • Updated dependencies [3ea4d36]
  • Updated dependencies [3b53009]
    • @embr-modules/periscope-web@0.7.3
4 Likes

Ok, I'm trying to use FlexRepeaterPlus, and I cannot get the instances to populate correctly....
I have a view with 3 input parameters, and I'd like to use two of them as common, and one as variable on each instance.

I'm feeding this transform a list of the instances I want:
	a={}
	counter=0
	value.sort(reverse=True)
	for i in value:	
		instance={"viewParams":{"Station":i},
  			"viewStyle": {"classes": ""},
  			"viewPosition": {},
  			"key": counter
			}
		a[counter]=instance
		
		counter=counter+1
	
	return a

My array looks correct if I setup a custom property with the transform,

{
  "0": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 180
    },
    "key": 0
  },
  "1": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 172
    },
    "key": 1
  },
  "2": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 171
    },
    "key": 2
  },
  "3": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 170
    },
    "key": 3
  },
  "4": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 160
    },
    "key": 4
  },
  "5": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 150
    },
    "key": 5
  },
  "6": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 130
    },
    "key": 6
  },
  "7": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 110
    },
    "key": 7
  },
  "8": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 100
    },
    "key": 8
  },
  "9": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 93
    },
    "key": 9
  },
  "10": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 91
    },
    "key": 10
  },
  "11": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 90
    },
    "key": 11
  },
  "12": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 81
    },
    "key": 12
  },
  "13": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 80
    },
    "key": 13
  },
  "14": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 70
    },
    "key": 14
  },
  "15": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 60
    },
    "key": 15
  },
  "16": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 50
    },
    "key": 16
  },
  "17": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 41
    },
    "key": 17
  },
  "18": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 21
    },
    "key": 18
  },
  "19": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 20
    },
    "key": 19
  },
  "20": {
    "viewStyle": {
      "classes": ""
    },
    "viewPosition": {},
    "viewParams": {
      "Station": 10
    },
    "key": 20
  }
}

But I cannot get the actual instances list to change, regardless of using a binding to the array or using the script push method as a startup event. I'm currently using periscope 0.7.4 (b2025050202)
Thanks!

Could you provide a project export that demonstrates the problem?

I can’t really follow your example.

Found it!
the a={} needed to be a=
I had the wrong keys due to how it looks if you copy the JSON to notepad...

I’m glad you figured it out.

TIL that open/closed brackets will render as a square. []

Today I learned that if you have rights to edit a post, clicking that square will actually check the checkbox :laughing:

3 Likes

oooo!

Cross linking this post:

1 Like

Testing out the Swiper-component and I really like it. Much smoother then Carousel. I have it all setup like I want it but I can't figure out how to jump to a specific index/slide.

I'm opening Swiper as a popup and want to jump to a specific slide depending om the link clicked. "initialSlide" has to be set with a persistent value, otherwise it gets set after the component loads and does nothing. "activeIndex" doesn't seem to exist?

Is there any way to run the methods om Swiper? Like "swiper.slideTo()" or "swiper.init()"

The Swiper component currently doesn't have JavaScript proxy support, I'll change that.

Once updated you'll be able to get a JavaScript proxy for the Swiper instance that you can use to call the API methods.

3 Likes

Ta-da.

def runAction(self, event):
	component = self.getSibling("Swiper")
	
	proxy = component.getJavaScriptProxy("swiper")
	proxy.runAsync('''() => {
		this.slideNext()
	}''')

In the future (i.e. correlating with the ApexCharts component release), support will be dropped for multiple-proxyable properties, meaning you will no longer need to pass the object name to getJavaScriptProxy (e.g. you will be able to do getJavaScriptProxy() instead of getJavaScriptProxy("swiper")).

The current function signature will continue to work, but the name parameter will be ignored.

4 Likes

Wow! That was fast =) It's working as intended. Huge thanks!

2 Likes

@bmusson Love this. Can you share the perspective project that has the toast constructor with all the options?

2 Likes

Hey! It available in the Example Views section of the original post, kind towards the bottom.

1 Like

Is it possible to add lazy load to Swiper? Swiper documentation says to add "loading = 'lazy'" on each image, but in Ignition we load views and can't really add it this way?