├── project ├── __init__.py ├── asgi.py ├── wsgi.py └── urls.py ├── .prettierignore ├── www ├── templatetags │ ├── __init__.py │ └── utils.py ├── templates │ └── www │ │ ├── examples │ │ ├── todo.html │ │ ├── search.html │ │ ├── select.html │ │ ├── click-counter.html │ │ ├── validation.html │ │ └── count-characters.html │ │ ├── screencasts.html │ │ ├── bases │ │ ├── examples.html │ │ └── screencasts.html │ │ ├── sponsors.html │ │ ├── screencasts │ │ └── installation.html │ │ └── articles.html ├── urls.py └── views.py ├── docs ├── _static │ ├── scripts │ │ ├── furo-extensions.js │ │ ├── furo.js.LICENSE.txt │ │ └── main.js │ ├── custom.css │ ├── file.png │ ├── minus.png │ ├── plus.png │ ├── check-solid.svg │ ├── graphviz.css │ ├── copy-button.svg │ ├── documentation_options.js │ ├── design-tabs.js │ ├── debug.css │ ├── styles │ │ └── unicorn.css │ ├── copybutton.css │ └── copybutton_funcs.js ├── _sources │ ├── components │ │ └── actions.md.txt │ ├── installation.rst.txt │ ├── api │ │ ├── startunicorn │ │ │ └── index.rst.txt │ │ └── django_unicorn │ │ │ ├── templatetags │ │ │ ├── index.rst.txt │ │ │ └── unicorn │ │ │ │ └── index.rst.txt │ │ │ ├── db │ │ │ └── index.rst.txt │ │ │ ├── urls │ │ │ └── index.rst.txt │ │ │ ├── views │ │ │ ├── action_parsers │ │ │ │ ├── index.rst.txt │ │ │ │ ├── sync_input │ │ │ │ │ └── index.rst.txt │ │ │ │ ├── call_method │ │ │ │ │ └── index.rst.txt │ │ │ │ └── utils │ │ │ │ │ └── index.rst.txt │ │ │ ├── utils │ │ │ │ └── index.rst.txt │ │ │ ├── objects │ │ │ │ └── index.rst.txt │ │ │ └── index.rst.txt │ │ │ ├── components │ │ │ ├── fields │ │ │ │ └── index.rst.txt │ │ │ ├── index.rst.txt │ │ │ ├── mixins │ │ │ │ └── index.rst.txt │ │ │ ├── updaters │ │ │ │ └── index.rst.txt │ │ │ └── unicorn_template_response │ │ │ │ └── index.rst.txt │ │ │ ├── decorators │ │ │ └── index.rst.txt │ │ │ ├── typing │ │ │ └── index.rst.txt │ │ │ ├── index.rst.txt │ │ │ ├── cacher │ │ │ └── index.rst.txt │ │ │ ├── settings │ │ │ └── index.rst.txt │ │ │ ├── call_method_parser │ │ │ └── index.rst.txt │ │ │ ├── utils │ │ │ └── index.rst.txt │ │ │ ├── errors │ │ │ └── index.rst.txt │ │ │ ├── typer │ │ │ └── index.rst.txt │ │ │ └── serializer │ │ │ └── index.rst.txt │ ├── index.rst.txt │ ├── queue-requests.md.txt │ ├── direct-view.md.txt │ ├── custom-morphers.md.txt │ ├── javascript.md.txt │ ├── dirty-states.md.txt │ ├── cli.md.txt │ ├── troubleshooting.md.txt │ ├── visibility.md.txt │ ├── getting-started.md.txt │ ├── partial-updates.md.txt │ ├── messages.md.txt │ ├── introduction.md.txt │ ├── installation.md.txt │ ├── settings.md.txt │ ├── redirecting.md.txt │ ├── architecture.md.txt │ ├── polling.md.txt │ └── loading-states.md.txt ├── objects.inv ├── unicorn-latest.pdf ├── _images │ └── social_previews │ │ ├── summary_cli_63c0dc4b.png │ │ ├── summary_faq_eb30496d.png │ │ ├── summary_actions_333108d1.png │ │ ├── summary_index_bc786ce5.png │ │ ├── summary_polling_884e9da5.png │ │ ├── summary_views_8b18f15a.png │ │ ├── summary_changelog_1a120bff.png │ │ ├── summary_changelog_42730650.png │ │ ├── summary_messages_7a53ae5a.png │ │ ├── summary_settings_65d7d295.png │ │ ├── summary_templates_254d134f.png │ │ ├── summary_architecture_6d4bd531.png │ │ ├── summary_components_62ff22c2.png │ │ ├── summary_direct-view_422cf1a2.png │ │ ├── summary_dirty-states_529d8250.png │ │ ├── summary_installation_af8c48af.png │ │ ├── summary_installation_dd93ef79.png │ │ ├── summary_javascript_8d589da4.png │ │ ├── summary_redirecting_d7a57503.png │ │ ├── summary_validation_a62ab460.png │ │ ├── summary_visibility_eae56b73.png │ │ ├── summary_django-models_250bf753.png │ │ ├── summary_loading-states_647850b3.png │ │ ├── summary_queue-requests_57f5c4f7.png │ │ ├── summary_child-components_2deacd60.png │ │ ├── summary_code-of-conduct_76aefa6b.png │ │ ├── summary_custom-morphers_80bbecc8.png │ │ ├── summary_getting-started_39f7164f.png │ │ ├── summary_partial-updates_cd522ecb.png │ │ ├── summary_troubleshooting_ccafcb1e.png │ │ ├── summary_api_startunicorn_index_1dbcdb7a.png │ │ ├── summary_api_django_unicorn_index_2e0b4934.png │ │ ├── summary_api_django_unicorn_db_index_505b4485.png │ │ ├── summary_api_django_unicorn_urls_index_fccefb84.png │ │ ├── summary_api_django_unicorn_cacher_index_e2409b8f.png │ │ ├── summary_api_django_unicorn_errors_index_3f9648c4.png │ │ ├── summary_api_django_unicorn_typer_index_3b3551a2.png │ │ ├── summary_api_django_unicorn_typing_index_47fd05be.png │ │ ├── summary_api_django_unicorn_utils_index_cf39ac98.png │ │ ├── summary_api_django_unicorn_views_index_4f8dbf06.png │ │ ├── summary_api_django_unicorn_settings_index_d0186d72.png │ │ ├── summary_api_django_unicorn_components_index_15a78d7b.png │ │ ├── summary_api_django_unicorn_decorators_index_a5530e41.png │ │ ├── summary_api_django_unicorn_serializer_index_e7875a35.png │ │ ├── summary_api_django_unicorn_templatetags_index_fbccbbc4.png │ │ ├── summary_api_django_unicorn_views_utils_index_5e575f66.png │ │ ├── summary_api_django_unicorn_views_objects_index_d3ef82f7.png │ │ ├── summary_api_django_unicorn_components_fields_index_596f1a6f.png │ │ ├── summary_api_django_unicorn_components_mixins_index_bf1db447.png │ │ ├── summary_api_django_unicorn_call_method_parser_index_3ef0ff13.png │ │ ├── summary_api_django_unicorn_components_updaters_index_6584a2f4.png │ │ ├── summary_api_django_unicorn_templatetags_unicorn_index_82418033.png │ │ ├── summary_api_django_unicorn_views_action_parsers_index_2e95ccb5.png │ │ ├── summary_api_django_unicorn_components_unicorn_view_index_1ddd7c6c.png │ │ ├── summary_api_django_unicorn_views_action_parsers_utils_index_e48bde12.png │ │ ├── summary_api_django_unicorn_views_action_parsers_call_method_index_3562d4bf.png │ │ ├── summary_api_django_unicorn_views_action_parsers_sync_input_index_8a60afd4.png │ │ └── summary_api_django_unicorn_components_unicorn_template_response_index_c88e97b0.png ├── _sphinx_design_static │ └── design-tabs.js └── genindex.html ├── .flake8 ├── captain-definition ├── static ├── img │ ├── favicon.svg │ ├── zap.svg │ ├── code.svg │ ├── video.svg │ ├── book.svg │ ├── layers.svg │ ├── download.svg │ ├── heart.svg │ ├── layout.svg │ ├── link.svg │ ├── monitor.svg │ ├── feather.svg │ ├── tool.svg │ ├── twitter.svg │ ├── watch.svg │ ├── github.svg │ ├── mastodon.svg │ ├── unicorn-mono.svg │ └── unicorn.svg ├── highlight.js │ └── 10.1.1 │ │ └── styles │ │ └── solarized-light.css └── css │ └── main.css ├── .isort.cfg ├── unicorn ├── components │ ├── count_characters.py │ ├── todo.py │ ├── clicks.py │ ├── select.py │ ├── todo_bulma.py │ ├── validation.py │ └── search.py └── templates │ └── unicorn │ ├── clicks.html │ ├── count-characters.html │ ├── select.html │ ├── search.html │ ├── todo.html │ ├── todo-bulma.html │ └── validation.html ├── .env.example ├── gunicorn.conf.py ├── DEVELOPING.md ├── README.md ├── manage.py ├── source ├── queue-requests.md ├── direct-view.md ├── custom-morphers.md ├── javascript.md ├── dirty-states.md ├── cli.md ├── troubleshooting.md ├── _static │ └── styles │ │ └── unicorn.css ├── visibility.md ├── getting-started.md ├── partial-updates.md ├── messages.md ├── installation.md ├── settings.md ├── redirecting.md ├── architecture.md ├── polling.md └── loading-states.md ├── .github └── workflows │ └── deploy.yml ├── LICENSE ├── pyproject.toml ├── .gitignore └── Dockerfile /project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /www/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/scripts/furo-extensions.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_sources/components/actions.md.txt: -------------------------------------------------------------------------------- 1 | # Actions 2 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | select = C,E,F,W,B,B950 4 | ignore = E501 -------------------------------------------------------------------------------- /captain-definition: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "dockerfilePath": "./Dockerfile" 4 | } 5 | -------------------------------------------------------------------------------- /docs/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/objects.inv -------------------------------------------------------------------------------- /docs/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_static/file.png -------------------------------------------------------------------------------- /docs/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_static/minus.png -------------------------------------------------------------------------------- /docs/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_static/plus.png -------------------------------------------------------------------------------- /docs/unicorn-latest.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/unicorn-latest.pdf -------------------------------------------------------------------------------- /docs/_sources/installation.rst.txt: -------------------------------------------------------------------------------- 1 | Installation 2 | ================== 3 | 4 | * :ref:`genindex` 5 | * :ref:`modindex` 6 | * :ref:`search` -------------------------------------------------------------------------------- /static/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 🦄 4 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output=3 3 | include_trailing_comma=True 4 | force_grid_wrap=0 5 | combine_as_imports=True 6 | line_length=88 7 | -------------------------------------------------------------------------------- /docs/_sources/api/startunicorn/index.rst.txt: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | :py:mod:`startunicorn` 4 | ====================== 5 | 6 | .. py:module:: startunicorn 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_cli_63c0dc4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_cli_63c0dc4b.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_faq_eb30496d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_faq_eb30496d.png -------------------------------------------------------------------------------- /unicorn/components/count_characters.py: -------------------------------------------------------------------------------- 1 | from django_unicorn.components import UnicornView 2 | 3 | 4 | class CountCharactersView(UnicornView): 5 | name = "World" 6 | -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_actions_333108d1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_actions_333108d1.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_index_bc786ce5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_index_bc786ce5.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_polling_884e9da5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_polling_884e9da5.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_views_8b18f15a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_views_8b18f15a.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_changelog_1a120bff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_changelog_1a120bff.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_changelog_42730650.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_changelog_42730650.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_messages_7a53ae5a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_messages_7a53ae5a.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_settings_65d7d295.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_settings_65d7d295.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_templates_254d134f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_templates_254d134f.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_architecture_6d4bd531.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_architecture_6d4bd531.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_components_62ff22c2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_components_62ff22c2.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_direct-view_422cf1a2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_direct-view_422cf1a2.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_dirty-states_529d8250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_dirty-states_529d8250.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_installation_af8c48af.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_installation_af8c48af.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_installation_dd93ef79.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_installation_dd93ef79.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_javascript_8d589da4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_javascript_8d589da4.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_redirecting_d7a57503.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_redirecting_d7a57503.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_validation_a62ab460.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_validation_a62ab460.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_visibility_eae56b73.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_visibility_eae56b73.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_django-models_250bf753.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_django-models_250bf753.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_loading-states_647850b3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_loading-states_647850b3.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_queue-requests_57f5c4f7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_queue-requests_57f5c4f7.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_child-components_2deacd60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_child-components_2deacd60.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_code-of-conduct_76aefa6b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_code-of-conduct_76aefa6b.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_custom-morphers_80bbecc8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_custom-morphers_80bbecc8.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_getting-started_39f7164f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_getting-started_39f7164f.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_partial-updates_cd522ecb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_partial-updates_cd522ecb.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_troubleshooting_ccafcb1e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_troubleshooting_ccafcb1e.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_startunicorn_index_1dbcdb7a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_startunicorn_index_1dbcdb7a.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_index_2e0b4934.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_index_2e0b4934.png -------------------------------------------------------------------------------- /project/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.asgi import get_asgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 6 | 7 | application = get_asgi_application() 8 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_db_index_505b4485.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_db_index_505b4485.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_urls_index_fccefb84.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_urls_index_fccefb84.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_cacher_index_e2409b8f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_cacher_index_e2409b8f.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_errors_index_3f9648c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_errors_index_3f9648c4.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_typer_index_3b3551a2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_typer_index_3b3551a2.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_typing_index_47fd05be.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_typing_index_47fd05be.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_utils_index_cf39ac98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_utils_index_cf39ac98.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_views_index_4f8dbf06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_views_index_4f8dbf06.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_settings_index_d0186d72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_settings_index_d0186d72.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_components_index_15a78d7b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_components_index_15a78d7b.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_decorators_index_a5530e41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_decorators_index_a5530e41.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_serializer_index_e7875a35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_serializer_index_e7875a35.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_templatetags_index_fbccbbc4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_templatetags_index_fbccbbc4.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_views_utils_index_5e575f66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_views_utils_index_5e575f66.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_views_objects_index_d3ef82f7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_views_objects_index_d3ef82f7.png -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | from www import urls as www_urls 4 | 5 | 6 | urlpatterns = [ 7 | path("", include(www_urls)), 8 | path("unicorn/", include("django_unicorn.urls")), 9 | ] 10 | -------------------------------------------------------------------------------- /unicorn/templates/unicorn/clicks.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
-------------------------------------------------------------------------------- /unicorn/templates/unicorn/count-characters.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |

5 |

6 | Character count: {{ name|length }} 7 |

8 |
-------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_components_fields_index_596f1a6f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_components_fields_index_596f1a6f.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_components_mixins_index_bf1db447.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_components_mixins_index_bf1db447.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_call_method_parser_index_3ef0ff13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_call_method_parser_index_3ef0ff13.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_components_updaters_index_6584a2f4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_components_updaters_index_6584a2f4.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_templatetags_unicorn_index_82418033.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_templatetags_unicorn_index_82418033.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_views_action_parsers_index_2e95ccb5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_views_action_parsers_index_2e95ccb5.png -------------------------------------------------------------------------------- /docs/_static/scripts/furo.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * gumshoejs v5.1.2 (patched by @pradyunsg) 3 | * A simple, framework-agnostic scrollspy script. 4 | * (c) 2019 Chris Ferdinandi 5 | * MIT License 6 | * http://github.com/cferdinandi/gumshoe 7 | */ 8 | -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_components_unicorn_view_index_1ddd7c6c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_components_unicorn_view_index_1ddd7c6c.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_views_action_parsers_utils_index_e48bde12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_views_action_parsers_utils_index_e48bde12.png -------------------------------------------------------------------------------- /unicorn/components/todo.py: -------------------------------------------------------------------------------- 1 | from django_unicorn.components import UnicornView 2 | 3 | 4 | class TodoView(UnicornView): 5 | task = "" 6 | tasks = [] 7 | 8 | def add(self): 9 | self.tasks.append(self.task) 10 | self.task = "" 11 | -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_views_action_parsers_call_method_index_3562d4bf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_views_action_parsers_call_method_index_3562d4bf.png -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_views_action_parsers_sync_input_index_8a60afd4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_views_action_parsers_sync_input_index_8a60afd4.png -------------------------------------------------------------------------------- /unicorn/components/clicks.py: -------------------------------------------------------------------------------- 1 | from django_unicorn.components import UnicornView 2 | 3 | 4 | class ClicksView(UnicornView): 5 | count = 0 6 | 7 | def add(self): 8 | self.count += 1 9 | 10 | def subtract(self): 11 | self.count -= 1 12 | -------------------------------------------------------------------------------- /docs/_images/social_previews/summary_api_django_unicorn_components_unicorn_template_response_index_c88e97b0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamghill/django-unicorn.com/main/docs/_images/social_previews/summary_api_django_unicorn_components_unicorn_template_response_index_c88e97b0.png -------------------------------------------------------------------------------- /static/img/zap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /unicorn/templates/unicorn/select.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 | 9 | Selected fruit: '{{ selected_fruit }}' 10 |
-------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY=https://www.miniwebtool.com/django-secret-key-generator/ 2 | DATABASE_NAME=deploylogs 3 | DATABASE_USER=FAKE_USER 4 | DATABASE_HOST=localhost 5 | DATABASE_PORT=5432 6 | 7 | TMDB_API_KEY= 8 | TMDB_ACCESS_TOKEN= 9 | TMDB_REQUEST_TOKEN_REDIRECT_URL=http://localhost:8099/auth/tmdb/access_token 10 | -------------------------------------------------------------------------------- /static/img/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /unicorn/components/select.py: -------------------------------------------------------------------------------- 1 | from django_unicorn.components import UnicornView 2 | 3 | 4 | class SelectView(UnicornView): 5 | selected_fruit = "" 6 | fruits = [ 7 | "Apple", 8 | "Grape", 9 | "Banana", 10 | ] 11 | 12 | class Meta: 13 | javascript_exclude = ("fruits",) 14 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/templatetags/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.templatetags` 2 | ===================================== 3 | 4 | .. py:module:: django_unicorn.templatetags 5 | 6 | 7 | Submodules 8 | ---------- 9 | .. toctree:: 10 | :titlesonly: 11 | :maxdepth: 1 12 | 13 | unicorn/index.rst 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/db/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.db` 2 | =========================== 3 | 4 | .. py:module:: django_unicorn.db 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:class:: DbModel(name: str, model_class: django.db.models.Model, *, defaults: Optional[dict] = None) 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /static/img/video.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/check-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /static/img/book.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/urls/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.urls` 2 | ============================= 3 | 4 | .. py:module:: django_unicorn.urls 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:data:: app_name 11 | :value: 'django_unicorn' 12 | 13 | 14 | 15 | .. py:data:: urlpatterns 16 | :value: () 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /static/img/layers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/layout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/monitor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/feather.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/tool.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/graphviz.css: -------------------------------------------------------------------------------- 1 | /* 2 | * graphviz.css 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- graphviz extension. 6 | * 7 | * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | img.graphviz { 13 | border: 0; 14 | max-width: 100%; 15 | } 16 | 17 | object.graphviz { 18 | max-width: 100%; 19 | } 20 | -------------------------------------------------------------------------------- /static/img/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/views/action_parsers/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.views.action_parsers` 2 | ============================================= 3 | 4 | .. py:module:: django_unicorn.views.action_parsers 5 | 6 | 7 | Submodules 8 | ---------- 9 | .. toctree:: 10 | :titlesonly: 11 | :maxdepth: 1 12 | 13 | call_method/index.rst 14 | sync_input/index.rst 15 | utils/index.rst 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/components/fields/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.components.fields` 2 | ========================================== 3 | 4 | .. py:module:: django_unicorn.components.fields 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:class:: UnicornField 11 | 12 | 13 | Base class to provide a way to serialize a component field quickly. 14 | 15 | .. py:method:: to_json() 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/_static/copy-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | # Gunicorn configuration file 2 | # https://docs.gunicorn.org/en/stable/configure.html#configuration-file 3 | # https://docs.gunicorn.org/en/stable/settings.html 4 | # https://adamj.eu/tech/2021/12/29/set-up-a-gunicorn-configuration-file-and-test-it/ 5 | import multiprocessing 6 | 7 | max_requests = 1000 8 | max_requests_jitter = 50 9 | 10 | log_file = "-" 11 | 12 | bind = "0.0.0.0:80" 13 | workers = multiprocessing.cpu_count() * 2 + 1 14 | -------------------------------------------------------------------------------- /static/img/watch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/decorators/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.decorators` 2 | =================================== 3 | 4 | .. py:module:: django_unicorn.decorators 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:function:: timed(func, *args, **kwargs) 11 | 12 | Decorator that prints out the timing of a function. 13 | 14 | Slightly altered version of https://gist.github.com/bradmontgomery/bd6288f09a24c06746bbe54afe4b8a82. 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/views/action_parsers/sync_input/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.views.action_parsers.sync_input` 2 | ======================================================== 3 | 4 | .. py:module:: django_unicorn.views.action_parsers.sync_input 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:function:: handle(component_request: django_unicorn.views.objects.ComponentRequest, component: django_unicorn.components.UnicornView, payload: Dict) 11 | 12 | 13 | -------------------------------------------------------------------------------- /unicorn/components/todo_bulma.py: -------------------------------------------------------------------------------- 1 | from django_unicorn.components import UnicornView 2 | from django import forms 3 | 4 | 5 | class TodoForm(forms.Form): 6 | task = forms.CharField(min_length=2, max_length=20, required=True) 7 | 8 | 9 | class TodoBulmaView(UnicornView): 10 | form_class = TodoForm 11 | 12 | task = "" 13 | tasks = [] 14 | 15 | def add(self): 16 | if self.is_valid(): 17 | self.tasks.append(self.task) 18 | self.task = "" 19 | -------------------------------------------------------------------------------- /docs/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '0.61.0', 4 | LANGUAGE: 'en', 5 | COLLAPSE_INDEX: false, 6 | BUILDER: 'dirhtml', 7 | FILE_SUFFIX: '.html', 8 | LINK_SUFFIX: '.html', 9 | HAS_SOURCE: true, 10 | SOURCELINK_SUFFIX: '.txt', 11 | NAVIGATION_WITH_KEYS: false, 12 | SHOW_SEARCH_SUMMARY: true, 13 | ENABLE_SEARCH_SHORTCUTS: true, 14 | }; -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/views/utils/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.views.utils` 2 | ==================================== 3 | 4 | .. py:module:: django_unicorn.views.utils 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:function:: set_property_from_data(component_or_field: Union[django_unicorn.components.UnicornView, django_unicorn.components.UnicornField, django.db.models.Model], name: str, value: Any) -> None 11 | 12 | Sets properties on the component based on passed-in data. 13 | 14 | 15 | -------------------------------------------------------------------------------- /static/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/components/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.components` 2 | =================================== 3 | 4 | .. py:module:: django_unicorn.components 5 | 6 | 7 | Submodules 8 | ---------- 9 | .. toctree:: 10 | :titlesonly: 11 | :maxdepth: 1 12 | 13 | fields/index.rst 14 | mixins/index.rst 15 | unicorn_template_response/index.rst 16 | unicorn_view/index.rst 17 | updaters/index.rst 18 | 19 | 20 | Package Contents 21 | ---------------- 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/typing/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.typing` 2 | =============================== 3 | 4 | .. py:module:: django_unicorn.typing 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:data:: M_co 11 | 12 | 13 | 14 | .. py:class:: QuerySetType(model=None, query=None, using=None, hints=None) 15 | 16 | 17 | Bases: :py:obj:`Generic`\ [\ :py:obj:`M_co`\ ], :py:obj:`django.db.models.QuerySet` 18 | 19 | Type for QuerySet that can be used for a typehint in components. 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. Unicorn documentation master file, created by 2 | sphinx-quickstart on Sat Dec 26 14:58:44 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Unicorn's documentation! 7 | =================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | installation 13 | faq 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/components/mixins/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.components.mixins` 2 | ========================================== 3 | 4 | .. py:module:: django_unicorn.components.mixins 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:class:: ModelValueMixin 11 | 12 | 13 | Adds a `value` method to a model similar to `QuerySet.values(*fields)` which serializes 14 | a model into a dictionary with the fields as specified in the `fields` argument. 15 | 16 | .. py:method:: value(*fields) 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /unicorn/templates/unicorn/search.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |

