next.jssvg

Dynamic SVG Components within a dynamic SVG component. Renders one component for all


Overivew: The user can click on a AVpanel from the list and the panelviewer is called to display that panel svg along with the various labels/modules.

In theory, this is working perfectly. It loads the panel, adds the module labels with the text/bg colors. Below is an image of the panel component with the labels. Below the labels are where the module componenets are rendered.

Image of panel component

The issue I have is when it comes to adding the module components to the panel component. If the modules are all HDMI, they get added. If they are all Cat6 they get added. It's when there are 2 types of modules. If it's a HDMI module in shelf03modD, it renders this component for all modules. And the same if it's Cat6. Whichever one is last in the list, that component gets rendered for all.

AVPanels.ts

This file is used as a catalog/template for the panels/modules. It's used to create and select the imageComponent for the panelviewer & modules.

import dynamic from 'next/dynamic';

interface ModuleProps { 
x?: number;
y?: number;
model?: React.ComponentType<ModuleProps>;
}
interface PanelComponentProps { 
shelf01ModuleALabel?: string;
shelf01ModuleATextColor?: string;
shelf01ModuleABackgroundColor?: string;
shelf01ModuleAModel?: React.ComponentType<ModuleProps>;
}
export interface PanelTemplate {
imageComponent?: React.ComponentType<PanelComponentProps>;
}
export interface ModuleTemplate {
imageModuleComponent?: React.ComponentType<ModuleProps>;
}
export const genericPanels: PanelTemplate[] = [ {
imageComponent: dynamic(() => import('@/assets/svg/panels/1u/Generic1u4ModPanel').then(mod => mod.default as React.ComponentType<PanelComponentProps>)),
}]
export const avModules: ModuleTemplate[] = [{
imageModuleComponent: dynamic(() => import('@/assets/svg/modules/GenericHDMImodule').then(mod => mod.default as React.ComponentType<ModuleProps>)),
}]

PanelViewer.tsx

The getModuleDetails get's the props for the components to use.

const getModuleDetailsForShelf = (shelfPosition: string) => {
    if (!genericPanel) return {};

    const shelf = genericPanel.fibrePanelShelves.find(
      (s) => s.shelfPosition === shelfPosition
    );
    if (!shelf || shelf.genericPanelModules.length === 0) return {};

    return shelf.genericPanelModules.reduce((acc, module) => {
      const shelfNum = shelfPosition.split("_")[1].padStart(2, "0");
      const position = module.modulePosition;
      const moduleTemplate = getModuleTemplateByModel(module.moduleModel);

      const label = `shelf${shelfNum}Module${position}Label`;
      const txtColor = `shelf${shelfNum}Module${position}TextColor`;
      const bgColor = `shelf${shelfNum}Module${position}BackgroundColor`;
      const model = `shelf${shelfNum}Module${position}Model`;

      const textColor = module.moduleLabel.includes("HDMI")
        ? "green"
        : module.moduleLabel.includes("Cat6")
        ? "black"
        : "black";
      const backgroundColor = module.moduleLabel.includes("HDMI")
        ? "blue"
        : module.moduleLabel.includes("Cat6")
        ? "white"
        : "transparent";

      return {
        ...acc,
        [label]: module.moduleLabel,
        [txtColor]: textColor,
        [bgColor]: backgroundColor,
        [model]: moduleTemplate?.imageModuleComponent,
      };
    }, {});
  };

  const genericPanelTemplate = getPanelTemplateByModel(
      genericPanel.genericPanelModel
    );

  const ImageComponent = fibrePanelTemplate?.imageComponent;
  
    return (
      <div className="h-full w-full overflow-y-auto p-4 ease-linear">
        <div className="w-full">
          {image === "show" && ImageComponent && (
            <ImageComponent
              {...getModuleDetailsForShelf("Shelf_01")}
              {...getModuleDetailsForShelf("Shelf_02")}
              {...getModuleDetailsForShelf("Shelf_03")}
            />
          )}

GenericPanel.tsx

This is the panel SVG component

"use client";

interface ModuleProps {
  x: number;
  y: number;
  model?: React.ComponentType<ModuleProps>;
}

const ModuleRenderer = ({ x, y, model }: ModuleProps) => {
  if (!model) {
    return null;
  }

  const ModuleComponent = model;

  return <ModuleComponent x={x} y={y} model={model} />;
};

interface GenericPanelProps {
  shelf01ModuleALabel?: string;
  shelf01ModuleATextColor?: string;
  shelf01ModuleABackgroundColor?: string;
  shelf01ModuleAModel?: React.ComponentType<ModuleProps>;
}
const Generic1u4ModPanel = ({
  shelf01ModuleAModel,
}: GenericPanelProps) => {
  return

//This contains the SVG code for the panel. Then in each each section for the modules contains this... 

<g id="s1mAlabel" transform="translate(35.163 -14.93)">
  <path
    id="path84"
    d="M0 66.642h80.514v11.311H0z"
    className="st11"
    fill={shelf01ModuleABackgroundColor}
  />
  <text
    id="text84"
    x="40.257"
    y="73.5"
    className="st12"
    fill={shelf01ModuleATextColor}
  >
    {shelf01ModuleALabel}
  </text>
</g>
<g id="s1mA" transform="translate(35.163 -2.444)">
  <ModuleRenderer model={shelf01ModuleAModel} x={0} y={30.26} />
</g>

GenericHDMImodule.tsx

Each module has this added to it...

"use client";

interface ModuleProps {
  x?: number;
  y?: number;
}

const GenericHDMImodule = ({ x = 0, y = 0 }: ModuleProps) => {
  return (
    <g transform={`translate(${x} ${y})`} height={11.31} width={80.51}>

The clue is that the last module component in the array list get's rendered for all if there are 2 or more types of module in a panel. I had the same issue when I started with the labels. I resolved this by giving each label it's own prop(ie shelf01moduleAlabel, shelf01moduleBlabel). But this does not work for rendering the actual module components. It still uses the last one in the list and renders it for all modules because these are dynamic components. I just can't work out how to give each position a unique key for these to be rendered.


Solution

  • So much easier with modern standard Web Components

    <script>
      class BaseLinkBox extends HTMLElement {
        constructor() {
          const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
          super().attachShadow({ mode: 'open' })
            .append(
              createElement('style', {
                textContent: `div{width:150px;height:40px;font:16px Arial;background:var(---bgcolor,beige);
                                  display:flex;align-items:center;justify-content:center}`
              }),
              this.div = createElement('div', {
                innerHTML: "<slot/>"
              }));
          this.onclick = (evt) => alert("clicked " + this.nodeName);
        }
      }
      customElements.define('white-link-box', class extends BaseLinkBox {});
      customElements.define('blue-link-box', class extends BaseLinkBox {
        constructor() {
          super();
          this.div.style.backgroundColor = 'blue';
          this.div.style.color = 'white';
        }
      });
    </script>
    <style>
      av-panel { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px }
    </style>
    <av-panel>
      <white-link-box>Cat6 link to</white-link-box>
      <blue-link-box>HDMI link to</blue-link-box>
      <white-link-box>Cat6 link to</white-link-box>
      <blue-link-box>HDMI link to</blue-link-box>
      <white-link-box>Cat6 link to</white-link-box>
      <blue-link-box>HDMI link to</blue-link-box>
      <white-link-box>Cat6 link to</white-link-box>
      <blue-link-box>HDMI link to</blue-link-box>
    </av-panel>