diff --git a/src/myfasthtml/assets/myfasthtml.css b/src/myfasthtml/assets/myfasthtml.css
new file mode 100644
index 0000000..6d60374
--- /dev/null
+++ b/src/myfasthtml/assets/myfasthtml.css
@@ -0,0 +1,15 @@
+.mf-icon-20 {
+ width: 20px;
+ min-width: 20px;
+ height: 20px;
+ margin-top: auto;
+ margin-bottom: auto;
+}
+
+.mf-icon-16 {
+ width: 16px;
+ min-width: 16px;
+ height: 16px;
+ margin-top: auto;
+ margin-bottom: 4px;
+}
\ No newline at end of file
diff --git a/src/myfasthtml/controls/helpers.py b/src/myfasthtml/controls/helpers.py
new file mode 100644
index 0000000..44655c4
--- /dev/null
+++ b/src/myfasthtml/controls/helpers.py
@@ -0,0 +1,33 @@
+from fasthtml.components import *
+
+from myfasthtml.core.commands import Command
+from myfasthtml.core.utils import merge_classes
+
+
+class mk:
+
+ @staticmethod
+ def button(element, command: Command = None, **kwargs):
+ return mk.manage_command(Button(element, **kwargs), command)
+
+ @staticmethod
+ def icon(icon, size=20,
+ can_select=True,
+ can_hover=False,
+ cls='',
+ command: Command = None,
+ **kwargs):
+ merged_cls = merge_classes(f"mf-icon-{size}",
+ 'icon-btn' if can_select else '',
+ 'mmt-btn' if can_hover else '',
+ cls,
+ kwargs)
+
+ return mk.manage_command(Div(icon, cls=merged_cls, **kwargs), command)
+
+ @staticmethod
+ def manage_command(ft, command: Command):
+ htmx = command.get_htmx_params() if command else {}
+ ft.attrs |= htmx
+
+ return ft
diff --git a/src/myfasthtml/docs/__init__.py b/src/myfasthtml/docs/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/myfasthtml/docs/clickme.py b/src/myfasthtml/docs/clickme.py
new file mode 100644
index 0000000..1f7322d
--- /dev/null
+++ b/src/myfasthtml/docs/clickme.py
@@ -0,0 +1,26 @@
+from fasthtml import serve
+
+from myfasthtml.controls.helpers import mk
+from myfasthtml.core.commands import Command
+from myfasthtml.myfastapp import create_app
+
+
+# Define a simple command action
+def say_hello():
+ return "Hello, FastHtml!"
+
+
+# Create the command
+hello_command = Command("say_hello", "Responds with a greeting", say_hello)
+
+# Create the app
+app, rt = create_app(protect_routes=False)
+
+
+@rt("/")
+def get_homepage():
+ return mk.button("Click Me!", command=hello_command)
+
+
+if __name__ == "__main__":
+ serve(port=5002)
diff --git a/src/myfasthtml/docs/command_with_htmx_params.py b/src/myfasthtml/docs/command_with_htmx_params.py
new file mode 100644
index 0000000..bf20174
--- /dev/null
+++ b/src/myfasthtml/docs/command_with_htmx_params.py
@@ -0,0 +1,25 @@
+from fasthtml import serve
+from fasthtml.components import *
+
+from myfasthtml.controls.helpers import mk
+from myfasthtml.core.commands import Command
+from myfasthtml.icons.fa import icon_home
+from myfasthtml.myfastapp import create_app
+
+app, rt = create_app(protect_routes=False)
+
+
+def change_text():
+ return "New text"
+
+
+command = Command("change_text", "change the text", change_text).htmx(target="#text")
+
+
+@rt("/")
+def index():
+ return mk.button(Div(mk.icon(icon_home), Div("Hello World", id="text"), cls="flex"), command=command)
+
+
+if __name__ == "__main__":
+ serve(port=5002)
diff --git a/src/myfasthtml/docs/helloworld.py b/src/myfasthtml/docs/helloworld.py
new file mode 100644
index 0000000..fe63326
--- /dev/null
+++ b/src/myfasthtml/docs/helloworld.py
@@ -0,0 +1,15 @@
+from fasthtml import serve
+from fasthtml.components import *
+
+from myfasthtml.myfastapp import create_app
+
+app, rt = create_app(protect_routes=False)
+
+
+@rt("/")
+def get_homepage():
+ return Div("Hello, FastHtml!")
+
+
+if __name__ == "__main__":
+ serve(port=5002)
diff --git a/src/myfasthtml/test/__init__.py b/src/myfasthtml/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/myfasthtml/test/testclient.py b/src/myfasthtml/test/testclient.py
index 8aff99c..6fd1bfa 100644
--- a/src/myfasthtml/test/testclient.py
+++ b/src/myfasthtml/test/testclient.py
@@ -345,10 +345,6 @@ class TestableElement:
elif options:
self.fields[name] = options[0]['value']
- def _get_my_ft(self, element: Tag):
- _inner = element.find(self.tag) if self.tag and self.tag != element.name else element
- return MyFT(_inner.name, _inner.attrs)
-
@staticmethod
def _get_input_identifier(input_field, counter):
"""
@@ -432,14 +428,6 @@ class TestableElement:
# Default to string
return value
- @staticmethod
- def _get_element(html_fragment: str):
- html_fragment = html_fragment.strip()
- if (not html_fragment.startswith('
"
- return BeautifulSoup(html_fragment, 'html.parser').find()
-
@staticmethod
def _parse(tag, html_fragment: str):
elt = BeautifulSoup(html_fragment, 'html.parser')
@@ -801,8 +789,8 @@ class TestableForm(TestableElement):
# return value
-class TestableInput(TestableElement):
- def __init__(self, client, source):
+class TestableControl(TestableElement):
+ def __init__(self, client, source, tag):
super().__init__(client, source, "input")
assert len(self.fields) <= 1
self._input_name = next(iter(self.fields))
@@ -815,12 +803,40 @@ class TestableInput(TestableElement):
def value(self):
return self.fields[self._input_name]
+ def _send_value(self):
+ if self._input_name and self._support_htmx():
+ return self._send_htmx_request(data={self._input_name: self.value})
+ return None
+
+
+class TestableInput(TestableControl):
+ def __init__(self, client, source):
+ super().__init__(client, source, "input")
+
def send(self, value):
self.fields[self.name] = value
- if self.name and self._support_htmx():
- return self._send_htmx_request(data={self.name: self.value})
-
- return None
+ return self._send_value()
+
+
+class TestableCheckbox(TestableControl):
+ def __init__(self, client, source):
+ super().__init__(client, source, "input")
+
+ @property
+ def is_checked(self):
+ return self.fields[self._input_name] == True
+
+ def check(self):
+ self.fields[self._input_name] = True
+ return self._send_value()
+
+ def uncheck(self):
+ self.fields[self._input_name] = False
+ return self._send_value()
+
+ def toggle(self):
+ self.fields[self._input_name] = not self.fields[self._input_name]
+ return self._send_value()
# def get_value(tag):
diff --git a/tests/controls/__init__.py b/tests/controls/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/controls/test_helpers.py b/tests/controls/test_helpers.py
new file mode 100644
index 0000000..b5326df
--- /dev/null
+++ b/tests/controls/test_helpers.py
@@ -0,0 +1,48 @@
+import pytest
+from fasthtml.components import *
+from fasthtml.fastapp import fast_app
+
+from myfasthtml.controls.helpers import mk
+from myfasthtml.core.commands import Command
+from myfasthtml.test.matcher import matches
+from myfasthtml.test.testclient import MyTestClient
+
+
+@pytest.fixture()
+def user():
+ test_app, rt = fast_app(default_hdrs=False)
+ user = MyTestClient(test_app)
+ return user
+
+
+@pytest.fixture()
+def rt(user):
+ return user.app.route
+
+
+def test_i_can_mk_button():
+ button = mk.button('button')
+ expected = Button('button')
+
+ assert matches(button, expected)
+
+
+def test_i_can_mk_button_with_attrs():
+ button = mk.button('button', id="button_id", class_="button_class")
+ expected = Button('button', id="button_id", class_="button_class")
+ assert matches(button, expected)
+
+
+def test_i_can_mk_button_with_command(user, rt):
+ def new_value(value): return value
+
+ command = Command('test', 'TestingCommand', new_value, "this is my new value")
+
+ @rt('/')
+ def get(): return mk.button('button', command)
+
+ user.open("/")
+ user.should_see("button")
+
+ user.find_element("button").click()
+ user.should_see("this is my new value")
diff --git a/tests/testclient/test_testable_checkbox.py b/tests/testclient/test_testable_checkbox.py
new file mode 100644
index 0000000..4842a5f
--- /dev/null
+++ b/tests/testclient/test_testable_checkbox.py
@@ -0,0 +1,59 @@
+import pytest
+from fasthtml.fastapp import fast_app
+
+from myfasthtml.test.testclient import MyTestClient, TestableCheckbox
+
+
+@pytest.fixture
+def test_app():
+ test_app, rt = fast_app(default_hdrs=False)
+ return test_app
+
+
+@pytest.fixture
+def rt(test_app):
+ return test_app.route
+
+
+@pytest.fixture
+def test_client(test_app):
+ return MyTestClient(test_app)
+
+
+@pytest.mark.parametrize("html,expected_value", [
+ ('', True),
+ ('', False),
+])
+def test_i_can_read_input(test_client, html, expected_value):
+ input_elt = TestableCheckbox(test_client, html)
+
+ assert input_elt.name == "male"
+ assert input_elt.value == expected_value
+
+
+def test_i_can_read_input_with_label(test_client):
+ html = ''''''
+
+ input_elt = TestableCheckbox(test_client, html)
+ assert input_elt.fields_mapping == {"Male": "male"}
+ assert input_elt.name == "male"
+ assert input_elt.value == True
+
+
+def test_i_can_check_checkbox(test_client, rt):
+ html = ''''''
+
+ @rt('/submit')
+ def post(male: bool):
+ return f"Checkbox received {male=}"
+
+ input_elt = TestableCheckbox(test_client, html)
+
+ input_elt.check()
+ assert test_client.get_content() == "Checkbox received male=True"
+
+ input_elt.uncheck()
+ assert test_client.get_content() == "Checkbox received male=False"
+
+ input_elt.toggle()
+ assert test_client.get_content() == "Checkbox received male=True"
diff --git a/tests/testclient/test_testable_input.py b/tests/testclient/test_testable_input.py
new file mode 100644
index 0000000..2922ebe
--- /dev/null
+++ b/tests/testclient/test_testable_input.py
@@ -0,0 +1,51 @@
+import pytest
+from fasthtml.fastapp import fast_app
+
+from myfasthtml.test.testclient import TestableInput, MyTestClient
+
+
+@pytest.fixture
+def test_app():
+ test_app, rt = fast_app(default_hdrs=False)
+ return test_app
+
+
+@pytest.fixture
+def rt(test_app):
+ return test_app.route
+
+
+@pytest.fixture
+def test_client(test_app):
+ return MyTestClient(test_app)
+
+
+def test_i_can_read_input(test_client):
+ html = ''''''
+
+ input_elt = TestableInput(test_client, html)
+
+ assert input_elt.name == "username"
+ assert input_elt.value == "john_doe"
+
+
+def test_i_can_read_input_with_label(test_client):
+ html = ''''''
+
+ input_elt = TestableInput(test_client, html)
+ assert input_elt.fields_mapping == {"Username": "username"}
+ assert input_elt.name == "username"
+ assert input_elt.value == "john_doe"
+
+
+def test_i_can_send_values(test_client, rt):
+ html = ''''''
+
+ @rt('/submit')
+ def post(username: str):
+ return f"Input received {username=}"
+
+ input_elt = TestableInput(test_client, html)
+ input_elt.send("another name")
+
+ assert test_client.get_content() == "Input received username='another name'"