Errors while using a custom component from a module

Hi, I am working on several projects with common UI elements and I am trying to build the UI using the Ui API and constructing elements within a component. I can move this component file ts file to a module and I can then add it to an element in the Studio editor but when I run it I get the following errors.

Unhandled promise rejection: Error: No attribute registered with name: ui-popup-test
    at Xp runtime.js
    at (anonymous) runtime.js
    at (anonymous) runtime.js
    at $R runtime.js
    at Ab runtime.js
    at (anonymous) runtime.js
    at Q runtime.js
    at Object.loadSpace runtime.js
Unhandled promise rejection: Error: No space hook set
    at Object.getActiveSpace runtime.js
    at M dev8.js
    at (anonymous) dev8.js
    at c dev8.js
    at Generator._invoke dev8.js
    at Generator.next dev8.js
    at j dev8.js
    at a dev8.js

What is the best way to export a component from a module? Are there any working examples of components used via modules I can work off of?

Thank you!

You can see on this sample project component that it exports the component for use in other places:

Usage:

Thank you, I see that working with classes within the same project scope works well by this example. My issue is with trying to access the component from within a module.

I have the export listed at the end of the file and within my module.js, however, I still get an Unhandled promise rejection error. Is there any further export or import steps I need to be aware of?

Forgive me also, my js is rusty.

Thank you!

Can you share the component with me?

For sure, here is an example of a component that builds some UI. My thought was that by building the UI via script and putting the script into a module, I can avoid copy pasting it into each project and change tweak it in one place.

import * as ecs from '@8thwall/ecs'

// Default background image for popup
const popupBgImage = require('../assets/img/popup_bg.png')
const closeButtonImage = require('../assets/img/close_button.png')

ecs.registerComponent({
  name: 'ui-popup',
  schema: {
    popupId: ecs.i32,
    contentImageUrl: ecs.string,  // URL for the image
    contentText: ecs.string,      // Text to display in the popup
    ctaText: ecs.string,          // Label for the call-to-action button
    ctaEvent: ecs.string,         // Event name to dispatch on CTA click
  },
  schemaDefaults: {
    popupId: 0,
    contentImageUrl: '',
    contentText: 'Your popup message here',
    ctaText: 'Take Action',
    ctaEvent: 'popupAction',
  },

  add: (world, component) => {
    const {eid} = component
    const {popupId, contentImageUrl, contentText, ctaText, ctaEvent} = component.schema

    // helper functions for show/hide
    const showPopup = () => ecs.Ui.set(world, eid, {display: 'flex'})
    const hidePopup = () => ecs.Ui.set(world, eid, {display: 'none'})

    //  Base popup overlay (initially hidden)
    ecs.Ui.set(world, eid, {
      type: 'overlay',
      width: '100%',
      height: '584px',
      image: popupBgImage,
      display: 'none',
      position: 'absolute',
      // top: '9%',
      flexDirection: 'column',
      alignItems: 'center',
    })

    // Content container (centers inner elements and constrains width)
    const content = world.createEntity()
    ecs.Ui.set(world, content, {
      type: 'overlay',
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      justifyContent: 'center',
      padding: '120px 80px 80px',
      width: '100%',
      height: '100%',
    })
    world.setParent(content, eid)

    // Configurable content image
    if (contentImageUrl) {
      const img = world.createEntity()
      ecs.Ui.set(world, img, {
        type: 'overlay',
        image: contentImageUrl,
        width: '100px',
        height: '100px',
      })
      world.setParent(img, content)
    }

    // 4) Text block (configurable)
    const textBlock = world.createEntity()
    ecs.Ui.set(world, textBlock, {
      type: 'overlay',
      text: contentText,
      fontSize: 16,
      color: '#FFFFFF',
      marginTop: '12px',
      marginBottom: '12px',
      width: '100%',
    })
    world.setParent(textBlock, content)

    // Call-to-action button (configurable)
    if (ctaText && ctaEvent) {
      const cta = world.createEntity()
      ecs.Ui.set(world, cta, {
        type: 'overlay',
        text: ctaText,
        width: '100%',
        height: '40px',
        background: '#FFFFFF',
        color: '#000000',
        borderRadius: 10,
        backgroundOpacity: 1.0,
        alignItems: 'center',
        justifyContent: 'center',
      })
      world.setParent(cta, content)
      world.events.addListener(cta, 'click', () => {
        console.log(ctaText, 'clicked')
        world.events.dispatch(world.events.globalId, ctaEvent, {popupId})
      })
    }

    // Close button (centered with circular semi-transparent background)
    const closeBtn = world.createEntity()
    ecs.Ui.set(world, closeBtn, {
      type: 'overlay',
      image: closeButtonImage,
      width: '36px',
      height: '36px',
      position: 'absolute',
      bottom: '62px',
      alignItems: 'center',
      justifyContent: 'center',
    })
    world.setParent(closeBtn, content)
    world.events.addListener(closeBtn, 'click', hidePopup)

    //  Global event listeners
    world.events.addListener(world.events.globalId, 'openPopup', (e) => {
      console.log('ui-popup received openPopup for', e.data.popupId)
      if (e.data && e.data.popupId === popupId) showPopup()
    })
    world.events.addListener(world.events.globalId, 'hidePopup', (e) => {
      console.log('ui-popup received hidePopup for', e.data.popupId)
      if (e.data && e.data.popupId === popupId) hidePopup()
    })

    // Debug: show popup
    showPopup()
  },

  remove: (world, component) => {
    // Cleanup if needed
  },
})

