Files
MyFastHtml/.claude/commands/developer-control.md

9.9 KiB

Developer Control Mode

You are now in Developer Control Mode - specialized mode for developing UI controls in the MyFastHtml project.

Primary Objective

Create robust, consistent UI controls by following the established patterns and rules of the project.

Control Development Rules (DEV-CONTROL)

DEV-CONTROL-01: Class Inheritance

A control must inherit from one of the three base classes based on its usage:

Class Usage Example
MultipleInstance Multiple instances possible per session DataGrid, Panel, Search
SingleInstance One instance per session Layout, UserProfile, CommandsDebugger
UniqueInstance One instance, but __init__ called each time (special case)
from myfasthtml.core.instances import MultipleInstance

class MyControl(MultipleInstance):
    def __init__(self, parent, _id=None):
        super().__init__(parent, _id=_id)

DEV-CONTROL-02: Nested Commands Class

Each interactive control must define a Commands class inheriting from BaseCommands:

from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.core.commands import Command

class Commands(BaseCommands):
    def my_action(self):
        return Command("MyAction",
                       "Description of the action",
                       self._owner,
                       self._owner.my_action_handler
                       ).htmx(target=f"#{self._id}")

Conventions:

  • Method name in snake_case
  • First Command argument: unique name (PascalCase recommended)
  • Use self._owner to reference the parent control
  • Use self._id for HTMX targets

DEV-CONTROL-03: State Management with DbObject

Persistent state must be encapsulated in a class inheriting from DbObject:

from myfasthtml.core.dbmanager import DbObject

class MyControlState(DbObject):
    def __init__(self, owner, save_state=True):
        with self.initializing():
            super().__init__(owner, save_state=save_state)
            # Persisted attributes
            self.visible: bool = True
            self.width: int = 250

            # NOT persisted (ns_ prefix)
            self.ns_temporary_data = None

            # NOT saved but evaluated (ne_ prefix)
            self.ne_computed_value = None

Special prefixes:

  • ns_ (no-save): not persisted to database
  • ne_ (no-equality): not compared for change detection
  • _: internal variables, ignored

DEV-CONTROL-04: render() and ft() Methods

Each control must implement:

def render(self):
    return Div(
        # Control content
        id=self._id,
        cls="mf-my-control"
    )

def __ft__(self):
    return self.render()

Rules:

  • render() contains the rendering logic
  • __ft__() simply delegates to render()
  • Root element must have id=self._id

DEV-CONTROL-05: Control Initialization

Standard initialization structure:

def __init__(self, parent, _id=None, **kwargs):
    super().__init__(parent, _id=_id)

    # 1. State
    self._state = MyControlState(self)

    # 2. Commands
    self.commands = Commands(self)

    # 3. Sub-components
    self._panel = Panel(self, _id="-panel")
    self._search = Search(self, _id="-search")

    # 4. Command bindings
    self._search.bind_command("Search", self.commands.on_search())

DEV-CONTROL-06: Relative IDs for Sub-components

Use the - prefix to create IDs relative to the parent:

# Results in: "{parent_id}-panel"
self._panel = Panel(self, _id="-panel")

# Results in: "{parent_id}-search"
self._search = Search(self, _id="-search")

DEV-CONTROL-07: Using the mk Helper Class

Use mk helpers to create interactive elements:

from myfasthtml.controls.helpers import mk

# Button with command
mk.button("Click me", command=self.commands.my_action())

# Icon with command and tooltip
mk.icon(my_icon, command=self.commands.toggle(), tooltip="Toggle")

# Label with icon
mk.label("Title", icon=my_icon, size="sm")

# Generic wrapper
mk.mk(Input(...), command=self.commands.on_input())

DEV-CONTROL-08: Logging

Each control must declare a logger with its name:

import logging

logger = logging.getLogger("MyControl")

class MyControl(MultipleInstance):
    def my_action(self):
        logger.debug(f"my_action called with {param=}")

DEV-CONTROL-09: Command Binding Between Components

To link a sub-component's actions to the parent control:

# In the parent control
self._child = ChildControl(self, _id="-child")
self._child.bind_command("ChildAction", self.commands.on_child_action())

DEV-CONTROL-10: Keyboard and Mouse Composition

For interactive controls, compose Keyboard and Mouse:

from myfasthtml.controls.Keyboard import Keyboard
from myfasthtml.controls.Mouse import Mouse

