
Hyva Alpine Component
- 813 installs
- 78 repo stars
- Updated July 31, 2026
- hyva-themes/hyva-ai-tools
hyva-alpine-component is a Hyvä AI Tools skill that generates Content-Security-Policy-compatible Alpine.js components for Hyvä Magento 2 themes when developers add interactivity to storefront templates.
About
hyva-alpine-component is a hyva-themes/hyva-ai-tools skill for writing CSP-compatible Alpine.js in Hyvä Magento 2 themes. The skill covers Alpine CSP patterns that operate without unsafe-inline eval, x-data component structure, and interactivity added to Hyvä PHTML templates. Trigger phrases include create alpine component, add interactivity, alpine for hyva, x-data component, and csp compatibility. Developers reach for hyva-alpine-component when Magento 2 storefronts on Hyvä need client-side behavior that passes strict Content Security Policy headers without breaking theme conventions.
- Generates CSP-compliant Alpine.js components required for PCI-DSS 4.0 compliance after April 2025
- Translates standard Alpine patterns into CSP-safe equivalents (methods instead of inline mutations, dataset attributes f
- Handles x-model removal by converting to :value + @input patterns
- Ensures all code works in both standard Alpine and Alpine CSP builds
- Provides ready-to-use x-data component structures for Hyvä templates
Hyva Alpine Component by the numbers
- 813 all-time installs (skills.sh)
- +30 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #440 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hyva-themes/hyva-ai-tools --skill hyva-alpine-componentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 813 |
|---|---|
| repo stars | ★ 78 |
| Last updated | July 31, 2026 |
| Repository | hyva-themes/hyva-ai-tools ↗ |
How do you write CSP-safe Alpine for Hyvä?
Generate CSP-compatible Alpine.js components for Hyvä Magento 2 themes.
Who is it for?
Magento 2 frontend developers building Hyvä themes who need Alpine.js interactivity under strict Content Security Policy headers.
Skip if: React or Knockout-based Magento Luma themes, backend PHP modules, or projects not using Hyvä theme architecture.
When should I use this skill?
The user asks to create an Alpine component, add Hyvä template interactivity, or write CSP-compliant JavaScript for Magento 2 Hyvä.
What you get
CSP-compatible Alpine.js component code with x-data patterns ready for Hyvä Magento 2 PHTML templates.
- CSP-compatible Alpine.js component
- x-data template snippets
Files
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:
<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
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:
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
return {
item: {
is_visible: true,
title: 'Product'
}
}<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):
<span x-show="!item.deleted"></span>
<span x-text="item.title || item.value"></span>Correct:
<span x-show="isItemNotDeleted"></span>
<span x-text="itemLabel"></span>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:
return {
deleted: false,
['!deleted']() {
return !this.deleted;
}
}<template x-if="!deleted">
<div>The item is present</div>
</template>Property Mutation Patterns
Extract Mutations to Methods
Wrong (CSP incompatible):
<button @click="open = !open">Toggle</button>Correct:
<button @click="toggle">Toggle</button>return {
open: false,
toggle() {
this.open = !this.open;
}
}Passing Arguments via Dataset
Wrong (CSP incompatible):
<button @click="selectItem(123)">Select</button>Correct:
<button @click="selectItem" data-item-id="<?= $escaper->escapeHtmlAttr($itemId) ?>">Select</button>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:
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
<input type="text"
:value="username"
@input="setUsername">return {
username: '',
setUsername() {
this.username = this.$event.target.value;
}
}Number Inputs
Use hyva.safeParseNumber() for numeric values:
return {
quantity: 1,
setQuantity() {
this.quantity = hyva.safeParseNumber(this.$event.target.value);
}
}Textarea
<textarea @input="setComment" x-text="comment"></textarea>return {
comment: '',
setComment() {
this.comment = this.$event.target.value;
}
}Checkboxes
<input type="checkbox"
:checked="isSubscribed"
@change="toggleSubscribed">return {
isSubscribed: false,
toggleSubscribed() {
this.isSubscribed = this.$event.target.checked;
}
}Checkbox Arrays
<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>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
<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>return {
selectedCountry: '',
isCountrySelected() {
return this.selectedCountry === this.country.code;
},
setCountry() {
this.selectedCountry = this.$event.target.value;
}
}x-for Patterns
Basic Iteration
<template x-for="(product, index) in products" :key="index">
<div x-text="product.name"></div>
</template>Using Methods in Loops
Loop variables (product, index) are accessible in methods:
<template x-for="(product, index) in products" :key="index">
<span :class="getItemClasses" @click="goToProduct" x-text="product.name"></span>
</template>return {
products: [],
getItemClasses() {
return {
'font-bold': this.index === 0,
'text-gray-500': this.product.disabled
};
},
goToProduct() {
window.location.href = `${BASE_URL}/product/${this.product.url_key}`;
}
}Function as Value Provider
The value provider can be a method (called without parentheses):
<template x-for="(item, index) in getFilteredItems" :key="index">
<div x-text="item.name"></div>
</template>return {
items: [],
filter: '',
getFilteredItems() {
return this.items.filter(item => item.name.includes(this.filter));
}
}Note: Range iteration (x-for="i in 10") is not supported in Alpine CSP.
Hyva Utility Functions
The global hyva object provides these utilities:
Form and Security
hyva.getFormKey()- Get/generate form key for POST requestshyva.getUenc()- Base64 encode current URL for redirectshyva.postForm({action, data, skipUenc})- Submit a POST form programmatically
Cookies
hyva.getCookie(name)- Get cookie value (respects consent)hyva.setCookie(name, value, days, skipSetDomain)- Set cookiehyva.setSessionCookie(name, value, skipSetDomain)- Set session cookie
Formatting
hyva.formatPrice(value, showSign, options)- Format currencyhyva.str(template, ...args)- String interpolation with %1, %2 placeholdershyva.strf(template, ...args)- Zero-based string interpolation (%0, %1)
Numbers
hyva.safeParseNumber(rawValue)- Parse number safely (for x-model.number replacement)
DOM
hyva.replaceDomElement(selector, content)- Replace DOM element with HTML contenthyva.trapFocus(rootElement)- Trap focus within element (for modals)hyva.releaseFocus(rootElement)- Release focus trap
Storage
hyva.getBrowserStorage()- Get localStorage/sessionStorage safely
Boolean Object Helper
For toggle components, use hyva.createBooleanObject:
function initToggle() {
return {
...hyva.createBooleanObject('open', false),
// Additional methods
};
}This generates: open(), notOpen(), toggleOpen(), setOpenTrue(), setOpenFalse()
Alpine Initialization
hyva.alpineInitialized(fn) // Run callback after Alpine initializesEvent Patterns
Listening to Custom Events
<div x-data="initMyComponent"
@private-content-loaded.window="onPrivateContentLoaded"
@update-gallery.window="onGalleryUpdate">return {
onPrivateContentLoaded() {
const data = this.$event.detail.data;
// Handle customer data
},
onGalleryUpdate() {
const images = this.$event.detail;
this.images = images;
}
}Dispatching Events
return {
updateQuantity() {
this.qty = newValue;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
}Common Hyvä Events
private-content-loaded- Customer section data loadedreload-customer-section-data- Request customer data refreshupdate-gallery- Product gallery images changedreset-gallery- Reset gallery to initial state
Event Listeners Object Pattern
For multiple window/document event listeners, use the x-bind pattern:
<div x-data="initGallery" x-bind="eventListeners">return {
eventListeners: {
['@keydown.window.escape']() {
if (!this.fullscreen) return;
this.closeFullScreen();
},
['@update-gallery.window'](event) {
this.receiveImages(event.detail);
},
['@keyup.arrow-right.window']() {
if (!this.fullscreen) return;
this.nextItem();
}
}
}Dynamic Classes Pattern
Return class objects from methods:
<div :class="containerClasses">return {
fullscreen: false,
containerClasses() {
return {
'w-full h-full fixed top-0 left-0 bg-white z-50': this.fullscreen,
'relative': !this.fullscreen
};
}
}Passing PHP Data to Components
Via Data Attributes
<div x-data="initProductList"
data-products="<?= $escaper->escapeHtmlAttr(json_encode($products)) ?>"
data-config="<?= $escaper->escapeHtmlAttr(json_encode($config)) ?>">return {
products: [],
config: {},
init() {
this.products = JSON.parse(this.$root.dataset.products || '[]');
this.config = JSON.parse(this.$root.dataset.config || '{}');
}
}Via Inline JavaScript (with escaping)
function initComponent() {
return {
productId: '<?= (int) $product->getId() ?>',
productName: '<?= $escaper->escapeJs($product->getName()) ?>',
config: <?= /* @noEscape */ json_encode($config) ?>
}
}Complete Example: Quantity Selector
<?php
declare(strict_types=1);
use Hyva\Theme\ViewModel\HyvaCsp;
use Magento\Framework\Escaper;
/** @var Escaper $escaper */
/** @var HyvaCsp $hyvaCsp */
$productId = (int) $product->getId();
$minQty = 1;
$maxQty = 100;
$defaultQty = 1;
?>
<div x-data="initQtySelector">
<label for="qty-<?= $productId ?>" class="sr-only">
<?= $escaper->escapeHtml(__('Quantity')) ?>
</label>
<div class="flex items-center">
<button type="button"
class="btn"
@click="decrement"
:disabled="isMinQty"
:class="decrementClasses">
-
</button>
<input type="number"
id="qty-<?= $productId ?>"
name="qty"
:value="qty"
@input="onInput"
min="<?= $minQty ?>"
max="<?= $maxQty ?>"
class="form-input w-16 text-center">
<button type="button"
class="btn"
@click="increment"
:disabled="isMaxQty"
:class="incrementClasses">
+
</button>
</div>
</div>
<script>
function initQtySelector() {
return {
qty: <?= (int) $defaultQty ?>,
minQty: <?= (int) $minQty ?>,
maxQty: <?= (int) $maxQty ?>,
productId: '<?= $productId ?>',
onInput() {
let value = hyva.safeParseNumber(this.$event.target.value);
if (value < this.minQty) value = this.minQty;
if (value > this.maxQty) value = this.maxQty;
this.qty = value;
this.$dispatch('update-qty-' + this.productId, this.qty);
},
increment() {
if (this.qty < this.maxQty) {
this.qty++;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
},
decrement() {
if (this.qty > this.minQty) {
this.qty--;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
},
isMinQty() {
return this.qty <= this.minQty;
},
isMaxQty() {
return this.qty >= this.maxQty;
},
decrementClasses() {
return { 'opacity-50 cursor-not-allowed': this.isMinQty() };
},
incrementClasses() {
return { 'opacity-50 cursor-not-allowed': this.isMaxQty() };
}
}
}
window.addEventListener('alpine:init', () => Alpine.data('initQtySelector', initQtySelector), {once: true})
</script>
<?php $hyvaCsp->registerInlineScript() ?>References
- Hyvä CSP Documentation: https://docs.hyva.io/hyva-themes/writing-code/csp/alpine-csp.html
- Alpine.js Documentation: https://alpinejs.dev/
- Example components:
vendor/hyva-themes/magento2-default-theme-csp/ - Core utilities:
vendor/hyva-themes/magento2-theme-module/src/view/frontend/templates/page/js/hyva.phtml
<!-- Copyright © Hyvä Themes https://hyva.io. All rights reserved. Licensed under OSL 3.0 -->
Related skills
FAQ
Why does hyva-alpine-component use Alpine CSP?
hyva-alpine-component targets Alpine CSP, a specialized Alpine.js build that works under strict Content Security Policy without unsafe-inline or eval. Standard Alpine patterns that violate CSP will fail on hardened Hyvä Magento 2 storefronts.
When should you use hyva-alpine-component?
Use hyva-alpine-component when adding Alpine.js interactivity to Hyvä Magento 2 theme templates. Trigger phrases include create alpine component, x-data component, and csp compliant javascript for Hyvä themes.