The issue is you’re using require. You should never use require, only the url returns from ecs.assets.load OR pass in the asset path directly to whereever you need it in a component schema.

Like this: Studio: Scavenger Hunt | 8th Wall | 8th Wall

1 Like

I’ve tried some more basic tests and still not getting it to compile.

First I tried just adding this component which lives in my ui-module

import * as ecs from '@8thwall/ecs'  // This is how you access the ecs library.

const TestUI = ecs.registerComponent({
  name: 'Test UI',
  // schema: {
  // },
  // schemaDefaults: {
  // },
  // data: {
  // },
  add: (world, component) => {
    const {eid} = component

    // 1) Host entity becomes the bottom nav container
    ecs.Ui.set(world, eid, {
      type: 'overlay',                 // keep nav on top
      width: '100%',                   // full width of screen
      height: '100px',                 // nav height
      background: '#FF0000',           // same dark semi-transparent as top nav
      backgroundOpacity: 0.5,
      display: 'flex',                 // flex layout for children
      flexDirection: 'row',            // horizontal alignment
      justifyContent: 'space-evenly',  // left and right
      alignItems: 'center',            // vertical centering
      position: 'absolute',            // absolute positioning for bottom alignment
      bottom: '0px',                   // align at bottom of screen
      padding: '0 24px',               // horizontal padding inside nav
    })
  },
  // tick: (world, component) => {
  // },
  // remove: (world, component) => {
  // },
  stateMachine: ({world, eid, schemaAttribute, dataAttribute}) => {
    ecs.defineState('default').initial()
  },
})

export {TestUI}

It shows up in the component list in the studio and when added to an element it gives me this error still.

Unhandled promise rejection: Error: No attribute registered with name: Test UI

Then I tried to create a local component and use the set function like in the example.

import * as ecs from '@8thwall/ecs'  // This is how you access the ecs library.
import TestUI from 'ui-module'

const TestUIComponent = ecs.registerComponent({
  name: 'test-ui-component',
  // schema: {
  // },
  // schemaDefaults: {
  // },
  // data: {
  // },
  add: (world, component) => {
    const {eid} = component

    TestUI.set(world, eid, component)
  },
  // tick: (world, component) => {
  // },
  // remove: (world, component) => {
  // },
  // stateMachine: ({world, eid, schemaAttribute, dataAttribute}) => {
  //   ecs.defineState('default').initial()
  // },
})

export {TestUIComponent}

Adding this I have another error

Unhandled promise rejection: TypeError: ui_module_default.a.set is not a function
    at add test-ui-component.js:18:11
    at (anonymous) runtime.js
    at (anonymous) runtime.js
    at u runtime.js
    at w runtime.js
    at Object.loadScene runtime.js
    at Object.init runtime.js

I feel like I’m close with the second attempt. It looks like the import is not finding the TestUI class? Do you see anything I’m doing wrong here?

I’ve added the export also to the module.js file.