# Musson Industrial's Embr-Periscope Module

**URL:** <https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047>\
**Category:** 3rd Party Modules\
**Tags:** perspective\
**Created:** [November 24, 2024, 10:26pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047 "2024-11-24T22:26:31Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [November 24, 2024, 10:26pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/1 "2024-11-24T22:26:31Z")

</div>

# Embr-Periscope

An Ignition module that includes design extensions and enhancements for Perspective.

> A periscope provides a new perspective by allowing users to see over, around, or through obstacles, offering a unique view otherwise obstructed.

> **_NOTE:_** This modules is still in pre-release. All versions before v1.0.0 should be considered unstable.

## Components

### [Embedded View +](https://docs.mussonindustrial.com/ignition/embr-periscope/components/embedded-view-plus/)

This component is a enhancement of the default `Embedded View` component with server-side property mapping. In testing, this reduced property transit time on a locally hosted gateway from ~100ms per view layer to ~0.220ms.

### [Flex Repeater +](https://docs.mussonindustrial.com/ignition/embr-periscope/components/flex-repeater-plus/)

This component is a enhancement of the default `Flex Repeater` component with server-side property mapping.

Additionally, it supports per-instance view paths, sharing of common parameters into all views, manually specified instance keys, and component functions for removing, adding, and inserting instances.

### [Swiper](https://docs.mussonindustrial.com/ignition/embr-periscope/components/swiper/)

[Swiper](https://swiperjs.com/) is the most modern free and open source mobile touch slider with hardware accelerated transitions and amazing native behavior.

This component merges Swiper's functionality with that of a Flex Repeater.

## Docs

[https://docs.mussonindustrial.com/ignition/embr-periscope](https://docs.mussonindustrial.com/ignition/embr-periscope)

## Sponsorship

If you benefit from this module for commercial use, we ask you to consider sponsoring our efforts through [GitHub sponsors](https://github.com/sponsors/mussonindustrial). Your support will allow us to continue development of open-source modules and tools that benefit the entire community (plus there are some bonuses for sponsors 🙂).

## Download

[Embr-Periscope-0.4.1.modl](https://github.com/mussonindustrial/embr/releases/download/%40embr-modules%2Fperiscope%400.4.1/Embr-Periscope-0.4.1.modl)  
The latest release can always been found on GitHub:

> **[periscope · Releases · mussonindustrial/embr](https://github.com/mussonindustrial/embr/releases?q=periscope&expanded=true)**
>
> 🔥 Open-source modules for Ignition by Inductive Automation. The home of Charts, Thermo, Periscope, and more. - mussonindustrial/embr

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [December 6, 2024, 10:54am UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/2 "2024-12-06T10:54:39Z")

</div>

Here's a gun, don't shoot yourself with it. This uses [the same function constructor](https://github.com/mussonindustrial/embr/tree/main/libraries/javascript/utils/src/toFunction) as my `Chart.js` module.

### system.perspective.runJavaScriptBlocking(function, [args])

Runs a JavaScript function in the client's page and returns the result, blocking for the result. `function` is a literal string containing a single arrow function. `args` is an optional dictionary, where the keys correspond to the names of the function arguments.

### system.perspective.runJavaScriptAsync(function, [args], [callback])

Same as above, but calls a callback with the result (non-blocking).

* * *

### Examples

#### Blocking Example

```python
# Button [onActionPerformed]
def runAction(self, event):
	code = '''(count) =>
	console.log(`The current count is ${count}`);
	return count + 1;
	'''
	
	count = 0
	
	for i in range(500):
		
		count = system.perspective.runJavaScriptBlocking(code, { 
			'count': count
		})
		
		self.props.text = count

```

#### Async Example

```python
# Button [onActionPerformed]
def runAction(self, event):
	code = '''(count) =>
	const start = window.performance.now();
	
	sum = 0
	for (let i = 0; i < count; i++) {
		sum += i;
		console.log(i);
	}
	
	const end = window.performance.now();
	return {
		sum,
		duration: end - start
	}
	'''
		
	count = 500000
		
	def cb(result):
		message = 'Total: {sum}, Duration: {duration:.2f}ms'.format(**result)
		self.props.text = message
		
	system.perspective.runJavaScriptAsync(code, { 'count': count }, cb)

```

### Function Globals

All functions have access to the Perspective client store through a globally scoped variable `client`.

#### Function Globals Example

```python
# Button [onActionPerformed]
def runAction(self, event):
	code = '''() =>
	return client.projectName;
	'''
	
	self.props.text = system.perspective.runJavaScriptBlocking(code)

```

* * *

Lightly tested.

~~[Embr-Periscope-0.4.2-BETA.modl](https://forum.inductiveautomation.com/uploads/short-url/xzC9R7RQF4hL4BmPkw3r9iMRb2h.modl) (2.0 MB)~~

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [December 8, 2024, 11:51pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/3 "2024-12-08T23:51:16Z")

</div>

`runJavaScriptAsync` and `runJavaScriptBlocking` are fully merged.

~~[Embr-Periscope-0.4.3.modl](https://github.com/mussonindustrial/embr/releases/download/%40embr-modules%2Fperiscope%400.4.3/Embr-Periscope-0.4.3.modl)~~  
(Oops, changed timeout units without updating the value. A 30 second timeout became a 30,000 second timeout 🙃)  
[Embr-Periscope-0.4.4.modl](https://github.com/mussonindustrial/embr/releases/download/%40embr-modules%2Fperiscope%400.4.4/Embr-Periscope-0.4.4.modl)

Improvements:

- Support for async JavaScript function defintions. You can either directly specify your function definition as `async`:

```js
 async (param1, param2) => {
  const promise = new Promise((resolve) => {
    setTimeout(() => {
      resolve(param1 + param2)
    }, 3000)
  })

  return await promise
}	

```

Or you can return a promise:

```js
(param1, param2) => {
  const promise = new Promise((resolve) => {
    setTimeout(() => {
      resolve(param1 + param2)
    }, 3000)
  })
  return promise
}

```

Either way, once the promise is resolved the gateway will be notified (unblocking or running the callback, depending on which version you called).

- If any errors are thrown in your JavaScript, the stack trace is displayed in the designer.
- Support for specifying `sessionId`/`pageId` for execution on arbitrary pages.

~~Documentation site will be updated soon.~~

> **[system.perspective.runJavaScriptAsync | Musson Industrial](https://docs.mussonindustrial.com/ignition/embr-periscope/scripting/perspective/system-perspective-runJavaScriptAsync/)**
>
> Overview

> **[system.perspective.runJavaScriptBlocking | Musson Industrial](https://docs.mussonindustrial.com/ignition/embr-periscope/scripting/perspective/system-perspective-runJavaScriptBlocking/)**
>
> Overview

(For the folks paying attention, this idea will be the basis accessing methods of the `Chart.js` object in my [embr-charts](https://github.com/mussonindustrial/embr/tree/main/modules/charts) module. You will have access to a `ClientChartProxy` object that you can manipulate fairly directly, calling methods/updating properties.)

---

<div class="post-metadata">

**Author:** ![liam1](https://avatars.discourse-cdn.com/v4/letter/l/e68b1a/32.png) [@liam1](https://forum.inductiveautomation.com/u/liam1)\
**Post date:** [February 18, 2025, 8:27am UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/4 "2025-02-18T08:27:00Z")

</div>

Hi @bmusson I want to get the event.touches.length using system.perspective.runJavaScriptAsync since touch events in ignition do not expose this property , is this possible?

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [February 18, 2025, 10:36am UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/5 "2025-02-18T10:36:38Z")

</div>

Sure.

In a component startup script, you can use `runJavaScriptAsync/Blocking` to install whatever custom listeners you want.

Edit: These scripting functions are provided by my `Periscope` module, if someone could move this thread over there it’d be the most appropriate.

> [@Musson Industrial's Embr-Periscope Module](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047):
>
> Embr-Periscope An Ignition module that includes design extensions and enhancements for Perspective. A periscope provides a new perspective by allowing users to see over, around, or through obstacles, offering a unique view otherwise obstructed. NOTE: This modules is still in pre-release. All versions before v1.0.0 should be considered unstable. Components [Embedded View +](https://docs.mussonindustrial.com/ignition/embr-periscope/components/embedded-view-plus/) This component is a enhancement of the default Embedded View component with server-side property mapping. In testing, t…

---

<div class="post-metadata">

**Author:** ![liam1](https://avatars.discourse-cdn.com/v4/letter/l/e68b1a/32.png) [@liam1](https://forum.inductiveautomation.com/u/liam1)\
**Post date:** [February 18, 2025, 11:22am UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/6 "2025-02-18T11:22:55Z")

</div>

Thank you for the reply,

When you say install, what is meant by this?

Just for reference I am trying to implement a pinch and zoom,

My idea would be to eventually update the transform here after performing the calculations.

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/7/7/77be0171a6c371752494f4467a1f0205b6f08c96.png)

But considering what you just said, this native JavaScript run can update it directly and will persist while the page is open?

```python
const pinchZoom = (element) => {
  let scale = 1;
  let start = { x: 0, y: 0, distance: 0 };

  const targetComponent = document.querySelector('#touchElement');

  targetComponent.addEventListener('touchstart', (e) => {
    if (e.touches.length === 2) {
      e.preventDefault();
      start.x = (e.touches[0].pageX + e.touches[1].pageX) / 2;
      start.y = (e.touches[0].pageY + e.touches[1].pageY) / 2;
      start.distance = distance(e);
    }
  });

  targetComponent.addEventListener('touchmove', (e) => {
    if (e.touches.length === 2) {
      e.preventDefault();
      const currentDistance = distance(e);
      const newScale = Math.min(Math.max(1, currentDistance / start.distance), 4);
      
      const deltaX = ((e.touches[0].pageX + e.touches[1].pageX)/2 - start.x) * 2;
      const deltaY = ((e.touches[0].pageY + e.touches[1].pageY)/2 - start.y) * 2;

      targetComponent.style.transform = `scale(${newScale}) translate(${deltaX}px, ${deltaY}px) `;
    }
  });

  targetComponent.addEventListener('touchend', () => {
    targetComponent.style.transform = '';
  });
};

```

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [February 18, 2025, 12:37pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/7 "2025-02-18T12:37:26Z")

</div>

> [@liam1](#):
>
> Just for reference I am trying to implement a pinch and zoom,

This isn’t going to go well using gateway-side properties. You’ll want to directly update the position of the elements in the client without relying on a gateway roundtrip.

> [@liam1](#):
>
> But considering what you just said, this native JavaScript run can update it directly and will persist while the page is open?

Yes, exactly. Run your script when your view/component starts, installing the event listeners. The event listeners will persist for the lifetime of the client.

Worth noting, there is a branch of Periscope with a work-in-progress pan/pinch/zoom container. You’d have to build it from source yourself.

> <https://github.com/mussonindustrial/embr/blob/dev/periscope-coordinate-canvas/modules/periscope/web/src/components/container/coordinate-canvas/CoordinateCanvas.tsx>

It’s about 75% done, but it’s been put on the back burner by other tools that are more immediately useful for me.

---

<div class="post-metadata">

**Author:** ![liam1](https://avatars.discourse-cdn.com/v4/letter/l/e68b1a/32.png) [@liam1](https://forum.inductiveautomation.com/u/liam1)\
**Post date:** [February 18, 2025, 3:58pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/8 "2025-02-18T15:58:16Z")

</div>

Okay this working,

`

```
js_function='''
	() => {
	    // State variables for transform properties
	    let currentScale = 1,
	      currentX = 0,
	      currentY = 0;
	    const sensitivity = 0.65,
	      minScale = 0.5,
	      maxScale = 4;
	
	    // Elements: canvasView is our main container.
	    // Its immediate child contains the inline dimensions.
	    const targetComponent = document.querySelector('#touchElement');
	    const canvasView = document.querySelector('#canvasView');
	    const target = targetComponent; // using the canvasView as target for events
	    const child = document.querySelector("#canvasView > .ia_container--primary.view");
	
	    
	
	    // Function to update the transform style of canvasView.
	    function updateTransform() {
	        canvasView.style.transition = "transform 100ms linear";
	        canvasView.style.transform = `scale(${currentScale}) translate(${currentX}px, ${currentY}px)`;
	    }
	
	    // A helper function that limits a value to a given [min, max] range.
	    function clamp(value, min, max) {
	      return Math.min(Math.max(value, min), max);
	    }
	  
	    // ------------------------------
	    // Touch Events for Pinch‑Zoom and Panning
	
	    let touchStart = { x: 0, y: 0, distance: 0 };
	
	    // Calculate the distance between two touch points.
	    function getDistance(t1, t2) {
	      const dx = t1.pageX - t2.pageX;
	      const dy = t1.pageY - t2.pageY;
	      return Math.hypot(dx, dy);
	    }
	
	
	    // On touchstart, if two fingers are placed then store the midpoint and distance.
	    target.addEventListener(
	      "touchstart",
	      (e) => {
	        if (e.touches.length === 2) {
	          const mid = getMidpoint(e.touches[0], e.touches[1]);
	          touchStart.x = mid.x;
	          touchStart.y = mid.y;
	          touchStart.distance = getDistance(e.touches[0], e.touches[1]);
	        }
	      },
	      { passive: false }
	    );
	
	    // On touchmove, update the scale and translation based on pinch movement.
	    target.addEventListener(
	      "touchmove",
	      (e) => {
	        if (e.touches.length === 2) {
	          e.preventDefault();
	          const mid = getMidpoint(e.touches[0], e.touches[1]);
	          const currentDistance = getDistance(e.touches[0], e.touches[1]);
	          if (!touchStart.distance) return;
	
	          const scaleDelta =
	            ((currentDistance / touchStart.distance) - 1) * sensitivity + 1;
	          currentScale = clamp(currentScale * scaleDelta, minScale, maxScale);
	
	          // Adjust translation based on how the midpoint has moved.
	          const dx = mid.x - touchStart.x;
	          const dy = mid.y - touchStart.y;
	          currentX += dx / currentScale;
	          currentY += dy / currentScale;
	
	          // Update starting values for the next event.
	          touchStart.x = mid.x;
	          touchStart.y = mid.y;
	          touchStart.distance = currentDistance;
	
	          updateTransform();
	        }
	      },
	      { passive: false }
	    );
	
	    // ------------------------------
	    // Pointer (mouse/stylus) Drag for Panning
	    target.addEventListener("pointermove", (e) => {
	      if (e.buttons > 0) {
	        e.preventDefault();
	        // Adjust translation relative to the current scale so pan speed stays consistent.
	        currentX += e.movementX / currentScale;
	        currentY += e.movementY / currentScale;
	        updateTransform();
	      }
	    });
	
	    // ------------------------------
	    // Wheel Event for Zooming
	    target.addEventListener("wheel", (e) => {
	      e.preventDefault();
	      const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
	      currentScale = clamp(currentScale * zoomFactor, minScale, maxScale);
	      console.log('wheel event');
	      updateTransform();
	    });
	  }
'''
	
system.perspective.runJavaScriptAsync(js_function)`

```

Is there of passing up the style transform state changes back up to the ignition scope so I can use the event changes for some other scripting functionality. Is that what the call back is for ? Or will that call back only run once on startup?

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [February 18, 2025, 4:48pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/9 "2025-02-18T16:48:44Z")

</div>

> [@Using system.perspective.runJavaScriptAsync to get event.touches.length](https://forum.inductiveautomation.com/t/using-system-perspective-runjavascriptasync-to-get-event-touches-length/102468/5):
>
> Is that what the call back is for ? Or will that call back only run once on startup?

The callback runs after the JavaScript code completes, and is supplied with the return value of the JavaScript function. Not useful for your use case.

> [@Using system.perspective.runJavaScriptAsync to get event.touches.length](https://forum.inductiveautomation.com/t/using-system-perspective-runjavascriptasync-to-get-event-touches-length/102468/5):
>
> Is there of passing up the style transform state changes back up to the ignition scope so I can use the event changes for some other scripting functionality.

You can write the transform to component/view/page/session properties and then work with them from there. In a function called by `runJavaScriptAsync`, you have access to a global scoped object named `perspective` that has a `context` that you can use to access the `view`, `page`, or `session` that’s available.

Edit: looks like `runPerspectiveAsync` only has access to the `session` context currently.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [March 6, 2025, 5:23am UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/11 "2025-03-06T05:23:20Z")

</div>

# Json View Component

The [latest release of Periscope (0.5.4)](https://github.com/mussonindustrial/embr/releases/tag/%40embr-modules%2Fperiscope%400.5.4) now includes a `Json View` component.

This component allows you to render a view by providing `view.json` content into the component's properties.

> **Example View JSON**
>
> ```python
> {
> "custom": {
> "text": "Neat!",
> "type": "ia.input.text-field"
> },
> "params": {},
> "propConfig": {
> "custom.text": {
> "binding": {
> "config": {
> "bidirectional": true,
> "path": "/root/Text/TextField.props.text"
> },
> "type": "property"
> },
> "persistent": true
> },
> "custom.type": {
> "binding": {
> "config": {
> "bidirectional": true,
> "path": "/root/Type/TextField.props.text"
> },
> "type": "property"
> },
> "persistent": true
> }
> },
> "props": {
> "defaultSize": {
> "width": 325
> }
> },
> "root": {
> "children": [
> {
> "meta": {
> "name": "JsonView"
> },
> "position": {
> "basis": "300px"
> },
> "propConfig": {
> "props.viewJson.root.children[1].type": {
> "binding": {
> "config": {
> "path": "view.custom.type"
> },
> "type": "property"
> },
> "persistent": true
> },
> "props.viewParams.text": {
> "binding": {
> "config": {
> "bidirectional": true,
> "path": "view.custom.text"
> },
> "type": "property"
> }
> }
> },
> "props": {
> "viewJson": {
> "custom": {},
> "params": {
> "text": "Text"
> },
> "propConfig": {
> "params.text": {
> "paramDirection": "inout",
> "persistent": true
> }
> },
> "props": {},
> "root": {
> "children": [
> {
> "meta": {
> "name": "Label"
> },
> "position": {
> "basis": "auto"
> },
> "propConfig": {
> "props.text": {
> "binding": {
> "config": {
> "path": "view.params.text"
> },
> "type": "property"
> }
> }
> },
> "type": "ia.display.label"
> },
> {
> "meta": {
> "name": "TextField"
> },
> "position": {
> "basis": "auto"
> },
> "propConfig": {
> "props.text": {
> "binding": {
> "config": {
> "bidirectional": true,
> "path": "view.params.text"
> },
> "type": "property"
> }
> }
> },
> "props": {
> "style": {
> "minHeight": "50px"
> }
> },
> "type": "ia.input.text-field"
> }
> ],
> "meta": {
> "name": "root"
> },
> "props": {
> "direction": "column",
> "style": {
> "gap": "1rem"
> }
> },
> "type": "ia.container.flex"
> }
> }
> },
> "type": "embr.periscope.embedding.json-view"
> },
> {
> "children": [
> {
> "meta": {
> "name": "Label"
> },
> "position": {
> "basis": "50px"
> },
> "props": {
> "text": "Text"
> },
> "type": "ia.display.label"
> },
> {
> "meta": {
> "name": "TextField"
> },
> "position": {
> "grow": 1
> },
> "props": {
> "text": "Neat!"
> },
> "type": "ia.input.text-field"
> }
> ],
> "meta": {
> "name": "Text"
> },
> "position": {
> "basis": "32px"
> },
> "type": "ia.container.flex"
> },
> {
> "children": [
> {
> "meta": {
> "name": "Label"
> },
> "position": {
> "basis": "50px"
> },
> "props": {
> "text": "Type"
> },
> "type": "ia.display.label"
> },
> {
> "meta": {
> "name": "TextField"
> },
> "position": {
> "grow": 1
> },
> "props": {
> "text": "ia.input.text-field"
> },
> "type": "ia.input.text-field"
> }
> ],
> "meta": {
> "name": "Type"
> },
> "position": {
> "basis": "32px"
> },
> "type": "ia.container.flex"
> }
> ],
> "meta": {
> "name": "root"
> },
> "props": {
> "direction": "column"
> },
> "type": "ia.container.flex"
> }
> }
> 
> ```

## But Why?

This started as an exercise in trying to understand the full process of view loading. Then, I realized this component was possible and that I already had solved the hardest parts during the development of the `Embedded View +` component.

## What Does This Enable?

This component allows you to programmatically generate views from _within_ Ignition. You've always been able to modify the `view.json` files from an external editor, but you don't get any visual feedback until you import them; this component re-renders as the `viewJson` property changes. The idea is:

1. Generate the `viewJson` structure however you want (scripting, bindings, database queries, manually, etc.).
2. Copy the `viewJson` property and paste it to a view using the `Paste JSON` option.
3. Enjoy your new view.

The component does work in the client, you could generate the `viewJson` at runtime to have completely dynamic view structure. I don't think this is a good idea, but I can't stop you, so do whatever you want 🤷‍♂️.

If you do decide you want to use dynamic views in the client, you can provide parameters using the `viewParams` property. Passing parameters works like any other view (input, output, in-out, etc.).

### Known Issues

1. The property tree does not allow you to use periods in object keys. This causes an issue when defining a component's `propConfig` properties. (I'd like to recommend that IA drop this limitation, as well as the limitation against string numbers as keys. JSON keys are strings, and strings containing periods and numbers are strings).

## Download

> **[Release @embr-modules/periscope@0.5.4 · mussonindustrial/embr](https://github.com/mussonindustrial/embr/releases/tag/%40embr-modules%2Fperiscope%400.5.4)**
>
> Patch Changes
> 
> a01a32e: Add View Json component. Allows rendering a view from a view.json schema, enabling dunamic view creation from inside of Ignition.
> Updated dependencies \[a01a32e\]
> Updated depe...

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [March 6, 2025, 12:46pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/12 "2025-03-06T12:46:05Z")

</div>

> [@bmusson](#):
>
> I'd like to recommend that IA drop this limitation

I will argue against lifting this limitation. The further you get from keeping keys valid identifiers in all contexts, the further you get from any hope of intuitive interoperability.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [March 6, 2025, 1:51pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/13 "2025-03-06T13:51:01Z")

</div>

> [@pturmel](#):
>
> The further you get from keeping keys valid identifiers in all contexts, the further you get from any hope of intuitive interoperability.

That’s exactly why I think they should drop it 🙃.

If I can make both a Python dictionary a JavaScript object with keys (and tags with the same name):

```json
{
  “key”: “value”,
  “20”: “key is a string”,
  “prop.test”: “key is still a string”
}

```

then I would say it’s _more surprising_ that they are invalid in this context.

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [March 6, 2025, 1:54pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/14 "2025-03-06T13:54:53Z")

</div>

Assign that dictionary to a perspective custom property and see what happens.

What do you get from `JSON.parse()` of that in a browser?

If you **could** make that work in Perspective, how would you expect to access `prop.test` in a script?

{ It is my not-so-humble opinion that the square bracket operator is **not** intuitive for object properties. }

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [March 6, 2025, 5:52pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/15 "2025-03-06T17:52:02Z")

</div>

> [@pturmel](#):
>
> Assign that dictionary to a perspective custom property and see what happens.

 ![java_ljOLmwWXOv](https://us1.discourse-cdn.com/inductiveautomation/original/3X/d/e/de9ea4022c87c55a119d9d7dda2aac98d118574d.png)

The property tree complains, but works with expression bindings.

It does not work with property bindings. When you select using the property editor, it does apply square bracket notation (which should work IMO), but it only evaluates to an invalid path syntax error.

 ![java_BQ9DyzqA10](https://us1.discourse-cdn.com/inductiveautomation/original/3X/9/9/991f6933fcf6e21b8c35839287dcaf327bbb79b1.png)

> [@pturmel](#):
>
> What do you get from `JSON.parse()` of that in a browser?

```js
// JavaScript
const string = `{
  "key": "value",
  "20": "key of numeric string",
  "prop.test": "key with a period"
}`

const obj = JSON.parse(string)

console.log(obj["20"]) // key of numeric string
console.log(obj["prop.test"]) // key with a period

```

```python
# Python
string = `{
  "key": "value",
  "20": "key of numeric string",
  "prop.test": "key with a period"
}`

obj = system.util.jsonDecode(string)

print obj["20"] # key of numeric string
print obj["prop.test"] # key with a period

```

> [@pturmel](#):
>
> If you **could** make that work in Perspective, how would you expect to access `prop.test` in a script?

The exact same way that it works now.

 ![java_Ex4MQz4QEg](https://us1.discourse-cdn.com/inductiveautomation/original/3X/d/b/dbf289e086356bdeb096f4c150bae70192102bff.png)

> [@pturmel](#):
>
> { It is my not-so-humble opinion that the square bracket operator is **not** intuitive for object properties. }

Oh I totally agree with you, I'm not saying that I want to start using weird and unusual keys everywhere. The dot-accessible syntax is nice.

But, string keys are string keys, and it should be possible to use strings containing periods and numbers, even if the dot-access syntax breaks.

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/8/a/8aa14db1f336d5bd040441c5b0089a1bc865d546.jpeg)  
(I kid.)

Edit: It's worth noting that I had to paste the object structure in. The designer does not let you manually add the "invalid" keys.

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [March 6, 2025, 6:17pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/16 "2025-03-06T18:17:28Z")

</div>

I'm a big fan of **not** breaking the dot operator, and the status quo helps not break the dot operator.

What concrete use-case do you have, **now** , that requires non-identifier keys?

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [March 6, 2025, 7:02pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/17 "2025-03-06T19:02:50Z")

</div>

> [@pturmel](#):
>
> What concrete use-case do you have, **now** , that requires non-identifier keys?

I'd like to use numeric keys ("10", "20", "50", "100") for defining gradients.

Like:

```javascript
{
  '10': '#222222',
  '20': '#444444',
  '50': '#aaaaaa',
  '100': '#ffffff'
}

```

---

<div class="post-metadata">

**Author:** ![YF129701](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/yf129701/32/60498_2.png) [@YF129701](https://forum.inductiveautomation.com/u/YF129701)\
**Post date:** [March 6, 2025, 8:07pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/18 "2025-03-06T20:07:35Z")

</div>

I ran into this just yesterday...

Something like this isn't possible currently in the designer:

```python
view.custom.lookup = [
    "0": {...},
    "1": {...}
]

```

There are situations where this would be useful for constant-time lookups when you know the index, but if I want to do that now, I do something like `"_0"` and then when I do the lookup, I prepend the `_` character.

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [March 6, 2025, 8:16pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/19 "2025-03-06T20:16:59Z")

</div>

For those cases, consider using lists of pairs (two-element lists). Those can be converted on the fly to more tolerate maps with my toolkit's `asMap()` expression function. The numeric-ish keys can remain actual numbers in that case.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [March 14, 2025, 10:37pm UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/20 "2025-03-14T22:37:37Z")

</div>

# Portal Component

> **[Release @embr-modules/periscope@0.6.0 · mussonindustrial/embr](https://github.com/mussonindustrial/embr/releases/tag/%40embr-modules%2Fperiscope%400.6.0)**
>
> Minor Changes
> 
> dde1698: Add Portal component.
> 
> Patch Changes
> 
> Updated dependencies \[4709891\]
> Updated dependencies \[dde1698\]
> 
> @embr-modules/periscope-web@0.6.0

This update adds a `Portal` component, that uses React's `createPortal` to add children wherever you want them.

This allows you to:

- Add elements _inside_ existing components/views.
- Decouple the design time structure of your views from the runtime structure of the HTML elements.
- Develop re-usable views from the _outside_ in.

## Examples

### Example, Basic Usage

This example demonstrates how the elements of the Portal are added to the target.

### Example, Alert Information

This example shows a portal configured to add a warning border around the target element by using absolute positioning on the Portal's child elements

### Example, Target `onMount` and `onUnmount`

This example uses the `onMount` and `onUnmount` callbacks to modify the properties of the target element, adding a border when mounted and removing a border when unmounted.

## Element Selection

The `element` property can either be:

1. A string containing the `domId` of the component/element you want to target.
2. A JavaScript arrow function that returns an `Element`, allowing you to specify _exactly_ where in the DOM you want to render your elements, even if it doesn't have an `domId`

---

<div class="post-metadata">

**Author:** ![Samuel\_Sueur](https://avatars.discourse-cdn.com/v4/letter/s/e36b37/32.png) [@Samuel\_Sueur](https://forum.inductiveautomation.com/u/Samuel_Sueur)\
**Post date:** [March 15, 2025, 10:36am UTC](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047/21 "2025-03-15T10:36:38Z")

</div>

Hi Ben,

I think your latest addition is interesting, but I'm kind of struggling to have a viable use case for it in the industry.

Could you perhaps give us real-life applications of when your Portal component would be handy ? (if you have examples, otherwise that's completely fine! More options for everyone to use and play with 🙂 )

[Next page](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047.md?page=2)
