Reusable Ui Template for V-Card / V-Window

I have a dashboard ui-template which is using v-card/v-window and works, however I am looking for a way to consolidate the card data into a single object to be used in the element. When I consolidate the card data I loose any line breaks I have with the current code. My goal is to make a reusable ui template where all I need to do is send the formatted data to the ui template

<template>
  <v-card class="panel" variant="tonal" rounded="xl">
    <v-window v-model="activeCardIndex" show-arrows>
      <v-window-item
        v-for="(card, index) in cardsData"
        :key="card.id"
        :value="index"      
      >        
        <div class="d-flex flex-column justify-center align-center h-100 mt-5">
          <v-card-title class="logo mt-n5">
            📡 {{ card.title }}
          </v-card-title>
          <span class="api-stat-value text-center">     
            <div>{{ card.ble }}</div>     <!-- card 0 -->
            <div>{{ card.server}}</div>
            <div>{{ card.switch_0}}</div> <!-- card 1 -->
            <div>{{ card.switch_1}}</div>
            <div>{{ card.switch_2}}</div>
            <div>{{ card.switch_3}}</div>
            <div>{{ card.uptime}}</div>   <!-- card 2 -->
            <div>{{ card.resetReason}}</div>
            <div>{{ card.availableUpdates }}</div>
            <div>{{ card.deviceId}}</div> <!-- card 3 -->
            <div>{{ card.gen}}</div>
            <div>{{ card.ip}}</div>

          </span>   
        </div>
      </v-window-item>
    </v-window>
  </v-card>
</template>

<script>
export default {
  // Data properties
  data() {
    return {
      // Use an index (0-based) instead of a 1-based index for the window
      activeCardIndex: 0, 
      // Define your unique data for each card in an array of objects
      cardsData: [
        { id: 1, },
        { id: 2, },
        { id: 3, },
        { id: 4, }
      ],
      // The original `msg` payload for dynamic updates (if needed)
       msg: { payload: { app: 'default' } }
    };
  },
  // Computed property to get the total number of cards
  computed: {
    length() {
      return this.cardsData.length;
    }
  },
  // Methods for interactions
  methods: {
    nextWindow() {
      // Logic to go to the next window, wrapping around if necessary
      // Note: We use 0-based indexing now.
      if (this.activeCardIndex === this.length - 1) {
        this.activeCardIndex = 0;
      } else {
        this.activeCardIndex++;
      }
    },
    prevWindow() {
      // Logic to go to the previous window, wrapping around if necessary
      if (this.activeCardIndex === 0) {
        this.activeCardIndex = this.length - 1;
      } else {
        this.activeCardIndex--;
      }
    },
    // Method to update a specific card dynamically
    updateCardTitle(index, newTitle) {
      if (this.cardsData[index]) {
        this.cardsData[index].title = newTitle;
      }
    }
  },
  mounted() {
    this.mounted = true;
    if (this.$socket) {
      this.$socket.on('msg-input:' + this.id, (msg) => {
        this.msg.payload = msg.payload;

        this.cardsData[0].title = `${msg.payload.app} Connect`
        this.cardsData[0].ble = `Ble${msg.payload.ble.enable === true ? '✅' : '⛔'} LAN${msg.payload.eth === true ? '✅' : '⛔'}  RPC${msg.payload.rpc === true ? '✅' : '⛔'} RPC Updates${msg.payload.rpc_ntf === true ? '✅' : '⛔'} Wifi${msg.payload.wifi === true ? '✅' : '⛔'}`
        this.cardsData[0].server = `MQTT ${msg.payload.mqttserver}${msg.payload.mqtt === true ? '✅' : '⛔'}`

        this.cardsData[1].title = `${msg.payload.app} Switch`
        this.cardsData[1].switch_0 = `SW ${msg.payload.switchId_0} ${msg.payload.switch_0_name === null ? 'Not in use' : msg.payload.switch_0_name} - Voltage${msg.payload.autorecover_voltage_errors === false ? '✅' : '⛔' + msg.payload.autorecover_voltage_errors}`
        this.cardsData[1].switch_1 = `SW ${msg.payload.switchId_1} ${msg.payload.switch_1_name === null ? 'Not in use' : msg.payload.switch_1_name}`
        this.cardsData[1].switch_2 = `SW ${msg.payload.switchId_2} ${msg.payload.switch_2_name === null ? 'Not in use' : msg.payload.switch_2_name}`
        this.cardsData[1].switch_3 = `SW ${msg.payload.switchId_3} ${msg.payload.switch_3_name === null ? 'Not in use' : msg.payload.switch_3_name}`       

        this.cardsData[2].title = `${msg.payload.app} General`
        this.cardsData[2].uptime= `Up Time: ${Math.round(msg.payload.uptime / 60 / 1440)} days  - Last Update: ${msg.payload.time}`
        this.cardsData[2].resetReason= `Last Reset: ${msg.payload.resetreason} - Restart Required: ${msg.payload.restart_required === false ? 'No' : 'Yes'}`
        this.cardsData[2].availableUpdates= `Available Updates: ${msg.payload.availableUpdates === undefined ? 'None' : msg.payload.availableUpdates}`

        this.cardsData[3].title = `${msg.payload.app} Device`
        this.cardsData[3].deviceId = `ID: ${msg.payload.device_id}`
        this.cardsData[3].gen= `Model: ${msg.payload.model} - Gen: ${msg.payload.gen} - Ver: ${msg.payload.ver}`
        this.cardsData[3].ip= `Ip: ${msg.payload.ip} - MAC: ${msg.payload.mac}`
        

      });
    }
  },
};
</script>

<style scoped>

@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400..900&display=swap');

v-card-text-subtitle-2 {
font-size: 1rem; /* Or use em, rem, px */
/* For responsive changes: */
/* @media (max-width: 600px) { font-size: 0.875rem; } */
}

.panel {
border: 1px solid rgb(0 255 136 / 45%); //0.5px solid rgb(0 255 136, 0.5); /* You can replace #000 with any color name or hex code */
// padding: 2px; /* Optional: add some padding so content isn't touching the border */
// background: linear-gradient(135deg, #0a0f1a 0%, #1a1f2e 50%, #0d1117 100%);
background: rgba(0, 255, 136, 0.02);
}

.logo {
font-family: 'Orbitron', sans-serif;
font-size: 1.8rem;
font-weight: 900;
background: linear-gradient(135deg, #00ff88, #00ccff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
color: #00ff88;
}

.api-stat-value {
  font-family: 'JetBrains Mono', monospace;
  font-size: 1.2rem;
  color: #00ccff;
}

.v-window__left, .v-window__right {
  background-color: rgba(0, 255, 136, 0.07) !important; /* Example: Semi-transparent black */
  color: white !important; /* Change icon color */
  position: absolute;
  top: 25%;
  transform: translateY(-50%);
  z-index: 10;
}

.v-window__left {
  left: 10px; /* Adjust from default 0 */
}

.v-window__right {
  right: 10px; /* Adjust from default 0 */
}

</style>

You can put the ui-template in a subflow and pass the group to the subflow meaning you write the ui-template once then re-use it as many times as needed

  1. add an env var for group

  2. select that in the template node

  3. pass/select a group in on your instances

I see where you are going and am thinking how I can use this. I will take a look.

Note though that dashboard subflows only work well if the subflow is the only thing in the group, due to Unable to position a subflow containing a ui-node on the dashboard · Issue #710 · FlowFuse/node-red-dashboard · GitHub