From 18010133b2224fb06122c68f31f44b6b51fe8966 Mon Sep 17 00:00:00 2001 From: David Coates Date: Mon, 3 Jun 2024 17:17:10 +1000 Subject: [PATCH] doc: frontend doc rework - Add documentation for templatetags in page so we have full control over formatting. The autofunction solution doesn't work well for templatetags as it can't properly extract any arguments - Add an overview page - Put all API style docs under the API section - Enhance the alliance_ui docs --- docs/conf.py | 8 +- .../frontend/templatetags/alliance_ui.py | 59 +- .../frontend/templatetags/bundler.py | 76 -- .../frontend/templatetags/react.py | 133 +--- .../alliance_platform/frontend/util.py | 11 +- packages/ap-frontend/docs/alliance_ui.rst | 426 ++++++++++- .../ap-frontend/docs/{vite.rst => api.rst} | 54 +- packages/ap-frontend/docs/bundler.rst | 35 - packages/ap-frontend/docs/index.rst | 4 +- packages/ap-frontend/docs/installation.rst | 19 + packages/ap-frontend/docs/overview.rst | 182 +++++ packages/ap-frontend/docs/templatetags.rst | 667 +++++++++++++++++- 12 files changed, 1353 insertions(+), 321 deletions(-) rename packages/ap-frontend/docs/{vite.rst => api.rst} (77%) delete mode 100644 packages/ap-frontend/docs/bundler.rst create mode 100644 packages/ap-frontend/docs/overview.rst diff --git a/docs/conf.py b/docs/conf.py index 4fd10e2..555b9a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -89,7 +89,7 @@ def get_project_mapping(project_name: str): "https://docs.djangoproject.com/en/stable/", ("https://docs.djangoproject.com/en/stable/_objects/"), ), - "python": ("https://docs.python.org/3", "https://docs.python.org/3/objects.inv"), + "python": ("https://docs.python.org/3", None), } # Sphinx defaults to automatically resolve *unresolved* labels using all your Intersphinx mappings. @@ -129,6 +129,12 @@ def setup(app): rolename="tfilter", indextemplate="pair: %s; template filter", ) + # Allows usage of setting role, e.g. :setting:`FORM_RENDERER ` + app.add_crossref_type( + directivename="setting", + rolename="setting", + indextemplate="pair: %s; setting", + ) generate_sidebar(current_dir, globals()) diff --git a/packages/ap-frontend/alliance_platform/frontend/templatetags/alliance_ui.py b/packages/ap-frontend/alliance_platform/frontend/templatetags/alliance_ui.py index 2150205..1a58792 100644 --- a/packages/ap-frontend/alliance_platform/frontend/templatetags/alliance_ui.py +++ b/packages/ap-frontend/alliance_platform/frontend/templatetags/alliance_ui.py @@ -5,6 +5,7 @@ from allianceutils.auth.permission import reverse_if_probably_allowed from django import template from django.db.models import Model +from django.http import HttpRequest from django.template import Context from django.urls import reverse @@ -77,38 +78,6 @@ def url_with_perm_filter(value, arg1=None): """Resolve a URL and check if the current user has permission to access it. If permission check fails, the component that uses the value will be omitted from rendering. - - Usage:: - - {% component "a" href="my_url_name"|url_with_perm:2 %} - - The above example will resolve the URL "my_url_name" with the argument ``2``. - - If you need multiple arguments you can use the ``with_arg`` filter:: - - - {% component "a" href="my_url_name"|url_with_perm:2|with_arg:"another arg" %} - - To pass kwargs you can use the ``with_kwargs`` filter:: - - {% component "a" href="my_url_name"|url_with_perm|with_kwargs:my_kwargs %} - - Note that as there's no way to define a dictionary in the standard Django template language, you'll need to - pass it in context. - - To do object level permission checks use the ``with_perm_obj`` filter to pass through the object:: - - {% component "a" href="my_url_name"|url_with_perm:obj.pk|with_obj:obj %} - - Note that you still have to pass through the pk to resolve the URL with. Passing the object just allows the permission - checks to work without having to manually look up the object. Due to how the ``DetailView.get_object`` method works, - if you are not going to pass the object you must use ``with_kwargs`` to pass through the ID rather than a positional - argument:: - - {% component "a" href="my_url_name"|url_with_perm|with_kwargs:kwargs %} - - Note that the above would do a query to retrieve the object, so it's better to pass the object if you have already - retrieved it. """ pfv = NamedUrlDeferredProp(value, True) if arg1 is not None: @@ -159,3 +128,29 @@ def unwrap_list(value): raise ValueError(f"Expected list of length 1, received length {len(value)}: {value}") return value[0] return value + + +@register.filter +def table_sort_order(request: HttpRequest, ordering_param: str = "ordering"): + """Extract the current sort ordering from the request GET parameters and return it as a list of dicts + + Each ordering entry is a dict with keys "column" and "direction". + + For example, the URL /foo?ordering=-bar,email would return:: + + [ + {'column': 'email', 'direction': 'ascending'}, + {'column': 'name', 'direction': 'descending'} + ] + """ + query = request.GET.copy() + current_sorting_str = query.get(ordering_param) + if current_sorting_str: + return [ + { + "column": x[1:] if x.startswith("-") else x, + "direction": "descending" if x.startswith("-") else "ascending", + } + for x in current_sorting_str.split(",") + ] + return [] diff --git a/packages/ap-frontend/alliance_platform/frontend/templatetags/bundler.py b/packages/ap-frontend/alliance_platform/frontend/templatetags/bundler.py index daa9ee8..f05f889 100644 --- a/packages/ap-frontend/alliance_platform/frontend/templatetags/bundler.py +++ b/packages/ap-frontend/alliance_platform/frontend/templatetags/bundler.py @@ -147,82 +147,6 @@ def bundler_embed(parser: template.base.Parser, token: template.base.Token): By default, the tags are added to the HTML by the :meth:`~alliance_platform.frontend.templatetags.bundler.bundler_embed_collected_assets`. This allows assets to be embedded as needed in templates but all added in one place in the HTML (most likely the ````). You can force the tags to be outputted inline with ``inline=True``. - - NOTE: This tag only accepts static values as :class:`extract_frontend_assets ` - needs to be able to statically generate a list of paths that need to be built for production. - - Args: - content_type: (optional) If set to either 'css' or 'js' only assets of the matching type will be embedded. If omitted - both types will be included (if available). - inline: If ``True`` the tags will be embedded inline, otherwise they will be added using the - :meth:`~alliance_platform.frontend.templatetags.bundler.bundler_embed_collected_assets` tag. Defaults to ``False``. - html_*: any parameter with a ``html_`` prefix will have the ``html_`` stripped and passed through to the embed tag - eg ``html_id="foo"`` would render `` - - - - My Component Logo -

My View

- - - - Using ``inline=True`` instead: - - .. code-block:: html - - {% extends "base.html" %} - {% block body %} - {% bundler_embed "MyComponent.ts" inline=True %} -

My View

- {% endblock %} - - would output: - - .. code-block:: html - - - - - - - -

My View

