Перейти к содержанию

Разработка логики работы визуальной составляющей блока (component.js)

В js-файле находится код визуализатора.

Код должен содержать класс CustomVismind<название компонента> с методами:

  • initProps - для инициализации компонента
  • emit - для вызова событий

Жизненный цикл Web компонентов

  1. Создание компонента Создаётся экземпляр класса пользовательского элемента.
  2. Вызывается метод constructor().
  3. Можно инициализировать внутренние поля и состояние.

  4. Добавление в DOM Что вызывается:

  5. connectedCallback()

Что делает компонент: - Выполняет основную инициализацию, требующую присутствия элемента в DOM. - Добавляет обработчики событий, подписки и визуальные элементы. - В контексте визуализатора: - Настраивает визуальную часть. - Подключает события из перечня, полученного с api.

  1. Инициализация данных (initProps) Когда вызывается: Сразу после загрузки скрипта и создания HTML-элемента (после connectedCallback).

Что делает метод: - Метод initProps(props) вызывается системой визуализаторов для передачи данных, полученных из api.
- Содержит параметры, необходимые для настройки или отрисовки. - Может вызываться: - При первой инициализации. - При обновлении данных, если тег визуализатора не менялся.

Поведение: - Если тег не изменился → повторный вызов initProps(props). - Если тег изменился → старый элемент удаляется (disconnectedCallback), затем загружается новый скрипт и создаётся новый элемент.

  1. Удаление из DOM Когда происходит: При удалении визуализатора со страницы или пересоздании с другим тегом.

Что вызывается: - disconnectedCallback()

Что делает компонент:
- Отписывается от событий. - Очищает ресурсы и таймеры. - Подготавливается к удалению из памяти.

Вызов событий из component.js

Когда пользовательский блок выполняет действие (например, нажатие кнопки или изменение какого-то значения), создается CustomEvent, который будет иметь название события (указанное в манифесте) и дополнительные данные, переданные через detail.

После того как событие сгенерировано на фронте, оно может быть передано через API на сервер. Это обычно происходит с помощью JavaScript-функции dispatchEvent(). Пример:

const event = new CustomEvent('OnCheckDate', {
  detail: {
    data: {
        someData: value,
        anotherData: moreValue
    }
  }
});
element.dispatchEvent(event);

Примеры

Пример
class CustomVismindGeocoderComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.state = {
            theme: 'light',
            lang: 'en'
        };
        this.props = {};
        this.current = 0;
        this.intervalId = null;
    }

    async initProps(props) {
      this.createTableFromArray(props, "VismindGeocoderTable");
    }   

    createTableFromArray(data, tableId) {
      var table = this.shadowRoot.getElementById(tableId);
      if (!table) {
          const html = `
              <style>
                :host {
                  display: block;
                  padding: 1em;
                  border-radius: 8px;
                  font-family: sans-serif;
                }
                .light {
                  background: #fff;
                  color: #000;
                }
                .dark {
                  background: #2c3e50;
                  color: #ecf0f1;
                }
                img { max-width: 300px; border-radius: 8px; }
              </style>
              <div class="${this.state.theme}">
                <button id="VismindGeocoderButton">Click Me</button>
                <table id="${tableId}"></table>
              </div>
          `;

        console.log("Table with id '" + tableId + "' not found.");

        const tmp = document.createElement('div');
        tmp.innerHTML = html;
        const tpl = tmp.querySelector('template') || tmp;
        const content = tpl.content ? tpl.content.cloneNode(true) : tpl.cloneNode(true);

        this.shadowRoot.innerHTML = '';
        this.shadowRoot.appendChild(content);

        table = this.shadowRoot.getElementById(tableId);
      }

      // Очистить таблицу перед заполнением (если нужно)
      table.innerHTML = "";

      var self = this;
      table.addEventListener("click", function(event) {
        const clickedElement = event.target;

        if (clickedElement.tagName === "TD") {
          console.log("Clicked cell content: " + clickedElement.innerHTML);
        } else if (clickedElement.tagName === "TR") {
          console.log("Clicked on a row.");
        }

        self.emit('OnClick', { data: {address: clickedElement.innerHTML} });
      });

      // Добавить заголовки столбцов, если данные - массив объектов
      if (data.length > 0 && typeof data[0] === 'object') {
        const headerRow = table.insertRow();
        const headers = Object.keys(data[0]);
        headers.forEach(headerText => {
          const header = document.createElement("th");
          const text = document.createTextNode(headerText);
          header.appendChild(text);
          headerRow.appendChild(header);
        });
      }

      data.forEach(rowData => {
        const row = table.insertRow();
        if (typeof rowData === 'object') {
          // Если данные - объект, итерируем по ключам
          Object.values(rowData).forEach(cellData => {
            const cell = row.insertCell();
            cell.innerHTML = cellData;
          });
        } else {
          // Иначе обрабатываем как простой массив
          const cell = row.insertCell();
          cell.innerHTML = rowData;
        }
      });

      const button = this.shadowRoot.getElementById("VismindGeocoderButton");
      button.addEventListener("click", () => {
        console.log("Clicked button");
        self.emit('OnCheckDate', { data: {data: "data"}, callback: this.setButtonName.bind(this) });
      });
    }

    emit(name, detail) {
        this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true}));
    }

    setButtonName(data) {
        console.log("exec setButtonName");
        const tmp = this.shadowRoot.getElementById("VismindGeocoderButton");
        tmp.innerHTML = data.now;
    }
}

