SPA shell ========= Embed a client framework (Svelte in this example) inside djcrud's navigation shell while keeping sidebar navigation server-rendered in HTML. Example app: ``spa_example`` (``/spa/``). Complete :doc:`drf` first if you want the demo to call ``/api/product/`` from the browser. The SPA chapter itself only needs ``djcrud.site`` — no ViewSet registration required. SPA template convention ----------------------- :py:class:`~djcrud.views.spa.SPAView` uses ``djcrud/base_spa.html`` and sets ``unpoly_target = 'body'`` for you. Custom SPA shells should follow the same ``*base_spa.html`` naming convention. Menu links on SPA pages always use ``up-follow="false"`` so every hop reloads the full document and exits the SPA shell cleanly. View and registration --------------------- Subclass :py:class:`~djcrud.views.spa.SPAView` and append it to :data:`djcrud.site` — no model router required. Extend ``class Media`` to append your ES module paths as :class:`~django.forms.widgets.Script` entries with ``type="module"``. :attr:`~djcrud.route.Route.urlpath` defaults to the view codename (``spa/`` for :class:`SpaView`): .. literalinclude:: ../../src/djcrud_example/spa_example/djcrud.py Mounting your app ----------------- There is no ``index.html`` in the frontend package. Django renders :file:`djcrud/base_spa.html` (sidebar, CSRF, ``{{ view.media }}``); your bootstrap HTML and client bundle fill in the rest. Put the mount node on your view — same pattern as ``icon`` or ``tags`` (see :doc:`../philosophy`): .. code-block:: python mount_element = '
' :file:`djcrud/base_spa.html` renders it inside ``[up-main]`` via ``{{ view.mount_element|safe }}``. Your ES module must query the same element and attach your framework: .. literalinclude:: ../../src/djcrud_example/spa_example/frontend/src/main.js After ``npm run build``, Vite writes a content-hashed bundle (e.g. ``app.9f3c2a1b.js``) together with ``.vite/manifest.json`` under ``spa_example/static/spa_example/js/``. The ``djcrud.py`` entry uses :func:`~djcrud.static.vite_asset` so the logical name stays stable while the actual served file name contains the hash for cache busting:: from djcrud.static import vite_asset ... Script(vite_asset("spa_example/js/app.js"), type="module") ``vite_asset`` falls back to the original path when no manifest is present. Frontend build configuration ---------------------------- Configure Vite to emit content-hashed filenames (for cache busting) and the manifest consumed by :func:`~djcrud.static.vite_asset`: .. literalinclude:: ../../src/djcrud_example/spa_example/frontend/vite.config.js :language: js Key settings: - ``manifest: true`` Emits ``.vite/manifest.json`` (next to the built files) that maps your original entry point to the final hashed filename(s). - ``entryFileNames`` / ``assetFileNames`` containing ``[hash]`` Ensures the output files include a content hash (e.g. ``app.BDdKb6Vl.js``). This is the main mechanism that prevents stale browser caches after redeploys. - ``outDir`` Points the build output directly into a Django static directory (relative to the frontend package). The example uses ``../static/spa_example/js`` so the files are served automatically by ``django.contrib.staticfiles``. - ``emptyOutDir: true`` Cleans the output directory on each build so old hashed files do not accumulate. The built files (including the manifest) are committed in the tutorial repository so ``/spa/`` works without running the frontend build. Re-run ``npm run build`` only when you change the client-side code. Authentication -------------- The HTML shell uses the same rules as every djcrud view: * **Anonymous users** are redirected to login before the SPA page renders (:py:meth:`~djcrud.view.ViewMixin.dispatch`). * **Logged-in users** must pass :py:meth:`~djcrud.views.spa.SPAView.has_permission` (superuser by default, or a grant from :func:`~djcrud.permissions.add_perm`). * **CSRF** meta tags are in ``base_spa.html``; Unpoly picks them up via ``unpoly-config.js``. The Svelte demo calls ``/api/product/`` with ``credentials: 'same-origin'``, so the browser sends the Django session cookie and DRF accepts the request when the user is already logged in (requires :doc:`drf`). For token-based clients, use :doc:`../reference/djcrud_api/index` (``POST /api/login/`` and Bearer auth) — see the codegen section below. SPA shell template ------------------ :file:`djcrud/base_spa.html` renders a collapsible sidebar (``is-hidden`` by default), introspected navigation, CSRF meta tags, the mount node (see `Mounting your app`_), and ``{{ view.media.css }}`` / ``{{ view.media.js }}`` from the view's :class:`~django.forms.Media` — no inline JavaScript. Svelte app ---------- Import ``hamburger.js`` and render the burger inside a Bulma ``.navbar`` so span colors match the standard shell. The component toggles the server-rendered ``#sidebar``: .. literalinclude:: ../../src/djcrud_example/spa_example/frontend/src/App.svelte Rebuild the committed bundle after editing Svelte sources: .. code-block:: bash cd src/djcrud_example/spa_example/frontend npm ci npm run build Cross-shell navigation ---------------------- Standard djcrud pages swap content inside ``[up-main]``. SPA pages use a different document shell (no top navbar). When the active view uses a ``base_spa.html`` template, :meth:`~djcrud.view.ViewMixin.unpoly_link_attributes` returns plain links for every menu item so the browser always full-reloads. Try it ------ Log in and visit `http://localhost:8000/spa/