# Salla Component Hooks

The Salla Component Hooks System is a JavaScript API that lets developers work with Salla’s web components at runtime, like changing the UI, intercepting validation, and tracking lifecycle events. It does all this without touching core theme code or worrying about when scripts load.

<CardGroup>
  <Card title="Timing Independent" icon="material-two-tone-alarm">
  Register hooks before or after components load. The system handles both cases automatically.
  </Card>
  <Card title="Whitelist Secured" icon="material-two-tone-security">
  Only approved hook names are accepted. Unauthorized registrations are blocked with a console warning.
  </Card>
  <Card title="Error Isolated" icon="material-two-tone-report">
  Errors in your callback never crash the core component. Partner code fails safely.
  </Card>
  <Card title="No Duplicates" icon="material-two-tone-filter">
  Components are tracked in a Set, the same element never triggers your hook twice.
  </Card>
</CardGroup>

---

## Template Hooks vs. Component Hooks | What's the Difference?

Salla provides **two** distinct hook systems. Knowing which one you need saves a lot of confusion:

| | Template Hooks | Component Hooks |
|---|---|---|
| **Syntax** | `` {% hook 'head:start' %} `` (Twig) | `Salla.hooks.registerHook(...)` (JavaScript) |
| **Side** | Server-side | Client-side (runtime) |
| **Purpose** | Inject HTML into page template slots | Interact with rendered web components |
| **Used in** | `.twig` theme files | in scripts / JS integrations |