customElements.define('vismind-geocoder', CustomVismindGeocoderComponent);

Важная информация

В последней строчке скрипта у метода customElements.define передаются teg-пользовательского блока и название класса, описанного в этом скрипте

Пример на Vue.js 3 + vite

Реализацию простого примера на vue.js можно глянуть в репозитории библиотеки блоков. Разберем визуализатор vismind-checkbox.

Пример файла manifest.json
{
  "name": "Checkbox с подписью",
  "tag": "vismind-checkbox",
  "desc": "",
  "docLink": "/Документация_ЦП_УвП/12_Работа_пользователей/Библиотека_блоков/Пользовательские_блоки/Блок_Checkbox_с_подписью/",
  "package": "vmcustom",
  "module": "python",
  "class_name": "CUserCustomComponent",
  "version": "1.0.0",
  "visualizer": 1,
  "type": "custom",
  "settings": [
      {
          "id": "checkBoxSettings",
          "name": "Настройки checkbox",
          "desc": "",
          "type": "json",
          "elements": [
              {
                  "name": "Текст подписи",
                  "id": "labelText",
                  "type": "string",
                  "desc": "",
                  "default": "text",
                  "mandatory": 0
              },
              {
                  "name": "Отметка по умолчанию",
                  "id": "checked",
                  "type": "bool",
                  "desc": "",
                  "default": 0,
                  "mandatory": 0
              },
              {
                  "name": "Заблокировать Checkbox",
                  "id": "disabled",
                  "type": "bool",
                  "desc": "",
                  "default": 0,
                  "hint": "Флаг, отвечающий за недоступность компонента",
                  "mandatory": 0
              },
              {
                  "name": "Стили",
                  "id": "componentStyles",
                  "type": "html",
                  "desc": "",
                  "default": "",
                  "hint": "Стили компонента",
                  "mandatory": 0
              }
          ]
      }
  ],
  "inputs": [
      {
          "name": "Отметка по умолчанию",
          "id": "checked",
          "type": "bool",
          "desc": "",
          "default": 0,
          "mandatory": 0
      },
      {
          "name": "Заблокировать Checkbox",
          "id": "disabled",
          "desc": "",
          "type": "bool",
          "mandatory": 0,
          "array": 0
      },
      {
          "name": "Стили",
          "id": "componentStyles",
          "desc": "",
          "type": "html",
          "mandatory": 0,
          "array": 0
      }

  ],
  "outputs": [
      {
          "id": "OnSelectionChange",
          "name": "Событие смены выбора",
          "type": "event",
          "mandatory": 0,
          "array": 0,
          "module": "event_on_selection_change",
          "elements": [
              {
                  "name": "Отметка",
                  "id": "checked",
                  "desc": "",
                  "type": "bool",
                  "mandatory": 0,
                  "array": 0
              }
          ]
      }
  ],
  "style": {
      "icon": "far fa-flag"
  },
  "settingsRules": []
}
Пример бэкенда блока component.py
# -*- coding: utf-8 -*-
from abc import ABC
from enum import Enum
from typing import Any, Dict, List, Optional
from distutils.util import strtobool


