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.