6 | Matching states: 7 | 8 | {% if states %} 9 |

14 | 15 | 16 | {% else %} 17 | n/a 18 | {% endif %} 19 |

20 |
21 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/views/action_parsers/call_method/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.views.action_parsers.call_method` 2 | ========================================================= 3 | 4 | .. py:module:: django_unicorn.views.action_parsers.call_method 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:function:: get_origin(tp: Any) -> Optional[Any] 11 | 12 | 13 | .. py:function:: handle(component_request: django_unicorn.views.objects.ComponentRequest, component: django_unicorn.components.UnicornView, payload: Dict) 14 | 15 | 16 | -------------------------------------------------------------------------------- /www/templatetags/utils.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from cache_memoize import cache_memoize 4 | 5 | 6 | register = template.Library() 7 | 8 | 9 | @cache_memoize(60 + 10) 10 | @register.simple_tag 11 | def code_include(file_name): 12 | file_contents = "" 13 | 14 | if file_name.endswith(".html"): 15 | file_contents = f"\n" 16 | elif file_name.endswith(".py"): 17 | file_contents = f"# {file_name}\n" 18 | 19 | with open(file_name) as f: 20 | file_contents += f.read() 21 | 22 | return file_contents 23 | -------------------------------------------------------------------------------- /unicorn/templates/unicorn/todo.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 |

8 | {% if tasks %} 9 |

14 | 15 | 16 | {% else %} 17 | No tasks 🎉 18 | {% endif %} 19 |

20 |
21 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | ## Install 4 | 5 | 1. `git clone git@github.com:adamghill/django-unicorn.git` 6 | 1. `git clone git@github.com:adamghill/django-unicorn.com.git` 7 | 1. `cd django-unicorn.com` 8 | 1. `poetry install` 9 | 10 | ## Build latest docs 11 | 12 | 1. Make sure `django-unicorn` directory has latest code 13 | 1. `poe l` to get the latest docs from `django-unicorn`, build the documentation, and start http://localhost:8000 14 | 1. Update `www/templates/www/index.html` with the latest version 15 | 1. Remove the comparison box from `source/index.md` and run `poe sp` to build the pdf documentation 16 | -------------------------------------------------------------------------------- /www/templates/www/examples/todo.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/examples.html" %} 2 | {% load unicorn utils %} 3 | 4 | {% block subtitle %}Todo{% endblock subtitle %} 5 | 6 | {% block example_title %}Todo{% endblock example_title %} 7 | 8 | {% block example_content %} 9 | {% unicorn 'todo' %} 10 | {% endblock example_content %} 11 | 12 | {% block example_source %} 13 |
{% code_include 'unicorn/templates/unicorn/todo.html' %}
14 |
{% code_include 'unicorn/components/todo.py' %}
15 | {% endblock example_source %} 16 | -------------------------------------------------------------------------------- /www/templates/www/examples/search.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/examples.html" %} 2 | {% load unicorn utils %} 3 | 4 | {% block subtitle %}Search{% endblock subtitle %} 5 | 6 | {% block example_title %}Search{% endblock example_title %} 7 | 8 | {% block example_content %} 9 | {% unicorn 'search' %} 10 | {% endblock example_content %} 11 | 12 | {% block example_source %} 13 |
{% code_include 'unicorn/templates/unicorn/search.html' %}
14 |
{% code_include 'unicorn/components/search.py' %}
15 | {% endblock example_source %} 16 | -------------------------------------------------------------------------------- /www/templates/www/examples/select.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/examples.html" %} 2 | {% load unicorn utils %} 3 | 4 | {% block subtitle %}Select{% endblock subtitle %} 5 | 6 | {% block example_title %}Select{% endblock example_title %} 7 | 8 | {% block example_content %} 9 | {% unicorn 'select' %} 10 | {% endblock example_content %} 11 | 12 | {% block example_source %} 13 |
{% code_include 'unicorn/templates/unicorn/select.html' %}
14 |
{% code_include 'unicorn/components/select.py' %}
15 | {% endblock example_source %} 16 | -------------------------------------------------------------------------------- /www/templates/www/examples/click-counter.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/examples.html" %} 2 | {% load unicorn utils %} 3 | 4 | {% block subtitle %}Click{% endblock subtitle %} 5 | 6 | {% block example_title %}Click counter{% endblock example_title %} 7 | 8 | {% block example_content %} 9 | {% unicorn 'clicks' %} 10 | {% endblock example_content %} 11 | 12 | {% block example_source %} 13 |
{% code_include 'unicorn/templates/unicorn/clicks.html' %}
14 |
{% code_include 'unicorn/components/clicks.py' %}
15 | {% endblock example_source %} 16 | -------------------------------------------------------------------------------- /www/templates/www/examples/validation.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/examples.html" %} 2 | {% load unicorn utils %} 3 | 4 | {% block subtitle %}Validation{% endblock subtitle %} 5 | 6 | {% block example_title %}Validation{% endblock example_title %} 7 | 8 | {% block example_content %} 9 | {% unicorn 'validation' %} 10 | {% endblock example_content %} 11 | 12 | {% block example_source %} 13 |
{% code_include 'unicorn/templates/unicorn/validation.html' %}
14 |
{% code_include 'unicorn/components/validation.py' %}
15 | {% endblock example_source %} 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-unicorn.com 2 | 3 | This is the documentation site for [`Unicorn`](https://www.django-unicorn.com/). It includes the Sphinx-generated docs, code examples, screenshares, and a landing page. 4 | 5 | ## Why? 6 | 7 | [readthedocs](https://readthedocs.org/) is an awesome resource and I love that they provide a free way to host documentation for open-source projects. However, to show off the power of `Unicorn` it seems best for the documentation site to have real-world examples of how it works. Plus, serving the Sphinx docs directly (via [django-docs](https://django-docs.readthedocs.io/)) improves the SEO traffic (since the docs are served from a sub-folder instead of a third-level domain). 8 | -------------------------------------------------------------------------------- /www/templates/www/examples/count-characters.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/examples.html" %} 2 | {% load unicorn utils %} 3 | 4 | {% block subtitle %}Count characters{% endblock subtitle %} 5 | 6 | {% block example_title %}Count characters{% endblock example_title %} 7 | 8 | {% block example_content %} 9 | {% unicorn 'count-characters' %} 10 | {% endblock example_content %} 11 | 12 | {% block example_source %} 13 |
{% code_include 'unicorn/templates/unicorn/count-characters.html' %}
14 |
{% code_include 'unicorn/components/count_characters.py' %}
15 | {% endblock example_source %} 16 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /www/templates/www/screencasts.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/base.html" %} 2 | {% load compress static unicorn utils %} 3 | 4 | {% block title %}django-unicorn{% endblock title %} 5 | 6 | {% block styles %} 7 | 8 | {% compress css %} 9 | 10 | 11 | {% endcompress %} 12 | 13 | {% endblock styles %} 14 | 15 | {% block content %} 16 | 17 |
18 |
19 |
20 |

21 | Screencasts coming soon! 22 |

23 |
24 |
25 |
26 | 27 | {% endblock content %} -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn` 2 | ======================== 3 | 4 | .. py:module:: django_unicorn 5 | 6 | 7 | Subpackages 8 | ----------- 9 | .. toctree:: 10 | :titlesonly: 11 | :maxdepth: 3 12 | 13 | components/index.rst 14 | templatetags/index.rst 15 | views/index.rst 16 | 17 | 18 | Submodules 19 | ---------- 20 | .. toctree:: 21 | :titlesonly: 22 | :maxdepth: 1 23 | 24 | cacher/index.rst 25 | call_method_parser/index.rst 26 | db/index.rst 27 | decorators/index.rst 28 | errors/index.rst 29 | serializer/index.rst 30 | settings/index.rst 31 | typer/index.rst 32 | typing/index.rst 33 | urls/index.rst 34 | utils/index.rst 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/_static/design-tabs.js: -------------------------------------------------------------------------------- 1 | var sd_labels_by_text = {}; 2 | 3 | function ready() { 4 | const li = document.getElementsByClassName("sd-tab-label"); 5 | for (const label of li) { 6 | syncId = label.getAttribute("data-sync-id"); 7 | if (syncId) { 8 | label.onclick = onLabelClick; 9 | if (!sd_labels_by_text[syncId]) { 10 | sd_labels_by_text[syncId] = []; 11 | } 12 | sd_labels_by_text[syncId].push(label); 13 | } 14 | } 15 | } 16 | 17 | function onLabelClick() { 18 | // Activate other inputs with the same sync id. 19 | syncId = this.getAttribute("data-sync-id"); 20 | for (label of sd_labels_by_text[syncId]) { 21 | if (label === this) continue; 22 | label.previousElementSibling.checked = true; 23 | } 24 | window.localStorage.setItem("sphinx-design-last-tab", syncId); 25 | } 26 | 27 | document.addEventListener("DOMContentLoaded", ready, false); 28 | -------------------------------------------------------------------------------- /docs/_sphinx_design_static/design-tabs.js: -------------------------------------------------------------------------------- 1 | var sd_labels_by_text = {}; 2 | 3 | function ready() { 4 | const li = document.getElementsByClassName("sd-tab-label"); 5 | for (const label of li) { 6 | syncId = label.getAttribute("data-sync-id"); 7 | if (syncId) { 8 | label.onclick = onLabelClick; 9 | if (!sd_labels_by_text[syncId]) { 10 | sd_labels_by_text[syncId] = []; 11 | } 12 | sd_labels_by_text[syncId].push(label); 13 | } 14 | } 15 | } 16 | 17 | function onLabelClick() { 18 | // Activate other inputs with the same sync id. 19 | syncId = this.getAttribute("data-sync-id"); 20 | for (label of sd_labels_by_text[syncId]) { 21 | if (label === this) continue; 22 | label.previousElementSibling.checked = true; 23 | } 24 | window.localStorage.setItem("sphinx-design-last-tab", syncId); 25 | } 26 | 27 | document.addEventListener("DOMContentLoaded", ready, false); 28 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/templatetags/unicorn/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.templatetags.unicorn` 2 | ============================================= 3 | 4 | .. py:module:: django_unicorn.templatetags.unicorn 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:data:: register 11 | 12 | 13 | 14 | .. py:data:: MINIMUM_ARGUMENT_COUNT 15 | :value: 2 16 | 17 | 18 | 19 | .. py:function:: unicorn_scripts() 20 | 21 | 22 | .. py:function:: unicorn_errors(context) 23 | 24 | 25 | .. py:function:: unicorn(parser, token) 26 | 27 | 28 | .. py:class:: UnicornNode(component_name: django.template.base.FilterExpression, args: Optional[List] = None, kwargs: Optional[Dict] = None, unparseable_kwargs: Optional[Dict] = None) 29 | 30 | 31 | Bases: :py:obj:`django.template.Node` 32 | 33 | .. py:method:: render(context) 34 | 35 | Return the node rendered as a string. 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/views/objects/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.views.objects` 2 | ====================================== 3 | 4 | .. py:module:: django_unicorn.views.objects 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:class:: Action(data) 11 | 12 | 13 | Action that gets queued. 14 | 15 | 16 | .. py:function:: sort_dict(d) 17 | 18 | 19 | .. py:class:: ComponentRequest(request, component_name) 20 | 21 | 22 | Parses, validates, and stores all of the data from the message request. 23 | 24 | .. py:method:: validate_checksum() 25 | 26 | Validates that the checksum in the request matches the data. 27 | 28 | :returns: Raises `AssertionError` if the checksums don't match. 29 | 30 | 31 | 32 | .. py:class:: Return(method_name, args=None, kwargs=None) 33 | 34 | 35 | .. py:property:: value 36 | 37 | 38 | .. py:method:: get_data() 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /unicorn/components/validation.py: -------------------------------------------------------------------------------- 1 | from django_unicorn.components import UnicornView 2 | 3 | from django import forms 4 | from django.utils import timezone 5 | 6 | 7 | class ValidationForm(forms.Form): 8 | text = forms.CharField(min_length=3, max_length=10) 9 | now = forms.DateTimeField() 10 | number = forms.IntegerField() 11 | 12 | 13 | class ValidationView(UnicornView): 14 | form_class = ValidationForm 15 | 16 | text = "hello" 17 | number = "" 18 | 19 | _now = None 20 | 21 | @property 22 | def now(self): 23 | self._now = timezone.now() 24 | return self._now 25 | 26 | @now.setter 27 | def now(self, val): 28 | self._now = val 29 | 30 | def set_text_no_validation(self): 31 | self.text = "no validation" 32 | 33 | def set_text_with_validation(self): 34 | self.text = "validation" 35 | self.validate() 36 | 37 | def set_number(self, number): 38 | self.number = number 39 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/components/updaters/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.components.updaters` 2 | ============================================ 3 | 4 | .. py:module:: django_unicorn.components.updaters 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:class:: Update 11 | 12 | 13 | Base class for updaters. 14 | 15 | .. py:method:: to_json() 16 | 17 | 18 | 19 | .. py:class:: HashUpdate(url_hash: str) 20 | 21 | 22 | Bases: :py:obj:`Update` 23 | 24 | Updates the current URL hash from an action method. 25 | 26 | 27 | .. py:class:: LocationUpdate(redirect: django.http.response.HttpResponseRedirect, title: Optional[str] = None) 28 | 29 | 30 | Bases: :py:obj:`Update` 31 | 32 | Updates the current URL from an action method. 33 | 34 | 35 | .. py:class:: PollUpdate(*, timing: Optional[int] = None, method: Optional[str] = None, disable: bool = False) 36 | 37 | 38 | Bases: :py:obj:`Update` 39 | 40 | Updates the current poll from an action method. 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/views/action_parsers/utils/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.views.action_parsers.utils` 2 | =================================================== 3 | 4 | .. py:module:: django_unicorn.views.action_parsers.utils 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:function:: set_property_value(component: django_unicorn.components.UnicornView, property_name: Optional[str], property_value: Any, data: Optional[Dict] = None, call_resolved_method=True) -> None 11 | 12 | Sets properties on the component. 13 | Also updates the data dictionary which gets set back as part of the payload. 14 | 15 | :param param component: Component to set attributes on. 16 | :param param property_name: Name of the property. 17 | :param param property_value: Value to set on the property. 18 | :param param data: Dictionary that gets sent back with the response. Defaults to {}. 19 | :param call_resolved_method: Whether or not to call the resolved method. Defaults to True. 20 | 21 | 22 | -------------------------------------------------------------------------------- /source/queue-requests.md: -------------------------------------------------------------------------------- 1 | # Queue Requests 2 | 3 | This is an experimental feature of that queues up slow-processing component views to prevent race conditions. For simple components this should not be necessary. 4 | 5 | Serialization is turned off by default, but can be enabled in the [settings](settings.md#serial). 6 | 7 | ```{warning} 8 | This feature will be disabled automatically if the cache backend is set to ["django.core.cache.backends.dummy.DummyCache"](https://docs.djangoproject.com/en/stable/topics/cache/#dummy-caching-for-development). 9 | 10 | [Local memory caching](https://docs.djangoproject.com/en/3.1/topics/cache/#local-memory-caching) (the default if no `CACHES` setting is provided) will work fine if the web server only has one process. For more production use cases, consider using [`redis`](https://github.com/jazzband/django-redis), [`Memcache`](https://docs.djangoproject.com/en/stable/topics/cache/#memcached), or [database caching](https://docs.djangoproject.com/en/stable/topics/cache/#database-caching). 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/_sources/queue-requests.md.txt: -------------------------------------------------------------------------------- 1 | # Queue Requests 2 | 3 | This is an experimental feature of that queues up slow-processing component views to prevent race conditions. For simple components this should not be necessary. 4 | 5 | Serialization is turned off by default, but can be enabled in the [settings](settings.md#serial). 6 | 7 | ```{warning} 8 | This feature will be disabled automatically if the cache backend is set to ["django.core.cache.backends.dummy.DummyCache"](https://docs.djangoproject.com/en/stable/topics/cache/#dummy-caching-for-development). 9 | 10 | [Local memory caching](https://docs.djangoproject.com/en/3.1/topics/cache/#local-memory-caching) (the default if no `CACHES` setting is provided) will work fine if the web server only has one process. For more production use cases, consider using [`redis`](https://github.com/jazzband/django-redis), [`Memcache`](https://docs.djangoproject.com/en/stable/topics/cache/#memcached), or [database caching](https://docs.djangoproject.com/en/stable/topics/cache/#database-caching). 11 | ``` 12 | -------------------------------------------------------------------------------- /static/img/mastodon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | amd64: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Login to ghcr.io 21 | uses: docker/login-action@v3.3.0 22 | with: 23 | registry: ${{ env.REGISTRY }} 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | - name: Build image and push to registry 27 | uses: docker/build-push-action@v4 28 | with: 29 | context: . 30 | file: Dockerfile 31 | platforms: linux/amd64 32 | push: true 33 | tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 34 | - name: Call webhook 35 | run: | 36 | curl --request GET '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}' 37 | -------------------------------------------------------------------------------- /www/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path, re_path 2 | from django.views.generic.base import RedirectView 3 | 4 | from . import views 5 | 6 | app_name = "www" 7 | 8 | urlpatterns = [ 9 | path("", views.index, name="index"), 10 | path("articles", views.articles, name="articles"), 11 | path("sponsors", views.sponsors, name="sponsors"), 12 | # screencasts 13 | re_path(r"^screencasts/(?P[\w/-]+)$", views.screencasts, name="screencasts",), 14 | path("screencasts", RedirectView.as_view(url="/screencasts/installation")), 15 | # examples 16 | re_path(r"^examples/(?P[\w/-]+)$", views.examples, name="examples",), 17 | path("examples", RedirectView.as_view(url="/examples/todo")), 18 | # old documentation path 19 | path("documentation", views.documentation, name="documentation"), 20 | re_path( 21 | r"^documentation/(?P[\w/-]+)$", views.documentation, name="documentation", 22 | ), 23 | # sphinx-generated documentation 24 | re_path(r"docs/(?P[\w-]+[^/])$", views.docs_redirect), 25 | path("docs/", include("docs.urls")), 26 | ] 27 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/cacher/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.cacher` 2 | =============================== 3 | 4 | .. py:module:: django_unicorn.cacher 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:class:: PointerUnicornView(component_cache_key) 11 | 12 | 13 | 14 | .. py:class:: CacheableComponent(component: django_unicorn.views.UnicornView) 15 | 16 | 17 | Updates a component into something that is cacheable/pickleable. Also set pointers to parents/children. 18 | Use in a `with` statement or explicitly call `__enter__` `__exit__` to use. It will restore the original component 19 | on exit. 20 | 21 | .. py:method:: components() -> List[django_unicorn.views.UnicornView] 22 | 23 | 24 | 25 | .. py:function:: cache_full_tree(component: django_unicorn.views.UnicornView) 26 | 27 | 28 | .. py:function:: restore_from_cache(component_cache_key: str, request: Optional[django.http.HttpRequest] = None) -> django_unicorn.views.UnicornView 29 | 30 | Gets a cached unicorn view by key, restoring and getting cached parents and children 31 | and setting the request. 32 | 33 | 34 | -------------------------------------------------------------------------------- /www/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect, render 2 | from django.views.decorators.cache import cache_page 3 | from django.views.decorators.csrf import csrf_protect 4 | from fbv.decorators import render_html 5 | 6 | 7 | @cache_page(60 * 15) 8 | @csrf_protect 9 | @render_html("www/index.html") 10 | def index(request): 11 | return {} 12 | 13 | 14 | @cache_page(60 * 15) 15 | @render_html("www/articles.html") 16 | def articles(request): 17 | return {} 18 | 19 | 20 | @cache_page(60 * 15) 21 | def screencasts(request, name="installation"): 22 | template_name = f"www/screencasts/{name}.html" 23 | return render(request, template_name) 24 | 25 | 26 | def examples(request, name="todo"): 27 | template_name = f"www/examples/{name}.html" 28 | return render(request, template_name) 29 | 30 | 31 | @cache_page(60 * 15) 32 | @render_html("www/sponsors.html") 33 | def sponsors(request): 34 | return {} 35 | 36 | 37 | @csrf_protect 38 | def documentation(request, name="introduction"): 39 | return redirect("/docs/") 40 | 41 | 42 | def docs_redirect(request, name): 43 | return redirect(name + "/") 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adam Hill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/settings/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.settings` 2 | ================================= 3 | 4 | .. py:module:: django_unicorn.settings 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:data:: SETTINGS_KEY 11 | :value: 'UNICORN' 12 | 13 | 14 | 15 | .. py:data:: LEGACY_SETTINGS_KEY 16 | 17 | 18 | 19 | .. py:data:: DEFAULT_MORPHER_NAME 20 | :value: 'morphdom' 21 | 22 | 23 | 24 | .. py:data:: MORPHER_NAMES 25 | :value: ('morphdom', 'alpine') 26 | 27 | 28 | 29 | .. py:function:: get_settings() 30 | 31 | 32 | .. py:function:: get_setting(key, default=None) 33 | 34 | 35 | .. py:function:: get_serial_settings() 36 | 37 | 38 | .. py:function:: get_cache_alias() 39 | 40 | 41 | .. py:function:: get_morpher_settings() 42 | 43 | 44 | .. py:function:: get_script_location() 45 | 46 | Valid choices: "append", "after". Default is "after". 47 | 48 | 49 | .. py:function:: get_serial_enabled() 50 | 51 | Default serial enabled is `False`. 52 | 53 | 54 | .. py:function:: get_serial_timeout() 55 | 56 | Default serial timeout is 60 seconds. 57 | 58 | 59 | .. py:function:: get_minify_html_enabled() 60 | 61 | 62 | -------------------------------------------------------------------------------- /source/direct-view.md: -------------------------------------------------------------------------------- 1 | # Direct View 2 | 3 | Usually components will be included in a regular Django template, however a component can also be specified in a `urls.py` file in instances where having an additional template is not necessary. 4 | 5 | ## Template Requirements 6 | 7 | - there must be one (and only one) element that wraps around the portion of the template that should be handled by `Unicorn` 8 | - the wrapping element must include `unicorn:view` as an attribute 9 | - the template must included the `unicorn_scripts` and `csrf_token` template tags 10 | 11 | Similar to a class-based view, `Unicorn` components have a `as_view` function which is used in `urls.py`. 12 | 13 | ## Example 14 | 15 | ```python 16 | # book.py 17 | from django_unicorn.components import UnicornView 18 | 19 | class BookView(UnicornView): 20 | title = "" 21 | ``` 22 | 23 | ```html 24 | 25 | {% load unicorn %} 26 | 27 | 28 | 29 | {% unicorn_scripts %} 30 | 31 | 32 | {% csrf_token %} 33 |

Book

34 | 35 |
36 |
37 | {{ title }} 38 |
39 | 40 | 41 | ``` 42 | 43 | ```python 44 | # urls.py 45 | from django.urls import path 46 | from unicorn.components.book import BookView 47 | 48 | urlpatterns = [ 49 | path("book", BookView.as_view(), name="book"), 50 | ] 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/_sources/direct-view.md.txt: -------------------------------------------------------------------------------- 1 | # Direct View 2 | 3 | Usually components will be included in a regular Django template, however a component can also be specified in a `urls.py` file in instances where having an additional template is not necessary. 4 | 5 | ## Template Requirements 6 | 7 | - there must be one (and only one) element that wraps around the portion of the template that should be handled by `Unicorn` 8 | - the wrapping element must include `unicorn:view` as an attribute 9 | - the template must included the `unicorn_scripts` and `csrf_token` template tags 10 | 11 | Similar to a class-based view, `Unicorn` components have a `as_view` function which is used in `urls.py`. 12 | 13 | ## Example 14 | 15 | ```python 16 | # book.py 17 | from django_unicorn.components import UnicornView 18 | 19 | class BookView(UnicornView): 20 | title = "" 21 | ``` 22 | 23 | ```html 24 | 25 | {% load unicorn %} 26 | 27 | 28 | 29 | {% unicorn_scripts %} 30 | 31 | 32 | {% csrf_token %} 33 |

Book

34 | 35 |
36 |
37 | {{ title }} 38 |
39 | 40 | 41 | ``` 42 | 43 | ```python 44 | # urls.py 45 | from django.urls import path 46 | from unicorn.components.book import BookView 47 | 48 | urlpatterns = [ 49 | path("book", BookView.as_view(), name="book"), 50 | ] 51 | ``` 52 | -------------------------------------------------------------------------------- /static/highlight.js/10.1.1/styles/solarized-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #fdf6e3; 12 | color: #657b83; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #93a1a1; 18 | } 19 | 20 | /* Solarized Green */ 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-addition { 24 | color: #859900; 25 | } 26 | 27 | /* Solarized Cyan */ 28 | .hljs-number, 29 | .hljs-string, 30 | .hljs-meta .hljs-meta-string, 31 | .hljs-literal, 32 | .hljs-doctag, 33 | .hljs-regexp { 34 | color: #2aa198; 35 | } 36 | 37 | /* Solarized Blue */ 38 | .hljs-title, 39 | .hljs-section, 40 | .hljs-name, 41 | .hljs-selector-id, 42 | .hljs-selector-class { 43 | color: #268bd2; 44 | } 45 | 46 | /* Solarized Yellow */ 47 | .hljs-attribute, 48 | .hljs-attr, 49 | .hljs-variable, 50 | .hljs-template-variable, 51 | .hljs-class .hljs-title, 52 | .hljs-type { 53 | color: #b58900; 54 | } 55 | 56 | /* Solarized Orange */ 57 | .hljs-symbol, 58 | .hljs-bullet, 59 | .hljs-subst, 60 | .hljs-meta, 61 | .hljs-meta .hljs-keyword, 62 | .hljs-selector-attr, 63 | .hljs-selector-pseudo, 64 | .hljs-link { 65 | color: #cb4b16; 66 | } 67 | 68 | /* Solarized Red */ 69 | .hljs-built_in, 70 | .hljs-deletion { 71 | color: #dc322f; 72 | } 73 | 74 | .hljs-formula { 75 | background: #eee8d5; 76 | } 77 | 78 | .hljs-emphasis { 79 | font-style: italic; 80 | } 81 | 82 | .hljs-strong { 83 | font-weight: bold; 84 | } 85 | -------------------------------------------------------------------------------- /docs/_static/debug.css: -------------------------------------------------------------------------------- 1 | /* 2 | This CSS file should be overridden by the theme authors. It's 3 | meant for debugging and developing the skeleton that this theme provides. 4 | */ 5 | body { 6 | font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, 7 | "Apple Color Emoji", "Segoe UI Emoji"; 8 | background: lavender; 9 | } 10 | .sb-announcement { 11 | background: rgb(131, 131, 131); 12 | } 13 | .sb-announcement__inner { 14 | background: black; 15 | color: white; 16 | } 17 | .sb-header { 18 | background: lightskyblue; 19 | } 20 | .sb-header__inner { 21 | background: royalblue; 22 | color: white; 23 | } 24 | .sb-header-secondary { 25 | background: lightcyan; 26 | } 27 | .sb-header-secondary__inner { 28 | background: cornflowerblue; 29 | color: white; 30 | } 31 | .sb-sidebar-primary { 32 | background: lightgreen; 33 | } 34 | .sb-main { 35 | background: blanchedalmond; 36 | } 37 | .sb-main__inner { 38 | background: antiquewhite; 39 | } 40 | .sb-header-article { 41 | background: lightsteelblue; 42 | } 43 | .sb-article-container { 44 | background: snow; 45 | } 46 | .sb-article-main { 47 | background: white; 48 | } 49 | .sb-footer-article { 50 | background: lightpink; 51 | } 52 | .sb-sidebar-secondary { 53 | background: lightgoldenrodyellow; 54 | } 55 | .sb-footer-content { 56 | background: plum; 57 | } 58 | .sb-footer-content__inner { 59 | background: palevioletred; 60 | } 61 | .sb-footer { 62 | background: pink; 63 | } 64 | .sb-footer__inner { 65 | background: salmon; 66 | } 67 | .sb-article { 68 | background: white; 69 | } 70 | -------------------------------------------------------------------------------- /source/custom-morphers.md: -------------------------------------------------------------------------------- 1 | # Custom Morphers 2 | 3 | The morpher is a library used to update specific parts of the DOM element instead of replacing the entire element. This improves performance and maintains the state of unchanged DOM elements, such as the cursor position in an input. 4 | 5 | The default morpher used in Unicorn is [`morphdom`](https://github.com/patrick-steele-idem/morphdom). The only alternative morpher available is the [Alpine.js morph plugin](https://alpinejs.dev/plugins/morph). 6 | 7 | ## `Morphdom` 8 | 9 | `morphdom` is the default morpher so no extra settings or installation is required to use it. 10 | 11 | ## `Alpine` 12 | 13 | Components which use both `Unicorn` and `Alpine.js` should use the `Alpine.js` morpher to prevent losing state when it gets re-rendered. 14 | 15 | ## Django Settings 16 | 17 | ```python 18 | # settings.py 19 | 20 | UNICORN = { 21 | ... 22 | "MORPHER": { 23 | "NAME": "alpine", 24 | } 25 | ... 26 | } 27 | ``` 28 | 29 | ```{note} 30 | `MORPHER.RELOAD_SCRIPT_ELEMENTS` is not currently supported for the `Alpine.js` morpher. 31 | ``` 32 | 33 | ### JavaScript Installation 34 | 35 | `Alpine.js` is not included in `Unicorn` so you will need to manually include it. Make sure to include `Alpine.js` and the morpher plugin by adding the following line to your template before `{% unicorn_scripts %}`. 36 | 37 | ```html 38 | ... 39 | 40 | 41 | 42 | {% unicorn_scripts %} 43 | 44 | ... 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/_sources/custom-morphers.md.txt: -------------------------------------------------------------------------------- 1 | # Custom Morphers 2 | 3 | The morpher is a library used to update specific parts of the DOM element instead of replacing the entire element. This improves performance and maintains the state of unchanged DOM elements, such as the cursor position in an input. 4 | 5 | The default morpher used in Unicorn is [`morphdom`](https://github.com/patrick-steele-idem/morphdom). The only alternative morpher available is the [Alpine.js morph plugin](https://alpinejs.dev/plugins/morph). 6 | 7 | ## `Morphdom` 8 | 9 | `morphdom` is the default morpher so no extra settings or installation is required to use it. 10 | 11 | ## `Alpine` 12 | 13 | Components which use both `Unicorn` and `Alpine.js` should use the `Alpine.js` morpher to prevent losing state when it gets re-rendered. 14 | 15 | ## Django Settings 16 | 17 | ```python 18 | # settings.py 19 | 20 | UNICORN = { 21 | ... 22 | "MORPHER": { 23 | "NAME": "alpine", 24 | } 25 | ... 26 | } 27 | ``` 28 | 29 | ```{note} 30 | `MORPHER.RELOAD_SCRIPT_ELEMENTS` is not currently supported for the `Alpine.js` morpher. 31 | ``` 32 | 33 | ### JavaScript Installation 34 | 35 | `Alpine.js` is not included in `Unicorn` so you will need to manually include it. Make sure to include `Alpine.js` and the morpher plugin by adding the following line to your template before `{% unicorn_scripts %}`. 36 | 37 | ```html 38 | ... 39 | 40 | 41 | 42 | {% unicorn_scripts %} 43 | 44 | ... 45 | ``` 46 | -------------------------------------------------------------------------------- /source/javascript.md: -------------------------------------------------------------------------------- 1 | # JavaScript Integration 2 | 3 | ## Call JavaScript from View 4 | 5 | To integrate with other JavaScript functions, view methods can call an arbitrary JavaScript function after it gets rendered. 6 | 7 | ```html 8 | 9 |
10 | 15 | 16 | 17 | 18 |
19 | ``` 20 | 21 | ```python 22 | # call_javascript.py 23 | from django_unicorn.components import UnicornView 24 | 25 | class CallJavascriptView(UnicornView): 26 | name = "" 27 | 28 | def mount(self): 29 | self.call("hello", "world") 30 | 31 | def hello(self): 32 | self.call("hello", self.name) 33 | ``` 34 | 35 | ## Trigger Model Update 36 | 37 | Normally when a model element gets changed by a user it will trigger an event which `Unicorn` listens for (either `input` or `blur` depending on if it has a `lazy` modifier). However, when setting an element with JavaScript those events do not fire. `Unicorn.trigger()` provides a way to trigger that event from JavaScript manually. 38 | 39 | The first argument to `trigger` is the component name. The second argument is the value for the element's `unicorn:key`. 40 | 41 | ```html 42 | 43 | 49 | 50 | 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/_sources/javascript.md.txt: -------------------------------------------------------------------------------- 1 | # JavaScript Integration 2 | 3 | ## Call JavaScript from View 4 | 5 | To integrate with other JavaScript functions, view methods can call an arbitrary JavaScript function after it gets rendered. 6 | 7 | ```html 8 | 9 |
10 | 15 | 16 | 17 | 18 |
19 | ``` 20 | 21 | ```python 22 | # call_javascript.py 23 | from django_unicorn.components import UnicornView 24 | 25 | class CallJavascriptView(UnicornView): 26 | name = "" 27 | 28 | def mount(self): 29 | self.call("hello", "world") 30 | 31 | def hello(self): 32 | self.call("hello", self.name) 33 | ``` 34 | 35 | ## Trigger Model Update 36 | 37 | Normally when a model element gets changed by a user it will trigger an event which `Unicorn` listens for (either `input` or `blur` depending on if it has a `lazy` modifier). However, when setting an element with JavaScript those events do not fire. `Unicorn.trigger()` provides a way to trigger that event from JavaScript manually. 38 | 39 | The first argument to `trigger` is the component name. The second argument is the value for the element's `unicorn:key`. 40 | 41 | ```html 42 | 43 | 49 | 50 | 54 | ``` 55 | -------------------------------------------------------------------------------- /static/img/unicorn-mono.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/call_method_parser/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.call_method_parser` 2 | =========================================== 3 | 4 | .. py:module:: django_unicorn.call_method_parser 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:exception:: InvalidKwargError 11 | 12 | 13 | Bases: :py:obj:`Exception` 14 | 15 | Common base class for all non-exit exceptions. 16 | 17 | 18 | .. py:function:: eval_value(value) 19 | 20 | Uses `ast.literal_eval` to parse strings into an appropriate Python primitive. 21 | 22 | Also returns an appropriate object for strings that look like they represent datetime, 23 | date, time, duration, or UUID. 24 | 25 | 26 | .. py:function:: parse_kwarg(kwarg: str, *, raise_if_unparseable=False) -> Dict[str, Any] 27 | 28 | Parses a potential kwarg as a string into a dictionary. 29 | 30 | .. rubric:: Example 31 | 32 | `parse_kwarg("test='1'")` == `{"test": "1"}` 33 | 34 | :param kwarg: Potential kwarg as a string. e.g. "test='1'". 35 | :param raise_if_unparseable: Raise an error if the `kwarg` cannot be parsed. Defaults to `False`. 36 | 37 | :returns: Dictionary of key-value pairs. 38 | 39 | 40 | .. py:function:: parse_call_method_name(call_method_name: str) -> Tuple[str, Tuple[Any, Ellipsis], Mapping[str, Any]] 41 | 42 | Parses the method name from the request payload into a set of parameters to pass to 43 | a method. 44 | 45 | :param param call_method_name: String representation of a method name with parameters, 46 | e.g. "set_name('Bob')" 47 | 48 | :returns: Tuple of method_name, a list of arguments and a dict of keyword arguments 49 | 50 | 51 | -------------------------------------------------------------------------------- /source/dirty-states.md: -------------------------------------------------------------------------------- 1 | # Dirty States 2 | 3 | `Unicorn` can provide context to the user that some data has been changed and will be updated. 4 | 5 | ## Toggling Attributes 6 | 7 | Elements can include an `unicorn:dirty` attribute with either an `attr` or `class` modifier. 8 | 9 | ### attr 10 | 11 | Set the specified attribute on the element that is changed. 12 | 13 | This example will set the input to be readonly when the model is changed. The attribute will be removed once the name is synced or if the input value is changed back to the original value. 14 | 15 | :::{code} html 16 | :force: true 17 | 18 | 19 |
20 | 21 |
22 | ::: 23 | 24 | ### class 25 | 26 | Add the specified class(es) to the model that is changed. 27 | 28 | This example will add _dirty_ and _changing_ classes to the input when the model is changed. The classes will be removed once the model is synced or if the input value is changed back to the original value. 29 | 30 | :::{code} html 31 | :force: true 32 | 33 | 34 |
35 | 36 |
37 | ::: 38 | 39 | ### class.remove 40 | 41 | Remove the specified class(es) from the model that is changed. 42 | 43 | This example will remove the _clean_ class from the input when the model is changed. The class will be added back once the model is synced or if the input value is changed back to the original value. 44 | 45 | :::{code} html 46 | :force: true 47 | 48 | 49 |
50 | 51 |
52 | ::: 53 | -------------------------------------------------------------------------------- /source/cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | `Unicorn` provides a Django management command to create new components. The first argument is the name of the Django app to create components in. Every argument after is the name of components that should be created. 4 | 5 | ```shell 6 | python manage.py startunicorn unicorn hello-world 7 | ``` 8 | 9 | This example would create a `unicorn` directory, and `templates` and `components` sub-directories if necessary. Underneath the `components` directory there will be a new module and subclass of `django_unicorn.components.UnicornView`. Underneath the `templates/unicorn` directory will be a example template. 10 | 11 | The following is an example folder structure. 12 | 13 | ``` 14 | unicorn/ 15 | components/ 16 | __init__.py 17 | hello_world.py 18 | templates/ 19 | unicorn/ 20 | hello-world.html 21 | ``` 22 | 23 | ```{note} 24 | If you have an existing Django app, you can use that instead of `unicorn` like the example above. The management command will create the the directories and files as needed. 25 | ``` 26 | 27 | ## Sub-folders 28 | 29 | `startunicorn` supports creating components in sub-folders. Separate each folder by a dot (similar to Python modules) to create a nested structure. 30 | 31 | ```shell 32 | python manage.py startunicorn unicorn hello.world 33 | ``` 34 | 35 | ``` 36 | unicorn/ 37 | components/ 38 | __init__.py 39 | hello/ 40 | __init__.py 41 | world.py 42 | templates/ 43 | unicorn/ 44 | hello/ 45 | world.html 46 | ``` 47 | 48 | The nested component would be included in a template like: 49 | 50 | ```html 51 | {% unicorn 'hello.world' %} 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/_sources/dirty-states.md.txt: -------------------------------------------------------------------------------- 1 | # Dirty States 2 | 3 | `Unicorn` can provide context to the user that some data has been changed and will be updated. 4 | 5 | ## Toggling Attributes 6 | 7 | Elements can include an `unicorn:dirty` attribute with either an `attr` or `class` modifier. 8 | 9 | ### attr 10 | 11 | Set the specified attribute on the element that is changed. 12 | 13 | This example will set the input to be readonly when the model is changed. The attribute will be removed once the name is synced or if the input value is changed back to the original value. 14 | 15 | :::{code} html 16 | :force: true 17 | 18 | 19 |
20 | 21 |
22 | ::: 23 | 24 | ### class 25 | 26 | Add the specified class(es) to the model that is changed. 27 | 28 | This example will add _dirty_ and _changing_ classes to the input when the model is changed. The classes will be removed once the model is synced or if the input value is changed back to the original value. 29 | 30 | :::{code} html 31 | :force: true 32 | 33 | 34 |
35 | 36 |
37 | ::: 38 | 39 | ### class.remove 40 | 41 | Remove the specified class(es) from the model that is changed. 42 | 43 | This example will remove the _clean_ class from the input when the model is changed. The class will be added back once the model is synced or if the input value is changed back to the original value. 44 | 45 | :::{code} html 46 | :force: true 47 | 48 | 49 |
50 | 51 |
52 | ::: 53 | -------------------------------------------------------------------------------- /docs/_sources/cli.md.txt: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | `Unicorn` provides a Django management command to create new components. The first argument is the name of the Django app to create components in. Every argument after is the name of components that should be created. 4 | 5 | ```shell 6 | python manage.py startunicorn unicorn hello-world 7 | ``` 8 | 9 | This example would create a `unicorn` directory, and `templates` and `components` sub-directories if necessary. Underneath the `components` directory there will be a new module and subclass of `django_unicorn.components.UnicornView`. Underneath the `templates/unicorn` directory will be a example template. 10 | 11 | The following is an example folder structure. 12 | 13 | ``` 14 | unicorn/ 15 | components/ 16 | __init__.py 17 | hello_world.py 18 | templates/ 19 | unicorn/ 20 | hello-world.html 21 | ``` 22 | 23 | ```{note} 24 | If you have an existing Django app, you can use that instead of `unicorn` like the example above. The management command will create the the directories and files as needed. 25 | ``` 26 | 27 | ## Sub-folders 28 | 29 | `startunicorn` supports creating components in sub-folders. Separate each folder by a dot (similar to Python modules) to create a nested structure. 30 | 31 | ```shell 32 | python manage.py startunicorn unicorn hello.world 33 | ``` 34 | 35 | ``` 36 | unicorn/ 37 | components/ 38 | __init__.py 39 | hello/ 40 | __init__.py 41 | world.py 42 | templates/ 43 | unicorn/ 44 | hello/ 45 | world.html 46 | ``` 47 | 48 | The nested component would be included in a template like: 49 | 50 | ```html 51 | {% unicorn 'hello.world' %} 52 | ``` 53 | -------------------------------------------------------------------------------- /unicorn/components/search.py: -------------------------------------------------------------------------------- 1 | from django_unicorn.components import UnicornView 2 | 3 | 4 | class SearchView(UnicornView): 5 | state = "" 6 | 7 | ALL_STATES = ( 8 | "Alabama", 9 | "Alaska", 10 | "Arizona", 11 | "Arkansas", 12 | "California", 13 | "Colorado", 14 | "Connecticut", 15 | "Delaware", 16 | "Florida", 17 | "Georgia", 18 | "Hawaii", 19 | "Idaho", 20 | "Illinois", 21 | "Indiana", 22 | "Iowa", 23 | "Kansas", 24 | "Kentucky", 25 | "Louisiana", 26 | "Maine", 27 | "Maryland", 28 | "Massachusetts", 29 | "Michigan", 30 | "Minnesota", 31 | "Mississippi", 32 | "Missouri", 33 | "Montana", 34 | "Nebraska", 35 | "Nevada", 36 | "New Hampshire", 37 | "New Jersey", 38 | "New Mexico", 39 | "New York", 40 | "North Carolina", 41 | "North Dakota", 42 | "Ohio", 43 | "Oklahoma", 44 | "Oregon", 45 | "Pennsylvania", 46 | "Rhode Island", 47 | "South Carolina", 48 | "South Dakota", 49 | "Tennessee", 50 | "Texas", 51 | "Utah", 52 | "Vermont", 53 | "Virginia", 54 | "Washington", 55 | "West Virginia", 56 | "Wisconsin", 57 | "Wyoming", 58 | ) 59 | 60 | def clear_states(self): 61 | self.state = "" 62 | 63 | def states(self): 64 | if not self.state: 65 | return [] 66 | 67 | return [s for s in self.ALL_STATES if s.lower().startswith(self.state.lower())] 68 | 69 | class Meta: 70 | exclude = ("ALL_STATES",) 71 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/views/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.views` 2 | ============================== 3 | 4 | .. py:module:: django_unicorn.views 5 | 6 | 7 | Subpackages 8 | ----------- 9 | .. toctree:: 10 | :titlesonly: 11 | :maxdepth: 3 12 | 13 | action_parsers/index.rst 14 | 15 | 16 | Submodules 17 | ---------- 18 | .. toctree:: 19 | :titlesonly: 20 | :maxdepth: 1 21 | 22 | objects/index.rst 23 | utils/index.rst 24 | 25 | 26 | Package Contents 27 | ---------------- 28 | 29 | .. py:data:: MIN_VALIDATION_ERROR_ARGS 30 | :value: 2 31 | 32 | 33 | 34 | .. py:function:: handle_error(view_func) 35 | 36 | Returns a JSON response with an error if necessary. 37 | 38 | 39 | .. py:function:: message(request: django.http.HttpRequest, component_name: Optional[str] = None) -> django.http.JsonResponse 40 | 41 | Endpoint that instantiates the component and does the correct action 42 | (set an attribute or call a method) depending on the JSON payload in the body. 43 | 44 | :param param request: HttpRequest for the function-based view. 45 | :param param: component_name: Name of the component, e.g. "hello-world". 46 | 47 | :returns: { 48 | "id": component_id, 49 | "dom": html, # re-rendered version of the component after actions in the payload are completed 50 | "data": {}, # updated data after actions in the payload are completed 51 | "errors": {}, # form validation errors 52 | "return": {}, # optional return value from an executed action 53 | "parent": {} # optional representation of the parent component 54 | } 55 | :rtype: `JsonRequest` with the following structure in the body 56 | 57 | 58 | -------------------------------------------------------------------------------- /source/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## Disallowed MIME type error on Windows 4 | 5 | Apparently Windows system-wide MIME type configuration sometimes won't load up JavaScript modules in certain browsers. The errors would be something like `Loading module from “http://127.0.0.1:8000/static/js/unicorn.js” was blocked because of a disallowed MIME type (“text/plain”)` or `Failed to load module script: The server responded with a non-JavaScript MIME type of "text/plain".` 6 | 7 | One suggested solution is to add the following to the bottom of the settings file: 8 | 9 | ```python 10 | # settings.py 11 | 12 | if DEBUG: 13 | import mimetypes 14 | mimetypes.add_type("application/javascript", ".js", True) 15 | ``` 16 | 17 | See this [Windows MIME type detection pitfalls](https://www.taricorp.net/2020/windows-mime-pitfalls/) article, this [StackOverflow answer](https://stackoverflow.com/a/16355034), or [issue #201](https://github.com/adamghill/django-unicorn/issues/201) for more details. 18 | 19 | ## Missing CSRF token or 403 Forbidden errors 20 | 21 | `Unicorn` uses CSRF to protect its endpoint from malicious actors. The two parts that are required for CSRF are `"django.middleware.csrf.CsrfViewMiddleware"` in `MIDDLEWARE` and `{% csrf_token %}` in the template that includes any `Unicorn` components. 22 | 23 | ```python 24 | # settings.py 25 | 26 | ... 27 | MIDDLEWARE = [ 28 | ... 29 | "django.middleware.csrf.CsrfViewMiddleware", 30 | ... 31 | ] 32 | ``` 33 | 34 | ```html 35 | 36 | {% load unicorn %} 37 | 38 | 39 | 40 | {% unicorn_scripts %} 41 | 42 | 43 | {% csrf_token %} 44 | 45 | {% unicorn 'hello-world' %} 46 | 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/_sources/troubleshooting.md.txt: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## Disallowed MIME type error on Windows 4 | 5 | Apparently Windows system-wide MIME type configuration sometimes won't load up JavaScript modules in certain browsers. The errors would be something like `Loading module from “http://127.0.0.1:8000/static/js/unicorn.js” was blocked because of a disallowed MIME type (“text/plain”)` or `Failed to load module script: The server responded with a non-JavaScript MIME type of "text/plain".` 6 | 7 | One suggested solution is to add the following to the bottom of the settings file: 8 | 9 | ```python 10 | # settings.py 11 | 12 | if DEBUG: 13 | import mimetypes 14 | mimetypes.add_type("application/javascript", ".js", True) 15 | ``` 16 | 17 | See this [Windows MIME type detection pitfalls](https://www.taricorp.net/2020/windows-mime-pitfalls/) article, this [StackOverflow answer](https://stackoverflow.com/a/16355034), or [issue #201](https://github.com/adamghill/django-unicorn/issues/201) for more details. 18 | 19 | ## Missing CSRF token or 403 Forbidden errors 20 | 21 | `Unicorn` uses CSRF to protect its endpoint from malicious actors. The two parts that are required for CSRF are `"django.middleware.csrf.CsrfViewMiddleware"` in `MIDDLEWARE` and `{% csrf_token %}` in the template that includes any `Unicorn` components. 22 | 23 | ```python 24 | # settings.py 25 | 26 | ... 27 | MIDDLEWARE = [ 28 | ... 29 | "django.middleware.csrf.CsrfViewMiddleware", 30 | ... 31 | ] 32 | ``` 33 | 34 | ```html 35 | 36 | {% load unicorn %} 37 | 38 | 39 | 40 | {% unicorn_scripts %} 41 | 42 | 43 | {% csrf_token %} 44 | 45 | {% unicorn 'hello-world' %} 46 | 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-unicorn.com" 3 | version = "0.1.0" 4 | description = "Documentation of django-unicorn" 5 | authors = ["Adam Hill"] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">3.7,<4" 10 | django = "^3.1.0" 11 | python-dotenv = "^0.14.0" 12 | gunicorn = "^20.0.4" 13 | whitenoise = "^5.1.0" 14 | django-compressor = "^2.4" 15 | django-unicorn = "0.42.0" 16 | django-cache-memoize = "^0.1.7" 17 | django-redis = "^4.12.1" 18 | httpx = "^0.14.2" 19 | sentry-sdk = "^0" 20 | django-docs = {git = "https://github.com/littlepea/django-docs", rev = "08912dac5d589ef57812a6cfdccb8398a57f0da7"} 21 | django-fbv = "<1" 22 | 23 | [tool.poetry.group.dev.dependencies] 24 | Sphinx = "*" 25 | linkify-it-py = "*" 26 | myst-parser = "*" 27 | furo = "*" 28 | sphinx-copybutton = "*" 29 | sphinx-autobuild = "*" 30 | rst2pdf = "*" 31 | sphinx-autoapi = "*" 32 | sphinxext-opengraph = "*" 33 | toml = "*" 34 | sphinx-design = "*" 35 | 36 | [tool.poe.tasks] 37 | r = { cmd = "./manage.py runserver 0:8000", help = "Run the dev server" } 38 | mm = { cmd = "./manage.py makemigrations", help = "Make migrations" } 39 | m = { cmd = "./manage.py migrate", help = "Migrate" } 40 | md = ["mm", "m"] 41 | ma = { cmd = "./manage.py", help = "manage.py" } 42 | t = { cmd = "pytest", help = "Run tests" } 43 | cd = { shell = "rm -f 'source/*.md' && cp ../django-unicorn/docs/source/*.md source/ && cp ../django-unicorn/docs/source/conf.py source/", help = "Copy docs" } 44 | sp = { cmd = "sphinx-build -E -a -b pdf source docs", help = "Build PDF documentation" } 45 | sa = { cmd = "sphinx-autobuild -b dirhtml source docs", help = "Build documentation and host at localhost:8000" } 46 | sb = { cmd = "sphinx-build -E -a -b dirhtml source docs", help = "Build documentation" } 47 | l = ["cd", "sb", "r"] 48 | 49 | [build-system] 50 | requires = ["poetry>=0.12"] 51 | build-backend = "poetry.masonry.api" 52 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/utils/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.utils` 2 | ============================== 3 | 4 | .. py:module:: django_unicorn.utils 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:data:: function_signature_cache 11 | 12 | 13 | 14 | .. py:function:: generate_checksum(data: Union[bytes, str, Dict]) -> str 15 | 16 | Generates a checksum for the passed-in data. 17 | 18 | :param data: The raw input to generate the checksum against. 19 | 20 | :returns: The generated checksum. 21 | 22 | 23 | .. py:function:: dicts_equal(dictionary_one: Dict, dictionary_two: Dict) -> bool 24 | 25 | Return True if all keys and values are the same between two dictionaries. 26 | 27 | 28 | .. py:function:: get_method_arguments(func) -> List[str] 29 | 30 | Gets the arguments for a method. 31 | 32 | :returns: A list of strings, one for each argument. 33 | 34 | 35 | .. py:function:: sanitize_html(html: str) -> django.utils.safestring.SafeText 36 | 37 | Escape all the HTML/XML special characters with their unicode escapes, so 38 | value is safe to be output in JSON. 39 | 40 | This is the same internals as `django.utils.html.json_script` except it takes a string 41 | instead of an object to avoid calling DjangoJSONEncoder. 42 | 43 | 44 | .. py:function:: is_non_string_sequence(obj) 45 | 46 | Checks whether the object is a sequence (i.e. `list`, `tuple`, `set`), but _not_ `str` or `bytes` type. 47 | Helpful when you expect to loop over `obj`, but explicitly don't want to allow `str`. 48 | 49 | 50 | .. py:function:: is_int(s: str) -> bool 51 | 52 | Checks whether a string is actually an integer. 53 | 54 | 55 | .. py:function:: create_template(template_html: Union[str, collections.abc.Callable], engine_name: Optional[str] = None) -> django.template.backends.django.Template 56 | 57 | Create a `Template` from a string or callable. 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/_static/styles/unicorn.css: -------------------------------------------------------------------------------- 1 | nav { 2 | background-color: #fafafa; 3 | } 4 | nav { 5 | min-height: 3.25rem; 6 | } 7 | 8 | .announcement { 9 | height: 52px; 10 | overflow-x: unset; 11 | } 12 | 13 | .announcement-content { 14 | min-width: 100%; 15 | padding: 0; 16 | } 17 | 18 | .navbar, 19 | .navbar-end, 20 | .navbar-menu, 21 | .navbar-start { 22 | align-items: stretch; 23 | display: flex; 24 | } 25 | 26 | .navbar { 27 | background-color: #fff; 28 | min-height: 3.25rem; 29 | position: relative; 30 | z-index: 30; 31 | } 32 | 33 | .navbar-brand, 34 | .navbar-tabs { 35 | align-items: stretch; 36 | display: flex; 37 | flex-shrink: 0; 38 | min-height: 3.25rem; 39 | } 40 | 41 | .navbar-menu { 42 | flex-grow: 1; 43 | flex-shrink: 0; 44 | } 45 | 46 | .navbar, 47 | .navbar-end, 48 | .navbar-menu, 49 | .navbar-start { 50 | align-items: stretch; 51 | display: flex; 52 | } 53 | 54 | .navbar-link, 55 | a.navbar-item { 56 | cursor: pointer; 57 | } 58 | 59 | .navbar-item, 60 | .navbar-link { 61 | align-items: center; 62 | display: flex; 63 | } 64 | 65 | .navbar-item, 66 | .navbar-link { 67 | color: #4a4a4a !important; 68 | display: block; 69 | line-height: 1.5; 70 | padding: 0.5rem 0.75rem; 71 | position: relative; 72 | } 73 | 74 | .navbar-end { 75 | justify-content: flex-end; 76 | margin-left: auto; 77 | } 78 | 79 | .navbar, 80 | .navbar-end, 81 | .navbar-menu, 82 | .navbar-start { 83 | align-items: stretch; 84 | display: flex; 85 | } 86 | 87 | .navbar-item { 88 | flex-grow: 0; 89 | flex-shrink: 0; 90 | } 91 | 92 | a { 93 | text-decoration: none; 94 | } 95 | 96 | .sidebar-brand { 97 | display: none; 98 | } 99 | 100 | .sidebar-drawer { 101 | border-top: 1px solid var(--color-sidebar-search-border); 102 | } 103 | 104 | .highlight .err { 105 | border: none; 106 | } 107 | -------------------------------------------------------------------------------- /source/_static/styles/unicorn.css: -------------------------------------------------------------------------------- 1 | nav { 2 | background-color: #fafafa; 3 | } 4 | nav { 5 | min-height: 3.25rem; 6 | } 7 | 8 | .announcement { 9 | height: 52px; 10 | overflow-x: unset; 11 | } 12 | 13 | .announcement-content { 14 | min-width: 100%; 15 | padding: 0; 16 | } 17 | 18 | .navbar, 19 | .navbar-end, 20 | .navbar-menu, 21 | .navbar-start { 22 | align-items: stretch; 23 | display: flex; 24 | } 25 | 26 | .navbar { 27 | background-color: #fff; 28 | min-height: 3.25rem; 29 | position: relative; 30 | z-index: 30; 31 | } 32 | 33 | .navbar-brand, 34 | .navbar-tabs { 35 | align-items: stretch; 36 | display: flex; 37 | flex-shrink: 0; 38 | min-height: 3.25rem; 39 | } 40 | 41 | .navbar-menu { 42 | flex-grow: 1; 43 | flex-shrink: 0; 44 | } 45 | 46 | .navbar, 47 | .navbar-end, 48 | .navbar-menu, 49 | .navbar-start { 50 | align-items: stretch; 51 | display: flex; 52 | } 53 | 54 | .navbar-link, 55 | a.navbar-item { 56 | cursor: pointer; 57 | } 58 | 59 | .navbar-item, 60 | .navbar-link { 61 | align-items: center; 62 | display: flex; 63 | } 64 | 65 | .navbar-item, 66 | .navbar-link { 67 | color: #4a4a4a !important; 68 | display: block; 69 | line-height: 1.5; 70 | padding: 0.5rem 0.75rem; 71 | position: relative; 72 | } 73 | 74 | .navbar-end { 75 | justify-content: flex-end; 76 | margin-left: auto; 77 | } 78 | 79 | .navbar, 80 | .navbar-end, 81 | .navbar-menu, 82 | .navbar-start { 83 | align-items: stretch; 84 | display: flex; 85 | } 86 | 87 | .navbar-item { 88 | flex-grow: 0; 89 | flex-shrink: 0; 90 | } 91 | 92 | a { 93 | text-decoration: none; 94 | } 95 | 96 | .sidebar-brand { 97 | display: none; 98 | } 99 | 100 | .sidebar-drawer { 101 | border-top: 1px solid var(--color-sidebar-search-border); 102 | } 103 | 104 | .highlight .err { 105 | border: none; 106 | } 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # vscode 107 | .vscode/ 108 | .DS_Store 109 | pip-wheel-metadata 110 | TODO.md 111 | 112 | node_modules/ 113 | staticfiles/ 114 | 115 | docs/.doctrees 116 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Creating a python base with shared environment variables 2 | FROM python:3.10.8-slim as python-base 3 | ENV PYTHONUNBUFFERED=1 \ 4 | PYTHONDONTWRITEBYTECODE=1 \ 5 | PIP_NO_CACHE_DIR=off \ 6 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 7 | PIP_DEFAULT_TIMEOUT=100 \ 8 | POETRY_HOME="/opt/poetry" \ 9 | POETRY_VIRTUALENVS_IN_PROJECT=true \ 10 | POETRY_NO_INTERACTION=1 \ 11 | PYSETUP_PATH="/opt/pysetup" \ 12 | VENV_PATH="/opt/pysetup/.venv" 13 | 14 | ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" 15 | 16 | 17 | # builder-base is used to build dependencies 18 | FROM python-base as builder-base 19 | RUN apt-get update \ 20 | && apt-get install --no-install-recommends -y \ 21 | curl \ 22 | build-essential 23 | 24 | # Install Poetry - respects $POETRY_VERSION & $POETRY_HOME 25 | ENV POETRY_VERSION=1.2.2 26 | RUN curl -sSL https://install.python-poetry.org | python3 - 27 | 28 | # We copy our Python requirements here to cache them 29 | # and install only runtime deps using poetry 30 | WORKDIR $PYSETUP_PATH 31 | COPY ./poetry.lock ./pyproject.toml ./ 32 | RUN poetry install --only main 33 | 34 | 35 | # 'production' stage uses the clean 'python-base' stage and copies 36 | # in only our runtime deps that were installed in the 'builder-base' 37 | FROM python-base as production 38 | 39 | COPY --from=builder-base $VENV_PATH $VENV_PATH 40 | 41 | COPY . /app 42 | WORKDIR /app 43 | 44 | EXPOSE 80 45 | 46 | # Install curl, collect static assets, compress static assets 47 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \ 48 | apt-get update --fix-missing && \ 49 | apt-get install --no-install-recommends -y curl wget 50 | 51 | HEALTHCHECK --interval=1m --timeout=10s --start-period=5s --retries=3 \ 52 | CMD curl -f http://0.0.0.0:80/ || exit 1 53 | 54 | # Run gunicorn 55 | CMD ["gunicorn", "project.wsgi", "--config="gunicorn.conf.py"] 56 | -------------------------------------------------------------------------------- /unicorn/templates/unicorn/todo-bulma.html: -------------------------------------------------------------------------------- 1 |
2 | 28 | 29 | {% if unicorn.errors %} 30 |
31 | {{ unicorn.errors.task.0.message }} 32 |
33 | {% endif %} 34 | 35 |
36 |

37 |

38 | 39 |
40 |

41 |

42 |    43 |

44 | 45 |
46 | 47 |
48 |
49 | 50 |

51 | {% if tasks %} 52 |

    53 | {% for task in tasks %} 54 |
  • {{ task }}
  • 55 | {% endfor %} 56 |
57 | {% else %} 58 | No tasks 🎉 59 | {% endif %} 60 |

61 |
62 | -------------------------------------------------------------------------------- /unicorn/templates/unicorn/validation.html: -------------------------------------------------------------------------------- 1 | {% load unicorn %} 2 | 3 |
4 | 31 | 32 |
33 | {% unicorn_errors %} 34 | 35 |

Plain text (minimum of 3 characters, max of 10)

36 |
37 | text: {{ text }} 38 | 39 |

Number validation

40 |
41 | number: {{ number }}
42 | 43 |

Datetime with property setter

44 |
45 | {{ unicorn.errors.now.0.message }} 46 | now: {{ now }}
47 | now|date:"s": {{ now|date:"s" }}
48 | 49 |

Actions

50 | 51 | 52 | 53 | 55 |
56 |
57 | -------------------------------------------------------------------------------- /source/visibility.md: -------------------------------------------------------------------------------- 1 | # Visibility 2 | 3 | `unicorn:visible` can be added to any element to have it call the specified view method when it scrolls into view. 4 | 5 | ```python 6 | # visibility.py 7 | from django_unicorn.components import UnicornView 8 | 9 | class VisibilityView(UnicornView): 10 | visibility_count = 0 11 | 12 | def add_count(self): 13 | self.visibility_count += 1 14 | ``` 15 | 16 | ```html 17 | 18 |
19 |
20 | 21 |
22 |
23 | ``` 24 | 25 | ```{note} 26 | In some cases, the element with the `unicorn:visible` attribute will always be in the viewport, so the event will continue to fire and the method will continue to execute. However, this will not happen in the following instances: 27 | 28 | - the fields of component do not change, so the AJAX request will return a 304 status code 29 | - the method explicitly returns `False` 30 | ``` 31 | 32 | ## Modifiers 33 | 34 | There are a few modifiers for `unicorn:visible` and they are able to be chained if necessary. 35 | 36 | ### Debounce 37 | 38 | Similar to the debounce modifier on a [model](templates.md#debounce) or [actions](actions.md#debounce), wait the specified number of milliseconds before calling the specified method. 39 | 40 | :::{code} html 41 | :force: true 42 | 43 | 44 |
45 |
46 | 47 |
48 |
49 | ::: 50 | 51 | ### Threshold 52 | 53 | The percentage (as an integer) that should be visible before being triggered. For example, `0` means that as soon as 1 pixel of the element is visible it would be fired, `25` would be 25% of the target element is visible, `100` would require 100% of the element to be completely visible. 54 | 55 | :::{code} html 56 | :force: true 57 | 58 | 59 |
60 |
61 | 62 |
63 |
64 | ::: 65 | -------------------------------------------------------------------------------- /source/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Create a Django Project 4 | 5 | `Unicorn` requires a working Django project before it can be used. While it is recommended that you have some experience working with Python and Django, below is a list of steps you would need do to use `Unicorn`. 6 | 7 | ```{dropdown} Install the latest version of Python 8 | `Unicorn` works with Python 3.8 or greater. If you don't have Python installed, you will need to download and install it on your local machine. 9 | 10 | You can find the [latest version](https://www.python.org/downloads/) of Python at [python.org](https://www.python.org). 11 | 12 | Once you have installed Python, make sure that it has been added to your `PATH`. 13 | 14 | `python --version` 15 | ``` 16 | 17 | ````{dropdown} Create a virtual environment 18 | Before installing Django, you will need to create a new directory for your project. It is also recommended to create a [virtual environment](https://docs.python.org/3/library/venv.html) where you can install Django, `Unicorn`, and any other dependencies. Once you have created and navigated to the directory, type the following command to create a virtual environment. 19 | 20 | `python -m venv .venv` 21 | 22 | ```{note} 23 | Some package managers automatically create a virtual environment so this step might not be required. But, a virtual environment is suggested if directly using `pip` to install dependencies. 24 | ``` 25 | ```` 26 | 27 | ````{dropdown} Install Django and start a project 28 | You are now ready to install Django and start your project. 29 | 30 | ```sh 31 | python -m pip install Django 32 | django-admin startproject project-name . 33 | ``` 34 | 35 | If you have never created a Django project before, you may want to get acquainted with it first. There are several resources, including some [official tutorials](https://docs.djangoproject.com/en/stable/intro/), though the [Django Girls Tutorial](https://tutorial.djangogirls.org/en/django_start_project/) is also highly recommended. 36 | ```` 37 | 38 | Once you have a working Django project, you are ready to [install `Unicorn`](installation.md). -------------------------------------------------------------------------------- /docs/_sources/visibility.md.txt: -------------------------------------------------------------------------------- 1 | # Visibility 2 | 3 | `unicorn:visible` can be added to any element to have it call the specified view method when it scrolls into view. 4 | 5 | ```python 6 | # visibility.py 7 | from django_unicorn.components import UnicornView 8 | 9 | class VisibilityView(UnicornView): 10 | visibility_count = 0 11 | 12 | def add_count(self): 13 | self.visibility_count += 1 14 | ``` 15 | 16 | ```html 17 | 18 |
19 |
20 | 21 |
22 |
23 | ``` 24 | 25 | ```{note} 26 | In some cases, the element with the `unicorn:visible` attribute will always be in the viewport, so the event will continue to fire and the method will continue to execute. However, this will not happen in the following instances: 27 | 28 | - the fields of component do not change, so the AJAX request will return a 304 status code 29 | - the method explicitly returns `False` 30 | ``` 31 | 32 | ## Modifiers 33 | 34 | There are a few modifiers for `unicorn:visible` and they are able to be chained if necessary. 35 | 36 | ### Debounce 37 | 38 | Similar to the debounce modifier on a [model](templates.md#debounce) or [actions](actions.md#debounce), wait the specified number of milliseconds before calling the specified method. 39 | 40 | :::{code} html 41 | :force: true 42 | 43 | 44 |
45 |
46 | 47 |
48 |
49 | ::: 50 | 51 | ### Threshold 52 | 53 | The percentage (as an integer) that should be visible before being triggered. For example, `0` means that as soon as 1 pixel of the element is visible it would be fired, `25` would be 25% of the target element is visible, `100` would require 100% of the element to be completely visible. 54 | 55 | :::{code} html 56 | :force: true 57 | 58 | 59 |
60 |
61 | 62 |
63 |
64 | ::: 65 | -------------------------------------------------------------------------------- /docs/_sources/getting-started.md.txt: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Create a Django Project 4 | 5 | `Unicorn` requires a working Django project before it can be used. While it is recommended that you have some experience working with Python and Django, below is a list of steps you would need do to use `Unicorn`. 6 | 7 | ```{dropdown} Install the latest version of Python 8 | `Unicorn` works with Python 3.8 or greater. If you don't have Python installed, you will need to download and install it on your local machine. 9 | 10 | You can find the [latest version](https://www.python.org/downloads/) of Python at [python.org](https://www.python.org). 11 | 12 | Once you have installed Python, make sure that it has been added to your `PATH`. 13 | 14 | `python --version` 15 | ``` 16 | 17 | ````{dropdown} Create a virtual environment 18 | Before installing Django, you will need to create a new directory for your project. It is also recommended to create a [virtual environment](https://docs.python.org/3/library/venv.html) where you can install Django, `Unicorn`, and any other dependencies. Once you have created and navigated to the directory, type the following command to create a virtual environment. 19 | 20 | `python -m venv .venv` 21 | 22 | ```{note} 23 | Some package managers automatically create a virtual environment so this step might not be required. But, a virtual environment is suggested if directly using `pip` to install dependencies. 24 | ``` 25 | ```` 26 | 27 | ````{dropdown} Install Django and start a project 28 | You are now ready to install Django and start your project. 29 | 30 | ```sh 31 | python -m pip install Django 32 | django-admin startproject project-name . 33 | ``` 34 | 35 | If you have never created a Django project before, you may want to get acquainted with it first. There are several resources, including some [official tutorials](https://docs.djangoproject.com/en/stable/intro/), though the [Django Girls Tutorial](https://tutorial.djangogirls.org/en/django_start_project/) is also highly recommended. 36 | ```` 37 | 38 | Once you have a working Django project, you are ready to [install `Unicorn`](installation.md). -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/errors/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.errors` 2 | =============================== 3 | 4 | .. py:module:: django_unicorn.errors 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:exception:: UnicornCacheError 11 | 12 | 13 | Bases: :py:obj:`Exception` 14 | 15 | Common base class for all non-exit exceptions. 16 | 17 | 18 | .. py:exception:: UnicornViewError 19 | 20 | 21 | Bases: :py:obj:`Exception` 22 | 23 | Common base class for all non-exit exceptions. 24 | 25 | 26 | .. py:exception:: ComponentLoadError(*args, locations=None, **kwargs) 27 | 28 | 29 | Bases: :py:obj:`Exception` 30 | 31 | Common base class for all non-exit exceptions. 32 | 33 | 34 | .. py:exception:: ComponentModuleLoadError(*args, locations=None, **kwargs) 35 | 36 | 37 | Bases: :py:obj:`ComponentLoadError` 38 | 39 | Common base class for all non-exit exceptions. 40 | 41 | 42 | .. py:exception:: ComponentClassLoadError(*args, locations=None, **kwargs) 43 | 44 | 45 | Bases: :py:obj:`ComponentLoadError` 46 | 47 | Common base class for all non-exit exceptions. 48 | 49 | 50 | .. py:exception:: RenderNotModifiedError 51 | 52 | 53 | Bases: :py:obj:`Exception` 54 | 55 | Common base class for all non-exit exceptions. 56 | 57 | 58 | .. py:exception:: MissingComponentElementError 59 | 60 | 61 | Bases: :py:obj:`Exception` 62 | 63 | Common base class for all non-exit exceptions. 64 | 65 | 66 | .. py:exception:: MissingComponentViewElementError 67 | 68 | 69 | Bases: :py:obj:`Exception` 70 | 71 | Common base class for all non-exit exceptions. 72 | 73 | 74 | .. py:exception:: NoRootComponentElementError 75 | 76 | 77 | Bases: :py:obj:`Exception` 78 | 79 | Common base class for all non-exit exceptions. 80 | 81 | 82 | .. py:exception:: MultipleRootComponentElementError 83 | 84 | 85 | Bases: :py:obj:`Exception` 86 | 87 | Common base class for all non-exit exceptions. 88 | 89 | 90 | .. py:exception:: ComponentNotValidError 91 | 92 | 93 | Bases: :py:obj:`Exception` 94 | 95 | Common base class for all non-exit exceptions. 96 | 97 | 98 | -------------------------------------------------------------------------------- /source/partial-updates.md: -------------------------------------------------------------------------------- 1 | # Partial Updates 2 | 3 | Normally `Unicorn` will send the entire component's rendered HTML on every action to make sure that any changes to the context is reflected on the page. However, to reduce latency and minimize the amount of data that has to be sent over the network, `Unicorn` can only update a portion of the page by utilizing the `unicorn:partial` attribute. 4 | 5 | ```{note} 6 | By default, `unicorn:partial` will look in the current component's template for an `id` or `unicorn:key`. If an element can't be found with the specified target, the entire component will be morphed like usual. 7 | ``` 8 | 9 | ```python 10 | # partial_update.py 11 | from django_unicorn.components import UnicornView 12 | 13 | class PartialUpdateView(UnicornView): 14 | checked = False 15 | ``` 16 | 17 | ```html 18 | 19 |
20 | {{ checked }} 21 | 24 |
25 | ``` 26 | 27 | ## Target by id 28 | 29 | To only target an element `id` add the `id` modifier to `unicorn:partial`. 30 | 31 | :::{code} html 32 | :force: true 33 | 34 | 35 |
36 | {{ checked }} 37 | 40 |
41 | ::: 42 | 43 | ## Target by key 44 | 45 | To only target an element `unicorn:key` add the `key` modifier to `unicorn:partial`. 46 | 47 | :::{code} html 48 | :force: true 49 | 50 | 51 |
52 | {{ checked }} 53 | 56 |
57 | ::: 58 | 59 | ```{note} 60 | Multiple partials can be targetted by adding multiple attributes to the element. 61 | 62 | :::{code} html 63 | :force: true 64 | 65 | 19 | 20 | 21 | 22 | ::: 23 | 24 | ```python 25 | # messages.py 26 | from django.contrib import messages 27 | from django_unicorn.components import UnicornView 28 | 29 | class MessagesView(UnicornView): 30 | def update(self): 31 | messages.success(self.request, "update called") 32 | ``` 33 | 34 | ## Redirecting 35 | 36 | When the action returns a `redirect`, `Unicorn` will defer the messages so they do not get rendered in the component (since the user will never see the re-rendered component). Once the redirect has happened `messages` will be available for rendering by the template as expected. 37 | 38 | :::{code} html 39 | :force: true 40 | 41 | 42 |
43 | 44 |
45 | 46 | ::: 47 | 48 | :::{code} html 49 | :force: true 50 | 51 | 52 | 53 | {% if messages %} 54 | 55 |
    56 | {% for message in messages %} 57 | {{ message }} 58 | {% endfor %} 59 |
60 | {% endif %} 61 | 62 | ::: 63 | 64 | ```python 65 | # messages_when_redirecting.py 66 | from django.contrib import messages 67 | from django.shortcuts import redirect 68 | from django_unicorn.components import UnicornView 69 | 70 | class MessagesWhenRedirectingView(UnicornView): 71 | def update(self): 72 | messages.success(self.request, "update called") 73 | 74 | return redirect("new-url") 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/_sources/partial-updates.md.txt: -------------------------------------------------------------------------------- 1 | # Partial Updates 2 | 3 | Normally `Unicorn` will send the entire component's rendered HTML on every action to make sure that any changes to the context is reflected on the page. However, to reduce latency and minimize the amount of data that has to be sent over the network, `Unicorn` can only update a portion of the page by utilizing the `unicorn:partial` attribute. 4 | 5 | ```{note} 6 | By default, `unicorn:partial` will look in the current component's template for an `id` or `unicorn:key`. If an element can't be found with the specified target, the entire component will be morphed like usual. 7 | ``` 8 | 9 | ```python 10 | # partial_update.py 11 | from django_unicorn.components import UnicornView 12 | 13 | class PartialUpdateView(UnicornView): 14 | checked = False 15 | ``` 16 | 17 | ```html 18 | 19 |
20 | {{ checked }} 21 | 24 |
25 | ``` 26 | 27 | ## Target by id 28 | 29 | To only target an element `id` add the `id` modifier to `unicorn:partial`. 30 | 31 | :::{code} html 32 | :force: true 33 | 34 | 35 |
36 | {{ checked }} 37 | 40 |
41 | ::: 42 | 43 | ## Target by key 44 | 45 | To only target an element `unicorn:key` add the `key` modifier to `unicorn:partial`. 46 | 47 | :::{code} html 48 | :force: true 49 | 50 | 51 |
52 | {{ checked }} 53 | 56 |
57 | ::: 58 | 59 | ```{note} 60 | Multiple partials can be targetted by adding multiple attributes to the element. 61 | 62 | :::{code} html 63 | :force: true 64 | 65 | 19 | 20 | 21 | 22 | ::: 23 | 24 | ```python 25 | # messages.py 26 | from django.contrib import messages 27 | from django_unicorn.components import UnicornView 28 | 29 | class MessagesView(UnicornView): 30 | def update(self): 31 | messages.success(self.request, "update called") 32 | ``` 33 | 34 | ## Redirecting 35 | 36 | When the action returns a `redirect`, `Unicorn` will defer the messages so they do not get rendered in the component (since the user will never see the re-rendered component). Once the redirect has happened `messages` will be available for rendering by the template as expected. 37 | 38 | :::{code} html 39 | :force: true 40 | 41 | 42 |
43 | 44 |
45 | 46 | ::: 47 | 48 | :::{code} html 49 | :force: true 50 | 51 | 52 | 53 | {% if messages %} 54 | 55 |
    56 | {% for message in messages %} 57 | {{ message }} 58 | {% endfor %} 59 |
60 | {% endif %} 61 | 62 | ::: 63 | 64 | ```python 65 | # messages_when_redirecting.py 66 | from django.contrib import messages 67 | from django.shortcuts import redirect 68 | from django_unicorn.components import UnicornView 69 | 70 | class MessagesWhenRedirectingView(UnicornView): 71 | def update(self): 72 | messages.success(self.request, "update called") 73 | 74 | return redirect("new-url") 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/genindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | - Unicorn 8 | 9 | 10 | 11 | 12 | 13 | 14 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/typer/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.typer` 2 | ============================== 3 | 4 | .. py:module:: django_unicorn.typer 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:function:: get_args(tp: Any) -> Tuple[Any, Ellipsis] 11 | 12 | 13 | .. py:data:: type_hints_cache 14 | 15 | 16 | 17 | .. py:data:: function_signature_cache 18 | 19 | 20 | 21 | .. py:data:: CASTERS 22 | 23 | 24 | 25 | .. py:function:: get_type_hints(obj) -> Dict 26 | 27 | Get type hints from an object. These get cached in a local memory cache for quicker look-up later. 28 | 29 | :returns: An empty dictionary if no type hints can be retrieved. 30 | 31 | 32 | .. py:function:: cast_value(type_hint, value) 33 | 34 | Try to cast the value based on the type hint and 35 | `django_unicorn.call_method_parser.CASTERS`. 36 | 37 | Additional features: 38 | - convert `int`/`float` epoch to `datetime` or `date` 39 | - instantiate the `type_hint` class with passed-in value 40 | 41 | 42 | .. py:function:: cast_attribute_value(obj, name, value) 43 | 44 | Try to cast the value of an object's attribute based on the type hint. 45 | 46 | 47 | .. py:function:: get_method_arguments(func) -> List[str] 48 | 49 | Gets the arguments for a method. 50 | 51 | :returns: A list of strings, one for each argument. 52 | 53 | 54 | .. py:function:: is_queryset(obj, type_hint, value) 55 | 56 | Determines whether an obj is a `QuerySet` or not based on the current instance of the 57 | component or the type hint. 58 | 59 | 60 | .. py:function:: create_queryset(obj, type_hint, value) -> django.db.models.QuerySet 61 | 62 | Create a queryset based on the `value`. If needed, the queryset will be created based on the `QuerySetType`. 63 | 64 | For example, all of these ways fields are equivalent: 65 | 66 | class TestComponent(UnicornView): 67 | queryset_with_empty_list: QuerySetType[SomeModel] = [] 68 | queryset_with_none: QuerySetType[SomeModel] = None 69 | queryset_with_empty_queryset: QuerySetType[SomeModel] = SomeModel.objects.none() 70 | queryset_with_no_typehint = SomeModel.objects.none() 71 | 72 | Params: 73 | obj: Object. 74 | type_hint: The optional type hint for the field. 75 | value: JSON. 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/serializer/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.serializer` 2 | =================================== 3 | 4 | .. py:module:: django_unicorn.serializer 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:data:: PydanticBaseModel 11 | 12 | 13 | 14 | .. py:data:: django_json_encoder 15 | 16 | 17 | 18 | .. py:exception:: JSONDecodeError 19 | 20 | 21 | Bases: :py:obj:`Exception` 22 | 23 | Common base class for all non-exit exceptions. 24 | 25 | 26 | .. py:exception:: InvalidFieldNameError(field_name: str, data: Optional[Dict] = None) 27 | 28 | 29 | Bases: :py:obj:`Exception` 30 | 31 | Common base class for all non-exit exceptions. 32 | 33 | 34 | .. py:exception:: InvalidFieldAttributeError(field_name: str, field_attr: str, data: Optional[Dict] = None) 35 | 36 | 37 | Bases: :py:obj:`Exception` 38 | 39 | Common base class for all non-exit exceptions. 40 | 41 | 42 | .. py:function:: dumps(data: Dict, *, fix_floats: bool = True, exclude_field_attributes: Optional[Tuple[str, Ellipsis]] = None, sort_dict: bool = True) -> str 43 | 44 | Converts the passed-in dictionary to a string representation. 45 | 46 | Handles the following objects: dataclass, datetime, enum, float, int, numpy, str, uuid, 47 | Django Model, Django QuerySet, Pydantic models (`PydanticBaseModel`), any object with `to_json` method. 48 | 49 | :param param fix_floats: Whether any floats should be converted to strings. Defaults to `True`, 50 | but will be faster without it. 51 | :param param exclude_field_attributes: Tuple of strings with field attributes to remove, i.e. "1.2" 52 | to remove the key `2` from `{"1": {"2": "3"}}` 53 | :param param sort_dict: Whether the `dict` should be sorted. Defaults to `True`, but 54 | will be faster without it. 55 | 56 | Returns a `str` instead of `bytes` (which deviates from `orjson.dumps`), but seems more useful. 57 | 58 | 59 | .. py:function:: loads(string: str) -> dict 60 | 61 | Converts a string representation to dictionary. 62 | 63 | 64 | .. py:function:: model_value(model: django.db.models.Model, *fields: str) 65 | 66 | Serializes a model into a dictionary with the fields as specified in the `fields` argument. 67 | 68 | 69 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | background: #EFF3F4; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 5 | } 6 | 7 | .hero-body .container { 8 | max-width: 700px; 9 | } 10 | 11 | .hero-body .title { 12 | color: hsl(192, 17%, 99%) !important; 13 | } 14 | 15 | .hero-body .subtitle { 16 | color: hsl(192, 17%, 99%) !important; 17 | padding-top: 2rem; 18 | line-height: 1.5; 19 | } 20 | 21 | .features { 22 | padding: 5rem 0; 23 | } 24 | 25 | .box.cta { 26 | border-radius: 0; 27 | border-left: none; 28 | border-right: none; 29 | } 30 | 31 | .card-image>.fa { 32 | font-size: 8rem; 33 | padding-top: 2rem; 34 | padding-bottom: 2rem; 35 | color: #209cee; 36 | } 37 | 38 | .card-content .content { 39 | font-size: 14px; 40 | margin: 1rem 1rem; 41 | } 42 | 43 | .card-content .content h4 { 44 | font-size: 16px; 45 | font-weight: 700; 46 | } 47 | 48 | .card { 49 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.18); 50 | margin-bottom: 2rem; 51 | } 52 | 53 | .intro { 54 | padding: 5rem 0; 55 | text-align: center; 56 | } 57 | 58 | .sandbox { 59 | padding: 5rem 0; 60 | } 61 | 62 | .tile.notification { 63 | display: flex; 64 | justify-content: center; 65 | flex-direction: column; 66 | } 67 | 68 | .is-shady { 69 | animation: flyintoright .4s backwards; 70 | background: #fff; 71 | box-shadow: rgba(0, 0, 0, .1) 0 1px 0; 72 | border-radius: 4px; 73 | display: inline-block; 74 | margin: 10px; 75 | position: relative; 76 | transition: all .2s ease-in-out; 77 | } 78 | 79 | .is-shady:hover { 80 | box-shadow: 0 10px 16px rgba(0, 0, 0, .13), 0 6px 6px rgba(0, 0, 0, .19); 81 | } 82 | 83 | code { 84 | border-radius: 2px; 85 | color: #e96900; 86 | font-size: .8rem; 87 | margin: 0 2px; 88 | padding: 3px 5px; 89 | white-space: pre-wrap 90 | } 91 | 92 | code, pre { 93 | background-color: #f8f8f8; 94 | font-family: Roboto Mono,Monaco,courier,monospace 95 | } 96 | 97 | pre { 98 | -moz-osx-font-smoothing: initial; 99 | -webkit-font-smoothing: initial; 100 | line-height: 1.5rem; 101 | margin: 1.2em 0; 102 | overflow: auto; 103 | padding: 0 1.4rem; 104 | position: relative; 105 | word-wrap: normal 106 | } -------------------------------------------------------------------------------- /docs/_sources/introduction.md.txt: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `Unicorn` is a magical full-stack framework for Django. Instead of building additional API endpoints to provide website interactivity, `Unicorn` provides the pieces of a modern website experience without moving away from all of the benefits of Django. 4 | 5 | `Unicorn` seamlessly extends Django past its server-side framework roots without giving up all of its niceties or re-building your website. 6 | 7 | ## Related projects 8 | 9 | `Unicorn` stands on the shoulders of giants, in particular [morphdom](https://github.com/patrick-steele-idem/morphdom) which is integral for merging DOM changes. 10 | 11 | A few inspirational projects in other languages and frameworks. 12 | 13 | - [Livewire](https://laravel-livewire.com/), a full-stack framework for the PHP web framework, Laravel. 14 | - [LiveView](https://github.com/phoenixframework/phoenix_live_view), a library for the Elixir web framework, Phoenix, that uses websockets. 15 | - [StimulusReflex](https://docs.stimulusreflex.com), a library for the Ruby web framework, Ruby on Rails, that uses websockets. 16 | - [Hotwire](https://hotwire.dev), "is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire". Uses AJAX, but can also use websockets. 17 | 18 | Some Python packages which aim to solve similar problems as `Unicorn`. 19 | 20 | - [Reactor](https://github.com/edelvalle/reactor/), a port of Elixir's `LiveView` to Django. Especially interesting for more complicated use-cases like chat rooms, keeping multiple browsers in sync, etc. Uses Django channels and websockets to work its magic. 21 | - [Flask-Meld](https://github.com/mikeabrahamsen/Flask-Meld), a port of `Unicorn` to Flask. Uses websockets. 22 | - [Sockpuppet](https://sockpuppet.argpar.se/), a port of Ruby on Rail's `StimulusReflex`. Requires Django channels and websockets. 23 | - [Django inertia.js adapter](https://github.com/zodman/inertia-django) allows Django to use inertia.js to build an SPA without building an API. 24 | - [django-components](https://gitlab.com/Mojeer/django_components), which provides declarative and composable components for Django, inspired by JavaScript frameworks. 25 | - [django-page-components](https://github.com/andreyfedoseev/django-page-components), a minimalistic framework for creating page components and using them in your Django views and templates. 26 | -------------------------------------------------------------------------------- /docs/_static/copybutton.css: -------------------------------------------------------------------------------- 1 | /* Copy buttons */ 2 | button.copybtn { 3 | position: absolute; 4 | display: flex; 5 | top: .3em; 6 | right: .3em; 7 | width: 1.7em; 8 | height: 1.7em; 9 | opacity: 0; 10 | transition: opacity 0.3s, border .3s, background-color .3s; 11 | user-select: none; 12 | padding: 0; 13 | border: none; 14 | outline: none; 15 | border-radius: 0.4em; 16 | /* The colors that GitHub uses */ 17 | border: #1b1f2426 1px solid; 18 | background-color: #f6f8fa; 19 | color: #57606a; 20 | } 21 | 22 | button.copybtn.success { 23 | border-color: #22863a; 24 | color: #22863a; 25 | } 26 | 27 | button.copybtn svg { 28 | stroke: currentColor; 29 | width: 1.5em; 30 | height: 1.5em; 31 | padding: 0.1em; 32 | } 33 | 34 | div.highlight { 35 | position: relative; 36 | } 37 | 38 | /* Show the copybutton */ 39 | .highlight:hover button.copybtn, button.copybtn.success { 40 | opacity: 1; 41 | } 42 | 43 | .highlight button.copybtn:hover { 44 | background-color: rgb(235, 235, 235); 45 | } 46 | 47 | .highlight button.copybtn:active { 48 | background-color: rgb(187, 187, 187); 49 | } 50 | 51 | /** 52 | * A minimal CSS-only tooltip copied from: 53 | * https://codepen.io/mildrenben/pen/rVBrpK 54 | * 55 | * To use, write HTML like the following: 56 | * 57 | *

Short

58 | */ 59 | .o-tooltip--left { 60 | position: relative; 61 | } 62 | 63 | .o-tooltip--left:after { 64 | opacity: 0; 65 | visibility: hidden; 66 | position: absolute; 67 | content: attr(data-tooltip); 68 | padding: .2em; 69 | font-size: .8em; 70 | left: -.2em; 71 | background: grey; 72 | color: white; 73 | white-space: nowrap; 74 | z-index: 2; 75 | border-radius: 2px; 76 | transform: translateX(-102%) translateY(0); 77 | transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); 78 | } 79 | 80 | .o-tooltip--left:hover:after { 81 | display: block; 82 | opacity: 1; 83 | visibility: visible; 84 | transform: translateX(-100%) translateY(0); 85 | transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); 86 | transition-delay: .5s; 87 | } 88 | 89 | /* By default the copy button shouldn't show up when printing a page */ 90 | @media print { 91 | button.copybtn { 92 | display: none; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /source/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Install `Unicorn` 4 | 5 | Install `Unicorn` the same as any other Python package (preferably into a [virtual environment](https://docs.python.org/3/tutorial/venv.html)). 6 | 7 | `````{tab-set} 8 | 9 | ````{tab-item} pip 10 | ```sh 11 | python -m pip install django-unicorn 12 | ``` 13 | ```` 14 | 15 | ````{tab-item} poetry 16 | ```sh 17 | poetry add django-unicorn 18 | ``` 19 | ```` 20 | 21 | ````{tab-item} pdm 22 | ```sh 23 | pdm add django-unicorn 24 | ``` 25 | ```` 26 | 27 | ````{tab-item} rye 28 | ```sh 29 | rye add django-unicorn 30 | ``` 31 | ```` 32 | 33 | ````{tab-item} pipenv 34 | ```sh 35 | pipenv install django-unicorn 36 | ``` 37 | ```` 38 | 39 | ````` 40 | 41 | ```{note} 42 | If attempting to install `django-unicorn` and `orjson` is preventing the installation from succeeding, check whether it is using 32-bit Python. Unfortunately, `orjson` is only supported on 64-bit Python. More details in [issue #105](https://github.com/adamghill/django-unicorn/issues/105). 43 | ``` 44 | 45 | ## Integrate `Unicorn` with Django 46 | 47 | 1\. Add `django_unicorn` to the `INSTALLED_APPS` list in the Django settings file (normally `settings.py`). 48 | 49 | ```python 50 | # settings.py 51 | INSTALLED_APPS = ( 52 | # other apps 53 | "django_unicorn", # required for Django to register urls and templatetags 54 | # other apps 55 | ) 56 | ``` 57 | 58 | 2\. Add `path("unicorn/", include("django_unicorn.urls")),`into the project's`urls.py`. 59 | 60 | ```python 61 | # urls.py 62 | urlpatterns = ( 63 | # other urls 64 | path("unicorn/", include("django_unicorn.urls")), 65 | ) 66 | ``` 67 | 68 | 3\. Add `{% load unicorn %}` to the top of the Django HTML template. 69 | 70 | ```{note} 71 | Generally, your Django HTML templates are typically created in the `myapp/templates/myapp` directory. You will need to add `{% load unicorn %}` at the top of each of the templates utilizing a `Unicorn` component. Alternatively, you can create one "base" template that is extended by other templates, in which case, you would only need to add `{% load unicorn %}` to the top of your base template. 72 | ``` 73 | 74 | 4\. Add `{% unicorn_scripts %}` into the Django HTML template and make sure there is a `{% csrf_token %}` in the template as well. 75 | 76 | ```html 77 | 78 | {% load unicorn %} 79 | 80 | 81 | {% unicorn_scripts %} 82 | 83 | 84 | {% csrf_token %} 85 | 86 | 87 | ``` 88 | 89 | Then, [create a component](components.md). 90 | -------------------------------------------------------------------------------- /docs/_sources/installation.md.txt: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Install `Unicorn` 4 | 5 | Install `Unicorn` the same as any other Python package (preferably into a [virtual environment](https://docs.python.org/3/tutorial/venv.html)). 6 | 7 | `````{tab-set} 8 | 9 | ````{tab-item} pip 10 | ```sh 11 | python -m pip install django-unicorn 12 | ``` 13 | ```` 14 | 15 | ````{tab-item} poetry 16 | ```sh 17 | poetry add django-unicorn 18 | ``` 19 | ```` 20 | 21 | ````{tab-item} pdm 22 | ```sh 23 | pdm add django-unicorn 24 | ``` 25 | ```` 26 | 27 | ````{tab-item} rye 28 | ```sh 29 | rye add django-unicorn 30 | ``` 31 | ```` 32 | 33 | ````{tab-item} pipenv 34 | ```sh 35 | pipenv install django-unicorn 36 | ``` 37 | ```` 38 | 39 | ````` 40 | 41 | ```{note} 42 | If attempting to install `django-unicorn` and `orjson` is preventing the installation from succeeding, check whether it is using 32-bit Python. Unfortunately, `orjson` is only supported on 64-bit Python. More details in [issue #105](https://github.com/adamghill/django-unicorn/issues/105). 43 | ``` 44 | 45 | ## Integrate `Unicorn` with Django 46 | 47 | 1\. Add `django_unicorn` to the `INSTALLED_APPS` list in the Django settings file (normally `settings.py`). 48 | 49 | ```python 50 | # settings.py 51 | INSTALLED_APPS = ( 52 | # other apps 53 | "django_unicorn", # required for Django to register urls and templatetags 54 | # other apps 55 | ) 56 | ``` 57 | 58 | 2\. Add `path("unicorn/", include("django_unicorn.urls")),`into the project's`urls.py`. 59 | 60 | ```python 61 | # urls.py 62 | urlpatterns = ( 63 | # other urls 64 | path("unicorn/", include("django_unicorn.urls")), 65 | ) 66 | ``` 67 | 68 | 3\. Add `{% load unicorn %}` to the top of the Django HTML template. 69 | 70 | ```{note} 71 | Generally, your Django HTML templates are typically created in the `myapp/templates/myapp` directory. You will need to add `{% load unicorn %}` at the top of each of the templates utilizing a `Unicorn` component. Alternatively, you can create one "base" template that is extended by other templates, in which case, you would only need to add `{% load unicorn %}` to the top of your base template. 72 | ``` 73 | 74 | 4\. Add `{% unicorn_scripts %}` into the Django HTML template and make sure there is a `{% csrf_token %}` in the template as well. 75 | 76 | ```html 77 | 78 | {% load unicorn %} 79 | 80 | 81 | {% unicorn_scripts %} 82 | 83 | 84 | {% csrf_token %} 85 | 86 | 87 | ``` 88 | 89 | Then, [create a component](components.md). 90 | -------------------------------------------------------------------------------- /www/templates/www/bases/examples.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/base.html" %} 2 | {% load compress static unicorn utils %} 3 | 4 | {% block title %}{% block subtitle %}{% endblock subtitle %} Example - Unicorn{% endblock title %} 5 | 6 | {% block styles %} 7 | {% compress css %} 8 | 9 | 10 | {% endcompress %} 11 | {% endblock styles %} 12 | 13 | {% block head_js %} 14 | {% unicorn_scripts %} 15 | {% endblock head_js %} 16 | 17 | {% block content %} 18 | {% csrf_token %} 19 | 20 |
21 |
22 |
23 |
24 |

25 | Examples 26 |

27 | 28 |

Note that all examples are deliberately left unstyled to present the simplest implementation.

29 | 30 |

31 | {% block example_title %}{% endblock example_title %} 32 |

33 | 34 | {% block example_content %}{% endblock example_content %} 35 | 36 |
37 |

Source

38 | {% block example_source %}{% endblock example_source %} 39 |
40 |
41 |
42 | 67 |
68 |
69 | 70 | {% endblock content %} -------------------------------------------------------------------------------- /source/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | `Unicorn` stores all settings in a dictionary under the `UNICORN` attribute in the Django settings file. All settings are optional. 4 | 5 | ```python 6 | # settings.py 7 | UNICORN = { 8 | "APPS": ["unicorn",], 9 | "CACHE_ALIAS": "default", 10 | "MINIFY_HTML": False, 11 | "MINIFIED": True, 12 | "SERIAL": { 13 | "ENABLED": False, 14 | "TIMEOUT": 60, 15 | }, 16 | "SCRIPT_LOCATION": "after", 17 | "MORPHER": { 18 | "NAME": "morphdom", 19 | "RELOAD_SCRIPT_ELEMENTS": False, 20 | }, 21 | } 22 | ``` 23 | 24 | ## APPS 25 | 26 | Specify the modules to look for components. Defaults to `["unicorn",]`. 27 | 28 | ## CACHE_ALIAS 29 | 30 | The alias to use for caching. Only used by the experimental serialization of requests for now. Defaults to `"default"`. 31 | 32 | ## MINIFY_HTML 33 | 34 | Minify the HTML generated by `Unicorn` in the AJAX request. If set to `True` and [`htmlmin`](https://pypi.org/project/htmlmin/) is installed HTML will be minified. `htmlmin` can be installed with `Unicorn` via `poetry add django-unicorn[minify]` or `pip install django-unicorn[minify]`. Defaults to `False`. 35 | 36 | ## MINIFIED 37 | 38 | Provides a way to control if the minified version of the JavaScript bundle (i.e. `unicorn.min.js`) is used. Defaults to `!DEBUG`. 39 | 40 | ## SERIAL 41 | 42 | Settings for the experimental serialization of requests. Defaults to `{}`. 43 | 44 | ### ENABLED 45 | 46 | Whether slow requests to the same component should be queued or not. Defaults to `False`. 47 | 48 | ### TIMEOUT 49 | 50 | The number of seconds to wait for a request to finish for additional requests to queue behind it. Defaults to `60`. 51 | 52 | ## SCRIPT_LOCATION 53 | 54 | Where the initial JavaScript data is included on initial render. Two values are currently supported: `after` and `append`. 55 | 56 | **after** is the default and will render the JavaScript _outside_ of the HTML component, i.e. it will be output in the same hierarchy as the parent of the HTML component. 57 | 58 | **append** will render the JavaScript _inside_ of the HTML component. 59 | 60 | ## MORPHER 61 | 62 | Configures the library to use for diffing and merging the DOM. Defaults to `{}`. 63 | 64 | ### NAME 65 | 66 | The name of the morpher to use. Defaults to `"morphdom"`. Specify `"alpine"` to use the [Alpine.js Morph Plugin](https://alpinejs.dev/plugins/morph). See [Custom Morphers](custom-morphers.md) for more information. 67 | 68 | ### RELOAD_SCRIPT_ELEMENTS 69 | 70 | Whether script elements should be reloaded when a component is re-rendered. Defaults to `False`. Only available with the `"morphdom"` morpher. 71 | -------------------------------------------------------------------------------- /docs/_sources/settings.md.txt: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | `Unicorn` stores all settings in a dictionary under the `UNICORN` attribute in the Django settings file. All settings are optional. 4 | 5 | ```python 6 | # settings.py 7 | UNICORN = { 8 | "APPS": ["unicorn",], 9 | "CACHE_ALIAS": "default", 10 | "MINIFY_HTML": False, 11 | "MINIFIED": True, 12 | "SERIAL": { 13 | "ENABLED": False, 14 | "TIMEOUT": 60, 15 | }, 16 | "SCRIPT_LOCATION": "after", 17 | "MORPHER": { 18 | "NAME": "morphdom", 19 | "RELOAD_SCRIPT_ELEMENTS": False, 20 | }, 21 | } 22 | ``` 23 | 24 | ## APPS 25 | 26 | Specify the modules to look for components. Defaults to `["unicorn",]`. 27 | 28 | ## CACHE_ALIAS 29 | 30 | The alias to use for caching. Only used by the experimental serialization of requests for now. Defaults to `"default"`. 31 | 32 | ## MINIFY_HTML 33 | 34 | Minify the HTML generated by `Unicorn` in the AJAX request. If set to `True` and [`htmlmin`](https://pypi.org/project/htmlmin/) is installed HTML will be minified. `htmlmin` can be installed with `Unicorn` via `poetry add django-unicorn[minify]` or `pip install django-unicorn[minify]`. Defaults to `False`. 35 | 36 | ## MINIFIED 37 | 38 | Provides a way to control if the minified version of the JavaScript bundle (i.e. `unicorn.min.js`) is used. Defaults to `!DEBUG`. 39 | 40 | ## SERIAL 41 | 42 | Settings for the experimental serialization of requests. Defaults to `{}`. 43 | 44 | ### ENABLED 45 | 46 | Whether slow requests to the same component should be queued or not. Defaults to `False`. 47 | 48 | ### TIMEOUT 49 | 50 | The number of seconds to wait for a request to finish for additional requests to queue behind it. Defaults to `60`. 51 | 52 | ## SCRIPT_LOCATION 53 | 54 | Where the initial JavaScript data is included on initial render. Two values are currently supported: `after` and `append`. 55 | 56 | **after** is the default and will render the JavaScript _outside_ of the HTML component, i.e. it will be output in the same hierarchy as the parent of the HTML component. 57 | 58 | **append** will render the JavaScript _inside_ of the HTML component. 59 | 60 | ## MORPHER 61 | 62 | Configures the library to use for diffing and merging the DOM. Defaults to `{}`. 63 | 64 | ### NAME 65 | 66 | The name of the morpher to use. Defaults to `"morphdom"`. Specify `"alpine"` to use the [Alpine.js Morph Plugin](https://alpinejs.dev/plugins/morph). See [Custom Morphers](custom-morphers.md) for more information. 67 | 68 | ### RELOAD_SCRIPT_ELEMENTS 69 | 70 | Whether script elements should be reloaded when a component is re-rendered. Defaults to `False`. Only available with the `"morphdom"` morpher. 71 | -------------------------------------------------------------------------------- /docs/_sources/api/django_unicorn/components/unicorn_template_response/index.rst.txt: -------------------------------------------------------------------------------- 1 | :py:mod:`django_unicorn.components.unicorn_template_response` 2 | ============================================================= 3 | 4 | .. py:module:: django_unicorn.components.unicorn_template_response 5 | 6 | 7 | Module Contents 8 | --------------- 9 | 10 | .. py:data:: EMPTY_ELEMENTS 11 | :value: ('', '', '
', '', '', '
', '', '', '', '',... 12 | 13 | 14 | 15 | .. py:function:: is_html_well_formed(html: str) -> bool 16 | 17 | Whether the passed-in HTML is missing any closing elements which can cause issues when rendering. 18 | 19 | 20 | .. py:function:: assert_has_single_wrapper_element(root_element: bs4.element.Tag, component_name: str) -> None 21 | 22 | Assert that there is at least one child in the root element. And that there is only 23 | one root element. 24 | 25 | 26 | .. py:function:: get_root_element(soup: bs4.BeautifulSoup) -> bs4.element.Tag 27 | 28 | Gets the first tag element for the component or the first element with a `unicorn:view` attribute for a direct 29 | view. 30 | 31 | :returns: BeautifulSoup tag element. 32 | 33 | Raises `Exception` if an element cannot be found. 34 | 35 | 36 | .. py:class:: UnsortedAttributes 37 | 38 | 39 | Bases: :py:obj:`bs4.formatter.HTMLFormatter` 40 | 41 | Prevent beautifulsoup from re-ordering attributes. 42 | 43 | .. py:method:: attributes(tag: bs4.element.Tag) 44 | 45 | Reorder a tag's attributes however you want. 46 | 47 | By default, attributes are sorted alphabetically. This makes 48 | behavior consistent between Python 2 and Python 3, and preserves 49 | backwards compatibility with older versions of Beautiful Soup. 50 | 51 | If `empty_boolean_attributes` is True, then attributes whose 52 | values are set to the empty string will be treated as boolean 53 | attributes. 54 | 55 | 56 | 57 | .. py:class:: UnicornTemplateResponse(template, request, *, context=None, content_type=None, status=None, charset=None, using=None, component=None, init_js=False, **kwargs) 58 | 59 | 60 | Bases: :py:obj:`django.template.response.TemplateResponse` 61 | 62 | An HTTP response class with a string as content. 63 | 64 | This content can be read, appended to, or replaced. 65 | 66 | .. py:method:: resolve_template(template) 67 | 68 | Override the TemplateResponseMixin to resolve a list of Templates. 69 | 70 | Calls the super which accepts a template object, path-to-template, or list of paths if the first 71 | object in the sequence is not a Template. 72 | 73 | 74 | .. py:method:: render() 75 | 76 | Render (thereby finalizing) the content of the response. 77 | 78 | If the content has already been rendered, this is a no-op. 79 | 80 | Return the baked response instance. 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /www/templates/www/sponsors.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/base.html" %} 2 | {% load compress static unicorn utils %} 3 | 4 | {% block title %}Sponsors - Unicorn{% endblock title %} 5 | 6 | {% block styles %} 7 | 8 | {% compress css %} 9 | 10 | 11 | {% endcompress %} 12 | 13 | 45 | 46 | {% endblock styles %} 47 | 48 | {% block content %} 49 | 50 |
51 |
52 |
53 |

54 | Unicorn appreciates all GitHub sponsors who help to encourage continued development. 55 |

56 | 57 |

58 | Sponsor 59 | Unicorn 60 |

61 | 62 |

63 | All Individual Advocates and higher tier sponsors: 64 |

65 | 66 |
67 |
    68 |
  • 69 |
    70 | 71 |
    72 |
    73 | 74 |
    75 |
  • 76 |
  • 77 |
    78 | 79 |
    80 |
    81 | 82 |
    83 |
  • 84 |
85 |
86 |
87 |
88 |
89 | {% endblock content %} -------------------------------------------------------------------------------- /docs/_static/copybutton_funcs.js: -------------------------------------------------------------------------------- 1 | function escapeRegExp(string) { 2 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 3 | } 4 | 5 | /** 6 | * Removes excluded text from a Node. 7 | * 8 | * @param {Node} target Node to filter. 9 | * @param {string} exclude CSS selector of nodes to exclude. 10 | * @returns {DOMString} Text from `target` with text removed. 11 | */ 12 | export function filterText(target, exclude) { 13 | const clone = target.cloneNode(true); // clone as to not modify the live DOM 14 | if (exclude) { 15 | // remove excluded nodes 16 | clone.querySelectorAll(exclude).forEach(node => node.remove()); 17 | } 18 | return clone.innerText; 19 | } 20 | 21 | // Callback when a copy button is clicked. Will be passed the node that was clicked 22 | // should then grab the text and replace pieces of text that shouldn't be used in output 23 | export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { 24 | var regexp; 25 | var match; 26 | 27 | // Do we check for line continuation characters and "HERE-documents"? 28 | var useLineCont = !!lineContinuationChar 29 | var useHereDoc = !!hereDocDelim 30 | 31 | // create regexp to capture prompt and remaining line 32 | if (isRegexp) { 33 | regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') 34 | } else { 35 | regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') 36 | } 37 | 38 | const outputLines = []; 39 | var promptFound = false; 40 | var gotLineCont = false; 41 | var gotHereDoc = false; 42 | const lineGotPrompt = []; 43 | for (const line of textContent.split('\n')) { 44 | match = line.match(regexp) 45 | if (match || gotLineCont || gotHereDoc) { 46 | promptFound = regexp.test(line) 47 | lineGotPrompt.push(promptFound) 48 | if (removePrompts && promptFound) { 49 | outputLines.push(match[2]) 50 | } else { 51 | outputLines.push(line) 52 | } 53 | gotLineCont = line.endsWith(lineContinuationChar) & useLineCont 54 | if (line.includes(hereDocDelim) & useHereDoc) 55 | gotHereDoc = !gotHereDoc 56 | } else if (!onlyCopyPromptLines) { 57 | outputLines.push(line) 58 | } else if (copyEmptyLines && line.trim() === '') { 59 | outputLines.push(line) 60 | } 61 | } 62 | 63 | // If no lines with the prompt were found then just use original lines 64 | if (lineGotPrompt.some(v => v === true)) { 65 | textContent = outputLines.join('\n'); 66 | } 67 | 68 | // Remove a trailing newline to avoid auto-running when pasting 69 | if (textContent.endsWith("\n")) { 70 | textContent = textContent.slice(0, -1) 71 | } 72 | return textContent 73 | } 74 | -------------------------------------------------------------------------------- /static/img/unicorn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /source/redirecting.md: -------------------------------------------------------------------------------- 1 | # Redirecting 2 | 3 | `Unicorn` has a few different ways to redirect from an action method. 4 | 5 | ## Redirect 6 | 7 | To redirect the user, return a `HttpResponseRedirect` from an action method. Using the Django shortcut [`redirect`](https://docs.djangoproject.com/en/stable/topics/http/shortcuts/#redirect) method is one way to do that in a typical Django manner. 8 | 9 | ```{note} 10 | `django.shortcuts.redirect` can take a Django model, Django view name, an absolute url, or a relative url. However, the `permanent` kwarg for `redirect` has no bearing in this context. 11 | ``` 12 | 13 | ```{tip} 14 | It is not required to use `django.shortcuts.redirect`. Anything that returns a `HttpResponseRedirect` will behave the same in `Unicorn`. 15 | ``` 16 | 17 | ```python 18 | # redirect.py 19 | from django.shortcuts import redirect 20 | from django_unicorn.components import UnicornView 21 | from .models import Book 22 | 23 | class BookView(UnicornView): 24 | title = "" 25 | 26 | def save_book(self): 27 | book = Book(title=self.title) 28 | book.save() 29 | self.reset() 30 | 31 | return redirect(f"/book/{book.id}") 32 | ``` 33 | 34 | ```html 35 | 36 |
37 |
38 | 39 |
40 | ``` 41 | 42 | ## HashUpdate 43 | 44 | To avoid a server-side page refresh and just update the hash at the end of the url, return `HashUpdate` from the action method. 45 | 46 | ```python 47 | # hash_update.py 48 | from django_unicorn.components import HashUpdate, UnicornView 49 | from .models import Book 50 | 51 | class BookView(UnicornView): 52 | title = "" 53 | 54 | def save_book(self): 55 | book = Book(title=self.title) 56 | book.save() 57 | self.reset() 58 | 59 | return HashUpdate(f"#{book.id}") 60 | ``` 61 | 62 | ```html 63 | 64 |
65 |
66 | 67 |
68 | ``` 69 | 70 | ## LocationUpdate 71 | 72 | To avoid a server-side page refresh and update the whole url, return a `LocationUpdate` from the action method. 73 | 74 | `LocationUpdate` is instantiated with a `HttpResponseRedirect` arg and an optional `title` kwarg. 75 | 76 | ```{note} 77 | `LocationUpdate` uses [`window.history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) so the new url must be relative or the same origin as the original url. 78 | ``` 79 | 80 | ```python 81 | # location_update.py 82 | from django.shortcuts import redirect 83 | from django_unicorn.components import LocationUpdate, UnicornView 84 | from .models import Book 85 | 86 | class BookView(UnicornView): 87 | title = "" 88 | 89 | def save_book(self): 90 | book = Book(title=self.title) 91 | book.save() 92 | self.reset() 93 | 94 | return LocationUpdate(redirect(f"/book/{book.id}"), title=f"{book.title}") 95 | ``` 96 | 97 | ```html 98 | 99 |
100 |
101 | 102 |
103 | ``` 104 | -------------------------------------------------------------------------------- /docs/_sources/redirecting.md.txt: -------------------------------------------------------------------------------- 1 | # Redirecting 2 | 3 | `Unicorn` has a few different ways to redirect from an action method. 4 | 5 | ## Redirect 6 | 7 | To redirect the user, return a `HttpResponseRedirect` from an action method. Using the Django shortcut [`redirect`](https://docs.djangoproject.com/en/stable/topics/http/shortcuts/#redirect) method is one way to do that in a typical Django manner. 8 | 9 | ```{note} 10 | `django.shortcuts.redirect` can take a Django model, Django view name, an absolute url, or a relative url. However, the `permanent` kwarg for `redirect` has no bearing in this context. 11 | ``` 12 | 13 | ```{tip} 14 | It is not required to use `django.shortcuts.redirect`. Anything that returns a `HttpResponseRedirect` will behave the same in `Unicorn`. 15 | ``` 16 | 17 | ```python 18 | # redirect.py 19 | from django.shortcuts import redirect 20 | from django_unicorn.components import UnicornView 21 | from .models import Book 22 | 23 | class BookView(UnicornView): 24 | title = "" 25 | 26 | def save_book(self): 27 | book = Book(title=self.title) 28 | book.save() 29 | self.reset() 30 | 31 | return redirect(f"/book/{book.id}") 32 | ``` 33 | 34 | ```html 35 | 36 |
37 |
38 | 39 |
40 | ``` 41 | 42 | ## HashUpdate 43 | 44 | To avoid a server-side page refresh and just update the hash at the end of the url, return `HashUpdate` from the action method. 45 | 46 | ```python 47 | # hash_update.py 48 | from django_unicorn.components import HashUpdate, UnicornView 49 | from .models import Book 50 | 51 | class BookView(UnicornView): 52 | title = "" 53 | 54 | def save_book(self): 55 | book = Book(title=self.title) 56 | book.save() 57 | self.reset() 58 | 59 | return HashUpdate(f"#{book.id}") 60 | ``` 61 | 62 | ```html 63 | 64 |
65 |
66 | 67 |
68 | ``` 69 | 70 | ## LocationUpdate 71 | 72 | To avoid a server-side page refresh and update the whole url, return a `LocationUpdate` from the action method. 73 | 74 | `LocationUpdate` is instantiated with a `HttpResponseRedirect` arg and an optional `title` kwarg. 75 | 76 | ```{note} 77 | `LocationUpdate` uses [`window.history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) so the new url must be relative or the same origin as the original url. 78 | ``` 79 | 80 | ```python 81 | # location_update.py 82 | from django.shortcuts import redirect 83 | from django_unicorn.components import LocationUpdate, UnicornView 84 | from .models import Book 85 | 86 | class BookView(UnicornView): 87 | title = "" 88 | 89 | def save_book(self): 90 | book = Book(title=self.title) 91 | book.save() 92 | self.reset() 93 | 94 | return LocationUpdate(redirect(f"/book/{book.id}"), title=f"{book.title}") 95 | ``` 96 | 97 | ```html 98 | 99 |
100 |
101 | 102 |
103 | ``` 104 | -------------------------------------------------------------------------------- /source/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | `Unicorn` is made up of multiple pieces which are all integrated tightly together. The following is a summary of how some of it all fits together, although it skips over a lot of the complexity and advanced functionality. However, for all of the details the code is available at https://github.com/adamghill/django-unicorn/. 4 | 5 | ## Template tags 6 | 7 | Starting with the integration with a normal Django template, there are the `unicorn_scripts` and `unicorn` template tags. `unicorn_scripts` renders out the entire JavaScript library and initializes the global `Unicorn` object. The `unicorn` template tag provides the ability to add the component wherever it is needed on the page. Based on the name passed into the `unicorn` template tag, conventions are used to find the correct component view and component template (e.g. if "hello-world" is passed into the template tag, a class of `hello_world.HelloWorldView` and a template named `hello-world.html` will be searched for). 8 | 9 | Once the component view and template are found, a serialized version of all of the public attributes of the component view is generated into a JSON object for the page, and the template is rendered with a context of those same public attributes. 10 | 11 | ## JavaScript initialization 12 | 13 | After the template is rendered, the JavaScript library parses the HTML for DOM elements that start with `unicorn:` or `u:` and creates a list of attributes that end with `:model`, `:poll`, or other specific `Unicorn` functionality. For attributes that are left, the assumption is that they are an event type (e.g. `unicorn:click`). 14 | 15 | For anything that is a model, the JavaScript sets the value for the element based on the serialized data of the publicly available attributes from the component view. Event listeners are attached for all event types. Then, other custom functionality is setup (e.g. polling). 16 | 17 | ## Models 18 | 19 | For all inputs which have a `model` attribute, an event listener is attached (either `change` or `blur` depending on if the `lazy` modifier is used). The `defer` modifier will store the action to be bundled with an action event that might happen later. 20 | 21 | Once a model event is fired it is sent over the wire to the defined AJAX endpoint with a specific JSON structure which tells `Unicorn` what the updated data from the input should be. The component class is re-instantiated and the data is updated from the front-end, then re-rendered and the HTML is returned in the response. 22 | 23 | ## Actions 24 | 25 | Actions follow a similar path as the models above, however there is a different JSON stucture. Also, the method, arguments, and kwargs that are passed from the front-end get parsed with a mix of `ast.parse` and `ast.literal_eval` to convert the strings into the appropriate Python types (i.e. change the string "1" to the integer `1`). After the component is re-initialized, the method is called with the passed-in arguments and kwargs. Once all of the actions have been called, the component view is re-rendered and the HTML is returned in the response. 26 | 27 | ## HTML Diff 28 | 29 | After the AJAX endpoint returns its response, the newly rendered DOM is merged into the old DOM and input values are set again based on the new data in the AJAX response. By default, a library called `morphdom` is used to do the diffing and merging of the DOM. However, this can be overridden by setting the `MORPHER` setting to use a different library. 30 | -------------------------------------------------------------------------------- /docs/_sources/architecture.md.txt: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | `Unicorn` is made up of multiple pieces which are all integrated tightly together. The following is a summary of how some of it all fits together, although it skips over a lot of the complexity and advanced functionality. However, for all of the details the code is available at https://github.com/adamghill/django-unicorn/. 4 | 5 | ## Template tags 6 | 7 | Starting with the integration with a normal Django template, there are the `unicorn_scripts` and `unicorn` template tags. `unicorn_scripts` renders out the entire JavaScript library and initializes the global `Unicorn` object. The `unicorn` template tag provides the ability to add the component wherever it is needed on the page. Based on the name passed into the `unicorn` template tag, conventions are used to find the correct component view and component template (e.g. if "hello-world" is passed into the template tag, a class of `hello_world.HelloWorldView` and a template named `hello-world.html` will be searched for). 8 | 9 | Once the component view and template are found, a serialized version of all of the public attributes of the component view is generated into a JSON object for the page, and the template is rendered with a context of those same public attributes. 10 | 11 | ## JavaScript initialization 12 | 13 | After the template is rendered, the JavaScript library parses the HTML for DOM elements that start with `unicorn:` or `u:` and creates a list of attributes that end with `:model`, `:poll`, or other specific `Unicorn` functionality. For attributes that are left, the assumption is that they are an event type (e.g. `unicorn:click`). 14 | 15 | For anything that is a model, the JavaScript sets the value for the element based on the serialized data of the publicly available attributes from the component view. Event listeners are attached for all event types. Then, other custom functionality is setup (e.g. polling). 16 | 17 | ## Models 18 | 19 | For all inputs which have a `model` attribute, an event listener is attached (either `change` or `blur` depending on if the `lazy` modifier is used). The `defer` modifier will store the action to be bundled with an action event that might happen later. 20 | 21 | Once a model event is fired it is sent over the wire to the defined AJAX endpoint with a specific JSON structure which tells `Unicorn` what the updated data from the input should be. The component class is re-instantiated and the data is updated from the front-end, then re-rendered and the HTML is returned in the response. 22 | 23 | ## Actions 24 | 25 | Actions follow a similar path as the models above, however there is a different JSON stucture. Also, the method, arguments, and kwargs that are passed from the front-end get parsed with a mix of `ast.parse` and `ast.literal_eval` to convert the strings into the appropriate Python types (i.e. change the string "1" to the integer `1`). After the component is re-initialized, the method is called with the passed-in arguments and kwargs. Once all of the actions have been called, the component view is re-rendered and the HTML is returned in the response. 26 | 27 | ## HTML Diff 28 | 29 | After the AJAX endpoint returns its response, the newly rendered DOM is merged into the old DOM and input values are set again based on the new data in the AJAX response. By default, a library called `morphdom` is used to do the diffing and merging of the DOM. However, this can be overridden by setting the `MORPHER` setting to use a different library. 30 | -------------------------------------------------------------------------------- /source/polling.md: -------------------------------------------------------------------------------- 1 | # Polling 2 | 3 | `unicorn:poll` can be added to the root `div` element of a component to have it refresh the component automatically every 2 seconds. The polling is smart enough that it won't poll when the page is inactive. 4 | 5 | ```python 6 | # polling.py 7 | from django.utils.timezone import now 8 | from django_unicorn.components import UnicornView 9 | 10 | class PollingView(UnicornView): 11 | current_time = now() 12 | ``` 13 | 14 | ```html 15 | 16 |
{{ current_time }}
17 | ``` 18 | 19 | A method can also be specified if there is a specific method on the component that should called every time the polling fires. For example, `unicorn:poll="get_updates"` would call the `get_updates` method instead of the built-in `refresh` method. 20 | 21 | To define a different refresh time in milliseconds, a modifier can be added as well. `unicorn:poll-1000` would fire the `refresh` method every 1 second, instead of the default 2 seconds. 22 | 23 | ```html 24 | 25 |
26 | 27 | {{ update }} 28 |
29 | ``` 30 | 31 | ## Disable poll 32 | 33 | Polling can dynamically be disabled by checking a boolean field from the component. 34 | 35 | ```python 36 | # poll_disable.py 37 | from django.utils.timezone import now 38 | from django_unicorn.components import UnicornView 39 | 40 | class PollDisableView(UnicornView): 41 | polling_disabled = False 42 | current_time = now() 43 | 44 | def get_date(self): 45 | self.current_time = now() 46 | ``` 47 | 48 | :::{code} html 49 | :force: 50 | 51 | 52 |
53 | current_time: {{ current_time|date:"s" }}
54 | 55 |
56 | ::: 57 | 58 | ````{note} 59 | The field passed into `unicorn:poll.disable` can be negated with an exclamation point. 60 | 61 | ```python 62 | # poll_disable_negation.py 63 | from django.utils.timezone import now 64 | from django_unicorn.components import UnicornView 65 | 66 | class PollDisableNegationView(UnicornView): 67 | polling_enabled = True 68 | current_time = now() 69 | 70 | def get_date(self): 71 | self.current_time = now() 72 | ``` 73 | 74 | :::{code} html 75 | :force: 76 | 77 | 78 |
79 | current_time: {{ current_time|date:"s" }}
80 | 81 |
82 | ::: 83 | ```` 84 | 85 | ## PollUpdate 86 | 87 | A poll can be dynamically updated by returning a `PollUpdate` object from an action method. The timing and method can be updated, or it can be disabled. 88 | 89 | ```python 90 | # poll_update.py 91 | from django.utils.timezone import now 92 | from django_unicorn.components import PollUpdate, UnicornView 93 | 94 | class PollingUpdateView(UnicornView): 95 | polling_disabled = False 96 | current_time = now() 97 | 98 | def get_date(self): 99 | self.current_time = now() 100 | return PollUpdate(timing=2000, disable=False, method="get_date") 101 | ``` 102 | 103 | :::{code} html 104 | :force: 105 | 106 | 107 |
108 | current_time: {{ current_time|date:"s" }}
109 |
110 | ::: 111 | -------------------------------------------------------------------------------- /docs/_sources/polling.md.txt: -------------------------------------------------------------------------------- 1 | # Polling 2 | 3 | `unicorn:poll` can be added to the root `div` element of a component to have it refresh the component automatically every 2 seconds. The polling is smart enough that it won't poll when the page is inactive. 4 | 5 | ```python 6 | # polling.py 7 | from django.utils.timezone import now 8 | from django_unicorn.components import UnicornView 9 | 10 | class PollingView(UnicornView): 11 | current_time = now() 12 | ``` 13 | 14 | ```html 15 | 16 |
{{ current_time }}
17 | ``` 18 | 19 | A method can also be specified if there is a specific method on the component that should called every time the polling fires. For example, `unicorn:poll="get_updates"` would call the `get_updates` method instead of the built-in `refresh` method. 20 | 21 | To define a different refresh time in milliseconds, a modifier can be added as well. `unicorn:poll-1000` would fire the `refresh` method every 1 second, instead of the default 2 seconds. 22 | 23 | ```html 24 | 25 |
26 | 27 | {{ update }} 28 |
29 | ``` 30 | 31 | ## Disable poll 32 | 33 | Polling can dynamically be disabled by checking a boolean field from the component. 34 | 35 | ```python 36 | # poll_disable.py 37 | from django.utils.timezone import now 38 | from django_unicorn.components import UnicornView 39 | 40 | class PollDisableView(UnicornView): 41 | polling_disabled = False 42 | current_time = now() 43 | 44 | def get_date(self): 45 | self.current_time = now() 46 | ``` 47 | 48 | :::{code} html 49 | :force: 50 | 51 | 52 |
53 | current_time: {{ current_time|date:"s" }}
54 | 55 |
56 | ::: 57 | 58 | ````{note} 59 | The field passed into `unicorn:poll.disable` can be negated with an exclamation point. 60 | 61 | ```python 62 | # poll_disable_negation.py 63 | from django.utils.timezone import now 64 | from django_unicorn.components import UnicornView 65 | 66 | class PollDisableNegationView(UnicornView): 67 | polling_enabled = True 68 | current_time = now() 69 | 70 | def get_date(self): 71 | self.current_time = now() 72 | ``` 73 | 74 | :::{code} html 75 | :force: 76 | 77 | 78 |
79 | current_time: {{ current_time|date:"s" }}
80 | 81 |
82 | ::: 83 | ```` 84 | 85 | ## PollUpdate 86 | 87 | A poll can be dynamically updated by returning a `PollUpdate` object from an action method. The timing and method can be updated, or it can be disabled. 88 | 89 | ```python 90 | # poll_update.py 91 | from django.utils.timezone import now 92 | from django_unicorn.components import PollUpdate, UnicornView 93 | 94 | class PollingUpdateView(UnicornView): 95 | polling_disabled = False 96 | current_time = now() 97 | 98 | def get_date(self): 99 | self.current_time = now() 100 | return PollUpdate(timing=2000, disable=False, method="get_date") 101 | ``` 102 | 103 | :::{code} html 104 | :force: 105 | 106 | 107 |
108 | current_time: {{ current_time|date:"s" }}
109 |
110 | ::: 111 | -------------------------------------------------------------------------------- /www/templates/www/screencasts/installation.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/screencasts.html" %} 2 | 3 | {% block subtitle %}Installation{% endblock subtitle %} 4 | 5 | {% block video_title %}Installation{% endblock video_title %} 6 | 7 | {% block video_id %}494214339{% endblock video_id %} 8 | 9 | {% block video_description %}How to install Unicorn and create a basic component.{% endblock video_description %} 10 | 11 | {% block video_content %} 12 |
13 |

14 | The code changes are reflected in these changes in the magical-creatures repository. 15 |

16 |
17 | 18 |
19 |

Transcript

20 |

21 | Hello! Welcome to the very first screencast for Unicorn, the magical full-stack framework for Django. 22 |

23 | 24 |

25 | In this episode I'll show you how to install Unicorn and create a basic component. You can see I just created a brand new Django project and we can see the fancy rocketship getting ready to blast off! 26 |

27 | 28 |

29 | So, let's look at the code. 30 |

31 | 32 |

33 | I already added a "www" app to have a simple index page to put our components on. 34 |

35 | 36 |

37 | I'm using poetry to install Python packages, but of course installing with pip will work just fine as well. 38 |

39 | 40 |

41 | Ok! So, the library is installed. There are two steps to start using it in Django. 42 |

43 | 44 |

45 | First, add "django_unicorn" to the INSTALLED_APPS in your settings file. 46 |

47 | 48 |

49 | Second, add the url route. 50 |

51 | 52 |

53 | Now, Unicorn is integrated with Django. Let's create a new component to see how it all works. Built into Unicorn is a Django management command to create new components. Let's create a new component to track magical creature sightings. 54 |

55 | 56 |

57 | Awesome! So, we have a component. Now what?! For our first component, we need to tell Django where to look for templates. Let's register the "unicorn" folder that just got created in INSTALLED_APPS. 58 |

59 | 60 |

61 | Alright, now we can start integrating that new component we created earlier into our template. 62 |

63 | 64 |

65 | We'll need to load the Unicorn template tags so they can be used later on. 66 |

67 | 68 |

69 | Make sure that the Django CSRF tokens are available because they will protect us from cross-site scripting attacks. 70 |

71 | 72 |

73 | Load the JavaScript required for Unicorn. 74 |

75 | 76 |

77 | And finally, our new component! 78 |

79 | 80 |

81 | Let's update the "sighting" component and refresh the page and voila! we now have a more complicated Django "includes" templatetag. :) 82 |

83 | 84 |

85 | But, now is where things get more fun! 86 |

87 | 88 |

89 | Let's add a textbox on our component to count how many times we've seen a magical creature. 90 |

91 | 92 |

93 | And that wraps up the first screencast on how to install Unicorn! 94 |

95 |
96 | {% endblock video_content %} 97 | -------------------------------------------------------------------------------- /docs/_static/scripts/main.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"function"==typeof define&&define.amd?define([],function(){return t(e)}):"object"==typeof exports?module.exports=t(e):e.Gumshoe=t(e)}("undefined"!=typeof global?global:"undefined"!=typeof window?window:this,function(u){"use strict";function d(e,t,n){n.settings.events&&(n=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:n}),t.dispatchEvent(n))}function n(e){var t=0;if(e.offsetParent)for(;e;)t+=e.offsetTop,e=e.offsetParent;return 0<=t?t:0}function f(e){e&&e.sort(function(e,t){return n(e.content)=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)}function m(e,t){var n,o,r=e[e.length-1];if(n=r,o=t,!(!s()||!l(n.content,o,!0)))return r;for(var c=e.length-1;0<=c;c--)if(l(e[c].content,t))return e[c]}function v(e,t){var n;!e||(n=e.nav.closest("li"))&&(n.classList.remove(t.navClass),e.content.classList.remove(t.contentClass),o(n,t),d("gumshoeDeactivate",n,{link:e.nav,content:e.content,settings:t}))}var h={navClass:"active",contentClass:"active",nested:!1,nestedClass:"active",offset:0,reflow:!1,events:!0},o=function(e,t){t.nested&&e.parentNode&&((e=e.parentNode.closest("li"))&&(e.classList.remove(t.nestedClass),o(e,t)))},p=function(e,t){!t.nested||(e=e.parentNode.closest("li"))&&(e.classList.add(t.nestedClass),p(e,t))};return function(e,t){var n,r,c,o,l,s={setup:function(){n=document.querySelectorAll(e),r=[],Array.prototype.forEach.call(n,function(e){var t=document.getElementById(decodeURIComponent(e.hash.substr(1)));t&&r.push({nav:e,content:t})}),f(r)}};s.detect=function(){var e,t,n,o=m(r,l);o?c&&o.content===c.content||(v(c,l),t=l,!(e=o)||(n=e.nav.closest("li"))&&(n.classList.add(t.navClass),e.content.classList.add(t.contentClass),p(n,t),d("gumshoeActivate",n,{link:e.nav,content:e.content,settings:t})),c=o):c&&(v(c,l),c=null)};function i(e){o&&u.cancelAnimationFrame(o),o=u.requestAnimationFrame(s.detect)}function a(e){o&&u.cancelAnimationFrame(o),o=u.requestAnimationFrame(function(){f(r),s.detect()})}s.destroy=function(){c&&v(c,l),u.removeEventListener("scroll",i,!1),l.reflow&&u.removeEventListener("resize",a,!1),l=o=c=n=r=null};return l=function(){var n={};return Array.prototype.forEach.call(arguments,function(e){for(var t in e){if(!e.hasOwnProperty(t))return;n[t]=e[t]}}),n}(h,t||{}),s.setup(),s.detect(),u.addEventListener("scroll",i,!1),l.reflow&&u.addEventListener("resize",a,!1),s}});var tocScroll=null,header=null;function scrollHandlerForHeader(){0==Math.floor(header.getBoundingClientRect().top)?header.classList.add("scrolled"):header.classList.remove("scrolled")}function scrollHandlerForTOC(e){null!==tocScroll&&(0==e?tocScroll.scrollTo(0,0):Math.ceil(e)>=Math.floor(document.documentElement.scrollHeight-window.innerHeight)?tocScroll.scrollTo(0,tocScroll.scrollHeight):document.querySelector(".scroll-current"))}function scrollHandler(e){scrollHandlerForHeader(),scrollHandlerForTOC(e)}function setupScrollHandler(){var t,n=!1;window.addEventListener("scroll",function(e){t=window.scrollY,n||(window.requestAnimationFrame(function(){scrollHandler(t),n=!1}),n=!0)}),window.scroll()}function setupScrollSpy(){null!==tocScroll&&new Gumshoe(".toc-tree a",{reflow:!0,recursive:!0,navClass:"scroll-current"})}function setup(){setupScrollHandler(),setupScrollSpy()}function main(){document.body.parentNode.classList.remove("no-js"),header=document.querySelector("header"),tocScroll=document.querySelector(".toc-scroll"),setup()}document.addEventListener("DOMContentLoaded",main); 2 | //# sourceMappingURL=main.js.map 3 | -------------------------------------------------------------------------------- /www/templates/www/bases/screencasts.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/base.html" %} 2 | {% load compress static unicorn utils %} 3 | 4 | {% block title %}{% block subtitle %}{% endblock subtitle %} Screencast - Unicorn{% endblock title %} 5 | 6 | {% block styles %} 7 | 8 | {% compress css %} 9 | 10 | 11 | {% endcompress %} 12 | 13 | {% endblock styles %} 14 | 15 | {% block content %} 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 |

{% block video_title %}{% endblock video_title %}

30 |

{% block video_description %}{% endblock video_description %}

31 |
32 |
33 | 34 | {% block video_content %}{% endblock video_content %} 35 |
36 |
37 |
38 |
39 | 61 | 62 | 83 |
84 |
85 |
86 | 87 | {% endblock content %} -------------------------------------------------------------------------------- /www/templates/www/articles.html: -------------------------------------------------------------------------------- 1 | {% extends "www/bases/base.html" %} 2 | {% load compress static %} 3 | 4 | {% block title %}Articles - Unicorn{% endblock title %} 5 | 6 | {% block styles %} 7 | 8 | 9 | {% compress css %} 10 | 11 | {% endcompress %} 12 | {% endblock styles %} 13 | 14 | {% block content %} 15 | 16 |
17 | 18 | 36 | 37 |
38 |

Other front-end frameworks

39 |
40 | 41 |

42 | Articles detailing how to integrate Django with other full-stack frameworks. 43 |

44 | 45 | 82 |
83 | 84 |
85 |

Django Channels

86 | 87 | 93 |
94 |
95 | 96 | {% endblock content %} -------------------------------------------------------------------------------- /source/loading-states.md: -------------------------------------------------------------------------------- 1 | # Loading States 2 | 3 | `Unicorn` requires an AJAX request for any component updates, so it is helpful to provide some context to the user that an action is happening. 4 | 5 | ## Toggling Elements 6 | 7 | Elements with the `unicorn:loading` attribute are only visible when an action is in process. 8 | 9 | ```html 10 | 11 |
12 | 13 | 14 |
Updating!
15 |
16 | ``` 17 | 18 | When the _Update_ button is clicked, the "Updating!" message will show until the action is complete, and then it will re-hide itself. 19 | 20 | ```{warning} 21 | Loading elements get shown or removed with the [`hidden`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden) attribute. One drawback to this approach is that setting the style `display` property overrides this functionality. 22 | ``` 23 | 24 | You can also hide an element while an action is processed by adding a `remove` modifier. 25 | 26 | :::{code} html 27 | :force: true 28 | 29 | 30 |
31 | 32 | 33 |
Not currently updating!
34 |
35 | ::: 36 | 37 | If there are multiple actions that happen in the component, you can show or hide a loading element for a specific action by targeting another element's `id` with `unicorn:target`. 38 | 39 | ```html 40 | 41 |
42 | 43 | 44 | 45 |
Updating!
46 |
Deleting!
47 |
48 | ``` 49 | 50 | An element's `unicorn:key` can also be targeted. 51 | 52 | ```html 53 | 54 |
55 | 56 | 57 | 58 |
Updating!
59 |
Deleting!
60 |
61 | ``` 62 | 63 | ```{note} 64 | An asterisk can be used as wildcard to target more than one element at a time. 65 | 66 | :::{code} html 67 | :force: true 68 | 69 | 70 |
71 | 72 | 73 | 74 |
Updating!
75 |
76 | ::: 77 | ``` 78 | 79 | ## Toggling Attributes 80 | 81 | Elements with an action event can also include an `unicorn:loading` attribute with either an `attr` or `class` modifier. 82 | 83 | ### attr 84 | 85 | Set the specified attribute on the element that is triggering the action. 86 | 87 | This example will disable the _Update_ button when it is clicked and remove the attribute once the action is completed. 88 | 89 | :::{code} html 90 | :force: true 91 | 92 | 93 |
94 | 95 |
96 | ::: 97 | 98 | ### class 99 | 100 | Add the specified class(es) to the element that is triggering the action. 101 | 102 | This example will add `loading` and `spinner` classes to the _Update_ button when it is clicked and remove the classes once the action is completed. 103 | 104 | :::{code} html 105 | :force: true 106 | 107 | 108 |
109 | 110 |
111 | ::: 112 | 113 | ### class.remove 114 | 115 | Remove the specified class from the element that is triggering the action. 116 | 117 | This example will remove a `active` class from the _Update_ button when it is clicked and add the class back once the action is completed. 118 | 119 | :::{code} html 120 | :force: true 121 | 122 | 123 |
124 | 127 |
128 | ::: 129 | -------------------------------------------------------------------------------- /docs/_sources/loading-states.md.txt: -------------------------------------------------------------------------------- 1 | # Loading States 2 | 3 | `Unicorn` requires an AJAX request for any component updates, so it is helpful to provide some context to the user that an action is happening. 4 | 5 | ## Toggling Elements 6 | 7 | Elements with the `unicorn:loading` attribute are only visible when an action is in process. 8 | 9 | ```html 10 | 11 |
12 | 13 | 14 |
Updating!
15 |
16 | ``` 17 | 18 | When the _Update_ button is clicked, the "Updating!" message will show until the action is complete, and then it will re-hide itself. 19 | 20 | ```{warning} 21 | Loading elements get shown or removed with the [`hidden`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden) attribute. One drawback to this approach is that setting the style `display` property overrides this functionality. 22 | ``` 23 | 24 | You can also hide an element while an action is processed by adding a `remove` modifier. 25 | 26 | :::{code} html 27 | :force: true 28 | 29 | 30 |
31 | 32 | 33 |
Not currently updating!
34 |
35 | ::: 36 | 37 | If there are multiple actions that happen in the component, you can show or hide a loading element for a specific action by targeting another element's `id` with `unicorn:target`. 38 | 39 | ```html 40 | 41 |
42 | 43 | 44 | 45 |
Updating!
46 |
Deleting!
47 |
48 | ``` 49 | 50 | An element's `unicorn:key` can also be targeted. 51 | 52 | ```html 53 | 54 |
55 | 56 | 57 | 58 |
Updating!
59 |
Deleting!
60 |
61 | ``` 62 | 63 | ```{note} 64 | An asterisk can be used as wildcard to target more than one element at a time. 65 | 66 | :::{code} html 67 | :force: true 68 | 69 | 70 |
71 | 72 | 73 | 74 |
Updating!
75 |
76 | ::: 77 | ``` 78 | 79 | ## Toggling Attributes 80 | 81 | Elements with an action event can also include an `unicorn:loading` attribute with either an `attr` or `class` modifier. 82 | 83 | ### attr 84 | 85 | Set the specified attribute on the element that is triggering the action. 86 | 87 | This example will disable the _Update_ button when it is clicked and remove the attribute once the action is completed. 88 | 89 | :::{code} html 90 | :force: true 91 | 92 | 93 |
94 | 95 |
96 | ::: 97 | 98 | ### class 99 | 100 | Add the specified class(es) to the element that is triggering the action. 101 | 102 | This example will add `loading` and `spinner` classes to the _Update_ button when it is clicked and remove the classes once the action is completed. 103 | 104 | :::{code} html 105 | :force: true 106 | 107 | 108 |
109 | 110 |
111 | ::: 112 | 113 | ### class.remove 114 | 115 | Remove the specified class from the element that is triggering the action. 116 | 117 | This example will remove a `active` class from the _Update_ button when it is clicked and add the class back once the action is completed. 118 | 119 | :::{code} html 120 | :force: true 121 | 122 | 123 |
124 | 127 |
128 | ::: 129 | --------------------------------------------------------------------------------