# Magewire - Features ## Index - [Best Practices](#best-practices) - [Folder Structure](#folder-structure) - [Javascript](#javascript) - [Document Events](#document-events) - [Lifecycle Hooks](#lifecycle-hooks) - [Block Structure](#block-structure) - [Dynamic Components](#dynamic-components) - [Blocks](#blocks--dc-) - [Widgets](#widgets--dc-) - [Templates](#templates) - [Switch Template](#switch-template) - [Directives](#directives) - [Wire Ignore](#wire-ignore) - [Children Block Rendering](#child-block-rendering) - [Wire Select](#wire-select) - [Component Types](#component-types) - [Magic Actions & Properties](#magic-actions--properties) - [Overwrites](#overwrites) - [Properties](#properties) - [Name Specific](#name-specific) - [Nesting](#nesting) - [Flash Messages](#flash-messages) - [Redirects](#redirects) - [Listeners & Emits](#listeners--emits) - [Javascript](#javascript--el-) - [Magic Actions Compatibility](#magic-actions-compatibility) - [Global Refresh Listener](#global-refresh-listener) - [Lifecycle Hooks](#lifecycle-hooks) - [Browser Events](#browser-events) - [Prefetching](#prefetching) - [Lazy Updating](#lazy-updating) - [Keydown Modifiers](#keydown-modifiers) - [Restricted Public Methods](#restricted-public-methods) - [Global](#global-rpm) - [Component](#component-rpm) - [Pagination](#pagination) - [Query String](#query-string) - [Display Loading State](#display-loading-state) - [Indicator Customization](#indicator-customization) - [Indicator Removal](#indicator-removal) - [Custom Example](#custom-example-dls) - [Notification Type Styling](#notification-type-styling) - [Plugins](#plugins) - [Plugin: Loader](#plugin--loader) - [Loader System Settings](#loader-system-settings) - [Plugin: Error](#plugin--error) - [Custom onError callback](#custom-onerror-callback) - [Component Resolvers](#component-resolvers) - [Custom Resolvers](#custom-resolver) - [Reset](#reset) - [Forms](#forms) - [Message Translations](#message-translations--f-) - [Message Displayment](#message-displayment--f-) - [Example 1](#example-1--md-) - [Example 2](#example-2--md-) - [Example 3](#example-3--md-) - [Example 4](#example-4--md-) ## Best Practices 1. Use the Magewire naming conventions and structures. 2. Use Hydrators to manipulate data before or after a method gets called. 3. Contribute or create an issue when you found bugs or sucurity issues. 4. Keep components small and clean. 5. Use the ```My/Module/Magewire/Component``` folder when you introduce a new component type. 6. The ```$magewire``` component object is available in the template by default. ## Folder Structure | Folder | Description | |--------|----------------------------------------------------------------------------------------------------| | /src | Root folder inside your module | | /src/Magewire | All Magewire components live here (subfolder allowed) | | /src/view/frontend/templates/magewire | All Magewire component template files live here (subfolder allowed) | ## JavaScript The ```Magewire``` (alias for ```Livewire```) object is globally available on the page. More information can be found inside the [Livewire docs](https://laravel-livewire.com/docs/2.x/reference#global-livewire-js). ### Document Events - magewire:load - magewire:update - magewire:available - magewire:loader:start ```(event) => {}``` - magewire:loader:stop ```(event) => {}``` ### Lifecycle Hooks [Read all about hooks](https://laravel-livewire.com/docs/2.x/reference#js-hooks) ## Block Structure ```xml \My\Module\Magewire\Explanation \My\Module\Magewire\Explanation \My\Module\Magewire\Explanation ``` > **Note**: Template will automatically be set if a Magewire component has been set. When your component is named Foo, > just create a foo.phtml inside the /view/templates/magewire folder. ## Dynamic Components There are cases where you want to use the full power of Magewire, but the block is not configured in a layout XML. ### Blocks (dc) ```php $this->layout->createBlock(Template::class)->toHtml() ``` To achieve this you have to add the same configuration as you do in a layout XML. ```php use Magento\Framework\View\Element\Template; use \Magento\Framework\App\ObjectManager; $this->layout->createBlock(Template::class) // Setting a template is optional since Magewire can auto-bind the belonging template. ->setTemplate('My_Module::path/to/component/phtml.phtml') // Bind a valid Magewire component onto the block so it can be recognized by the layout. ->setData('magewire', ObjectManager::getInstance()->create(\My\Module\Magewire\Explanation::class)) ->toHtml() ``` > **Note**: It's important to use create, otherwise when you try to use this Magewire component multiple they will > share the data. ### Widgets (dc) For widgets, the principle is the same. But here Magento does the rendering. So you cannot use ```setData()``` to assign the Magewire component. But we can set the component via the ```constructor``` method. ```php class MyWidget extends Template implements BlockInterface { public function __construct( Context $context, array $data = [] ) { $data['magewire'] = ObjectManager::getInstance()->create(\My\Module\Magewire\Explanation::class); parent::__construct($context, $data); } } ``` ### How does it work under the hood? On the initial page request all information needed are here and we have no problem. But on subsequent request Magento doesn't find the layout information. To solve this issue all information that are needed to create the block are transported in the fingerprint "dynamic_layout". The information contains the magewire component and all block data (widget configuration). With this information it is possible to dynamically create the missing block on a subsequent request. ## Templates Options within your block template. The ```$magewire``` variable is by default available in your Component template. ```php hasFoo(); // Get property value. $magewire->getFoo(); // Call a custom method inside your component (if not set as uncallable). $magewire->myCustomMethod(); // Overwrite a property after the (optional) mount method was executed. $magewire->foo = 'barbar'; ?> ``` ### Switch Template Switch to another template during a subsequent request. ```html
``` ```php public function login() { $this->switchTemplate('My_Module::customer/account/dashboard.phtml'); } ``` > **Tip**: Use the power of the layout xml to assign a "switch" template path as a data param assigned to the component. > This way your component becomes more dynamic and extensible for other developers. ## Directives ### Wire Ignore Ignore smart DOM diffing on specified elements within a Magewire component. ```html getFoo() ?> ``` #### Child Block Rendering This can be very powerful when you are in the situation where you want to keep the child blocks intact. ```html
getFoo() ?>
getChildHtml('child.block.name') ?>
``` ### Wire Select There are some specific cases where you have a select element including a ```wire:model``` attribute without any modifiers. In these cases, you want to save a selected option on change. This works fine for typical mobile or mouse interactions. But it has a couple of issues when using a keyboard where you have multiple options starting with the same couple of letters. The ```wire:select``` directive has two modifiers: ```debounce``` and ```blur```. By default, debounce uses a ```1500ms``` delay, but you can change this by specifying a different delay in milliseconds, such as ```debounce.2000ms```. blur syncs the model on element blur. **Modifiers** - Blur: Syncs model on element blur - Debounce: Debounce on each keydown or select change > **Important**: To use wire:select, you must always defer the model to let wire:select take over, and you cannot use a > value with the directive. Here's an example of how to use ```wire:select```: ```html ``` Using the wire:select directive improves the user experience by allowing them to select options with ease and continue typing without the options changing. ## Component Types The base idea behind de default component is to keep things as simple and clean as possible without any constructor dependencies. Therefore I've decided to create multiple component to inherit from, who give you the option to use stuff like for instance property validation. ```php class Explanation extends Magewirephp\Magewire\Component {} // OR class Explanation extends \Magewirephp\Magewire\Component\Pagination {} ``` > **Note**: More core component types will be added over time. ## Magic Actions & Properties Toggle, set or emit without writing any PHP. ```html ``` ## Flash Messages Show a flash message on the page without a reload. ```php public function myCustomMessageMethod(string $message) { $this->dispatchErrorMessage($message); $this->dispatchWarningMessage($message); $this->dispatchNoticeMessage($message); $this->dispatchSuccessMessage($message); $this->dispatchMessage($messageType, $message); } ``` > **Translations**: Messages will automatically be transformed into a translatable phrase. > **Message Types**: When using the `dispatchMessage()` function, the first parameter must be [one of the message types implemented by `Magento\Framework\Message\MessageInterface`](https://github.com/magento/magento2/blob/2.4-develop/lib/internal/Magento/Framework/Message/MessageInterface.php#L24-L39). ## Redirects Redirect your customer with or without additional parameters. ```php public function myCustomRedirectMethod() { $this->redirect('/some/custom/path'); } ``` ```html
Thanks for your purchase! You will be redirected after 5 seconds.
``` ## Listeners & Emits Emit functionality in targeted or non-targeted components based on event listeners. ```php /** * Component A. * @block my.custom.block.name */ class A extends \Magewirephp\Magewire\Component { public function letsCallSomeone() { // Emit to every component who listens to 'youCanCallMe'. $this->emit('youCanCallMe', ['value' => 'hi there']); // Emit only to the 'my.custom.block.name' component. $this->emitTo('my.custom.block.name', 'youCanCallMe', ['value' => 'hi there']); } } /** * Component B. */ class B extends \Magewirephp\Magewire\Component { protected $listeners = ['youCanCallMe']; public function youCanCallMe($value) {} // OR protected $listeners = ['youCanCallMe' => 'toDoSomething']; public function toDoSomething($value) {} } ``` ### JavaScript (E&L) ```js // Emit to every component who listens to 'youCanCallMe'. Magewire.emit('youCanCallMe', {value: 'hi there'}) // Emit only to the 'my.custom.block.name' component. Magewire.emitTo('my.custom.block.name', 'youCanCallMe', {value: 'hi there'}) ``` ### Magic Actions Compatibility You are able to use magic methods within your emits if this is required. Thanks to this feature you are able to for example refresh a component or set data without having to write this functionality inside your targeted component. ```php /** * Component A. * @block my.custom.block.name */ class A extends \Magewirephp\Magewire\Component { public function setSomeProperties() { // Force refreshes for a separate component who is listening. $this->emit('$refresh', []); // Set a public property value for a separate component who is listening. $this->emitTo('layout.block.name', '$set', ['property' => 'someProperty', 'message' => 'hello world']); // Toggle a property value for a separate component who is listening. $this->emitTo('layout.block.name', '$toggle', ['value' => 'boolProperty']); } } /** * Component B. */ class B extends \Magewirephp\Magewire\Component { public $stringProperty; public $boolProperty = false; protected $listeners = ['$refresh']; // OR ['myEventName' => '$refresh'] // OR protected $listeners = ['$set']; // OR ['myEventName' => '$set'] // OR protected $listeners = ['$toggle']; // OR ['myEventName' => '$toggle'] } ``` > **Note**: Emits only work during subsequent requests. They won't be dispatched on page load when you emit them > in for example the ```mount()``` method. Use ```wire:init``` to dispatch a method on page load where an emit could > take place. ### Global Refresh Listener Each Magewire component has by default no ```$listeners``` attached to itself. Still, you're able to refresh a component from within another component thanks to a global ```refresh``` listener who get's injected during a preceding request. Thanks to this global listener, Magewire introduces the emitToRefresh method. This gives the option to refresh any component on the page from within your own component. ```php public function refreshSomeOtherComponent() { $this->emitToRefresh('layout.block.name'); } ``` OR ```html hasMyContent()): echo $magewire->getMyContent(); endif; ?> ``` ## Lazy Updating Prevent sending out requests for every press of a button. ```php class Explanation extends \Magewirephp\Magewire\Component { public $lazyProperty; } ``` ```html ``` ## Keydown Modifiers Perform actions on keydown. ```php public function keyUp() { $this->random(random_int(100, 999)); } public function keyDown() { $this->random(random_int(10, 99)); } ``` ```html ``` You can also use vanilla JS instead of a PHP class method. **Quick List** - backspace - escape - shift - tab - arrow- right / left / up / down ## Restricted Public Methods Public methods can be restricted from subsequent request executions. Prevent method executions who are meant for inside the phtml template only. It's not a best practice and you should use a ViewModel in most cases. ### Global (rpm) ```xml myCustomSetMethod ``` **File**: etc/frontend/di.xml ### Component (rpm) ```php class Explanation extends \Magewirephp\Magewire\Component { protected array $uncallables = ['myCustomSetMethod']; } ``` ## Pagination Render a pagination pager inside your Component's view. ```php class Explanation extends \Magewirephp\Magewire\Component\Pagination { public $page = 1; public $pageSize = 20; } ``` ```html
renderPagination() ?> renderPagination('My_Module::html/pagination/custom_pager') ?>
``` ## Query String > **Note**: The query string feature is currently incomplete compared to the original Laravel Livewire implementation. > At the moment you can only set public properties values via the URL when loading a page. Define public properties via URL params on a page load ```php class MySearchForm extends \Magewirephp\Magewire\Component { public $searchText; /** * @url https://your.domain/customer/account/dashboard?searchText=foo */ protected $queryString = [ 'searchText' ]; // OR /** * @url https://your.domain/customer/account/dashboard?q=foo */ protected $queryString = [ 'searchText' => [ 'alias' => 'q' // map 'searchText' as 'q' ] ]; } ``` ## Display Loading State Display a loading state only when performing a (targeted) subsequent method call. ```php class Explanation extends \Magewirephp\Magewire\Component { public $bar = null // Show a loading state for specific methods. protected $loader = ['foo']; // Shows 'Updating foo' as a notification when 'foo' gets re-synced. protected $loader = ['foo' => 'Updating foo']; // Shows 'Loading...' as a notification when there is a interaction with the component. protected $loader = 'Loading...'; // Prevent any loading activity displayment. protected $loader = false; // Prevent notification displayment and only show the main loader. protected $loader = true; // Show both messages for method execution and property syncing. protected $loader = [ '{public_property}' => 'Updating something', // e.g. 'bar' '{public_method}' => 'Executing something', // e.g. 'foo' '{listener_event}' => 'Some event was executed, I\'m executing method foo' // e.g. 'some_event' ]; protected $listeners = ['some_event' => 'foo']; public function foo() { // Loading state will stay active until the listener has run // Loading state will disapear when there are no active listeners $this->emit('some_event'); } public function bar() { // } } ``` > **Note**: Keep in mind that the ```$loader``` mapping only understands subsequent executable methods. ```html ``` ### Indicator Customization Since version: `1.10.0` ```xml ``` **File**: view/frontend/layout/default_hyva.xml ```html ``` **File**: html/magewire/loader.phtml ### Indicator Removal In some cases you want to implement your own loader because you have a global one in place or your just don't need to notify your customer. Whatever the case, I centered all frontend logic into two phtml files to let you do whatever's needed for the project. ```xml ``` **File**: view/frontend/layout/default_hyva.xml ### Custom Example (dls) You're able to build it more custom for your wired component only. ```html
``` ```php public function start(int $seconds) { // Let's take a nap. sleep($seconds); // Unlock the disabled state. $this->dispatchBrowserEvent('switch-state'); } ``` > **Note**: This is just an example. For a disabled state you should or could use the ```wire:loading``` directive. ### Notification Type Styling Each notification item can be one of three types (syncInput, fireEvent, callAction). By default, a notification item has the ```magewire-notification``` class. A related (kebab-cased) subclass will be bind dynamically based on the notification type. - ```magewire-notification fire-event``` - ```magewire-notification sync-input``` - ```magewire-notification call-method``` ## Plugins Plugins are a good and easy way to create frontend functionality which can hook into initial page loads, subsequent request hooks. ### Register a custom plugin To register a custom plugin, you can reference a specific container which will take care of rendering your custom code at the right place. An example on how you should register a custom frontend plugin: ```xml ``` ### Plugin: Loader ```xml Settings > Advanced > Developer > Magewire** or remove the block through layout XML. #### Loader System Settings > **Note**: All Magewire specific settings by default can be found at Store > Settings > Advanced > Developer > Magewire. - **Loader / Show:** Show or hide the global loading spinner. - **Loader / Enable Notifications:** Show or hide optional notification messages. - **Loader / Notifications / Message Fadeout Timeout:** Determine the duration for the message to fade out after its target component has fully loaded. The loader is divided into several elements, giving you greater flexibility in customizing the appearance of both the global spinner and notifications, without having to overwrite everything. All elements can be found in the Magewire core layout `default_hyva.xml` or be found in `Magewirephp_Magewire::html/loader`. ### Plugin: Error ```xml ``` ```html ``` #### Custom onError callback You can also overwrite or extend Magewire's onError callback. ```xml ``` ```html ``` ## Component Resolvers Since version: `1.9.0` The ResolverInterface enables you to implement your own method for constructing and reconstructing a component. This pattern is useful for examples like Dynamic Blocks and Widgets, which are not typically implemented using Layout XML. With this pattern, you can inject custom ResolverInterfaces that implement a function called complies(). In this function, you can verify if a given Block belongs to your custom Resolver. If it does, the Resolver will grab a unique name from that Resolver and automatically bind it onto the request fingerprint. This way, the right Resolver can be re-used when a component is reconstructed on a subsequent request. "Reconstruct" means that it uses all the ingredients to try and rebuild the component in the exact same way as it was constructed on page load. The gateway into Magewire remains the same, requiring a Block "magewire" data key. As soon as Magewire finds the required data key, it passes the block. However, it does require some custom logic to ensure your block complies with this requirement. If a block does have a magewire key, but none of the Resolvers comply, it will automatically fall back to the original Layout Resolver. This pattern is fully backwards compatible with older Magewire versions. ### Custom Resolver ```php use \Magento\Widget\Block\BlockInterface as WidgetBlockInterface class Widget implements ResolverInterface { public function getName() { return 'widget'; } /** * Check if the given block is of instance type WidgetBlockInterface */ public function complies(BlockInterface $block): bool { return $block instanceof WidgetBlockInterface; } public function construct(Template $block): Component { // Load a widget, construct and return the Component. } public function reconstruct(RequestInterface $request): Component { // Use what's available in the RequestInterface to reconstruct and return the component. } } ``` > **Important**: When it complies, the resolver name will be cached inside the Magewire Resolver cache based on the Block cache key. > This way, the Component Resolver doesnt have to verify each block over and over to check which Resolver complies to > the given block. Therefor it's important to be aware of this caching layer. ```xml Example\Module\Model\Magewire\Component\Resolver\Widget ``` **File**: etc/frontend/di.xml ## Reset Reset public property values to their initial state. ```php class Explanation extends \Magewirephp\Magewire\Component { public $foo; public $bar = true; public function boot(): void { $this->foo = 1337; } // Will reset all available pulbic properties. public function resetAll() { $this->reset(); } // Will only reset the 'foo' property. public function resetFoo() { $this->reset(['foo']); } // Will only reset the 'foo' property and run the boot() method afterwards. public function resetFooWithBoot() { $this->reset(['foo'], true); } } ``` ## Forms Validate forms based on optional rules and messages. ```php class Explanation extends \Magewirephp\Magewire\Component\Form { public $foo; // Always make sure the nested 'bar' property has a default value to avoid // bar being seen as a value of key zero. public $nesting = ['bar' => '']; // Determine the rules for your properties (optional). protected $rules = [ 'foo' => 'required|min:2', 'nesting.bar' => 'required|min:2|max:6', ]; // Overwrite default rule messages or define a global for each property (optional). protected $messages = [ 'foo:min' => 'He! the minimal input length of :attribute needs to be 2 instead of :value.', 'nesting.bar:required' => 'The "Nesting Bar" property can\'t be empty...', 'nesting.bar:max' => 'Take it easy, just six characters allowed.', ]; public function save() { // Will throw a ValidationException which extends from AcceptableException who won't break the lifecycle when // it gets thrown. Still you can catch it and change course if you need to. $this->validate(); $this->dispatchSuccessMessage('Validation succes'); } } ``` Go and read the [Rakit/Validation](https://github.com/rakit/validation) documentation for more information. ### Message Translations (f) Use Magento's regular i18n translations to translate form validation messages. ```csv ":attribute value (:value) has a minimal length of two.",":attribute value (:value) has a minimal length of two." ``` > **Note**: Both ```:attribute``` and ```:value``` can be used when required. ### Message Displayment (f) By default, messages aren't shown on the page after a validation failure. You have to put in some work in order to let the user know what happend. This can be done in several ways. #### Example 1 (md) Show corresponding error messages below the field. ```html
hasError('foo')): ?> getError('foo') ?>
``` #### Example 2 (md) Display a stack of error messages on above the form. ```html hasErrors(): ?>
    getErrors() as $error): ?>
``` #### Example 3 (md) Within this example, we use a try-catch structure to catch optional validation failure. This isn't required by default where the lifecycle is able to handle these ValidationException's by default. ```php class Explanation extends \Magewirephp\Magewire\Component\Form { public $foo; public $rules = [ 'foo' => 'required|min:2', ]; public function save() { try { $this->validate(); } catch (\Magewirephp\Magewire\Exception\ValidationException $exception) { foreach ($this->getErrors() as $error) { $this->dispatchErrorMessage($error); } } } } ``` #### Example 4 (md) Catch global validation exceptions. ```php class Explanation extends \Magewirephp\Magewire\Component\Form { public $foo; public $rules = [ 'foo' => 'required|min:2', ]; public function save() { try { $this->validate(); } catch (\Magewirephp\Magewire\Exception\AcceptableException $exception) { // When you want to render the error in the view (key can be changed if required). $this->clearErrors()->error('validation_exception', $exception->getMessage()); // When you want to notify the customer with a flash message. $this->dispatchErrorMessage($exception->getMessage()); } } } ``` ```html
hasError('validation_exception'): ?>

Form exception thrown: getError('validation_exception') ?>

```