# Building Widgets Widgets render HTML for `html_page_exporter`. Niamoto validates widget params, calls `render(data, params)`, then wraps the result in the standard widget container. ## Runtime Contract Use `WidgetPlugin` and a typed params model. ```python import html from typing import Any, Optional from pydantic import Field from niamoto.core.plugins.base import PluginType, WidgetPlugin, register from niamoto.core.plugins.models import BasePluginParams class SpeciesCardsParams(BasePluginParams): label_field: str = Field(default="species") value_field: str = Field(default="count") empty_message: str = Field(default="No data available") @register("species_cards", PluginType.WIDGET) class SpeciesCardsWidget(WidgetPlugin): param_schema = SpeciesCardsParams def render(self, data: Optional[Any], params: SpeciesCardsParams) -> str: if data is None: return f"

{html.escape(params.empty_message)}

" rows = data.to_dict(orient="records") if hasattr(data, "to_dict") else data items = [] for row in rows: label = html.escape(str(row.get(params.label_field, ""))) value = html.escape(str(row.get(params.value_field, ""))) items.append(f"
  • {label}: {value}
  • ") return "" ``` ## What Goes In `param_schema` Put widget-specific options in the params model: - field names - display switches - formatting options - chart behavior Do not put `title`, `description`, or `layout` in the params model unless the widget needs its own internal title. `WidgetConfig` already handles those fields. ## `export.yml` Shape Widgets belong inside an export target and inside a group. ```yaml exports: - name: web_pages exporter: html_page_exporter params: template_dir: templates output_dir: exports/web groups: - group_by: plots widgets: - plugin: species_cards title: Dominant species description: Top species for this plot data_source: top_species params: label_field: species value_field: count empty_message: No species data ``` The runtime validates each widget entry as: - `plugin` - `data_source` - `title` - `description` - `params` - `layout` The old `type/config` shape is no longer the canonical widget format. ## Dependencies If your widget needs external CSS or JavaScript, override `get_dependencies()`. ```python def get_dependencies(self) -> set[str]: return { "https://cdn.jsdelivr.net/npm/chart.js", } ``` `html_page_exporter` collects those dependencies and injects them into the page. ## HTML Safety Escape every value that comes from the database or from config before you interpolate it into HTML. ```python import html title = html.escape(str(user_value)) tooltip = html.escape(str(description), quote=True) ``` Use the same rule for: - entity names - labels - field values - exception messages ## Preview The GUI preview engine uses the same widget plugins. If your widget renders correctly in exports and does not rely on browser globals outside its own markup, preview usually works with no extra code. ## Tests Put widget tests under `tests/core/plugins/widgets/`. Built-in widgets such as `bar_plot`, `info_grid`, and `raw_data_widget` show the current pattern: - validate params with `param_schema` - call `render(data, params_model)` - assert on the returned HTML ## Practical Rules - Keep `render()` pure. Return HTML. Do not write files from a widget. - Read input data from `data_source`, not from the filesystem. - Validate config with `param_schema`, not manual `dict.get(...)` chains. - Return a useful empty state when `data` is missing or empty. - Escape dynamic values before embedding them in HTML.