Разработка логики работы визуальной составляющей блока (component.js)
В js-файле находится код визуализатора.
Код должен содержать класс CustomVismind<название компонента> с методами:
- initProps - для инициализации компонента
- emit - для вызова событий
Жизненный цикл Web компонентов
- Создание компонента Создаётся экземпляр класса пользовательского элемента.
- Вызывается метод
constructor(). -
Можно инициализировать внутренние поля и состояние.
-
Добавление в DOM Что вызывается:
connectedCallback()
Что делает компонент: - Выполняет основную инициализацию, требующую присутствия элемента в DOM. - Добавляет обработчики событий, подписки и визуальные элементы. - В контексте визуализатора: - Настраивает визуальную часть. - Подключает события из перечня, полученного с api.
- Инициализация данных (
initProps) Когда вызывается: Сразу после загрузки скрипта и создания HTML-элемента (послеconnectedCallback).
Что делает метод:
- Метод initProps(props) вызывается системой визуализаторов для передачи данных, полученных из api.
- Содержит параметры, необходимые для настройки или отрисовки.
- Может вызываться:
- При первой инициализации.
- При обновлении данных, если тег визуализатора не менялся.
Поведение:
- Если тег не изменился → повторный вызов initProps(props).
- Если тег изменился → старый элемент удаляется (disconnectedCallback),
затем загружается новый скрипт и создаётся новый элемент.
- Удаление из 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"