├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── wire-extender.php ├── js └── wire-extender.js ├── package.json ├── routes └── api.php └── src ├── Attributes └── Embeddable.php ├── Http ├── Controllers │ └── EmbedController.php └── Middlewares │ └── IgnoreForWireExtender.php ├── WireExtender.php └── WireExtenderServiceProvider.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: PhiloNL 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Philo Hermans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Wire Extender

2 | 3 |

4 | Total Downloads 5 | Latest Stable Version 6 | License 7 |

8 | 9 | ## Embed Livewire Components Using Wire Extender 10 | Wire Extender allows you to embed any Livewire component on any website or even within a static html file. 11 | 12 | ## Documentation 13 | You can find the [full documentation on our site](https://wire-elements.dev/blog/embed-livewire-components-using-wire-extender). 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wire-elements/wire-extender", 3 | "description": "Embed your Livewire components anywhere.", 4 | "require": { 5 | "php": "^8.1", 6 | "livewire/livewire": "^v3.4.7" 7 | }, 8 | "license": "MIT", 9 | "autoload": { 10 | "psr-4": { 11 | "WireElements\\WireExtender\\": "src/" 12 | } 13 | }, 14 | "authors": [ 15 | { 16 | "name": "Philo Hermans", 17 | "email": "me@philo.dev" 18 | } 19 | ], 20 | "extra": { 21 | "laravel": { 22 | "providers": [ 23 | "WireElements\\WireExtender\\WireExtenderServiceProvider" 24 | ] 25 | } 26 | }, 27 | "minimum-stability": "dev", 28 | "prefer-stable": true 29 | } 30 | -------------------------------------------------------------------------------- /config/wire-extender.php: -------------------------------------------------------------------------------- 1 | [ 9 | // 'web', 10 | ], 11 | ]; 12 | -------------------------------------------------------------------------------- /js/wire-extender.js: -------------------------------------------------------------------------------- 1 | let livewireScript; 2 | let componentAssets; 3 | let currentScript = document.currentScript; 4 | let livewireStarted = false; 5 | 6 | function getUri(append = '') 7 | { 8 | let uri = document.querySelector('[data-uri]')?.getAttribute('data-uri'); 9 | 10 | if (!uri) { 11 | uri = new URL(currentScript.src).origin; 12 | } 13 | 14 | if (!uri.endsWith('/')) { 15 | uri += '/'; 16 | } 17 | 18 | return uri + append; 19 | } 20 | 21 | function getLivewireAssetUri() 22 | { 23 | return document.querySelector('[data-livewire-asset-uri]')?.getAttribute('data-livewire-asset-uri') ?? getUri('livewire/livewire.min.js'); 24 | } 25 | 26 | function getLivewireUpdateUri() 27 | { 28 | return document.querySelector('[data-update-uri]')?.getAttribute('data-update-uri') ?? getUri('livewire/update'); 29 | } 30 | 31 | function getEmbedUri() 32 | { 33 | const base = document.querySelector('[data-embed-uri]')?.getAttribute('data-embed-uri') ?? getUri('livewire/embed'); 34 | const queryString = window.location.search; 35 | 36 | return base + queryString; 37 | } 38 | 39 | function injectLivewire() 40 | { 41 | if (window.Livewire || livewireStarted) { 42 | return; 43 | } 44 | 45 | const style = document.createElement('style'); 46 | style.innerHTML = '[wire\\:loading][wire\\:loading], [wire\\:loading\\.delay][wire\\:loading\\.delay], [wire\\:loading\\.inline-block][wire\\:loading\\.inline-block], [wire\\:loading\\.inline][wire\\:loading\\.inline], [wire\\:loading\\.block][wire\\:loading\\.block], [wire\\:loading\\.flex][wire\\:loading\\.flex], [wire\\:loading\\.table][wire\\:loading\\.table], [wire\\:loading\\.grid][wire\\:loading\\.grid], [wire\\:loading\\.inline-flex][wire\\:loading\\.inline-flex] {display: none;}[wire\\:loading\\.delay\\.none][wire\\:loading\\.delay\\.none], [wire\\:loading\\.delay\\.shortest][wire\\:loading\\.delay\\.shortest], [wire\\:loading\\.delay\\.shorter][wire\\:loading\\.delay\\.shorter], [wire\\:loading\\.delay\\.short][wire\\:loading\\.delay\\.short], [wire\\:loading\\.delay\\.default][wire\\:loading\\.delay\\.default], [wire\\:loading\\.delay\\.long][wire\\:loading\\.delay\\.long], [wire\\:loading\\.delay\\.longer][wire\\:loading\\.delay\\.longer], [wire\\:loading\\.delay\\.longest][wire\\:loading\\.delay\\.longest] {display: none;}[wire\\:offline][wire\\:offline] {display: none;}[wire\\:dirty]:not(textarea):not(input):not(select) {display: none;}:root {--livewire-progress-bar-color: #2299dd;}[x-cloak] {display: none !important;}'; 47 | document.head.appendChild(style); 48 | 49 | livewireScript = document.createElement('script'); 50 | livewireScript.src = getLivewireAssetUri(); 51 | livewireScript.dataset.csrf = ''; 52 | livewireScript.dataset.updateUri = getLivewireUpdateUri(); 53 | document.body.appendChild(livewireScript); 54 | } 55 | 56 | function waitForLivewireAndStart() { 57 | if (livewireStarted) { 58 | return; 59 | } 60 | 61 | if(window.Livewire) { 62 | startLivewire(); 63 | } 64 | livewireScript.onload = async function () { 65 | await startLivewire(); 66 | } 67 | 68 | livewireStarted = true; 69 | } 70 | 71 | async function startLivewire(assets) 72 | { 73 | Livewire.hook('request', ({ options }) => { 74 | options.headers['X-Wire-Extender'] = ''; 75 | options.credentials = 'include'; 76 | }) 77 | await Livewire.triggerAsync('payload.intercept', {assets: componentAssets}); 78 | Livewire.start(); 79 | } 80 | 81 | function renderComponents(components) 82 | { 83 | injectLivewire(); 84 | 85 | fetch(getEmbedUri(), { 86 | method: 'POST', 87 | headers: { 88 | 'Content-Type': 'application/json', 89 | }, 90 | body: JSON.stringify({ 91 | components: components 92 | }), 93 | 'credentials': 'include' 94 | }) 95 | .then(response => response.json()) 96 | .then(data => { 97 | for (let component in data.components) { 98 | let el = document.querySelector(`[data-component-key="${component}"]`); 99 | el.innerHTML = data.components[component]; 100 | } 101 | 102 | componentAssets = data.assets; 103 | waitForLivewireAndStart(); 104 | }); 105 | } 106 | 107 | document.addEventListener('DOMContentLoaded', function() { 108 | let components = []; 109 | 110 | document.querySelectorAll('livewire').forEach((el) => { 111 | if (!el.hasAttribute('data-component-key')) { 112 | el.setAttribute('data-component-key', Math.random().toString(36).substring(2)); 113 | } 114 | 115 | components.push({ 116 | key: el.getAttribute('data-component-key'), 117 | name: el.getAttribute('data-component'), 118 | params: el.getAttribute('data-params') 119 | }); 120 | }); 121 | 122 | renderComponents(components); 123 | }); 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wire-elements/wire-extender", 3 | "version": "0.0.6", 4 | "description": "Wire Extender allows you to embed any Livewire component on any website or even within a static HTML file.", 5 | "main": "js/wire-extender.js", 6 | "files": [ 7 | "js/wire-extender.js" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/wire-elements/wire-extender.git" 12 | }, 13 | "keywords": [ 14 | "livewire", 15 | "embed", 16 | "laravel" 17 | ], 18 | "author": "Philo Hermans", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wire-elements/wire-extender/issues" 22 | }, 23 | "homepage": "https://github.com/wire-elements/wire-extender#readme" 24 | } 25 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | json('components', []))->mapWithKeys(function ($component) { 16 | $componentKey = $component['key']; 17 | $componentName = $component['name']; 18 | $componentParams = json_decode($component['params'], true) ?? []; 19 | 20 | if (WireExtender::isEmbeddable($componentName) === false) { 21 | return [$componentName => null]; 22 | } 23 | 24 | return [ 25 | $componentKey => Blade::render('@livewire($component, $params, key($key))', [ 26 | 'key' => $componentKey, 27 | 'component' => $componentName, 28 | 'params' => $componentParams, 29 | ]), 30 | ]; 31 | })->filter(); 32 | 33 | return [ 34 | 'components' => $components, 35 | 'assets' => SupportScriptsAndAssets::getAssets(), 36 | ]; 37 | } 38 | 39 | public static function middleware() 40 | { 41 | return config('wire-extender.middlewares', []); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/Middlewares/IgnoreForWireExtender.php: -------------------------------------------------------------------------------- 1 | isLivewireUpdateRequest($request)) { 24 | return parent::handle($request, $next); 25 | } 26 | 27 | // Loop through all components that are part of the update 28 | foreach ($request->json('components', []) as $component) { 29 | $snapshot = json_decode($component['snapshot'], true); 30 | $component = $snapshot['memo']['name'] ?? false; 31 | 32 | // All components must be embeddable otherwise we will apply the existing middleware 33 | if (WireExtender::isEmbeddable($component) === false) { 34 | return parent::handle($request, $next); 35 | } 36 | } 37 | 38 | return $next($request); 39 | } 40 | 41 | private function isLivewireUpdateRequest($request): bool 42 | { 43 | return $request->method() === 'POST' && 44 | app(LivewireManager::class)->getUpdateUri() === $request->getRequestUri() && 45 | $request->hasHeader('X-Wire-Extender') && 46 | $request->hasHeader('X-Livewire'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/WireExtender.php: -------------------------------------------------------------------------------- 1 | new($component)); 16 | $embedAttribute = $reflectionClass->getAttributes(Embeddable::class)[0] ?? null; 17 | 18 | return is_null($embedAttribute) === false; 19 | } catch (ComponentNotFoundException $e) { 20 | return false; 21 | } 22 | 23 | return true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/WireExtenderServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerPackageRoutes(); 15 | $this->registerAssets(); 16 | $this->registerConfig(); 17 | } 18 | 19 | /* 20 | * Register package routes. 21 | */ 22 | private function registerPackageRoutes(): void 23 | { 24 | $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); 25 | } 26 | 27 | /* 28 | * Register package assets. 29 | */ 30 | private function registerAssets(): void 31 | { 32 | $this->publishes([ 33 | __DIR__.'/../js' => public_path('vendor/wire-elements'), 34 | ], 'wire-extender'); 35 | } 36 | 37 | /* 38 | * Register package config. 39 | */ 40 | private function registerConfig(): void 41 | { 42 | $this->mergeConfigFrom(__DIR__.'/../config/wire-extender.php', 'wire-extender'); 43 | } 44 | } 45 | --------------------------------------------------------------------------------