Claude
Skills
Sign in
Back

hyva-alpine-component

Included with Lifetime
$97 forever

Write CSP-compatible Alpine.js components for Hyvä themes in Magento 2. This skill should be used when the user wants to create Alpine components, add interactivity to Hyvä templates, write JavaScript for Hyvä themes, or needs help with Alpine.js patterns that work with Content Security Policy. Trigger phrases include "create alpine component", "add interactivity", "alpine for hyva", "x-data component", "csp compatibility", "csp compliant javascript".

Design

What this skill does


# Hyvä Alpine Component

## Overview

This skill provides guidance for writing CSP-compatible Alpine.js components in Hyvä themes. Alpine CSP is a specialized Alpine.js build that operates without the `unsafe-eval` CSP directive, which is required for PCI-DSS 4.0 compliance on payment-related pages (mandatory from April 1, 2025).

**Key principle:** CSP-compatible code functions in both standard and Alpine CSP builds. Write all Alpine code using CSP patterns for future-proofing.

## CSP Constraints Summary

| Capability | Standard Alpine | Alpine CSP |
|------------|-----------------|------------|
| Property reads | `x-show="open"` | Same |
| Negation | `x-show="!open"` | Method: `x-show="isNotOpen"` |
| Mutations | `@click="open = false"` | Method: `@click="close"` |
| Method args | `@click="setTab('info')"` | Dataset: `@click="setTab" data-tab="info"` |
| `x-model` | Available | **Not supported** - use `:value` + `@input` |
| Range iteration | `x-for="i in 10"` | **Not supported** |

## Component Structure Pattern

Every Alpine component in Hyvä follows this structure:

```html
<div x-data="initComponentName">
    <!-- Template content -->
</div>
<script>
    function initComponentName() {
        return {
            // Properties
            propertyName: initialValue,

            // Lifecycle
            init() {
                // Called when component initializes
            },

            // Methods for state access
            isPropertyTrue() {
                return this.propertyName === true;
            },

            // Methods for mutations
            setPropertyValue() {
                this.propertyName = this.$event.target.value;
            }
        }
    }
    window.addEventListener('alpine:init', () => Alpine.data('initComponentName', initComponentName), {once: true})
</script>
<?php $hyvaCsp->registerInlineScript() ?>
```

**Critical requirements:**
1. Register constructor with `Alpine.data()` inside `alpine:init` event listener
2. Use `{once: true}` to prevent duplicate registrations
3. Call `$hyvaCsp->registerInlineScript()` after every `<script>` block
4. Use `$escaper->escapeJs()` for PHP values in JavaScript strings
5. Use `$escaper->escapeHtmlAttr()` for data attributes (not `escapeJs`)

## Constructor Functions

### Basic Registration

```javascript
function initMyComponent() {
    return {
        open: false
    }
}
window.addEventListener('alpine:init', () => Alpine.data('initMyComponent', initMyComponent), {once: true})
```

**Why named global functions?** Constructor functions are declared as named functions in global scope (not inlined in the `Alpine.data()` callback) so they can be proxied and extended in other templates. This is an extensibility feature of Hyvä Themes - other modules or child themes can wrap or override these functions before they are registered with Alpine.

### Composing Multiple Objects

When combining objects (e.g., with `hyva.modal`), use spread syntax inside the constructor:

```javascript
function initMyModal() {
    return {
        ...hyva.modal.call(this),
        ...hyva.formValidation(this.$el),
        customProperty: '',
        customMethod() {
            // Custom logic
        }
    };
}
```

Use `.call(this)` to pass Alpine context to composed functions.

## Property Access Patterns

### Value Properties with Dot Notation

```javascript
return {
    item: {
        is_visible: true,
        title: 'Product'
    }
}
```

```html
<span x-show="item.is_visible" x-text="item.title"></span>
```

### Transforming Values (Negation, Conditions)

CSP does not allow inline transformations. Create methods instead:

**Wrong (CSP incompatible):**
```html
<span x-show="!item.deleted"></span>
<span x-text="item.title || item.value"></span>
```