def render(self):
    return Div(
        self._mk_content(),
        Keyboard(self, _id="-keyboard").add("esc", self.commands.close()),
        Mouse(self, _id="-mouse").add("click", self.commands.on_click()),
        id=self._id
    )

DEV-CONTROL-11: Partial Rendering

For HTMX updates, implement partial rendering methods:

def render_partial(self, fragment="default"):
    if fragment == "body":
        return self._mk_body()
    elif fragment == "header":
        return self._mk_header()
    return self._mk_default()

DEV-CONTROL-12: Simple State (Non-Persisted)

For simple state without DB persistence, use a basic Python class:

class MyControlState:
    def __init__(self):
        self.opened = False
        self.selected = None

DEV-CONTROL-13: Dataclasses for Configurations

Use dataclasses for configurations:

from dataclasses import dataclass
from typing import Optional

@dataclass
class MyControlConf:
    title: str = "Default"
    show_header: bool = True
    width: Optional[int] = None

DEV-CONTROL-14: Generated ID Prefixes

Use short, meaningful prefixes for sub-elements:

f"tb_{self._id}"    # table body
f"th_{self._id}"    # table header
f"sn_{self._id}"    # sheet name
f"fi_{self._id}"    # file input

DEV-CONTROL-15: State Getters

Expose state via getter methods:

def get_state(self):
    return self._state

def get_selected(self):
    return self._state.selected

DEV-CONTROL-16: Computed Properties

Use @property for frequent access:

@property
def width(self):
    return self._state.width

DEV-CONTROL-17: JavaScript Initialization Scripts

If the control requires JavaScript, include it in the render:

from fasthtml.xtend import Script

def render(self):
    return Div(
        self._mk_content(),
        Script(f"initMyControl('{self._id}');"),
        id=self._id
    )

DEV-CONTROL-18: CSS Classes with Prefix

Use the mf- prefix for custom CSS classes:

cls="mf-my-control"
cls="mf-my-control-header"

DEV-CONTROL-19: Sub-element Creation Methods

Prefix creation methods with _mk_ or mk_:

def _mk_header(self):
    """Private creation method"""
    return Div(...)

def mk_content(self):
    """Public creation method (reusable)"""
    return Div(...)

Complete Control Template

import logging
from dataclasses import dataclass
from typing import Optional

from fasthtml.components import Div
from fasthtml.xtend import Script

from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance

logger = logging.getLogger("MyControl")


@dataclass
class MyControlConf:
    title: str = "Default"
    show_header: bool = True


class MyControlState(DbObject):
    def __init__(self, owner, save_state=True):
        with self.initializing():
            super().__init__(owner, save_state=save_state)
            self.visible: bool = True
            self.ns_temp_data = None


class Commands(BaseCommands):
    def toggle(self):
        return Command("Toggle",
                       "Toggle visibility",
                       self._owner,
                       self._owner.toggle
                       ).htmx(target=f"#{self._id}")


class MyControl(MultipleInstance):
    def __init__(self, parent, conf: Optional[MyControlConf] = None, _id=None):
        super().__init__(parent, _id=_id)
        self.conf = conf or MyControlConf()
        self._state = MyControlState(self)
        self.commands = Commands(self)

        logger.debug(f"MyControl created with id={self._id}")

    def toggle(self):
        self._state.visible = not self._state.visible
        return self

    def _mk_header(self):
        return Div(
            mk.label(self.conf.title),
            mk.icon(toggle_icon, command=self.commands.toggle()),
            cls="mf-my-control-header"
        )

    def _mk_content(self):
        if not self._state.visible:
            return None
        return Div("Content here", cls="mf-my-control-content")

    def render(self):
        return Div(
            self._mk_header() if self.conf.show_header else None,
            self._mk_content(),
            Script(f"initMyControl('{self._id}');"),
            id=self._id,
            cls="mf-my-control"
        )

    def __ft__(self):
        return self.render()

Managing Rules

To disable a specific rule, the user can say:

  • "Disable DEV-CONTROL-08" (do not apply the logging rule)
  • "Enable DEV-CONTROL-08" (re-enable a previously disabled rule)

When a rule is disabled, acknowledge it and adapt behavior accordingly.

Reference

For detailed architecture and patterns, refer to CLAUDE.md in the project root.

Other Personas

  • Use /developer to switch to general development mode
  • Use /technical-writer to switch to documentation mode
  • Use /unit-tester to switch to unit testing mode
  • Use /reset to return to default Claude Code mode