Skip to main content
Default Gray Amethyst

Common errors

As Modes UI is garnering a larger user base, some users have reported and asked about some issues with integrating Modes UI in their project. As projects have vastly varying dependencies and configurations, there are many unforeseen issues that may arise when adding Modes UI as a dependency. This documentation page covers some of the frequest issues and errors that have come up, and potential solutions.

Component importing

Components can either be imported from the index

import { MOButton } from '@metsooutotec/modes-web-components/dist/react';

or as individiual component modules from

import MOButton from '@metsooutotec/modes-web-components/dist/react/button';

Some configurations of create-react-app and webpack give a warning message if components are lazy-loaded using the first method:

Critical dependency: the request of a dependency is an expression

This may be fixed by importing the components individually from their respective modules. Individual imports will also ensure that tree-shaking works as expected, potentially resulting in lower production bundle size.

Testing with React + Jest

As mentioned in the React integration section, Web Components can have some issues when running in a Node environment (not an actual browser). Web Component rendering and importing has some differences to standard React components, and Jest may give you these error messages:

Element type is invalid: expected a string (for built-in components) or a class/function
(for composite components) but got: undefined. You likely forgot to export your component
from the file it's defined in, or you might have mixed up default and named imports.
Error: Unable to import outside of a module

The solution to this issue can be found in the React integration - Transform ES Modules section. In short, Modes UI components are ES Modules, Jest expects CommonJS modules. Adding an exception to Jest’s transformIgnorePatterns option should solve this issue. Follow the steps in the page mentioned above to achieve this.

Icons not loading

When importing the Modes UI components and using them directly in HTML, the library will automatically import and find the icons to use. However, when using a bundled version, particularly in React, the library requires some additional configuration to be able to access the assets. The React integration briefly mentions this, but it is easy to miss. Essentially, Modes UI comes with a setBasePath function that needs to be executed to show where the assets are located. Another caveat is that setting the base path as follows:

import { setBasePath } from '@metsooutotec/modes-web-components';
setBasePath('@metsooutotec/modes-web-components/dist/assets');

will not work. The assets cannot be accessed from the node_modules folder, as they will not be part of the production build. In order to ensure Modes UI assets are properly accessed and shown on components such as MOSelect, the build script of your project must first copy the assets to an accessible folder. In React a good example of this is the public folder, and its relative path is also easy to access. A correct folder structure looks like this:

public
│
└───assets
│   │
│   └───icons
│   │    │   3D-rotate.svg
│   │    │   alarm-fill.svg
│   │    │   ...
│   └──-fonts
│       │
│       │───GT-Eesti-Pro-Text
│       │   | GT-Eesti-Pro-Text-Light.otf
│       │   | ...

In this case the public path should be set as in the React integration example:

// App.tsx
import { setBasePath } from '@metsooutotec/modes-web-components/dist/utilities/base-path';

// Set base path to the public directory
setBasePath(process.env.PUBLIC_URL);

Sidebar/dialog closing unexpectedly

As components such as mo-dialog, mo-sidebar and mo-dropdown share the same mo-after-hide, mo-hide, etc. custom events, these events can bubble up the tree, causing unexpected effects when listening to them. For example including an mo-dropdown inside either an mo-dialog or mo-sidebar can cause the dialog or sidebar to close when it closes. This is caused by event bubbling, which means the events emitted by a component will bubble up to its parent elements.

In this example, closing the dropdown on the mo-select will also close the dialog:

Option 1 Option 2 Option 3
Close
Open Dialog
<mo-dialog label="Are you sure you want to confirm this?" class="dialog-incorrect">
  <div>
    <mo-select hoist label="Choose option" required placeholder="Select one">
      <mo-option value="option-1">Option 1</mo-option>
      <mo-option value="option-2">Option 2</mo-option>
      <mo-option value="option-3">Option 3</mo-option>
    </mo-select>
  </div>
  <mo-button slot="footer" variant="primary">Close</mo-button>
</mo-dialog>

<mo-button>Open Dialog</mo-button>

<script>
  let isOpen = false;
  const dialog = document.querySelector('.dialog-incorrect');
  dialog.open = isOpen;
  const openButton = dialog.nextElementSibling;
  const closeButton = dialog.querySelector('mo-button[slot="footer"]');
  const openDialog = () => {
    dialog.setAttribute('open', '');
  };

  const closeDialog = () => {
    dialog.removeAttribute('open');
  };

  // incorrect, will close both
  dialog.addEventListener('mo-after-hide', () => {
    console.log('after hide');
    closeDialog();
  });

  openButton.addEventListener('click', openDialog);
  closeButton.addEventListener('click', closeDialog);
</script>

Adding an additional check to the mo-after-hide event listener can fix the issue:

<script>
  // incorrect, will close both
  dialog.addEventListener('mo-after-hide', () => {
    console.log('after hide');
    closeDialog();
  });

  // correct, will check if dialog is the source
  dialog.addEventListener('mo-after-hide', event => {
    if (event.target === dialog) {
      console.log('after hide');
      closeDialog();
    }
  });

  // also correct, checks if event originated from the target
  dialog.addEventListener('mo-after-hide', event => {
    if (event.eventPhase === Event.AT_TARGET) {
      console.log('after hide');
      closeDialog();
    }
  });
</script>

The select in this example will no longer cause the dialog:

Option 1 Option 2 Option 3
Close
Open Dialog
<mo-dialog label="Are you sure you want to confirm this?" class="dialog-correct">
  <div>
    <mo-select hoist label="Choose option" required placeholder="Select one">
      <mo-option value="option-1">Option 1</mo-option>
      <mo-option value="option-2">Option 2</mo-option>
      <mo-option value="option-3">Option 3</mo-option>
    </mo-select>
  </div>
  <mo-button slot="footer" variant="primary">Close</mo-button>
</mo-dialog>

<mo-button>Open Dialog</mo-button>

<script>
  let isOpen = false;
  const dialog = document.querySelector('.dialog-correct');
  dialog.open = isOpen;
  const openButton = dialog.nextElementSibling;
  const closeButton = dialog.querySelector('mo-button[slot="footer"]');
  const openDialog = () => {
    dialog.setAttribute('open', '');
  };

  const closeDialog = () => {
    dialog.removeAttribute('open');
  };

  // correct, will check if dialog is the source
  dialog.addEventListener('mo-after-hide', event => {
    if (event.target === dialog) {
      console.log('after hide');
      closeDialog();
    }
  });

  openButton.addEventListener('click', openDialog);
  closeButton.addEventListener('click', closeDialog);
</script>