---
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:**
```php
// ❌ This plugin doesn't work with Hyvä
class SelectPlugin {
public function afterGetValuesHtml(Select $subject, string $result): string {
// Modifies HTML - but Hyvä doesn't call getValuesHtml()
}
}
```
**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 `
```
#### Pattern C: ViewModel for Data Preparation
**File:** `app/code/Your/Module/ViewModel/YourViewModel.php`
```php
yourModel = $yourModel;
}
/**
* Get data for Alpine.js/frontend
*
* @param int $entityId
* @return array
*/
public function getData(int $entityId): array
{
return [
'key' => 'value',
'items' => $this->yourModel->getItems($entityId),
];
}
/**
* Get data as JSON for Alpine.js
*
* @param int $entityId
* @return string
*/
public function getDataJson(int $entityId): string
{
return json_encode($this->getData($entityId), JSON_THROW_ON_ERROR);
}
}
```
**Usage in template:**
```php
require(\Your\Module\ViewModel\YourViewModel::class);
?>
```
### Step 4: Testing
```bash
# Clear caches
ddev exec bin/magento cache:flush
# Check for errors in console
# Browser DevTools → Console
# Check for errors in logs
tail -f var/log/system.log var/log/exception.log
# Test all option types if applicable
# - Select dropdowns
# - Radio buttons
# - Checkboxes
# - Multi-select
```
## Real-World Example: Custom Option Default Values
### Original (Luma-Compatible) Approach
```php
// Plugin: Plugin/Catalog/Block/Product/View/Options/Type/SelectPlugin.php
class SelectPlugin
{
public function afterGetValuesHtml(Select $subject, string $result): string
{
// Modify HTML to add selected="selected" attribute
// ❌ Doesn't work with Hyvä - method never called
}
}
```
### Hyvä-Compatible Approach
**1. Created ViewModel:**
```php
// ViewModel/CustomOptionImage.php
class CustomOptionImage implements ArgumentInterface
{
public function getDefaultValuesForProduct(int $productId): array
{
return $this->defaultValueResource->getDefaultValuesForProduct($productId);
}
}
```
**2. Modified Template:**
```php
// app/design/frontend/Uptactics/nto/Magento_Catalog/templates/product/view/options/options.phtml
use Uptactics\CustomOptionImage\ViewModel\CustomOptionImage;
/** @var CustomOptionImage $customOptionImageViewModel */
$customOptionImageViewModel = $viewModels->require(CustomOptionImage::class);
$defaultValues = $customOptionImageViewModel->getDefaultValuesForProduct((int)$product->getId());
```
**3. Added Alpine.js Logic:**
```javascript
function initOptions() {
return {
defaultValues: = /* @noEscape */ 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:**
```html
```
## Hyvä Theme Patterns Reference
### Alpine.js Directives
```html