**Correct:**
```html
<span x-show="isItemNotDeleted"></span>
<span x-text="itemLabel"></span>
```

```javascript
return {
    item: { deleted: false, title: '', value: '' },

    isItemNotDeleted() {
        return !this.item.deleted;
    },
    itemLabel() {
        return this.item.title || this.item.value;
    }
}
```

### Negation Method Shorthand

For simple boolean negation, use bracket notation:

```javascript
return {
    deleted: false,
    ['!deleted']() {
        return !this.deleted;
    }
}
```

```html
<template x-if="!deleted">
    <div>The item is present</div>
</template>
```

## Property Mutation Patterns

### Extract Mutations to Methods

**Wrong (CSP incompatible):**
```html
<button @click="open = !open">Toggle</button>
```

**Correct:**
```html
<button @click="toggle">Toggle</button>
```

```javascript
return {
    open: false,
    toggle() {
        this.open = !this.open;
    }
}
```

### Passing Arguments via Dataset

**Wrong (CSP incompatible):**
```html
<button @click="selectItem(123)">Select</button>
```

**Correct:**
```html
<button @click="selectItem" data-item-id="<?= $escaper->escapeHtmlAttr($itemId) ?>">Select</button>
```

```javascript
return {
    selected: null,
    selectItem() {
        this.selected = this.$el.dataset.itemId;
    }
}
```

**Important:** Use `escapeHtmlAttr` for data attributes, not `escapeJs`.

### Accessing Event and Loop Variables in Methods

Methods can access Alpine's special properties:

```javascript
return {
    onInput() {
        // Access event
        const value = this.$event.target.value;
        this.inputValue = value;
    },
    getItemUrl() {
        // Access x-for loop variable
        return `${BASE_URL}/product/id/${this.item.id}`;
    }
}
```

## x-model Alternatives

`x-model` is **not available** in Alpine CSP. Use two-way binding patterns instead.

### Text Inputs

```html
<input type="text"
       :value="username"
       @input="setUsername">
```

```javascript
return {
    username: '',
    setUsername() {
        this.username = this.$event.target.value;
    }
}
```

### Number Inputs

Use `hyva.safeParseNumber()` for numeric values:

```javascript
return {
    quantity: 1,
    setQuantity() {
        this.quantity = hyva.safeParseNumber(this.$event.target.value);
    }
}
```

### Textarea

```html
<textarea @input="setComment" x-text="comment"></textarea>
```

```javascript
return {
    comment: '',
    setComment() {
        this.comment = this.$event.target.value;
    }
}
```

### Checkboxes

```html
<input type="checkbox"
       :checked="isSubscribed"
       @change="toggleSubscribed">
```

```javascript
return {
    isSubscribed: false,
    toggleSubscribed() {
        this.isSubscribed = this.$event.target.checked;
    }
}
```

### Checkbox Arrays

```html
<template x-for="option in options" :key="option.id">
    <input type="checkbox"
           :value="option.id"
           :checked="isOptionSelected"
           @change="toggleOption"
           :data-option-id="option.id">
</template>
```

```javascript
return {
    selectedOptions: [],
    isOptionSelected() {
        return this.selectedOptions.includes(this.option.id);
    },
    toggleOption() {
        const optionId = this.$el.dataset.optionId;
        const index = this.selectedOptions.indexOf(optionId);
        if (index === -1) {
            this.selectedOptions.push(optionId);
        } else {
            this.selectedOptions.splice(index, 1);
        }
    }
}
```

### Select Elements

```html
<select @change="setCountry">
    <template x-for="country in countries" :key="country.code">
        <option :value="country.code"
                :selected="isCountrySelected"
                x-text="country.name"></option>
    </template>
</select>
```

```javascript
return {
    selectedCountry: '',
    isCountrySelected() {
        return this.selectedCountry === this.country.code;
    },
    setCountry() {
        this.selectedCountry = this.$event.target.value;
    }
}
```

## x-for Patterns

### Basic Iteration

```html
<template x-for="(product, index) in products" :key="index">
    <div x-text="product.name"></div>
</template>
```

### Using Methods in Loops

Loop variables (`product`, `inde

Related in Design