class CUserCustomComponent(ABC):
    class ECalculationNames(Enum):
        # Настройки блока
        CHECKBOX_SETTINGS_ID: str = "checkBoxSettings"
        LABEL_TEXT_SETTINGS_ID: str = "labelText"
        CHECK_SETTINGS_ID: str = "checked"
        DISABLED_SETTINGS_ID: str = "disabled"
        STYLES_SETTINGS_ID: str = "componentStyles"
        # I/O блока
        CHECK_INPUT_ID: str = "checked"
        DISABLED_INPUT_ID: str = "disabled"
        STYLES_INPUT_ID: str = "componentStyles"
        OUTPUT_ON_SELECTION_CHANGE_ID: str = "OnSelectionChange"
        OUTPUT_ON_SELECTION_CHANGE_CHECK_ID: str = "checked"

    def __init__(
        self,
        settings: Dict[str, Any],
        inputs: Dict[str, Any],
        log: Any,
        context: Dict[str, Any]
    ) -> None:
        self.settings = settings
        self.inputs = inputs
        self.log = log
        self.context = context
        self.checkbox_settings = self.checkbox_settings = self.settings.get(
            self.ECalculationNames.CHECKBOX_SETTINGS_ID.value, {}
        )
        self.default_styles = """
        .vismind-checkbox-container-label {
            font-family: Roboto, sans-serif;
            font-size: 15px;
            font-weight: 400;
            line-height: 22px;
            margin-bottom: 0px;
            pointer-events: auto;
            text-align: left;
            color: rgba(60, 102, 127, 1);
        }

        .vismind-checkbox-container {
            display: flex;
            align-items: center;
            justify-content: space-between;
          }

        .vismind-checkbox-container-input {
            appearance: none;
            background-color: rgba(60, 102, 127, 0.51);
            border-radius: 34px;
            border-style: none;
            flex-shrink: 0;
            margin: 0;
            position: relative;
            cursor: pointer;
            height: 34px;
            width: 59px;
        }

        .vismind-checkbox-container-input::before {
            bottom: -6px;
            content: "";
            left: -6px;
            position: absolute;
            right: -6px;
            top: -6px;
        }

        .vismind-checkbox-container-input,
        .vismind-checkbox-container-input::after {
            transition: all 100ms ease-out;
        }

        .vismind-checkbox-container-input::after {
            background-color: #fff;
            border-radius: 50%;
            content: "";
            height: 28px;
            left: 3px;
            position: absolute;
            top: 3px;
            width: 28px;
        }

        .vismind-checkbox-container-input:hover {
            background-color: rgba(60, 102, 127, 0.6);
            transition-duration: 0s;
        }

        .vismind-checkbox-container-input:checked {
            background-color: rgba(60, 102, 127, 1);
        }

        .vismind-checkbox-container-input:checked::after {
            background-color: #fff;
            left: 28px;
        }

        .vismind-checkbox-container-label {
            cursor: pointer;
            user-select: none;
        }

        .vismind-checkbox-container-input:focus:not(.focus-visible) {
            outline: 0;
        }
        """

    def execute(self) -> Dict[str, Any]:
        default_check: bool = bool(self.checkbox_settings.get(
            self.ECalculationNames.CHECK_SETTINGS_ID.value,
            False
        ))
        return {
            self.ECalculationNames.OUTPUT_ON_SELECTION_CHANGE_ID.value: {
                self.ECalculationNames.OUTPUT_ON_SELECTION_CHANGE_CHECK_ID.value: default_check
            }
        }

    def visualizer(self) -> Dict[str, Any]:
        label_text: str = self.checkbox_settings.get(
            self.ECalculationNames.LABEL_TEXT_SETTINGS_ID.value,
            "text"
        )
        check = self.checkbox_settings.get(self.ECalculationNames.CHECK_SETTINGS_ID.value, False)
        disabled = self.checkbox_settings.get(self.ECalculationNames.DISABLED_SETTINGS_ID.value, False)
        styles = self.checkbox_settings.get(self.ECalculationNames.STYLES_SETTINGS_ID.value, "")

        if self.ECalculationNames.CHECK_INPUT_ID.value in self.inputs.keys():
            check_input = self.inputs.get(self.ECalculationNames.CHECK_INPUT_ID.value, check)
            if check_input:
                check = check_input

        if self.ECalculationNames.DISABLED_INPUT_ID.value in self.inputs.keys():
            disabled_input = self.inputs.get(self.ECalculationNames.DISABLED_INPUT_ID.value)
            if disabled_input:
                disabled = disabled_input

        if self.ECalculationNames.STYLES_INPUT_ID.value in self.inputs.keys():
            styles_input = self.inputs.get(self.ECalculationNames.STYLES_INPUT_ID.value)
            if styles_input:
                styles = styles_input

        return {
            "data": [
                {"name": "labelText", "value": label_text},
                {"name": "check", "value": check},
                {"name": "disabled", "value": disabled},
                {"name": "componentStyles", "value": styles if styles else self.default_styles}
          ]
        }

    def event_on_selection_change(self) -> Dict[str, Any]:
        value = self.inputs.get(self.ECalculationNames.OUTPUT_ON_SELECTION_CHANGE_CHECK_ID.value)

        return {
            self.ECalculationNames.OUTPUT_ON_SELECTION_CHANGE_ID.value: {
                self.ECalculationNames.OUTPUT_ON_SELECTION_CHANGE_CHECK_ID.value: value
            }
        }
