# django-unicorn-playground 🦄🛝
The `Unicorn Playground` provides a way to prototype and debug [`Unicorn`](https://www.django-unicorn.com) components without creating a complete Django application. It can either be run as a standalone script or by installing the library.
## Standalone script
The benefit of the standalone script is that [inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) provides the information for `pipx` to create a virtual environment and install any dependencies automatically.
### Create an example component
1. Install [`pipx`](https://pipx.pypa.io/latest/installation/)
2. Create a new file called `counter.py` with the following code
```python
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "django-unicorn-playground"
# ]
# ///
from django_unicorn.components import UnicornView
class CounterView(UnicornView):
template_html = """<div>
<div>
Count: {{ count }}
</div>
<button unicorn:click="increment">+</button>
<button unicorn:click="decrement">-</button>
</div>
"""
count: int
def increment(self):
count += 1
def decrement(self):
count -= 1
if __name__ == "__main__":
from django_unicorn_playground import UnicornPlayground
UnicornPlayground(__file__).runserver()
```
3. `pipx run counter.py`
4. Go to https://localhost:8000
## Library CLI
The `django-unicorn-playground` library can also be installed to provide a command-line interface to try a component. This approach uses basically the same code, but doesn't require the script inline metadata or the call to `runserver` at the end -- the CLI takes care of running the development server directly. Those portions can be included, though, and will not prevent the CLI from working as expected.
### Create an example component
1. `pipx install django-unicorn-playground`
2. Create `counter.py` with the following code:
```python
from django_unicorn.components import UnicornView
class CounterView(UnicornView):
template_html = """<div>
<div>
Count: {{ count }}
</div>
<button unicorn:click="increment">+</button>
<button unicorn:click="decrement">-</button>
</div>
"""
count: int
def increment(self):
count += 1
def decrement(self):
count -= 1
```
3. `unicorn counter.py`
4. Go to https://localhost:8000
### Command-line options
#### port
Port for the developer webserver. Defaults to 8000. Required to be an integer.
#### template_dir
Directory for templates. Required to be a string path.
#### version
Shows the current version.
#### help
Show the available CLI options.
## Example components
There are a few example components in the `examples` directory.
They can be run with something like `pipx run --no-cache examples/counter.py`.
## Template HTML
The component's HTML can be initialized in a few ways.
### UnicornView.template_html attribute
The HTML can be set with a class-level `template_html` field.
```python
from django_unicorn.components import UnicornView
class TestView(UnicornView):
template_html = """<div>
<div>
Count: {{ count }}
</div>
<button unicorn:click="increment">+</button>
<button unicorn:click="decrement">-</button>
</div>
"""
...
```
### UnicornView.template_html method
The HTML can be returned from a `template_html` instance method.
```python
from django_unicorn.components import UnicornView
class TestView(UnicornView):
def template_html(self):
return """<div>
<div>
Count: {{ count }}
</div>
<button unicorn:click="increment">+</button>
<button unicorn:click="decrement">-</button>
</div>
"""
...
```
### HTML file
Similar to a typical `django-unicorn` setup, the component HTML can be a separate template file. This is the fallback and will only be searched for if the `template_view` field or method is not defined on the component.
1. `cd` to the same directory as the component Python file you created
2. `mkdir -p templates/unicorn`
3. `touch {COMPONENT-NAME}.html`, e.g. for a component Python named `counter.py` create `counter.html`
4. Add the component HTML to the newly created file
### Template directory
By default `django-unicorn-playground` will look in a `templates/unicorn` folder for templates. The template location can be changed by passing in `template_dir` into the `UnicornPlayground()` constructor or adding a `--template_dir` argument to the CLI.
### index.html
The root URL dynamically creates `index.html` which creates HTML for the component. It looks something like the following.
```html
{% extends "base.html" %}
{% block content %}
{% unicorn 'COMPONENT-NAME' %}
{% endblock content %}
```
It can be overridden by creating a custom `index.html` in the template directory.
### base.html
By default, `index.html` extends `base.html`. It can be overridden by creating a custom `base.html` in the template directory.
## Using a Python HTML builder
Any Python library that generates normal HTML strings works great with `django-unicorn-playground`.
### [haitch](https://pypi.org/project/haitch/)
```python
from django_unicorn.components import UnicornView
import haitch
class TestComponent(UnicornView)
def template_html(self):
return haitch.div()(
haitch.button("Increment +", **{"unicorn:click": "increment"}),
haitch.button("Decrement -", **{"unicorn:click": "decrement"}),
haitch.div("{{ count }}"),
)
...
```
### [htpy](https://pypi.org/project/htpy/)
```python
from django_unicorn.components import UnicornView
import htpy
class TestComponent(UnicornView)
def template_html(self):
return htpy.div()[
htpy.button({"unicorn:click": "increment"})["Increment +"],
htpy.button({"unicorn:click": "decrement"})["Decrement -"],
htpy.div()["{{ count }}"],
]
...
```
### [dominate](https://pypi.org/project/dominate/)
```python
from django_unicorn.components import UnicornView
from dominate import tags as dom
class TestComponent(UnicornView)
def template_html(self):
return dom.div(
dom.button("Increment +", **unicorn.click(self.increment)),
dom.button("Decrement -", **unicorn.click(self.decrement)),
dom.div("{{ count }}"),
)
...
```
## Unicorn HTML helpers
When using an HTML builder, `django-unicorn-playground` provides a few helper methods which make it a little cleaner to create `Unicorn`-specific HTML.
For example, if using `haitch` instead of doing this:
```python
import haitch
from django_unicorn.components import UnicornView
class TestComponent(UnicornView)
def template_html(self):
return haitch.div()(
haitch.button("Increment +", **{"unicorn:click": "increment"}),
haitch.button("Decrement -", **{"unicorn:click": "decrement"}),
haitch.div("{{ count }}"),
)
...
```
This is how it would work with the helper methods:
```python
import haitch
from django_unicorn.components import UnicornView
from django_unicorn_playground.html import django, unicorn
class TestComponent(UnicornView)
def template_html(self):
return haitch.div()(
haitch.button("Increment +", **unicorn.click(self.increment)),
haitch.button("Decrement -", **unicorn.click(self.decrement)),
haitch.div(django.variable("count")),
)
...
```
## Local development
### Standalone script
Using the inline script metadata with `pipx` seems a little quirky and I could not get editable installs working reliably. I also tried `hatch run` which had its own issues. Not sure if there are other (read: better) approaches.
As far as I can tell, the best approach is to use an absolute file path like `"django_unicorn_playground @ file:///Users/adam/Source/adamghill/django-unicorn-playground/dist/django_unicorn_playground-0.1.0-py3-none-any.whl"` (note the triple forward-slash after "file:") as a dependency, and rebuilding and re-running the script without any caching like this: `poetry build && pipx run --no-cache examples/counter.py` any time you make a code change.
Note: you will need to update the component's dependency so it points to the path and version on your machine.
There is a `just` command to make testing a standalone script _slightly_ less painful during local development.
1. [Install just](https://just.systems/man/en/chapter_4.html)
1. `just serve {COMPONENT-FILE-PATH}`, e.g. `just serve examples/counter.py`
### CLI
1. `poetry install`
1. `poetry run unicorn {COMPONENT-FILE-PATH}`, e.g. `poetry run unicorn examples/counter.py`
## Acknowledgments
- [phoenix_playground](https://github.com/phoenix-playground/phoenix_playground)
Raw data
{
"_id": null,
"home_page": "https://github.com/adamghill/django-unicorn-playground/",
"name": "django-unicorn-playground",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.10",
"maintainer_email": null,
"keywords": "django, python, css, html",
"author": "adamghill",
"author_email": "adamghill@yahoo.com",
"download_url": "https://files.pythonhosted.org/packages/a3/32/750bfb234e9351687ae02df56f0468f6b4f5df127e5f8aa4df89f1a3daf2/django_unicorn_playground-0.1.1.tar.gz",
"platform": null,
"description": "# django-unicorn-playground \ud83e\udd84\ud83d\udedd\n\nThe `Unicorn Playground` provides a way to prototype and debug [`Unicorn`](https://www.django-unicorn.com) components without creating a complete Django application. It can either be run as a standalone script or by installing the library.\n\n## Standalone script\n\nThe benefit of the standalone script is that [inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) provides the information for `pipx` to create a virtual environment and install any dependencies automatically.\n\n### Create an example component\n\n1. Install [`pipx`](https://pipx.pypa.io/latest/installation/)\n2. Create a new file called `counter.py` with the following code\n\n```python\n# /// script\n# requires-python = \">=3.10\"\n# dependencies = [\n# \"django-unicorn-playground\"\n# ]\n# ///\n\nfrom django_unicorn.components import UnicornView\n\nclass CounterView(UnicornView):\n template_html = \"\"\"<div>\n <div>\n Count: {{ count }}\n </div>\n\n <button unicorn:click=\"increment\">+</button>\n <button unicorn:click=\"decrement\">-</button>\n</div>\n\"\"\"\n\n count: int\n\n def increment(self):\n count += 1\n \n def decrement(self):\n count -= 1\n\nif __name__ == \"__main__\":\n from django_unicorn_playground import UnicornPlayground\n\n UnicornPlayground(__file__).runserver()\n```\n\n3. `pipx run counter.py`\n4. Go to https://localhost:8000\n\n## Library CLI\n\nThe `django-unicorn-playground` library can also be installed to provide a command-line interface to try a component. This approach uses basically the same code, but doesn't require the script inline metadata or the call to `runserver` at the end -- the CLI takes care of running the development server directly. Those portions can be included, though, and will not prevent the CLI from working as expected.\n\n### Create an example component\n\n1. `pipx install django-unicorn-playground`\n2. Create `counter.py` with the following code:\n\n```python\nfrom django_unicorn.components import UnicornView\n\nclass CounterView(UnicornView):\n template_html = \"\"\"<div>\n <div>\n Count: {{ count }}\n </div>\n\n <button unicorn:click=\"increment\">+</button>\n <button unicorn:click=\"decrement\">-</button>\n</div>\n\"\"\"\n\n count: int\n\n def increment(self):\n count += 1\n \n def decrement(self):\n count -= 1\n```\n\n3. `unicorn counter.py`\n4. Go to https://localhost:8000\n\n### Command-line options\n\n#### port\n\nPort for the developer webserver. Defaults to 8000. Required to be an integer.\n\n#### template_dir\n\nDirectory for templates. Required to be a string path.\n\n#### version\n\nShows the current version.\n\n#### help\n\nShow the available CLI options.\n\n## Example components\n\nThere are a few example components in the `examples` directory.\n\nThey can be run with something like `pipx run --no-cache examples/counter.py`.\n\n## Template HTML\n\nThe component's HTML can be initialized in a few ways.\n\n### UnicornView.template_html attribute\n\nThe HTML can be set with a class-level `template_html` field.\n\n```python\nfrom django_unicorn.components import UnicornView\n\nclass TestView(UnicornView):\n template_html = \"\"\"<div>\n <div>\n Count: {{ count }}\n </div>\n\n <button unicorn:click=\"increment\">+</button>\n <button unicorn:click=\"decrement\">-</button>\n</div>\n\"\"\"\n\n ...\n```\n\n### UnicornView.template_html method\n\nThe HTML can be returned from a `template_html` instance method.\n\n```python\nfrom django_unicorn.components import UnicornView\n\nclass TestView(UnicornView):\n def template_html(self): \n return \"\"\"<div>\n <div>\n Count: {{ count }}\n </div>\n\n <button unicorn:click=\"increment\">+</button>\n <button unicorn:click=\"decrement\">-</button>\n</div>\n\"\"\"\n\n ...\n```\n\n### HTML file\n\nSimilar to a typical `django-unicorn` setup, the component HTML can be a separate template file. This is the fallback and will only be searched for if the `template_view` field or method is not defined on the component.\n\n1. `cd` to the same directory as the component Python file you created\n2. `mkdir -p templates/unicorn`\n3. `touch {COMPONENT-NAME}.html`, e.g. for a component Python named `counter.py` create `counter.html`\n4. Add the component HTML to the newly created file\n\n### Template directory\n\nBy default `django-unicorn-playground` will look in a `templates/unicorn` folder for templates. The template location can be changed by passing in `template_dir` into the `UnicornPlayground()` constructor or adding a `--template_dir` argument to the CLI.\n\n### index.html\n\nThe root URL dynamically creates `index.html` which creates HTML for the component. It looks something like the following.\n\n```html\n{% extends \"base.html\" %}\n{% block content %}\n{% unicorn 'COMPONENT-NAME' %}\n{% endblock content %}\n```\n\nIt can be overridden by creating a custom `index.html` in the template directory.\n\n### base.html\n\nBy default, `index.html` extends `base.html`. It can be overridden by creating a custom `base.html` in the template directory.\n\n## Using a Python HTML builder\n\nAny Python library that generates normal HTML strings works great with `django-unicorn-playground`.\n\n### [haitch](https://pypi.org/project/haitch/)\n\n ```python\n from django_unicorn.components import UnicornView\n import haitch\n\n class TestComponent(UnicornView)\n def template_html(self):\n return haitch.div()(\n haitch.button(\"Increment +\", **{\"unicorn:click\": \"increment\"}),\n haitch.button(\"Decrement -\", **{\"unicorn:click\": \"decrement\"}),\n haitch.div(\"{{ count }}\"),\n )\n \n ...\n ```\n\n ### [htpy](https://pypi.org/project/htpy/)\n\n ```python\n from django_unicorn.components import UnicornView\n import htpy\n\n class TestComponent(UnicornView)\n def template_html(self):\n return htpy.div()[\n htpy.button({\"unicorn:click\": \"increment\"})[\"Increment +\"],\n htpy.button({\"unicorn:click\": \"decrement\"})[\"Decrement -\"],\n htpy.div()[\"{{ count }}\"],\n ]\n \n ...\n ```\n\n ### [dominate](https://pypi.org/project/dominate/)\n\n ```python\n from django_unicorn.components import UnicornView\n from dominate import tags as dom\n\n class TestComponent(UnicornView)\n def template_html(self):\n return dom.div(\n dom.button(\"Increment +\", **unicorn.click(self.increment)),\n dom.button(\"Decrement -\", **unicorn.click(self.decrement)),\n dom.div(\"{{ count }}\"),\n )\n \n ...\n ```\n\n## Unicorn HTML helpers\n\nWhen using an HTML builder, `django-unicorn-playground` provides a few helper methods which make it a little cleaner to create `Unicorn`-specific HTML.\n\nFor example, if using `haitch` instead of doing this:\n\n ```python\n import haitch\n from django_unicorn.components import UnicornView\n\n class TestComponent(UnicornView)\n def template_html(self):\n return haitch.div()(\n haitch.button(\"Increment +\", **{\"unicorn:click\": \"increment\"}),\n haitch.button(\"Decrement -\", **{\"unicorn:click\": \"decrement\"}),\n haitch.div(\"{{ count }}\"),\n )\n \n ...\n ```\n\nThis is how it would work with the helper methods:\n\n ```python\n import haitch\n from django_unicorn.components import UnicornView\n from django_unicorn_playground.html import django, unicorn\n\n class TestComponent(UnicornView)\n def template_html(self):\n return haitch.div()(\n haitch.button(\"Increment +\", **unicorn.click(self.increment)),\n haitch.button(\"Decrement -\", **unicorn.click(self.decrement)),\n haitch.div(django.variable(\"count\")),\n )\n \n ...\n ```\n\n## Local development\n\n### Standalone script\n\nUsing the inline script metadata with `pipx` seems a little quirky and I could not get editable installs working reliably. I also tried `hatch run` which had its own issues. Not sure if there are other (read: better) approaches.\n\nAs far as I can tell, the best approach is to use an absolute file path like `\"django_unicorn_playground @ file:///Users/adam/Source/adamghill/django-unicorn-playground/dist/django_unicorn_playground-0.1.0-py3-none-any.whl\"` (note the triple forward-slash after \"file:\") as a dependency, and rebuilding and re-running the script without any caching like this: `poetry build && pipx run --no-cache examples/counter.py` any time you make a code change.\n\nNote: you will need to update the component's dependency so it points to the path and version on your machine.\n\nThere is a `just` command to make testing a standalone script _slightly_ less painful during local development.\n\n1. [Install just](https://just.systems/man/en/chapter_4.html)\n1. `just serve {COMPONENT-FILE-PATH}`, e.g. `just serve examples/counter.py`\n\n### CLI\n\n1. `poetry install`\n1. `poetry run unicorn {COMPONENT-FILE-PATH}`, e.g. `poetry run unicorn examples/counter.py`\n\n## Acknowledgments\n\n- [phoenix_playground](https://github.com/phoenix-playground/phoenix_playground)\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Prototype and debug `Unicorn` components without creating a complete Django application.",
"version": "0.1.1",
"project_urls": {
"Documentation": "https://github.com/adamghill/django-unicorn-playground/",
"Funding": "https://github.com/sponsors/adamghill",
"Homepage": "https://github.com/adamghill/django-unicorn-playground/",
"Repository": "https://github.com/adamghill/django-unicorn-playground/"
},
"split_keywords": [
"django",
" python",
" css",
" html"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "57ac2f81d9b728ab738a8505f3b6da54856485130248e772c27f5af1c226967f",
"md5": "9bb03872d7cbd52799024c11cb0f43c1",
"sha256": "fce7b4ffb72b1d732e45e60b8aa81883bbba339007a0060f72ad0f9220e77a58"
},
"downloads": -1,
"filename": "django_unicorn_playground-0.1.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "9bb03872d7cbd52799024c11cb0f43c1",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.10",
"size": 10791,
"upload_time": "2024-07-27T00:39:00",
"upload_time_iso_8601": "2024-07-27T00:39:00.875254Z",
"url": "https://files.pythonhosted.org/packages/57/ac/2f81d9b728ab738a8505f3b6da54856485130248e772c27f5af1c226967f/django_unicorn_playground-0.1.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "a332750bfb234e9351687ae02df56f0468f6b4f5df127e5f8aa4df89f1a3daf2",
"md5": "18afb7be8b0c6d034d35f03dbbdd371a",
"sha256": "11d09e254c9cfb3c8490a7fc760f609b5f99d8651d04c197ed5dc6390ac45f65"
},
"downloads": -1,
"filename": "django_unicorn_playground-0.1.1.tar.gz",
"has_sig": false,
"md5_digest": "18afb7be8b0c6d034d35f03dbbdd371a",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.10",
"size": 10918,
"upload_time": "2024-07-27T00:39:02",
"upload_time_iso_8601": "2024-07-27T00:39:02.151355Z",
"url": "https://files.pythonhosted.org/packages/a3/32/750bfb234e9351687ae02df56f0468f6b4f5df127e5f8aa4df89f1a3daf2/django_unicorn_playground-0.1.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-07-27 00:39:02",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "adamghill",
"github_project": "django-unicorn-playground",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "django-unicorn-playground"
}