brandaddition / module-svelte
BA Svelte Module
Package info
github.com/Brand-Addition/ba-module-svelte
Language:JavaScript
Type:magento2-module
pkg:composer/brandaddition/module-svelte
Requires
- php: ~8.2||~8.3||~8.4
- magento/module-backend: *
- thecodingmachine/safe: ^3.4
Requires (Dev)
- bitexpert/phpstan-magento: ^0.42.0
- phpro/grumphp: ^2.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ~2.0
- phpunit/php-code-coverage: ^9.2
- phpunit/phpunit: ^9
This package is auto-updated.
Last update: 2026-07-07 15:34:44 UTC
README
BA_Svelte is the shared Magento 2 Svelte platform for BA storefront modules.
In practice a BA storefront module built on BA_Svelte usually looks like this:
- Layout XML declares a root
SvelteBlock - That root points at a
.sveltecomponent in your module - XML arguments, a
view_model, andcomputed_propsbecome component props - Optional view xml child blocks/containers become named Svelte slots
- The shared runtime mounts the root component on the page
- Your Svelte code imports shared BA platform helpers through
@modules
That keeps the authoring model simple:
- normal Magento layout and blocks
- normal Svelte components
- no extra registry file
- no second naming system
- Magento-native
before,after,move, andremovestill work
The Main Pieces
Simply, can be condensed to:
BA\Svelte\Block\SvelteBlockAny svelte block on frontend. Can be root or child components.view/frontend/templates/root.phtmlEmits the.svelte-rootwrapper, serialized config, and optional server fallback markup.view/frontend/web/svelte-srcShared Vite source used to build the runtime bundle against the deployed static-content tree.
To build your svelte interface, and run bin/magento setup:static-content:deploy -f --jobs=20 --area=frontend to show on frontend. No need to rm -rf generated or preprocessed folders.
All pieces:
BA\Svelte\Block\SvelteBlockAny svelte block on frontend. Can be root or child components.BA\Svelte\Block\SvelteLinkRoot-style Svelte block for sorted Magento link collections such astop.links.view/frontend/templates/root.phtmlEmits the.svelte-rootwrapper, serialized config, and optional server fallback markup.view/frontend/web/svelte-srcShared Vite source used to build the runtime bundle against the deployed static-content tree.view/frontend/web/js/lib/i18n.jsPublic translation helper.view/frontend/web/js/lib/messages.jsPublic message and Magento fragment update facade.view/frontend/web/js/lib/magento.jsBETA Public Magento URL and JSON request helper facade.view/frontend/web/js/lib/state.jsBETA Public customer-section and store hydration facade.view/frontend/web/js/lib/forms.jsALPHA Public validation and AJAX form facade.view/frontend/web/js/lib/commerce.jsTODO Public add-to-cart facade.BuildSveltePluginHooks intosetup:static-content:deployand builds the merged storefront Svelte bundle in each deployed static-content root that containsBA_Svelte.
Start A New Root Mount
The normal starting point is one root block in layout XML.
<block class="BA\Svelte\Block\SvelteBlock" name="example.root" <!-- Root must use a phtml template. Only child blocks can use svelte templates here, and leave the `svelte_component` argument --> template="BA_Svelte::root.phtml"> <arguments> <argument name="svelte_component" xsi:type="string">Vendor_Module::example-root.svelte</argument> <argument name="heading" xsi:type="string">Example</argument> <argument name="computed_props" xsi:type="array"> <item name="endpointUrl" xsi:type="array"> <item name="resolver" xsi:type="string">url</item> <item name="path" xsi:type="string">rest/V1/example</item> </item> </argument> </arguments> <container name="example.root.container.content" as="content"/> <container name="example.root.container.actions" as="actions"/> </block>
svelte_component uses Magento template notation:
Vendor_Module::example-root.svelte
That resolves to:
view/frontend/web/svelte/example-root.svelte
The matching root component can then read normal props plus the container helpers:
<script> import ContainerRenderer from '@modules/BA_Svelte/svelte/container-renderer.svelte'; let { heading = 'Example', get_container: getContainer, } = $props(); </script> <section> <h2>{heading}</h2> <ContainerRenderer items={getContainer?.('content') ?? []} /> </section>
Add Magento-Native Extension Points
Obviously, as its just view xml, a second module can extend the xml we added above:
<referenceContainer name="example.root.container.actions"> <block class="BA\Svelte\Block\SvelteBlock" name="example.root.primary_action"> <arguments> <argument name="svelte_component" xsi:type="string">Vendor_Module::actions/primary-button.svelte</argument> <argument name="label" xsi:type="string">Continue</argument> </arguments> </block> </referenceContainer>
Given the parent module uses $state in a seperate file imported using @modules, you can bring that in, and change the state from your added component.
Every mounted component receives:
- normal props from XML arguments and
computed_props containersget_container(name)has_container(name)default_container
Render nested configs through:
@modules/BA_Svelte/svelte/container-renderer.svelte@modules/BA_Svelte/svelte/block-renderer.svelte@modules/BA_Svelte/svelte/renderer.svelte
If you need to render one child block directly and optionally override props inline:
<script> import BlockRenderer from '@modules/BA_Svelte/svelte/block-renderer.svelte'; let { default_container: defaultContainer, } = $props(); </script> <BlockRenderer block={defaultContainer} value={row.value} />
You can also target a child by layout block name:
<BlockRenderer block="price_renderer" blocks={defaultContainer} value={row.value} />
Add A Static HTML Fallback
Root mounts can optionally render server HTML first and then let the runtime progressively enhance it.
To do that, add a normal Magento child block with alias fallback under the root SvelteBlock.
Example:
<block class="BA\Svelte\Block\SvelteBlock" name="example.root" template="BA_Svelte::root.phtml"> <arguments> <argument name="svelte_component" xsi:type="string">Vendor_Module::example-root.svelte</argument> </arguments> <block class="Magento\Framework\View\Element\Template" name="example.root.fallback" as="fallback" template="Vendor_Module::example/fallback.phtml"/> </block>
Behavior:
- the fallback markup stays visible initially
- BA_Svelte mounts the Svelte app into a hidden host inside the same root wrapper
- after the first successful client render, the runtime removes the fallback and reveals the mounted app
- if config parsing or mount fails, the fallback stays in place
This is root-mount behavior only. It is not a Svelte hydration contract and it does not preserve DOM identity between server HTML and the client component.
Quick render using the viewmodel
Sometimes you may need to render a svelte block without using the svelte block class. If you bring in BA\Svelte\ViewModel\SvelteBlockRenderer as a viewmodel, you can call render(). This wont include child components, but will in future.
Use A Svelte Mount In Sorted Link Collections
If the component needs to participate in link collections that expect SortLinkInterface, use BA\Svelte\Block\SvelteLink.
<referenceBlock name="top.links"> <block class="BA\Svelte\Block\SvelteLink" name="example.account.link" after="my-account-link"> <arguments> <argument name="sortOrder" xsi:type="number">60</argument> <argument name="svelte_component" xsi:type="string">Vendor_Module::account-link.svelte</argument> <argument name="view_model" xsi:type="object">Vendor\Module\ViewModel\AccountLink</argument> </arguments> <action method="setTemplate"> <argument name="template" xsi:type="string">Vendor_Module::link.phtml</argument> </action> </block> </referenceBlock>
Use this for:
top.links- customer account link groups
- theme-specific link builders that sort children through Magento link APIs
Pass Data Into Components
There are three normal ways to get data into a component:
1. Plain XML Arguments
Any non-reserved XML argument on a SvelteBlock is normalized into component props.
<argument name="heading" xsi:type="string">Example</argument> <argument name="show_summary" xsi:type="boolean">true</argument>
2. view_model
You can attach a Magento view_model and let BA_Svelte reflect its getter methods into props.
<argument name="view_model" xsi:type="object">Vendor\Module\ViewModel\Example</argument>
Example view model:
<?php declare(strict_types=1); namespace Vendor\Module\ViewModel; use Magento\Framework\View\Element\Block\ArgumentInterface; class Example implements ArgumentInterface { /** * @return string **/ public function getMessage(): string { return 'Hello from Magento'; } }
That becomes:
message
Reflection rules:
getCountryOptions()becomescountry_optionsisGuestCheckoutAllowed()becomesguest_checkout_allowedhasShippingPolicy()becomesshipping_policy
Only public zero-argument getter methods are included. Reflected view_model props are merged before computed_props, and explicit XML props still win if keys collide.
3. computed_props
Use resolver-backed props for values that should be calculated at render time.
Built-in resolvers:
urlassettranslate
You can also list all registered resolvers using the ba:svelte:resolver:list command.
Example:
<argument name="computed_props" xsi:type="array"> <item name="endpointUrl" xsi:type="array"> <item name="resolver" xsi:type="string">url</item> <item name="path" xsi:type="string">rest/V1/example</item> </item> </argument>
Add a new computed_props using di pool
If the built-in resolvers are not enough, add your own resolver class in your module and register it into BA\Svelte\Model\PropResolverPool through DI.
Example resolver:
<?php declare(strict_types=1); namespace Vendor\Module\Model\PropResolver; use BA\Svelte\Api\PropResolverInterface; use Magento\Framework\View\Element\Template; class ExamplePropResolver implements PropResolverInterface { public function resolve(string $propName, array $definition, Template $block): mixed { $value = $definition['value'] ?? null; return is_string($value) ? strtoupper($value) : null; } }
Register it in your module etc/di.xml:
<type name="BA\Svelte\Model\PropResolverPool"> <arguments> <argument name="resolvers" xsi:type="array"> <item name="example" xsi:type="object">Vendor\Module\Model\PropResolver\ExamplePropResolver</item> </argument> </arguments> </type>
Then use it in layout XML:
<argument name="computed_props" xsi:type="array"> <item name="exampleValue" xsi:type="array"> <item name="resolver" xsi:type="string">example</item> <item name="value" xsi:type="string">hello world</item> </item> </argument>
Rules for custom resolvers:
- implement
BA\Svelte\Api\PropResolverInterface - return data that can be safely serialized into component props
- read your resolver-specific config from the
$definitionarray - use the provided Magento
$blockwhen you need store-aware URLs, assets, or request/render context
Inspect The Final Prop Contract
If you want to see what a mounted block will actually receive, use the CLI helper:
bin/magento ba:svelte:props checkout_index_index checkout.root
The command loads default plus the handle you pass, resolves the named BA_Svelte block, and prints a typed Svelte 5 $props() snippet.
Use --store=<code> when the layout or computed props depend on store scope.
Write Svelte Code Against @modules
Svelte modules in this setup import shared code through the @modules Vite alias.
Example:
import ContainerRenderer from '@modules/BA_Svelte/svelte/container-renderer.svelte'; import { _ } from '@modules/BA_Svelte/js/lib/i18n.js'; import { buildRestUrl } from '@modules/BA_Svelte/js/lib/magento.js';
@modules points at the deployed static-content root for the current storefront, not at app/code directly.
So:
import ContainerRenderer from '@modules/BA_Svelte/svelte/container-renderer.svelte';
resolves against:
pub/static/<area>/<Vendor>/<Theme>/<Locale>/BA_Svelte/svelte/container-renderer.svelte
Why this matters:
- imports stay short
- one BA module can reuse public code from another BA module
- the build runs against the same merged static asset tree Magento serves
Rules:
- files under
svelte/and documentedjs/entrypoints are fair to reuse - files under
svelte-src/are build/runtime internals unless documented otherwise
The Public JS APIs
Most feature modules should build on the public facades below and avoid js/lib/runtime/*, http.js, and url.js directly.
Translation
import { _ } from '@modules/BA_Svelte/js/lib/i18n.js';
This uses window.baTranslate() and falls back to the original string when no Svelte translation entry exists.
BA_Svelte scans Svelte-backed .svelte, .ts, and .js source files on the Magento side and exposes a translated hashmap to the storefront runtime.
Use _('Literal phrase') for any Svelte-authored text that needs Magento translation. Dynamic expressions, concatenated strings, and template literals with expressions are not extracted into the BA_Svelte translation map, so they fall back to the original source text.
Example:
<script> import { _ } from '@modules/BA_Svelte/js/lib/i18n.js'; let { buttonLabel = 'Open size guide' } = $props(); </script> <button type="button"> {_(buttonLabel)} </button>
Price Rendering
If a module needs standard price output, use the shared price component:
import Price from '@modules/BA_Svelte/svelte/catalog/price.svelte';
This component handles:
- normal final price rendering
- special price plus old price output
- locale-aware currency formatting
- optional minimal price /
As low asoutput - optional minimal-price links back to the product URL
Props:
final_price, REQUIRED
regular_price, default = 0,
has_special_price, default = false,
currency_code, default = window.__baCurrentCurrency,
currency_symbol, default = '',
locale, default = window.__baCurrentLocale,
precision, default = 2,
show_minimal_price, default = false,
use_link_for_as_low_as, default = false,
minimal_price, default = null,
minimal_price_label, default = _('As low as'),
product_url, default = '',
special_price_label, default = _('Special Price'),
old_price_label, default = _('Was'),
Example:
<script> import Price from '@modules/BA_Svelte/svelte/catalog/price.svelte'; let price = { final_price: 79.99, regular_price: 99.99, has_special_price: true, currency_code: 'GBP', locale: 'en-GB', special_price_label: _('Your price'), // make sure you pass translated labels old_price_label: _('Instead of'), }; </script> <Price {...price} />
The component is for rendering only. Decide what the price data should be in Magento or in your owning feature module, then pass the normalized values in as props.
Messages
import { dispatchStorefrontMessage, STORE_MESSAGE_TYPES } from '@modules/BA_Svelte/js/lib/messages.js';
Use messages.js for:
- client-side success, error, and info messages
- Magento message fragments returned from AJAX responses
It emits an event using window.dispatchEvent(), theres a mixin to get this working with Luma. If you are not using a standard magento theme, you may need to add a listener like window.addEventListener('svelte:message', (event) => {...}. As an example see view/frontend/web/js/magento-mixin/messages.js
Example usage:
dispatchStorefrontMessage(STORE_MESSAGE_TYPES.error, _('Please enter a valid email')); dispatchStorefrontMessage(STORE_MESSAGE_TYPES.success, _('Quote shared'));
Magento Utilities
import { buildRestUrl, buildStorefrontUrl, requestMagentoJson, } from '@modules/BA_Svelte/js/lib/magento.js';
Use magento.js for:
- storefront URL construction
- store-scoped REST URLs
- generic JSON transport to Magento
Rules:
- prefer server-resolved URLs via props when the route is already known during render
- use
buildRestUrl()instead of feature-localrest/${storeCode}/...glue. You should only need to include the route configured inwebapi.xmlin your parameter. - use
requestMagentoJson()for transport concerns only, not feature-specific business rules
Todo:
- GraphQL helpers
State
Status: beta. Expect change.
import { customerSections } from '$lib/state';
Use state.js for:
- customer sections
Get a Section
const customer = customerSections.get('customer');
Get Multiple Sections
const sections = customerSections.getMany([ 'customer', 'cart', ]);
Load Sections
await customerSections.load([ 'customer', 'cart', ]);
Force Reload
await customerSections.reload( 'customer', 'cart', );
Update a Section
customerSections.set('customer', { firstname: 'Sam', });
Subscribe to Changes
const unsubscribe = customerSections.subscribe( 'cart', (_, cart) => { console.log(cart); } );
Cleanup:
unsubscribe();
Svelte 5 Example
let customer = $state( customerSections.get('customer') ); $effect(() => { return customerSections.subscribe( 'customer', (_, data) => { customer = data; } ); });
Recommended Usage
Use the shared singleton:
import { customerSections } from '$lib/state';
Only instantiate CustomerSections directly when testing or creating isolated instances.
Forms
Status: very alpha. Expect change.
import { applyValidationRules, createAjaxFormController, redirectTo, resolveFormElement, validateForm, } from '@modules/BA_Svelte/js/lib/forms.js';
Use forms.js for:
- validation
- AJAX submit lifecycle
- loading and disabled states
- redirect behavior after submit
Rules:
validateForm()is the replacement for manual Magento validation bootstraps in Svelte-driven flowscreateAjaxFormController()is the main AJAX form API- the controller owns
aria-busy, deduplicated submits, and button state - redirects should go through
redirectTo()
Commerce
Status: Cart behavior, Needs implementing.
Use Declarative Runtime Elements Where They Fit
The runtime bootstrap itself is internal, but these markup contracts are public:
<ba-collapsible><ba-accordion><ba-modal><ba-add-to-cart><ba-quantity-switch>
Example modal:
<ba-modal trigger=".open-size-guide"> <h2>Size guide</h2> <button type="button" data-ba-modal-close>Close</button> </ba-modal>
Example add-to-cart wrapper:
<ba-add-to-cart> <form data-role="tocart-form" action="/checkout/cart/add" method="post"> ... <button type="submit" class="action tocart primary"> <span>Add to Cart</span> </button> </form> </ba-add-to-cart>
<ba-quantity-switch> is also a public contract. Its controller lives behind the runtime, so feature modules should use the element instead of importing quantity-switch internals.
Replace Magento JS With BA_Svelte Contracts
When you are modernizing a module, these are the preferred replacements:
| Replace this | Use this BA_Svelte surface | Do not use |
|---|---|---|
customer-data wrappers |
state.js |
feature-local customer section adapters |
Magento_Ui/js/modal/modal |
Popup.svelte or <ba-modal> |
runtime modal internals |
Magento_Catalog/js/validate-product |
forms.js plus commerce.js |
feature-local validation bootstraps |
catalogAddToCart |
commerce.js or <ba-add-to-cart> |
feature-local add-to-cart wrappers - NEEDS IMPLEMENTING |
| repeated REST URL glue | magento.js |
feature-local URL helpers |
| feature-local JSON fetch wrappers | magento.js |
http.js from feature code |
| feature-local flash/server message plumbing | messages.js |
duplicate message fragment implementations |
Internal paths that should not appear in new feature code:
@modules/BA_Svelte/js/lib/runtime.js@modules/BA_Svelte/js/lib/runtime/*@modules/BA_Svelte/js/lib/http.js@modules/BA_Svelte/js/lib/url.js
Popup Pattern
The default popup direction is native.
Use:
@modules/BA_Svelte/svelte/popup.svelte<ba-modal>
Example:
<script> import Popup from '@modules/BA_Svelte/svelte/popup.svelte'; let popup = null; </script> <button type="button" onclick={() => popup?.open()}> Open size guide </button> <Popup bind:this={popup} title="Size guide"> <p>Use your usual fit. This product runs true to size.</p> </Popup>
Exit criteria for a migrated popup:
- it does not rely on Magento modal widget lifecycle
- it can render with native dialog semantics
- it can move to
Popup.svelteor<ba-modal>without behavior loss
Build And Deploy
Normal Magento Flow
- Run
bin/magento setup:static-content:deploy BA_Sveltedetects deployed storefront roots containing BA_Svelte assets- For each deployed root, it runs
npm run buildinsideview/frontend/web/svelte-src
Manual Build
cd src/app/code/BA/Svelte/view/frontend/web/svelte-src
npm run build
If you need to point at one deployed storefront root explicitly:
cd src/app/code/BA/Svelte/view/frontend/web/svelte-src SCD_ROOT=/absolute/path/to/pub/static/<area>/<Vendor>/<Theme>/<Locale> npm run build
Notes
assets.phtmldeduplicates the shared CSS and JS bundle if multiple Svelte modules include it on the same page.SvelteBlockis for root mounts that renderBA_Svelte::root.phtmland nested pluggable components inside root containers.SvelteLinkis for sorted link collections.- Use
.svelte-rootblocks or the documented custom elements for new work. Do not build feature modules against runtime internals.