Пример main.js для Vue js
import { createApp, h } from 'vue'
import DropDownComponent from './src/components/CheckBoxComponent.vue'


class CustomVismindCheckBox extends HTMLElement {
  constructor() {
    super();
    this.root = this.attachShadow({ mode: 'open' });
    this.vueProps = {};
    this.app = null;
  }

  async initProps(props) {
    this.vueProps = Object.fromEntries(props.map(p => [p.name, p.value]));
    if (this.app) {
      this.unmount();
    }
    this.mount();
  }

  mount() {
    if (this.app) {
      this.unmount();
    }

    this.app = createApp({
      render: () => h(DropDownComponent, {
        ...this.vueProps,
        onOnSelectionChange: (payload) => {
            this.dispatchEvent(new CustomEvent('OnSelectionChange', {
                detail: payload,
                bubbles: true,
                composed: true
            }));
          }
        }
      )
    });

    // Cоздание стилей внутри shadow-dom
    if (this.vueProps?.componentStyles) {
      const style = document.createElement('style');
      style.textContent = this.vueProps.componentStyles;
      this.root.appendChild(style);
    }

    // Монтируем в shadow-dom
    this.app.mount(this.root);
  }

  unmount() {
    if (this.app) {
      this.app.unmount();
      this.app = null;
    }
  }

  connectedCallback() {
    this.mount();
  }

  disconnectedCallback() {
    this.unmount();
  }
}

customElements.define('vismind-checkbox', CustomVismindCheckBox);
Пример компонента на vue.js
<template>
  <div class="vismind-checkbox-container">
    <label
      class="vismind-checkbox-container-label"
    >
      {{ labelText }}
    </label>
    <input
      type="checkbox"
      class="vismind-checkbox-container-input"
      :id="checkboxId"
      :value="check"
      :checked="isChecked"
      :disabled="disabled"
      @change="onChange"
    />
  </div>
</template>

<script>
export default {
  props: {
    labelText: {
      type: String,
      required: true,
    },
    check: {
      type: Boolean,
      default: 1,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
  },
  emits: ['OnSelectionChange'],
  data() {
    return {
    };
  },
  computed: {
    isChecked() {
      return this.check;
    },
  },
  methods: {
    onChange(event) {
      this.$emit('OnSelectionChange', {
        data: {
          checked: event.target.checked
        }
      });
    },
  },
};
</script>
Пример pyproject.toml
[tool.poetry]
name = "vismind-checkbox"
version = "1.0.0"
description = ""
authors = ["Kuznetsov Nikolay <nikolay.kuznetsov@bittechno.ru>"]
readme = "README.md"

[[tool.poetry.source]]
name = "nexus"
url = "https://nexus.int.bittechno.ru/repository/pypi-proxy/simple"

[tool.poetry.dependencies]
python = "~3.10"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Пример конфигурации сборщика vite (vite.config.js)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  define: {
    'process.env.NODE_ENV': '"production"'
  },
  build: {
    lib: {
      entry: './main.js',
      name: 'CheckBoxComponent',
      fileName: () => 'component.js',
      formats: ['iife']
    }
  }
})