- - - - Note that in the example above ``logo.png`` is always embedded inline as it is not a javascript or css file. """ tag_name = token.split_contents()[0] args, kwargs, target_var = parse_tag_arguments(parser, token, supports_as=True) diff --git a/packages/ap-frontend/alliance_platform/frontend/templatetags/react.py b/packages/ap-frontend/alliance_platform/frontend/templatetags/react.py index b038696..1faf1d4 100644 --- a/packages/ap-frontend/alliance_platform/frontend/templatetags/react.py +++ b/packages/ap-frontend/alliance_platform/frontend/templatetags/react.py @@ -95,138 +95,7 @@ def resolve_prop(value: Any, node: ComponentNode, context: Context) -> Component def component(parser: template.base.Parser, token: template.base.Token): """Render a React component with the specified props - There are three ways to specify which component to render. The first is for a `"common component" `_ - which is to say a built-in browser component (e.g. ``div``):: - - {% component "h2" %}Heading{% endcomponent %} - - The other two are for using a component defined in an external file. These will be loaded via - the specified bundler class (currently :class:`~alliance_platform.frontend.bundler.vite.ViteBundler`). With - a single argument it specifies that the default export from the file is the component to use:: - - {% component "components/Button" %}Click Me{% endcomponent %} - - With two arguments the first is the file path and the second is the named export from that file:: - - {% component "components/Table" "Column" %}Name{% endcomponent %} - - The last option has a variation for using a property of the export. This is useful for components - where related components are added as properties, e.g. ``Table`` and ``Table.Column``:: - - {% component "components" "Table.Column" %}Name{% endcomponent %} - - Note that this is only available when using named exports; default exports don't support it due to - ambiguity around whether the ``.`` indicates file extension or property access. - - You can omit the file extension - the above could resolve to ``components/Table.tsx`` (``.js`` and ``.ts`` are also - supported). See :ref:`resolving_paths` for details on how the file path is resolved. - - Props are specified as keyword arguments to the tag:: - - {% component "components/Button" variant="primary" %}Click Me{% endcomponent %} - - Additionally, a dict of props can be passed under the ``props`` kwarg:: - - {% component "components/Button" variant="primary" props=button_props %}Click Me{% endcomponent %} - - Children can be passed between the opening ``{% component %}`` and closing ``{% endcomponent %}``. Whitespace - is handled the same as in JSX: - - * Whitespace at the beginning and ending of lines is removed - * Blank lines are removed - * New lines adjacent to other components are removed - * New lines in the middle of a string literal is replaced with a single space - - So the following are all equivalent:: - - {% component "div" %}Hello World{% endcomponent %} - - {% component %} - Hello world - {% endcomponent %} - - - {% component %} - Hello - world - {% endcomponent %} - - - {% component %} - - Hello world - {% endcomponent %} - - Components can be nested:: - - {% component "components/Button" type="primary" %} - {% components "icons" "Menu" %}{% endcomponent %} - Open Menu - {% end_component %} - - You can use ``as `` to store in a variable in context that can then be passed to another tag:: - - {% component "icons" "Menu" as icon %}{% end_component %} - {% component "components/Button" type="primary" icon=icon %}Open Menu{% end_component %} - - All props must be JSON serializable. :class:`~alliance_platform.frontend.prop_handlers.ComponentProp` can be used to define - how to serialize data, with a matching implementation in ``propTransformers.tsx`` to de-serialize it. - - For example :class:`~alliance_platform.frontend.prop_handlers.DateProp` handles serializing a python ``datetime`` and - un-serializing it as a native JS ``Date`` on the frontend. See :class:`~alliance_platform.frontend.prop_handlers.ComponentProp` - for documentation about adding your own complex props. - - Components are rendered using the ``renderComponent`` function in ``ap_frontend_settings.REACT_RENDER_COMPONENT_FILE``. This can be modified as needed, - for example if a new provider is required. - - .. note:: All props passed through are converted to camel case automatically (i.e. ``my_prop`` will become ``myProp``) - - Server Side Rendering (SSR) - --------------------------- - - Components will automatically be rendered on the server. See :ref:`ssr` for details about how this works. - - To opt out of SSR pass ``ssr:disabled=True`` to the component after the component name:: - - {% component 'components/Button.tsx' ssr:disabled=True %}...{% endcomponent %} - - Options - ------- - - Various options can be passed to the component tag. To differentiate from actual props to the component they are - prefixed with `ssr:` for server side rendering options, `component:` for general component options, or `container:` - for options relating to the container the component is rendered into. - - - ``ssr:disabled=True`` - if specified, no server side rendering will occur for this component - - ``component:omit_if_empty=True`` - if specified, the component will not be rendered if it has no children. This is - useful for when components may not be rendered based on permission checks - - ``container:tag`` - the HTML tag to use for the container. Defaults to the custom element ``dj-component``. - - ``container:`` - any other props will be passed to the container element. For example, to add - an id to the container you can use ``container:id="my-id"``. Note that while you can pass a style string, it's - likely to be of little use with the default container style ``display: contents``. Most of the time you can just - do the styling on the component itself. - - For example:: - - {% component 'core-ui' 'Button' ssr:disabled=True variant="Outlined"%} - ... - {% endcomponent %} - - Limitations - ----------- - - Currently, attempting to render a django form widget that is itself a React component within another component will - not work. This is due to how django widgets have their own templates that are rendered in an isolated context. For - example, this will not work if ``form.field`` also uses the ``{% component %}`` tag: - - {% component 'MyComponent' %} - {{ form.field }} - {% endcomponent %} - - Alliance UI - ----------- - Alliance Core UI components have pre-made tags for more convenient implementation. - + See the templatetags.rst docs for more details. """ return parse_component_tag(parser, token) diff --git a/packages/ap-frontend/alliance_platform/frontend/util.py b/packages/ap-frontend/alliance_platform/frontend/util.py index bdef29b..d64a13b 100644 --- a/packages/ap-frontend/alliance_platform/frontend/util.py +++ b/packages/ap-frontend/alliance_platform/frontend/util.py @@ -16,9 +16,12 @@ def get_node_ver(node_path: str) -> Version | None: """ - Get the node verson from a given path + Get the node version from a given path if raise_errors is not True then exceptions will be swallowed and None will be returned on error + + Args: + node_path: The path to the node executable """ try: proc = subprocess.Popen( @@ -36,6 +39,12 @@ def get_node_ver(node_path: str) -> Version | None: def guess_node_path(nvmrc_path: Path) -> str | None: + """Attempt to guess the node path based on the .nvmrc file + + Args: + nvmrc_path: Path to the .nvmrc file + """ + # nvm's version strings aren't quite the same as those # used by python but the overlap makes it good enough diff --git a/packages/ap-frontend/docs/alliance_ui.rst b/packages/ap-frontend/docs/alliance_ui.rst index 5acfa78..ae52ff2 100644 --- a/packages/ap-frontend/docs/alliance_ui.rst +++ b/packages/ap-frontend/docs/alliance_ui.rst @@ -1,62 +1,436 @@ Alliance UI ************* +These template tags and filters provide a convenient way to render components from the Alliance UI React library in Django templates. + +You can load the tags and filters with: + +.. code-block:: html+django + + {% load alliance_ui %} + +You can see the main documentation for the Alliance UI components `here `_. +Not all components have a tag; you can render them using the :ttag:`component` tag. See the linked documentation +for the props each component accept as they aren't duplicated in this documentation. + +Any differences from the underlying React components are documented below. + Filters ------- -.. autofunction:: alliance_platform.frontend.templatetags.alliance_ui.url_with_perm_filter +.. templatefilter:: url_with_perm + +``url_with_perm`` +-------------------- + +Resolve a URL and check if the current user has permission to access it. + +If you don't need permission checks, use :tfilter:`url_with_perm`. + +If permission check fails, the component that uses the value will be omitted from rendering. + +Usage: + +.. code-block:: html+django + + {% component "a" href="my_url_name"|url_with_perm:2 %}Link{% endcomponent %} + +The above example will resolve the URL "my_url_name" with the argument ``2``. + +If you need multiple arguments you can use the ``with_arg`` filter: + +.. code-block:: html+django + + {% component "a" href="my_url_name"|url_with_perm:2|with_arg:"another arg" %}Link{% endcomponent %} + +To pass kwargs you can use the ``with_kwargs`` filter: + +.. code-block:: html+django + + {% component "a" href="my_url_name"|url_with_perm|with_kwargs:my_kwargs %}Link{% endcomponent %} + +Note that as there's no way to define a dictionary in the standard Django template language, you'll need to +pass it in context. + + +To do object level permission checks use the ``with_perm_obj`` filter to pass through the object: + +.. code-block:: html+django + + {% component "a" href="my_url_name"|url_with_perm:obj.pk|with_obj:obj %}Link{% endcomponent %} + +Note that you still have to pass through the pk to resolve the URL with. Passing the object just allows the permission +checks to work without having to manually look up the object. Due to how the ``DetailView.get_object`` method works, +if you are not going to pass the object you must use ``with_kwargs`` to pass through the ID rather than a positional +argument: + +.. code-block:: html+django + + {% component "a" href="my_url_name"|url_with_perm|with_kwargs:kwargs %}Link{% endcomponent %} + +Note that the above would do a query to retrieve the object, so it's better to pass the object if you have already +retrieved it. + +.. templatefilter:: url + +``url`` +------- + +Behaves same as :tfilter:`url_with_perm` but does not check any permissions. -.. autofunction:: alliance_platform.frontend.templatetags.alliance_ui.url_filter +.. code-block:: html+django -.. autofunction:: alliance_platform.frontend.templatetags.alliance_ui.with_arg + {% component "a" href="my_url_name"|url_with_perm|with_kwargs:kwargs %} -.. autofunction:: alliance_platform.frontend.templatetags.alliance_ui.with_kwargs +.. templatefilter:: with_arg -.. autofunction:: alliance_platform.frontend.templatetags.alliance_ui.with_perm_object +``with_arg`` +-------------------- + +Add an argument to a :tfilter:`url_with_perm` or :tfilter:`url` filter. This is useful when you need to pass multiple arguments to a URL. + +.. code-block:: html+django + + {# This will resolve "my_url_name" with args [2, "another arg"] + {% component "a" href="my_url_name"|url_with_perm:2|with_arg:"another arg" %}Link{% endcomponent %} + +.. templatefilter:: with_kwargs + +``with_kwargs`` +-------------------- + +Add kwargs to a :tfilter:`url_with_perm` or :tfilter:`url` filter. + +.. code-block:: html+django + + {% component "a" href="my_url_name"|url_with_perm|with_kwargs:my_kwargs %}Link{% endcomponent %} + +.. templatefilter:: with_perm_object + +``with_perm_object`` +-------------------- + +Add an object to a :tfilter:`url_with_perm` filter for the purposes of object level permission checks. + +This is useful if you already have the object and want to pass it through to the permission check, thereby avoiding +another database query. + +.. code-block:: html+django + + {% component "a" href="organisation_detail"|url_with_perm:organisation.pk|with_perm_obj:organisation %} + View + {% endButton %} + +.. templatefilter:: unwrap_list + +``unwrap_list`` +--------------- + +Unwrap a list of length 1 into the single item it contains. This is useful when you have a list of items but you know +there will only ever be one item in the list. For example, the django radio input widget value is always a list even +though there's only a single value. + +.. code-block:: html+django + + {% component "@alliancesoftware/ui" "RadioGroup" props=widget.attrs|merge_props:extra_widget_props|html_attr_to_jsx type=widget.type name=widget.name default_value=widget.value|unwrap_list %} + ... + {% endcomponent %} + + +.. templatefilter:: table_sort_order + +``table_sort_order`` +-------------------- + +For use with :ttag:`Table` components, this filter will return the current sort order from the request. + +For example, if the current url was ``/foo?ordering=-bar,email`` this filter would return:: + + [ + {'column': 'email', 'direction': 'ascending'}, + {'column': 'name', 'direction': 'descending'} + ] + +This filter expects to be passed the current :class:`~django:django.http.HttpRequest` object, and optionally the query parameter name to look for. +If the query parameter name is not specified it defaults to `"ordering"`. + +In no ordering is present in the URL an empty list ``[]`` will be returned. + +Usage: + +.. code-block:: html+django + + {% Table sort_order=request|table_sort_order:"order" %} + ... + {% endTable %} -.. autofunction:: alliance_platform.frontend.templatetags.alliance_ui.unwrap_list Template Tags ------------- -The Alliance UI template tags serve as a convenient alternative to the :function:`~alliance_platform.frontend.templatetags.react.component` +The Alliance UI template tags serve as a convenient alternative to the :ttag:`component` template tag, for easily embedding components from the Alliance UI library into Django templates. See the documentation for the component tag for instructions on passing arguments and filters. -.. autofunction:: alliance_platform.frontend.alliance_ui.button.button +.. templatetag:: Button + +``Button`` +---------- + +Render a `Button `_ component from the +Alliance UI React library with the specified props. -.. autofunction:: alliance_platform.frontend.alliance_ui.button.button_group +Usage: -.. autofunction:: alliance_platform.frontend.alliance_ui.date_picker.date_picker +.. code-block:: html+django -.. autofunction:: alliance_platform.frontend.alliance_ui.icon.icon + {% Button variant="outlined" %}Click Me{% endButton %} -.. autofunction:: alliance_platform.frontend.alliance_ui.inline_alert.inline_alert +You can render as a link by passing ``href``. To resolve named URLs, optionally with permission checks you can +use ``url`` or ``url_with_perm`` filters: -.. autofunction:: alliance_platform.frontend.alliance_ui.menubar.menubar -.. autofunction:: alliance_platform.frontend.alliance_ui.menubar.submenu +.. code-block:: html+django -.. autofunction:: alliance_platform.frontend.alliance_ui.menubar.menubar_item + {% Button variant="outlined" color="secondary" size="md" href="my_app:organisation_update"|url_with_perm:organisation.pk|with_perm_obj:organisation %} + Update + {% endButton %} + +.. templatetag:: ButtonGroup + +``ButtonGroup`` +--------------- + +Render a `ButtonGroup `_ component +from the Alliance UI React library with the specified props. + +The children of the tag should be :ttag:`Button` tags. + +.. code-block:: html+django + + {% ButtonGroup density="xs" size="sm" variant="link" %} + {% Button href="my_app:mymodel_update"|url_with_perm:obj.pk|with_perm_obj:obj aria-label="Edit" %} + {% Icon "Pencil01Outlined" %} + {% endButton %} + {% Button href="my_app:mymodel_detail"|url_with_perm:obj.pk|with_perm_obj:obj aria-label="View" %} + {% Icon "FileSearch01Outlined" %} + {% endButton %} + {% Button href="my_app:mymodel_delete"|url_with_perm:obj.pk|with_perm_obj:obj aria-label="Delete" %} + {% Icon "Trash01Outlined" %} + {% endButton %} + {% endButtonGroup %} + +.. templatetag:: DatePicker + +``DatePicker`` +--------------- + +Render a `DatePicker `_ component +from the Alliance UI React library with the specified props. + +This can be used as date picker, or a datetime picker, depending on the specified ``granularity`` (default is "day"). + +``granularity`` can be one of "day", "hour", "minute", "second". + +``default_value`` can be a string, in which case it will be parsed using :func:`~django:django.utils.dateparse.parse_date` +when the granularity is "day", otherwise :func:`~django:django.utils.dateparse.parse_datetime`. Otherwise ``default_value`` +should be a :class:`python:datetime.date` instance when ``granularity="day"``, otherwise a :class:`python:datetime.date` instance + +.. code-block:: html+django + + {% DatePicker name=widget.name default_value=raw_value %}{% endDatePicker %} + +.. templatetag:: Icon + +``Icon`` +-------- + +Render an icon from `@alliancesoftware/icons `_. + +See the `list of icons here `_. + +.. code-block:: html+django + + {% Icon "Trash01Outlined" %} + +It can be passed to other components props by using the ``as `` form + +.. code-block:: html+django + + {% Icon "Trash01Outlined" as icon %} + {% component "MyComponent" icon=icon %}{% endcomponent %} + + +.. templatetag:: InlineAlert + +``InlineAlert`` +--------------- + +Render an `InlineAlert `_ component +from the Alliance UI React library with the specified props. + +.. code-block:: html+django + + {% InlineAlert intent="success" %}Changes saved successfully{% endInlineAlert %} + +.. templatetag:: Menubar + +``Menubar`` +----------- + +Render an `Menubar `_ component. + +You can use ``Menubar.Section``, ``Menubar.Item``, and `Menubar.SubMenu`` components to build the menu. + +Here is a fully featured example that renders a Users section, followed by a link to an Audit logs page, and finally a +submenu with an icon for the current user's account management link and a logout button that submits a logout form. + +.. code-block:: html+django + + {# logout should occur via post, so add a form here that can be submitted from the menu %} +
+ {% csrf_token %} +
+ + {% Icon "User01Outlined" size="xs" as UserIcon %} + + {% Menubar %} + {% Menubar.Section title="Users" %} + {% Menubar.Item href="my_app:adminprofile_list"|url_with_perm key="admin" %} + Admin + {% endMenubar.Item %} + {% Menubar.Item href="my_app:client_list"|url_with_perm %} + Clients + {% endMenubar.Item %} + {% endMenubar.Section %} + {% Menubar.Item href="my_app:audit_logs"|url_with_perm %} + Audit + {% endMenubar.Item %} + {% Menubar.SubMenu text_value="My Account" title=UserIcon %} + {% Menubar.Item href="my_app:personal-account"|url_with_perm %} + My Account + {% endMenubar.Item %} + {% Menubar.Item element_type="button" type="submit" form="logout-form" %} + Logout + {% endMenubar.Item %} + {% endMenubar.SubMenu %} + {% endcomponent %} + + +.. templatetag:: Table + +``Table`` +----------- + +Render an `Table `_ component. + +You can use ``TableHeader``, ``TableBody``, ``Row``, ``Column`` and ``Cell`` components to build the menu. + +This example renders a list of records, and allows sorting of columns by clicking on the column headers. This makes +use of the :tfilter:`table_sort_order` filter to determine the current sort order of the column and pass it through +in the expected format. The sort order is stored in the ``order`` query param. + +.. code-block:: html+django + + {# Use this to render header as links to the current page with the sort column query param updated #} + {% ColumnHeaderLink sort_query_param="order" as header_element_type %}{% endColumnHeaderLink %} + + {% Table column_header_element_type=header_element_type sort_order=request|table_sort_order:"order" sort_mode="multiple" sort_behavior="replace" aria-label="User List" %} + {% TableHeader %} + {% Column allows_sorting=True %}Name{% endColumn %} + {% Column allows_sorting=True %}Email{% endColumn %} + {% Column %}Active{% endColumn %} + {% Column %}Actions{% endColumn %} + {% endTableHeader %} + {% TableBody %} + {% for obj in object_list %} + {% Row key=obj.pk %} + {% Cell %}{{ obj.name }}{% endCell %} + {% Cell %}{{ obj.email }}{% endCell %} + {% Cell %}{{ obj.is_active }}{% endCell %} + {% Cell %} + {% ButtonGroup density="xs" size="sm" variant="link" %} + {% Button href="my_app:user_update"|url_with_perm:obj.pk|with_perm_obj:obj aria-label="Edit" %} + {% Icon "Pencil01Outlined" %} + {% endButton %} + {% Button href="my_app:user_detail"|url_with_perm:obj.pk|with_perm_obj:obj aria-label="View" %} + {% Icon "FileSearch01Outlined" %} + {% endButton %} + {% Button href="my_app:user_delete"|url_with_perm:obj.pk|with_perm_obj:obj aria-label="Delete" %} + {% Icon "Trash01Outlined" %} + {% endButton %} + {% endButtonGroup %} + {% endCell %} + {% endRow %} + {% endfor %} + {% endTableBody %} + {% endTable %} + + +You can use it with the :ttag:`Pagination` component to render a paginated table. + +.. code-block:: html+django + + {% Pagination page=page_obj.number total=paginator.count page_size=paginator.per_page boundary_count=2 sibling_count=1 aria-label="Pagination" is_page_size_selectable=allow_page_size_selection as pagination %}{% endPagination %} + {% Table aria-label="User List" footer=pagination %} + {% TableHeader %} + {# omitted for brevity #} + {% endTableHeader %} + {% TableBody %} + {# omitted for brevity #} + {% endTableBody %} + {% endTable %} + +.. templatetag:: ColumnHeaderLink + +``ColumnHeaderLink`` +-------------------- + +For use with :ttag:`Table` components, this tag will render a link that updates the sort order query parameter when clicked. + +See the :ttag:`Table` documentation for an example of how to use this tag. + +.. templatetag:: Pagination + +``Pagination`` +-------------- + +Render an `Pagination `_ component. + +Usage: + +.. code-block:: html+django + + {% Pagination page=1 total=100 page_size=10 boundary_count=2 sibling_count=1 aria-label="Pagination" is_page_size_selectable=True %}{% endPagination %} + +.. templatetag:: TimeInput + +``TimeInput`` +------------- -.. autofunction:: alliance_platform.frontend.alliance_ui.menubar.menubar_section +Render an `TimeInput `_ component. -.. autofunction:: alliance_platform.frontend.alliance_ui.misc.fragment_component +The value passed to ``default_value`` can be a string, in which case it will be parsed using :func:`~django:django.utils.dateparse.parse_time`, +otherwise it should be :class:`python:datetime.time` instance. -.. autofunction:: alliance_platform.frontend.alliance_ui.pagination.pagination +Usage: -.. autofunction:: alliance_platform.frontend.alliance_ui.table.table +.. code-block:: html+django -.. autofunction:: alliance_platform.frontend.alliance_ui.table.table_header + {% TimeInput name="time" default_value="12:45" %}{% endTimeInput %} -.. autofunction:: alliance_platform.frontend.alliance_ui.table.table_body +.. templatetag:: Fragment -.. autofunction:: alliance_platform.frontend.alliance_ui.table.column_header_link +``Fragment`` +------------ -.. autofunction:: alliance_platform.frontend.alliance_ui.table.row +Render a React ``Fragment``. Can be used in cases where you need to wrap multiple components in a single parent element. -.. autofunction:: alliance_platform.frontend.alliance_ui.table.column +.. code-block:: html+django -.. autofunction:: alliance_platform.frontend.alliance_ui.table.cell + {% Fragment as buttons %} + {% Button variant="outlined" %}Click Me{% endButton %} + {% Button variant="outlined" %}Also Click Me{% endButton %} + {% endFragment %} -.. autofunction:: alliance_platform.frontend.alliance_ui.time_input.time_input + {% component "MyComponent" extra=buttons %}{% endcomponent %} diff --git a/packages/ap-frontend/docs/vite.rst b/packages/ap-frontend/docs/api.rst similarity index 77% rename from packages/ap-frontend/docs/vite.rst rename to packages/ap-frontend/docs/api.rst index 6152fed..655c43e 100644 --- a/packages/ap-frontend/docs/vite.rst +++ b/packages/ap-frontend/docs/api.rst @@ -1,5 +1,54 @@ +API +*** + +Utils +----- + +.. autofunction:: alliance_platform.frontend.util.transform_attribute_names + +.. autofunction:: alliance_platform.frontend.util.guess_node_path + +.. autofunction:: alliance_platform.frontend.util.get_node_ver + +Bundler +------- + +.. autofunction:: alliance_platform.frontend.bundler.get_bundler + +.. autoclass:: alliance_platform.frontend.bundler.base.BaseBundler + :members: + +.. autoclass:: alliance_platform.frontend.bundler.base.AssetFileEmbed + :members: + +.. autoclass:: alliance_platform.frontend.bundler.base.ResolveContext + :members: + +.. autoclass:: alliance_platform.frontend.bundler.base.PathResolver + :members: + +.. autoclass:: alliance_platform.frontend.bundler.base.RegExAliasResolver + :members: + +.. autoclass:: alliance_platform.frontend.bundler.base.SourceDirResolver + :members: + +.. autoclass:: alliance_platform.frontend.bundler.base.RelativePathResolver + :members: + +.. autoclass:: alliance_platform.frontend.bundler.asset_registry.FrontendAssetRegistry + :members: + +.. py:data:: alliance_platform.frontend.bundler.asset_registry.frontend_asset_registry + + The default registry that is used throughout the site. + +.. autoclass:: alliance_platform.frontend.bundler.base.DevServerCheck + :members: + + Vite Bundler -============ +------------- Example setup:: @@ -82,9 +131,6 @@ Example setup:: wait_for_server=wait_for_server, ) - - - .. autoclass:: alliance_platform.frontend.bundler.vite.ViteBundler :members: diff --git a/packages/ap-frontend/docs/bundler.rst b/packages/ap-frontend/docs/bundler.rst deleted file mode 100644 index a90e42e..0000000 --- a/packages/ap-frontend/docs/bundler.rst +++ /dev/null @@ -1,35 +0,0 @@ -Bundler -####### - -.. autofunction:: alliance_platform.frontend.bundler.get_bundler - -.. autoclass:: alliance_platform.frontend.bundler.base.BaseBundler - :members: - -.. autoclass:: alliance_platform.frontend.bundler.base.AssetFileEmbed - :members: - -.. autoclass:: alliance_platform.frontend.bundler.base.ResolveContext - :members: - -.. autoclass:: alliance_platform.frontend.bundler.base.PathResolver - :members: - -.. autoclass:: alliance_platform.frontend.bundler.base.RegExAliasResolver - :members: - -.. autoclass:: alliance_platform.frontend.bundler.base.SourceDirResolver - :members: - -.. autoclass:: alliance_platform.frontend.bundler.base.RelativePathResolver - :members: - -.. autoclass:: alliance_platform.frontend.bundler.asset_registry.FrontendAssetRegistry - :members: - -.. py:data:: alliance_platform.frontend.bundler.asset_registry.frontend_asset_registry - - The default registry that is used throughout the site. - -.. autoclass:: alliance_platform.frontend.bundler.base.DevServerCheck - :members: diff --git a/packages/ap-frontend/docs/index.rst b/packages/ap-frontend/docs/index.rst index d1f7ddd..9f86f66 100644 --- a/packages/ap-frontend/docs/index.rst +++ b/packages/ap-frontend/docs/index.rst @@ -25,10 +25,10 @@ embedded in the template with the :ttag:`component` tag. :caption: Contents: installation + overview templatetags alliance_ui - bundler - vite settings + api .. include:: ../../ap-core/docs/_sidebar.rst.inc diff --git a/packages/ap-frontend/docs/installation.rst b/packages/ap-frontend/docs/installation.rst index 89f0aff..6b6b73b 100644 --- a/packages/ap-frontend/docs/installation.rst +++ b/packages/ap-frontend/docs/installation.rst @@ -64,3 +64,22 @@ See the :doc:`settings` documentation for details about each of the available se "PRODUCTION_DIR": PROJECT_DIR / "frontend/build", }, } + +In the ``MIDDLEWARE`` setting, add the ``BundlerAssetContextMiddleware`` middleware. This is used by tags like +:ttag:`component` and :ttag:`bundler_embed`. + +.. code-block:: python + + MIDDLEWARE = [ + ... + "alliance_platform.frontend.bundler.middleware.BundlerAssetContextMiddleware", + ... + ] + +Finally, ``FORM_RENDERER`` should be set as follows: + +.. code-block:: python + + FORM_RENDERER = "alliance_platform.frontend.forms.renderers.FormInputContextRenderer" + +This is used by the :ttag:`form` and :ttag:`form_input` tags. diff --git a/packages/ap-frontend/docs/overview.rst b/packages/ap-frontend/docs/overview.rst new file mode 100644 index 0000000..ccf9755 --- /dev/null +++ b/packages/ap-frontend/docs/overview.rst @@ -0,0 +1,182 @@ +Overview +========= + +.. _styling: + +Styling +####### + +When styling components in this library the preferred approach is to use `Vanilla Extract `_ for the following reasons: + +- Automatic namespacing eliminates the need for naming conventions like BEM, making it easier to write and manage styles. +- The build system will extract used vanilla-extract styles to static CSS for use in production, which improves performance (i.e. there's no required JS runtime for the styles). +- Sharing common styles across components or pages is easier since you can just import from the file. +- Having a full featured programming language at your disposal is very useful for things like writing variations for a + component + +To load vanilla-extract styles in your templates, use the :ttag:`stylesheet` +template tag. This tag allows you to load the styles and get access to the class names in a context variable. + +In some cases, you may need to import vanilla CSS instead of using vanilla-extract, such as when working with third-party +modules. In those cases, you can use the :ttag:`bundler_embed` tag to import the CSS. + +.. _react: + +React +##### + +Django templates are easy and convenient, but oftentimes you need more than static HTML. Turning the whole page into a +React page is often overkill and means you lose the ability to use things like Django forms. However, you can use the +:ttag:`component` tag to easily render a React component in part of the page, or +the whole page as required. + +By combining Django templates with React components, you can take advantage of the benefits of both technologies. See +:ttag:`component` for details on how to do it. + +By default, props passed to the component must be JSON serializable. However, if you need to pass complex props, you can +use the :class:`~alliance_platform.frontend.prop_handlers.ComponentProp` to automatically convert them to a format that can be +passed to the component. For example, :class:`~alliance_platform.frontend.prop_handlers.DateProp` allows you to pass a ``datetime`` +object and have it passed to the component as a JavaScript ``Date``. + +Combine this with :ref:`Server Side Rendering (SSR) ` to get the best of both worlds. By using SSR, you can render the React +component on the server and send the generated HTML to the client, allowing for perceived faster load times and avoiding +flashes of content as things render dynamically. + +.. _resolving_paths: + +Resolving Paths +############### + +Various template tags accept a string to an asset, for example a React component or a stylesheet. This section +describes how the value passed here is handled. + +When a string representing a path is passed to a template tag, the bundler will resolve it +based on the settings in :data:`~alliance_platform.frontend.bundler.base.BaseBundler.path_resolvers`. +There is no default behaviour, but there are some classes you can use for common cases. + +To define your own behavior, you can subclass :class:`~alliance_platform.frontend.bundler.base.PathResolver`. In +this example, ``AlliancePlatformPackageResolver`` will resolve any usages of ``@alliancesoftware/ui`` or ``@alliancesoftware/icons`` +to the ``node_modules`` directory. This allows you to use ``{% component "@alliancesoftware/ui" "Table" %}`` rather than +``{% component "/node_modules/@alliancesoftware/ui" "Table" %}``: + +.. code-block:: python + + class AlliancePlatformPackageResolver(PathResolver): + """Resolve usages of @alliancesoftware/* packages to node_modules directory. + + Allows usages like ``{% component "@alliancesoftware/ui" "Table" %}``` rather than + ``{% component "/node_modules/@alliancesoftware/ui" "Table" %}```. + """ + + def resolve(self, path: str, context: ResolveContext): + if path.startswith("@alliancesoftware/ui") or path.startswith("@alliancesoftware/icons"): + return ap_frontend_settings.NODE_MODULES_DIR / path + return None + +.. note:: + + While we work with ``Path`` objects here, in production the bundler will handle these even if the source code + doesn't exist in the filesystem. For example, the ViteBundler will use the resolved paths to index into it's generated + manifest file. In the example above, the resolved path might be ``/node_modules/@alliancesoftware/ui/Table.tsx``, + which would have an entry in the manifest file mapping it to the generate file ``Table.hash123.js``. + + In development, extra checks are done to ensure the file used exists on the filesystem. + +Here is a more complete example of what ``path_resolvers`` could be set to: + +.. code-block:: python + + path_resolvers=[ + AlliancePlatformPackageResolver(), + RelativePathResolver(), + RegExAliasResolver("^/", str(settings.PROJECT_DIR) + "/"), + SourceDirResolver(root_dir / "frontend/src"), + ] + +This will resolve paths as follows: + +- If path is in the form of ``@alliancesoftware/ui`` or ``@alliancesoftware/icons``, it will be resolved to the ``node_modules`` directory. +- If the path is relative (starting with ``./`` or ``../``), it is resolved relative to the template file that contains the tag. +- If the path starts with ``/``, it is resolved relative to ``settings.PROJECT_DIR``. +- Otherwise, it is resolved relative to ``frontend/src``. + +So the following paths would be resolved as follows, assuming ``PROJECT_DIR`` is ``/root``: + +- ``@alliancesoftware/ui`` -> ``/root/node_modules/@alliancesoftware/ui`` +- ``./MyComponent`` called from within ``my_site/templates/file.html`` -> ``/root/my_site/templates/MyComponent`` +- ``components/MyComponent`` -> ``/root/frontend/src/components/MyComponent`` +- ``/my_file`` -> ``/root/my_file`` + +As most of the time you will be including components from one directory (e.g. ``frontend/src/``), this setup makes +that the easiest. + +.. _ssr: + +Server Side Rendering (SSR) +########################### + +Server Side Rendering (SSR) is a technique used to render components on the server, and send the generated HTML to the client, +which is then hydrated with JavaScript to allow interactivity. This technique can improve perceived website performance, as it +shows the content immediately rather than waiting for JavaScript to be loaded, parsed and executed. It can also benefit SEO or +potentially be used for things like PDF rendering that relies on static HTML. + +SSR is enabled by default and works as follows: + +- Each component rendered in a template queues itself to be rendered with :meth:`~alliance_platform.frontend.bundler.context.BundlerAssetContext.queue_ssr`. +- :class:`~alliance_platform.frontend.bundler.middleware.BundlerAssetContextMiddleware` accesses the context and retrieves all the queued SSR items. +- It then serializes the queued items and calls out to javascript, which renders each component to static HTML and returns it + + - In development this is handled by ``dev-server.ts``. This allows Vite to process the required modules without having to do a full production build + - In production it is handled by ``production-ssr-server.ts`` which works with the production built files. + +Currently, the only thing that gets rendered on the server is React components. :class:`~alliance_platform.frontend.templatetags.react.ComponentSSRItem` +is used to describe the component that needs to be rendered. See it's documentation for details on how each component +is serialized. + +.. admonition:: Disabling SSR + + To disable SSR entirely you can pass ``disable_ssr=True`` to :class:`~alliance_platform.frontend.bundler.vite.ViteBundler`. + +.. note:: + + The above references to ``dev-server.ts`` and ``production-ssr-server.ts`` are specific to the template-django setup. + These will be available in a separate package in the future. + + +Quick Reference +############### + +* To get the bundler instance use :func:`~alliance_platform.frontend.bundler.get_bundler` +* To render a React component in a template use the :ttag:`component` tag: + +.. code-block:: html+django + + {% load react %} + + + {% component "components/Button" type="primary" %} + Click Me + {% endcomponent %} + + {% component "components/Table" "Column" %}Header{% endcomponent %} + + {% component "components/Icons" "Menu" as menu_icon %} + {% component "components/Button" icon=menu_icon %}{% endcomponent %} + +* To include a Vanilla Extract stylesheet in a template use the :ttag:`stylesheet` tag: + +.. code-block:: html+django + + {% load vanilla_extract %} + {% stylesheet "./MyView.css.ts" as styles %} + +
+ ... +
+ +* To include a plain CSS file in a template use :ttag:`bundler_embed`: + +.. code-block:: html+django + + {% load bundler %} + {% bundler_embed "./normalize.css" %} diff --git a/packages/ap-frontend/docs/templatetags.rst b/packages/ap-frontend/docs/templatetags.rst index e89095d..2fbd9d0 100644 --- a/packages/ap-frontend/docs/templatetags.rst +++ b/packages/ap-frontend/docs/templatetags.rst @@ -1,22 +1,665 @@ -Template Tags -************* +Tags and Filters +**************** -.. autofunction:: alliance_platform.frontend.templatetags.bundler.bundler_embed +.. contents:: + :local: -.. autofunction:: alliance_platform.frontend.templatetags.bundler.bundler_url +.. _bundler-static-paths: -.. autofunction:: alliance_platform.frontend.templatetags.bundler.bundler_preamble +Static Paths +------------ -.. autofunction:: alliance_platform.frontend.templatetags.bundler.bundler_dev_checks +Various template tags accept a path to an asset. For example, :ttag:`bundler_embed` accepts the path to an asset to embed +and :ttag:`component` accepts the path to a React component. These paths must be static values, i.e. they cannot be +template variables or expressions. This is because the paths must be known when the frontend build occurs, so that +the bundler knows which files to include in the build. The :class:`extract_frontend_assets ` +management command will extract these paths from the templates and generate a list of files to include in the build. -.. autofunction:: alliance_platform.frontend.templatetags.bundler.bundler_embed_collected_assets +Examples: -.. autofunction:: alliance_platform.frontend.templatetags.react.component +.. code-block:: html+django -.. autofunction:: alliance_platform.frontend.templatetags.react.react_refresh_preamble + {# Valid, the path can be extracted by looking at the template file #} + {% bundler_embed "styles.css" %} -.. autofunction:: alliance_platform.frontend.templatetags.vanilla_extract.stylesheet + {# Invalid #} + {% bundler_embed "styles.css"|upper %} + {% bundler_embed variable %} -.. autofunction:: alliance_platform.frontend.templatetags.form.form +Bundler Tags +------------ -.. autofunction:: alliance_platform.frontend.templatetags.form.form_input +.. templatetag:: bundler_embed + +``bundler_embed`` +----------------- + +Return the embed HTML codes from the bundler to a specified asset. + +Each asset can have multiple files associated with it. For example, a component might have javascript and css. You +can control which types of tags are included using the ``content_type`` kwarg. Common types are ``text/css`` and ``text/javascript``, +but it is ultimately based on the file extension (e.g. ``.png`` will be ``image/png``). Note that ``.css.ts`` is handled +as ``text/css`` and ``.ts`` and ``.tsx`` are handled as ``text/javascript``. + +By default, the tags are added to the HTML by the :meth:`~alliance_platform.frontend.templatetags.bundler.bundler_embed_collected_assets`. +This allows assets to be embedded as needed in templates but all added in one place in the HTML (most likely the ````). +You can force the tags to be outputted inline with ``inline=True``. Note that this only applies CSS and JS; other assets, +like images, will always be outputted inline. + +Must be passed a :ref:`static path ` to an asset. + +Usage: + +.. code-block:: html+django + + {% load bundler %} + + {% bundler_embed [path] [[content_type="css|js"] [inline=True] [html_*=...]] %} + +================ ============================================================= +Argument Description +================ ============================================================= +``path`` The path to the asset to embed. This must be a :ref:`static value `, i.e. it cannot be a template variable. +``content_type`` (optional) If set to either 'css' or 'js' only assets of the matching type will be embedded. If omitted + both types will be included (if available). +``inline`` (optional) If ``True`` the tags will be embedded inline, otherwise they will be added using the + :ttag:`bundler_embed_collected_assets` tag. Defaults to ``False``. +``html_*`` Any parameter with the ``html_`` prefix will have the ``html_`` stripped and will be passed through + to the embed tag. e.g. ``html_id="foo"`` would render `` + + + + My Component Logo +

My View

+ + + +Using ``inline=True`` instead: + +.. code-block:: html+django + + {% extends "base.html" %} + {% block body %} + {% bundler_embed "MyComponent.ts" inline=True %} +

My View

+ {% endblock %} + +would output: + +.. code-block:: html + + + + + + + +

My View

+ + + +Note that in the example above ``logo.png`` is always embedded inline as it is not a javascript or css file. + +.. templatetag:: bundler_url + +``bundler_url`` +----------------- + +Return the URL from the bundler to a specified asset. + +If you want to embed the asset with the appropriate HTML tags, use :ttag:`bundler_embed` instead. + +Must be passed a :ref:`static path ` to an asset. + +If dev, this will return the path to the asset in the dev server. If not dev, this will return the path to the built +asset. + +Usage: + +.. code-block:: html+django + + {% load bundler %} + + {% bundler_url [static path] [as varname] %} + +Examples: + +.. code-block:: html+django + + {% bundler_url "style.css" %} + +would output, in dev:: + + http://localhost:5173/assets/style.css + +in production:: + + /assets/style-abc123.css + +.. code-block:: html+django + + {% bundler_url "script.js" as script_url %} + + {# script_url is now available as a template variable #} + +.. templatetag:: bundler_preamble + +``bundler_preamble`` +-------------------- + +Adds necessary code for things like enabling HMR. This tag accepts no arguments. + +Typically this is only required in development but that is up to the Bundler to decide - the tag should +be included for both production and development. + +Usage: + +.. code-block:: html+django + + {% load bundler %} + + {# In the element #} + {% bundler_preamble %} + +.. templatetag:: bundler_dev_checks + +``bundler_dev_checks`` +---------------------- + +Performs dev specific checks and may render some HTML to communicate messages to user + +Currently check if the dev server is running for this project, and if not displays an error. + +Error will be logged to Django dev console. In addition, an error icon and toggleable modal message will be shown +in the HTML unless :data:`~alliance_platform.frontend.settings.AlliancePlatformFrontendSettingsType.BUNDLER_DISABLE_DEV_CHECK_HTML` is set. + +This only applies in development, in production this tag is a no-op. + +This tag accepts no arguments. + +Usage: + +.. code-block:: html+django + + {% load bundler %} + + {# At the end of the element #} + + ... + {% bundler_dev_checks %} + + +.. templatetag:: bundler_embed_collected_assets + +``bundler_embed_collected_assets`` +---------------------------------- + +Add tags to header for assets required in page. This tag accepts no arguments. + +This makes using assets in templates easier, without needing to worry about adding it to the correct template area +or having duplicate tags from including the same asset more than once. You can embed assets as you need to use them, +at any level of the template hierarchy, and they will be added to the header in one place with no duplication. + +This works with :class:`~alliance_platform.frontend.bundler.context.BundlerAssetContext` to collect all the assets used +within a template. See :class:`~alliance_platform.frontend.bundler.middleware.BundlerAssetContextMiddleware` for how +this context is created for you in Django views. + +Because each asset must specify asset paths statically, this tag can retrieve assets from ``BundlerAssetContext`` +and embed the required tags before the rest of the template is rendered. + +Some existing assets are those created by the :func:`~alliance_platform.frontend.templatetags.vanilla_extract.stylesheet`, +:func:`~alliance_platform.frontend.templatetags.react.component`, or :func:`~alliance_platform.frontend.templatetags.bundler.bundler_embed` +tags. See the individual implementations for options that may influence how they are embedded (e.g. the ``inline`` +option provided by ``bundler_embed``). + +:data:`~alliance_platform.frontend.bundler.context.BundlerAssetContext.html_target` will control whether scripts are included +and whether CSS is outputted in line in ``style`` tags or linked externally. + +Generally, this tag should be used in the ```` of the HTML document. All script tags are non-blocking by default. + +Usage: + +.. code-block:: html+django + + {% load bundler %} + + {# In the element #} + + {% bundler_embed_collected_assets %} + + + + {# The actual output for this tag will be handled by bundler_embed_collected_assets, so will appear in head #} + {% bundler_embed "style.css" %} + + +React Tags +---------- + +.. templatetag:: component + +``component`` +------------- + +Render a React component with the specified props + +Usage: + +.. code-block:: html+django + + {% load react %} + + {# using common components, e.g. div, h1, etc. #} + {% component [dom element name] [prop_name=prop_value...] %} [children] {% endcomponent %} + + {# using a named export #} + {% component [module path] [component import name] [prop_name=prop_value...] %} [children] {% endcomponent %} + + {# component path should have a default export #} + {% component [component path] [component name] [prop_name=prop_value...] %} [children] {% endcomponent %} + +There are three ways to specify which component to render. The first is for a `"common component" `_ +which is to say a built-in browser component (e.g. ``div``): + +.. code-block:: html+django + + {% component "h2" %}Heading{% endcomponent %} + +The other two are for using a component defined in an external file. These will be loaded via +the specified bundler class (currently :class:`~alliance_platform.frontend.bundler.vite.ViteBundler`). With +a single argument it specifies that the default export from the file is the component to use: + +.. code-block:: html+django + + {% component "components/Button" %}Click Me{% endcomponent %} + +With two arguments the first is the file path and the second is the named export from that file: + +.. code-block:: html+django + + {% component "components/Table" "Column" %}Name{% endcomponent %} + +The last option has a variation for using a property of the export. This is useful for components +where related components are added as properties, e.g. ``Table`` and ``Table.Column``: + +.. code-block:: html+django + + {% component "components" "Table.Column" %}Name{% endcomponent %} + +Note that this is only available when using named exports; default exports don't support it due to +ambiguity around whether the ``.`` indicates file extension or property access. + +You can omit the file extension - the above could resolve to ``components/Table.tsx`` (``.js`` and ``.ts`` are also +supported). See :ref:`resolving_paths` for details on how the file path is resolved. + +Props are specified as keyword arguments to the tag: + +.. code-block:: html+django + + {% component "components/Button" variant="primary" %}Click Me{% endcomponent %} + +Additionally, a dict of props can be passed under the ``props`` kwarg: + +.. code-block:: html+django + + {% component "components/Button" variant="primary" props=button_props %}Click Me{% endcomponent %} + +Children can be passed between the opening ``{% component %}`` and closing ``{% endcomponent %}``. Whitespace +is handled the same as in JSX: + +* Whitespace at the beginning and ending of lines is removed +* Blank lines are removed +* New lines adjacent to other components are removed +* New lines in the middle of a string literal is replaced with a single space + +So the following are all equivalent: + +.. code-block:: html+django + + {% component "div" %}Hello World{% endcomponent %} + + {% component %} + Hello world + {% endcomponent %} + + + {% component %} + Hello + world + {% endcomponent %} + + + {% component %} + + Hello world + {% endcomponent %} + +Components can be nested: + +.. code-block:: html+django + + {% component "components/Button" type="primary" %} + {% components "icons" "Menu" %}{% endcomponent %} + Open Menu + {% end_component %} + +and you can include HTML tags as children: + +.. code-block:: html+django + + {% component "components/Button" type="primary" %} + Delete Item + {% end_component %} + +You can use ``as `` to store in a variable in context that can then be passed to another tag: + +.. code-block:: html+django + + {% component "icons" "Menu" as icon %}{% end_component %} + {% component "components/Button" type="primary" icon=icon %}Open Menu{% end_component %} + +All props must be JSON serializable. :class:`~alliance_platform.frontend.prop_handlers.ComponentProp` can be used to define +how to serialize data, with a matching implementation in ``propTransformers.tsx`` to de-serialize it. + +For example :class:`~alliance_platform.frontend.prop_handlers.DateProp` handles serializing a python ``datetime`` and +un-serializing it as a native JS ``Date`` on the frontend. See :class:`~alliance_platform.frontend.prop_handlers.ComponentProp` +for documentation about adding your own complex props. + +Components are rendered using the ``renderComponent`` function in :data:`~alliance_platform.frontend.settings.AlliancePlatformFrontendSettingsType.REACT_RENDER_COMPONENT_FILE`. This can be modified as needed, +for example if a new provider is required. + +.. note:: All props passed through are converted to camel case automatically (i.e. ``my_prop`` will become ``myProp``) + +Server Side Rendering (SSR) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Components will automatically be rendered on the server. See :ref:`ssr` for details about how this works. + +To opt out of SSR pass ``ssr:disabled=True`` to the component after the component name: + +.. code-block:: html+django + + {% component 'components/Button.tsx' ssr:disabled=True %}...{% endcomponent %} + +Alternatively, you can disable SSR entirely by passing ``disable_ssr=True`` to :class:`~alliance_platform.frontend.bundler.vite.ViteBundler`. + +Options +~~~~~~~ + +Various options can be passed to the component tag. To differentiate from actual props to the component they are +prefixed with `ssr:` for server side rendering options, `component:` for general component options, or `container:` +for options relating to the container the component is rendered into. + +- ``ssr:disabled=True`` - if specified, no server side rendering will occur for this component +- ``component:omit_if_empty=True`` - if specified, the component will not be rendered if it has no children. This is + useful for when components may not be rendered based on permission checks +- ``container:tag`` - the HTML tag to use for the container. Defaults to the custom element ``dj-component``. +- ``container:`` - any other props will be passed to the container element. For example, to add + an id to the container you can use ``container:id="my-id"``. Note that while you can pass a style string, it's + likely to be of little use with the default container style ``display: contents``. Most of the time you can just + do the styling on the component itself. + +For example: + +.. code-block:: html+django + + {% component 'core-ui' 'Button' ssr:disabled=True variant="Outlined"%} + ... + {% endcomponent %} + +Limitations +~~~~~~~~~~~ + +Currently, attempting to render a django form widget that is itself a React component within another component will +not work. This is due to how django widgets have their own templates that are rendered in an isolated context. For +example, this will not work if ``form.field`` also uses the ``{% component %}`` tag: + +.. code-block:: html+django + + {% component 'MyComponent' %} + {{ form.field }} + {% endcomponent %} + + +.. templatetag:: react_refresh_preamble + +``react_refresh_preamble`` +-------------------------- + +Add `react-refresh `_ support + +Currently only works with :class:`~alliance_platform.frontend.bundler.vite.ViteBundler`. This must appear after +:meth:`~alliance_platform.frontend.templatetags.bundler.bundler_preamble`. + +This is a development only feature; in production the tag is a no-op. + +See https://vitejs.dev/guide/backend-integration.html + +Usage: + +.. code-block:: html+django + + {% bundler_preamble %} + {% react_refresh_preamble %} + +Vanilla Extract Stylesheet +-------------------------- + +.. templatetag:: stylesheet + +``stylesheet`` +-------------- + +Add a vanilla extract CSS file the page, optionally exposing class name mapping in a template variable. + +Usage: + +.. code-block:: html+django + + {% load vanilla_extract %} + + {% stylesheet [path] [as varname] %} + +The tag accepts a single argument, the path to the vanilla extract CSS file. This path must be a :ref:`static value `. + +If the CSS file includes exported class names, you can access the mapping by specifying a variable with the syntax +``as ``. + +If you do not specify a variable using the ``as `` syntax, the styles will only be available globally, +and any specified variables will be ignored. + +For more information on how paths are resolved, refer to the documentation on :ref:`resolving_paths`. + +The CSS file is not embedded inline where the tag is used, rather it is added by the :ttag:`bundler_embed_collected_assets` +tag. + +Example: + +.. code-block:: html+django + + {% load vanilla_extract %} + + + {% bundler_embed_collected_assets %} + + + {% stylesheet "./myView.css.ts" as styles %} + +
+

My View

+ ... +
+ +.. note:: If you need to include a plain CSS file use the :ttag:`bundler_embed` tag instead. + +.. admonition:: Vite plugin required + + This functionality relies on the plugin defined by in ``frontend/vite/plugins/vanillaExtractWithExtras.ts`` + in the template proejct. + +Forms +----- +.. templatetag:: form + +``form`` +-------- + +Tag to setup a form context for form_input tags + +This tag doesn't render anything itself, it just sets up context for ``form_input`` tags. This is to support +the ``auto_focus`` behaviour. This works by adding an ``auto_focus`` prop to the first field with errors, or the +first rendered field if no errors are present. + +Usage: + +.. code-block:: html+django + + {% load form %} + + {% form form auto_focus=True %} +
` + +This is useful for props that should be passed as NaN to the component. The `NumberInput `_ component uses ``NaN`` +instead of ``null`` for no value. + +Usage: + +.. code-block:: html+django + + {% component "@alliancesoftware/ui" "NumberInput" default_value=widget.value|none_as_nan %}{% endcomponent %}