Loading...
Loading...
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".
npx skill4agent add hyva-themes/hyva-ai-tools hyva-alpine-componentunsafe-eval| Capability | Standard Alpine | Alpine CSP |
|---|---|---|
| Property reads | | Same |
| Negation | | Method: |
| Mutations | | Method: |
| Method args | | Dataset: |
| Available | Not supported - use |
| Range iteration | | Not supported |
<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() ?>Alpine.data()alpine:init{once: true}$hyvaCsp->registerInlineScript()<script>$escaper->escapeJs()$escaper->escapeHtmlAttr()escapeJsfunction initMyComponent() {
return {
open: false
}
}
window.addEventListener('alpine:init', () => Alpine.data('initMyComponent', initMyComponent), {once: true})Alpine.data()hyva.modalfunction initMyModal() {
return {
...hyva.modal.call(this),
...hyva.formValidation(this.$el),
customProperty: '',
customMethod() {
// Custom logic
}
};
}.call(this)return {
item: {
is_visible: true,
title: 'Product'
}
}<span x-show="item.is_visible" x-text="item.title"></span><span x-show="!item.deleted"></span>
<span x-text="item.title || item.value"></span><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;
}
}return {
deleted: false,
['!deleted']() {
return !this.deleted;
}
}<template x-if="!deleted">
<div>The item is present</div>
</template><button @click="open = !open">Toggle</button><button @click="toggle">Toggle</button>return {
open: false,
toggle() {
this.open = !this.open;
}
}<button @click="selectItem(123)">Select</button><button @click="selectItem" data-item-id="<?= $escaper->escapeHtmlAttr($itemId) ?>">Select</button>return {
selected: null,
selectItem() {
this.selected = this.$el.dataset.itemId;
}
}escapeHtmlAttrescapeJsreturn {
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<input type="text"
:value="username"
@input="setUsername">return {
username: '',
setUsername() {
this.username = this.$event.target.value;
}
}hyva.safeParseNumber()return {
quantity: 1,
setQuantity() {
this.quantity = hyva.safeParseNumber(this.$event.target.value);
}
}<textarea @input="setComment" x-text="comment"></textarea>return {
comment: '',
setComment() {
this.comment = this.$event.target.value;
}
}<input type="checkbox"
:checked="isSubscribed"
@change="toggleSubscribed">return {
isSubscribed: false,
toggleSubscribed() {
this.isSubscribed = this.$event.target.checked;
}
}<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 @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;
}
}<template x-for="(product, index) in products" :key="index">
<div x-text="product.name"></div>
</template>productindex<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}`;
}
}<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));
}
}x-for="i in 10"hyvahyva.getFormKey()hyva.getUenc()hyva.postForm({action, data, skipUenc})hyva.getCookie(name)hyva.setCookie(name, value, days, skipSetDomain)hyva.setSessionCookie(name, value, skipSetDomain)hyva.formatPrice(value, showSign, options)hyva.str(template, ...args)hyva.strf(template, ...args)hyva.safeParseNumber(rawValue)hyva.replaceDomElement(selector, content)hyva.trapFocus(rootElement)hyva.releaseFocus(rootElement)hyva.getBrowserStorage()hyva.createBooleanObjectfunction initToggle() {
return {
...hyva.createBooleanObject('open', false),
// Additional methods
};
}open()notOpen()toggleOpen()setOpenTrue()setOpenFalse()hyva.alpineInitialized(fn) // Run callback after Alpine initializes<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;
}
}return {
updateQuantity() {
this.qty = newValue;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
}private-content-loadedreload-customer-section-dataupdate-galleryreset-galleryx-bind<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();
}
}
}<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
};
}
}<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 || '{}');
}
}function initComponent() {
return {
productId: '<?= (int) $product->getId() ?>',
productName: '<?= $escaper->escapeJs($product->getName()) ?>',
config: <?= /* @noEscape */ json_encode($config) ?>
}
}<?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() ?>vendor/hyva-themes/magento2-default-theme-csp/vendor/hyva-themes/magento2-theme-module/src/view/frontend/templates/page/js/hyva.phtml