| name | hyva-module-compatibility |
| description | Identify and fix Magento 2 module compatibility issues with Hyvä Themes. Covers block plugin bypasses, RequireJS/Knockout replacements, ViewModels, and Alpine.js integration for modules that work in admin but fail on Hyvä frontend. |
Hyvä Module Compatibility Skill
Overview
This skill helps identify and fix Magento 2 module compatibility issues with Hyvä Themes. Hyvä uses Alpine.js and TailwindCSS instead of Luma's Knockout.js and RequireJS, which often breaks modules designed for the default Luma theme.
When to Use This Skill
- A Magento 2 module works in admin but not on Hyvä frontend
- Plugins targeting Magento blocks don't apply on frontend
- JavaScript-based features are missing or broken
- Custom rendering/UI components don't appear correctly
Common Hyvä Compatibility Issues
1. Block Plugins Don't Execute
Symptom: Plugins that modify block HTML output (e.g., after*Html() methods) don't apply on frontend.
Root Cause: Hyvä often uses custom ViewModels and templates that bypass standard Magento blocks.
Example from this project:
class SelectPlugin {
public function afterGetValuesHtml(Select $subject, string $result): string {
}
}
Solution: Instead of plugins, use:
- Template overrides in your theme
- Custom ViewModels injected via
ViewModelRegistry
- Alpine.js for client-side rendering
2. RequireJS/Knockout Dependencies
Symptom: JavaScript features don't load; console errors about missing modules.
Root Cause: Hyvä doesn't include RequireJS or Knockout.js by default.
Solution:
- Replace RequireJS modules with vanilla JavaScript or Alpine.js
- Use
<script> tags with modern ES6+ JavaScript
- Leverage Hyvä's ViewModels for data injection
3. Layout XML Blocks Not Rendering
Symptom: Blocks defined in layout XML don't appear on frontend.
Root Cause: Hyvä templates may not include the block reference points.
Solution:
- Check if Hyvä has an equivalent template
- Override Hyvä templates in your theme to add missing blocks
- Use
$block->getChildHtml() in templates
Hyvä Compatibility Workflow
Step 1: Identify the Issue
bin/magento theme:list
Step 2: Locate the Incompatible Code
Check for these patterns:
public function afterGetHtml(...) {}
public function afterToHtml(...) {}
<script type="text/x-magento-init">
require(['jquery', 'mage/...'], function($) {});
<div data-bind="scope: 'component'">
Find Hyvä equivalents:
find vendor/hyva-themes -name "*.phtml" | xargs grep -l "pattern"
ls app/code/Hyva/*/
Step 3: Implement Hyvä-Compatible Solution
Pattern A: Template Override with ViewModel
File: app/design/frontend/YourVendor/your-theme/Module_Name/templates/your-template.phtml
<?php
use Hyva\Theme\Model\ViewModelRegistry;
use Your\Module\ViewModel\YourViewModel;
$viewModels = $viewModels ?? \Magento\Framework\App\ObjectManager::getInstance()
->get(ViewModelRegistry::class);
$customViewModel = $viewModels->require(YourViewModel::class);
$data = $customViewModel->getData();
?>
<!-- Use Alpine.js for interactivity -->
<div x-data="{
myData: <?= $escaper->escapeHtmlAttr(json_encode($data)) ?>,
myMethod() {
// Alpine.js method
}
}" x-init="myMethod()">
<span x-text="myData.someValue"></span>
</div>
Pattern B: Alpine.js Integration
When: You need client-side reactivity (dropdowns, filters, dynamic updates)
<script>
function initYourFeature() {
return {
selectedValue: null,
options: <?= json_encode($options) ?>,
init() {
this.$nextTick(() => {
this.applyDefaults();
});
},
applyDefaults() {
if (this.defaultValue) {
this.selectedValue = this.defaultValue;
const element = document.querySelector('#my-select');
if (element) {
element.value = this.defaultValue;
}
}
},
handleChange($dispatch, value) {
this.selectedValue = value;
$dispatch('custom-event', { value });
}
}
}
</script>
<div x-data="initYourFeature()" x-init="init()">
<select x-on:change="handleChange($dispatch, $event.target.value)">
<template x-for="option in options">
<option :value="option.value" x-text="option.label"></option>
</template>
</select>
</div>
Pattern C: ViewModel for Data Preparation
File: app/code/Your/Module/ViewModel/YourViewModel.php
<?php
declare(strict_types=1);
namespace Your\Module\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
class YourViewModel implements ArgumentInterface
{
private $yourModel;
public function __construct(
\Your\Module\Model\YourModel $yourModel
) {
$this->yourModel = $yourModel;
}
public function getData(int $entityId): array
{
return [
'key' => 'value',
'items' => $this->yourModel->getItems($entityId),
];
}
public function getDataJson(int $entityId): string
{
return json_encode($this->getData($entityId), JSON_THROW_ON_ERROR);
}
}
Usage in template:
<?php
$viewModel = $viewModels->require(\Your\Module\ViewModel\YourViewModel::class);
?>
<div x-data='<?= $viewModel->getDataJson($product->getId()) ?>'>
<!-- Your Alpine.js component -->
</div>
Step 4: Testing
ddev exec bin/magento cache:flush
tail -f var/log/system.log var/log/exception.log
Real-World Example: Custom Option Default Values
Original (Luma-Compatible) Approach
class SelectPlugin
{
public function afterGetValuesHtml(Select $subject, string $result): string
{
}
}
Hyvä-Compatible Approach
1. Created ViewModel:
class CustomOptionImage implements ArgumentInterface
{
public function getDefaultValuesForProduct(int $productId): array
{
return $this->defaultValueResource->getDefaultValuesForProduct($productId);
}
}
2. Modified Template:
use Uptactics\CustomOptionImage\ViewModel\CustomOptionImage;
$customOptionImageViewModel = $viewModels->require(CustomOptionImage::class);
$defaultValues = $customOptionImageViewModel->getDefaultValuesForProduct((int)$product->getId());
3. Added Alpine.js Logic:
function initOptions() {
return {
defaultValues: <?= json_encode($defaultValues) ?>,
applyDefaultValues($dispatch) {
Object.entries(this.defaultValues).forEach(([optionId, optionTypeId]) => {
const selectElement = document.querySelector(`select[name="options[${optionId}]"]`);
if (selectElement) {
selectElement.value = optionTypeId;
this.updateCustomOptionValue($dispatch, optionId, selectElement);
}
});
}
}
}
4. Called on Initialization:
<div x-data="initOptions()"
x-init="$nextTick(() => { applyDefaultValues($dispatch); })">
Hyvä Theme Patterns Reference
Alpine.js Directives
<div x-data="{ count: 0 }"></div>
<div x-show="isVisible"></div>
<div x-if="shouldRender"></div>
<template x-for="item in items" :key="item.id">
<div x-text="item.name"></div>
</template>
<button x-on:click="handleClick()">Click</button>
<select x-on:change="handleChange($event)">
<div x-ref="myElement"></div>
<div x-init="init()"></div>
<div x-init="$nextTick(() => { /* code */ })"></div>
ViewModelRegistry Usage
$myViewModel = $viewModels->require(MyViewModel::class);
if ($viewModels->has(MyViewModel::class)) {
$myViewModel = $viewModels->require(MyViewModel::class);
}
Data Passing Patterns
<div x-data='{ value: "<?= $escaper->escapeHtmlAttr($value) ?>" }'></div>
<div x-data='<?= $escaper->escapeHtmlAttr(json_encode($data)) ?>'></div>
<script>
const data = <?= json_encode($data) ?>;
</script>
Common Gotchas
1. ViewModels Must Implement ArgumentInterface
class MyViewModel implements ArgumentInterface { }
class MyViewModel { }
2. JSON Encoding for Alpine.js
defaultValues: <?= json_encode($defaults) ?>,
defaultValues: <?= $escaper->escapeHtml(json_encode($defaults)) ?>,
3. Alpine.js Method Context
x-init="$nextTick(() => { this.myMethod() })"
x-init="$nextTick(function() { this.myMethod() })"
4. Template Override Location
app/design/frontend/
└── YourVendor/
└── your-theme/
└── Magento_Catalog/ ← Module name
└── templates/
└── product/
└── view/
└── options/
└── options.phtml
Checklist for Hyvä Compatibility
Resources
Hyvä Documentation
Hyvä Theme Locations
- Base theme:
vendor/hyva-themes/magento2-default-theme/
- Theme module:
vendor/hyva-themes/magento2-theme-module/
- Your theme:
app/design/frontend/YourVendor/your-theme/
Common Hyvä ViewModels
Hyva\Theme\ViewModel\CustomOption - Custom options rendering
Hyva\Theme\ViewModel\ProductPage - Product page utilities
Hyva\Theme\ViewModel\ProductPrice - Price formatting
Hyva\Theme\ViewModel\SvgIcons - Icon rendering
Tips
- Start with Hyvä's templates - Always check if Hyvä has a template for what you're modifying
- Use ViewModels for logic - Keep templates clean, put logic in ViewModels
- Leverage Alpine.js - Don't fight it, use it for reactivity
- Test thoroughly - Hyvä caching can mask issues
- Check Hyvä's compatibility module list - Someone may have already solved your problem
Example Commands
find vendor/hyva-themes -name "select.phtml"
grep -r "class CustomOption" vendor/hyva-themes
ddev exec bin/magento cache:flush