:::info[]
If you are editing `.twig` template files, you want the [**Template Hooks** article](https://docs.salla.dev/422552m0). If you are writing a partner script or JavaScript integration, you are in the right place.
:::

## 📙 What You'll Learn

- [How It Works](#how-it-works)
- [Registering a Hook](#registering-a-hook)
- [Available Components & Hooks](#available-components--hooks)
- [Properties Reference](#properties-reference)
- [Late Registration Support](#late-registration-support)
- [Hook Options: Priority & Once](#hook-options-priority--once)
- [Removing a Hook](#removing-a-hook)
- [Real-World Use Cases](#real-world-use-cases)
- [Security: The Whitelist System](#security-the-whitelist-system)

## How It Works

The hooks system lives at `Salla.hooks` and is built around three internal structures:


<CardGroup cols={3}>
  <Card title="Store" icon="material-two-tone-storage">
a <code>Map</code> of registered callbacks, keyed by
    component tag and hook name.
  </Card>

  <Card title="Whitelist" icon="material-two-tone-verified">
a <code>Map</code> that defines exactly which
    hook names are allowed per component tag.
  </Card>

  <Card title="Component History" icon="material-two-tone-history">
a <code>Map</code> that records every
    component element that has loaded on the page, enabling late registration.
  </Card>
</CardGroup>


:::tip[]
When a Salla component mounts, it calls `Salla.hooks.registerComponent(tag, element)`, which triggers any existing `componentDidLoad` callbacks and stores the element in history. This allows future hook registrations to access it retroactively.

:::


:::note[]
Always wrap your hook registrations inside `Salla.onReady()` to guarantee the Salla SDK is fully initialized before your code runs.
:::

---

## Registering a Hook

```javascript
// EXAMPLE
Salla.hooks.registerHook(tag, hookName, callback, options);
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `tag` | `string` | ✅ | The component tag name (e.g. `'salla-add-product-button'`) |
| `hookName` | `string` | ✅ | The hook to subscribe to (e.g. `'componentDidLoad'`, `'validate'`) |
| `callback` | `Function` | ✅ | Runs when the hook fires. Receives a context argument depending on the hook. |
| `options` | `object` | ❌ | `{ priority: number, once: boolean }` — both optional |

Returns an `unsubscribe` function. Call it to cleanly remove your hook.

```javascript
const unsubscribe = Salla.hooks.registerHook(
  'salla-add-product-button',
  'componentDidLoad',
  (element) => {
    console.log('Button loaded for product:', element.getAttribute('product-id'));
  }
);

// To remove it later:
unsubscribe();
```

---

## Available Components & Hooks


<AccordionGroup>
  <Accordion title="salla-add-product-button" defaultOpen>
| Hook Name | When Called | Context Received | What You Can Modify |
|-----------|-------------|-----------------|---------------------|
| `componentDidLoad` | When the button component finishes rendering | `element` (HTMLElement) | The element and its children |
| `validate` | During the add-to-cart validation flow | `{ isValid, productId, component }` | `ctx.isValid` to block or allow the action |

  </Accordion>
  <Accordion title="salla-products-list">
| Hook Name | When Called | Context Received | What You Can Modify |
|-----------|-------------|-----------------|---------------------|
| `componentDidLoad` | When the list component finishes rendering | `element` (HTMLElement) | The element and its children |
| `beforeBuildListUrl` | Before the product fetch URL is built or pagination cursor is consumed | `{ component, url }` | `ctx.url` to rewrite the request URL |
  </Accordion>
  <Accordion title="Other Whitelisted Components">
The following components currently support `componentDidLoad` only:

| Component Tag | Description |
|---------------|-------------|
| `salla-product-options` | Product options selector |
| `salla-products-slider` | Horizontal product slider |
| `salla-hook` | Internal theme slot mounting component |
| `salla-scopes` | Scopes / permissions component |
| `salla-user-menu` | User account menu |

:::note
New components and hook names are added with each Twilight release. Check back for updates.
:::
  </Accordion>
</AccordionGroup>


### Properties Reference

When your callback receives a `component` object, either directly as `element` in `componentDidLoad` or via `ctx.component` in `validate`, you have access to the full component instance.

The following properties are available on `salla-add-product-button`:

```typescript
component.productId: string         // The product's ID
component.productType: string       // product | service | codes | digital | food
                                    // donating | group_products | booking | financial_support
component.productStatus: string     // sale | out | out-and-notify
component.quantity: number          // Currently selected quantity
component.donatingAmount: number    // Donation amount (donating product type only)
component.hasPreOrder: boolean      // Whether this product supports pre-order
component.quickBuy: boolean         // Whether quick buy mode is enabled
component.supportStickyBar: boolean // Whether the sticky bar is active for this product
component.channels: string          // Out-of-stock notification channels
component.notifyOptionsAvailability: boolean
component.subscribedOptions: string
component.host: HTMLElement           // The component's root HTML element
```

:::caution[]
When you receive `{ isValid, productId, component }` in the `validate` hook, modifying `ctx.isValid` affects whether the add-to-cart action is allowed to proceed. Be deliberate.
:::
---

## Late Registration Support

This is one of the most powerful aspects of the system. Your script does **NOT** need to load before the components.

<Tabs>
  <Tab title="The Problem">
In a classic event-driven model, if a component fires its `load` event before your script is ready, you miss it entirely. This forces brittle load-order dependencies.
  </Tab>
  <Tab title="The Solution">
The hooks system keeps a **component history**. When you register a `componentDidLoad` hook, it immediately invokes your callback for every component of that type that has **already loaded**, then continues firing for future ones.

```javascript
// ✅ Works even if salla-add-product-button elements already loaded on the page
Salla.hooks.registerHook('salla-add-product-button', 'componentDidLoad', (element) => {
  console.log('Found button:', element.getAttribute('product-id'));
});
```
      

:::check[Here is exactly what happens under the hood:]

<Steps>
    <Step title="Component Loads">
        A `salla-add-product-button` element renders on the page. It internally calls `registerComponent()`, fires any already-registered `componentDidLoad` callbacks, and adds itself to the component history.
    </Step>
    <Step title="Your Script Loads Later">
        Your partner script loads, after the component, and calls `Salla.hooks.registerHook('salla-add-product-button', 'componentDidLoad', cb)`.
    </Step>
    <Step title="Immediate Catchup">
        The system checks the component history and immediately calls your callback for every existing `salla-add-product-button` element on the page.
    </Step>
    <Step title="Future Components Covered Too">
        Any new `salla-add-product-button` elements that load after your registration will also trigger your callback normally going forward.
    </Step>
</Steps>
:::
      
  </Tab>
</Tabs>




## Hook Options: Priority & Once

The optional fourth argument to `registerHook()` accepts two options.


<Tabs>
  <Tab title="Priority">


Controls execution order when multiple callbacks are registered for the same hook. Higher values run first. Default is `0`.

```javascript
// Runs first
Salla.hooks.registerHook('salla-add-product-button', 'validate', (ctx) => {
  // critical check
}, { priority: 10 });

// Runs second
Salla.hooks.registerHook('salla-add-product-button', 'validate', (ctx) => {
  // secondary check
}); // priority defaults to 0
```
  </Tab>
  <Tab title="Once">
When `true`, the callback fires only once and is automatically removed after that.

```javascript
Salla.hooks.registerHook('salla-add-product-button', 'componentDidLoad', (element) => {
  console.log('Fires once only, for the first button loaded.');
}, { once: true });
```

Or use the shorthand:

```javascript
Salla.hooks.registerHookOnce('salla-add-product-button', 'componentDidLoad', (element) => {
  console.log('Same result ~ fires once then cleans up.');
});
```
  </Tab>
</Tabs>


## Removing a Hook

You have three ways to remove hooks depending on how granular you need to be.


<Tabs>
  <Tab title="Via the Returned Unsubscribe Function">

The cleanest approach, store the return value of `registerHook()` and call it when done.

```javascript
const unsubscribe = Salla.hooks.registerHook(
  'salla-add-product-button',
  'componentDidLoad',
  myCallback
);

unsubscribe(); // cleanly removes just this callback
```
  </Tab>
  <Tab title="Via `removeHook()`">

```javascript
// Remove a specific callback
Salla.hooks.removeHook('salla-add-product-button', 'componentDidLoad', myCallback);

// Remove ALL callbacks for a given hook name on a tag
Salla.hooks.removeHook('salla-add-product-button', 'componentDidLoad');
```
  </Tab>
  <Tab title="Via `clear()`">
```javascript
// Clear all hooks registered for a specific tag
Salla.hooks.clear('salla-add-product-button');

// Nuclear option — clear everything
Salla.hooks.clear();
```
  </Tab>
</Tabs>



---

## Real-World Use Cases


<AccordionGroup>
  <Accordion title="Use Case 1: Subscription Products | Modify UI and Block Re-Purchase" defaultOpen>
The full integration pattern. Listens for a `Product Viewed` analytics event, then registers both a UI hook and a validation hook for that specific product.

```javascript
(function () {
  "use strict";

  function registerMyHookForProduct(product) {
    const productId = product.product_id;

    // Change the button text when the component loads
    Salla.hooks.registerHook('salla-add-product-button', 'componentDidLoad', (element) => {
      const componentProductId = element.getAttribute('product-id');

      if (componentProductId == productId) {
        const button = element.querySelector('salla-button');
        if (button) {
          button.setText('Subscribe Now');
        }
        element.setAttribute('data-subscription-product', 'true');
        Salla.log('Component Loaded via Hook', { product, element });
      }
    });

    // Block re-purchase if the user already has an active subscription
    Salla.hooks.registerHook('salla-add-product-button', 'validate', (ctx) => {
      if (ctx.component.productId == productId) {
        const hasSubscription = localStorage.getItem('has_active_subscription');

        if (hasSubscription) {
          ctx.isValid = false;
          Salla.notify.error('You already have an active subscription');
        }

        Salla.log('Validation Hook', { isValid: ctx.isValid, component: ctx.component });
      }
    });
  }

  Salla.onReady(() => {
    Salla.analytics.registerTracker({
      name: 'subscription-tracker',
      track: (eventName, payload) => {
        if (eventName === 'Product Viewed') {
          registerMyHookForProduct(payload);
        }
      },
      page: () => {}
    });
  });
})();
```
  </Accordion>
  <Accordion title="Use Case 2: Booking Permission Check">
```javascript
Salla.hooks.registerHook('salla-add-product-button', 'validate', (ctx) => {
  if (ctx.component.productType === 'booking') {
    if (!checkBookingPermission()) {
      ctx.isValid = false;
      Salla.notify.error('You need permission to book this service');
    }
  }
});
```
  </Accordion>
  <Accordion title="Use Case 3: Quick Button Text Change">
The simplest verified example, changes button text across all add-product buttons on the page:

```javascript
Salla.hooks.registerHook('salla-add-product-button', 'componentDidLoad', (component) => {
  component.btn.setText('Subscribe Now');
});
```
  </Accordion>
  <Accordion title="Use Case 4: Rewrite the Products List Fetch URL">

Use `beforeBuildListUrl` to inject custom filters or query parameters before the products list makes its API call:

```javascript
Salla.hooks.registerHook('salla-products-list', 'beforeBuildListUrl', (ctx) => {
  const url = new URL(ctx.url);
  url.searchParams.set('custom_filter', 'my_value');
  ctx.url = url.toString();
});
```
  </Accordion>
</AccordionGroup>




## Security | The Whitelist System

Every call to `registerHook()` is validated against the whitelist before any callback is stored. Two checks must pass:

1. Is the **tag** in the whitelist?
2. Is the **hook name** allowed for that tag?

If either check fails, the registration is ignored and a warning shows in the browser console. A no-op function is returned instead of a real unsubscribe handle, so your code won’t break, but the hook won’t do anything.

<Tabs>
    <Tab title="✅ Valid Usage">

```javascript
// Allowed — tag and hook name are both whitelisted
Salla.hooks.registerHook('salla-add-product-button', 'componentDidLoad', callback);

// Allowed — validate is whitelisted for this tag
Salla.hooks.registerHook('salla-add-product-button', 'validate', callback);

// Allowed — beforeBuildListUrl is whitelisted for salla-products-list
Salla.hooks.registerHook('salla-products-list', 'beforeBuildListUrl', callback);
```

    </Tab>
    <Tab title="❌ Blocked Usage">

```javascript
// ❌ Hook name not in whitelist for this tag
Salla.hooks.registerHook('salla-add-product-button', 'unknown-hook', callback);
// Console: [Hooks] Hook "unknown-hook" is not allowed for tag "salla-add-product-button"

// ❌ Tag not in whitelist at all
Salla.hooks.registerHook('unknown-tag', 'componentDidLoad', callback);
// Console: [Hooks] Tag "unknown-tag" is not in the whitelist
```

    </Tab>
</Tabs>
<!--
:::info
To register a new component tag or hook name, edit the `_whitelist` Map in `/packages/twilight/src/lib/hooks.js`:

```javascript
this._whitelist = new Map([
  ['salla-add-product-button', new Set(['validate', 'componentDidLoad'])],
  ['your-new-component', new Set(['componentDidLoad', 'your-new-hook'])],
]);
```
:::
-->
