├── .github ├── FUNDING.yml └── SECURITY.md ├── .gitignore ├── LICENSE.md ├── README.md ├── babel.config.js ├── composer.json ├── css └── styles.css ├── dist ├── alpinejs@3.13.10.min.js ├── init.js ├── manifest.json ├── wire.js └── wire.js.map ├── js ├── DirectiveManager.js ├── HookManager.js ├── Message.js ├── MessageBag.js ├── MessageBus.js ├── PrefetchMessage.js ├── Store.js ├── action │ ├── deferred-model.js │ ├── event.js │ ├── index.js │ ├── method.js │ └── model.js ├── component │ ├── DirtyStates.js │ ├── DisableForms.js │ ├── FileDownloads.js │ ├── FileUploads.js │ ├── LoadingStates.js │ ├── OfflineStates.js │ ├── Polling.js │ ├── PrefetchManager.js │ ├── SupportAlpine.js │ ├── SupportStacks.js │ ├── SyncBrowserHistory.js │ ├── UploadManager.js │ └── index.js ├── connection │ └── index.js ├── dom │ ├── dom.js │ ├── morphdom │ │ ├── index.js │ │ ├── morphAttrs.js │ │ ├── morphdom.js │ │ ├── specialElHandlers.js │ │ └── util.js │ └── polyfills │ │ ├── index.js │ │ └── modules │ │ ├── es.element.closest.js │ │ ├── es.element.get-attribute-names.js │ │ └── es.element.matches.js ├── index.js ├── node_initializer.js └── util │ ├── debounce.js │ ├── dispatch.js │ ├── getAppUrl.js │ ├── getCsrfToken.js │ ├── index.js │ ├── walk.js │ └── wire-directives.js ├── package-lock.json ├── package.json ├── roadmap.md ├── rollup.config.js ├── ruleset.xml ├── src ├── AddAttributesToRootTagOfHtml.php ├── CacheTrait.php ├── ComponentChecksumManager.php ├── ComponentConcerns │ ├── HandlesActions.php │ ├── PerformsRedirects.php │ ├── ReceivesEvents.php │ └── ValidatesInput.php ├── Controller │ ├── CanPretendToBeAFile.php │ ├── FilePreviewHandler.php │ ├── FileUploadHandler.php │ └── HttpConnectionHandler.php ├── Drush │ └── Generators │ │ ├── WireComponentCreateGenerator.php │ │ └── templates │ │ ├── _lib │ │ └── di.twig │ │ ├── wire_component.class.twig │ │ └── wire_component.tpl.twig ├── Element │ └── WireElement.php ├── Event.php ├── Exceptions │ ├── AccessDeniedException.php │ ├── CannotUseReservedWireComponentProperties.php │ ├── CorruptComponentPayloadException.php │ ├── DirectlyCallingLifecycleHooksNotAllowedException.php │ ├── MethodNotFoundException.php │ ├── MissingFileUploadsTraitException.php │ ├── NonPublicComponentMethodCall.php │ ├── PropertyNotFoundException.php │ ├── PublicPropertyNotFoundException.php │ ├── PublicPropertyTypeNotAllowedException.php │ └── ValidationException.php ├── Features │ ├── OptimizeRenderedDom.php │ ├── SupportBootMethod.php │ ├── SupportBrowserHistory.php │ ├── SupportComponentTraits.php │ ├── SupportEvents.php │ ├── SupportFileUploads.php │ ├── SupportRedirects.php │ └── SupportRootElementTracking.php ├── FileUploadConfiguration.php ├── InteractsWithProperties.php ├── LifecycleManager.php ├── Plugin │ ├── Annotation │ │ └── WireComponent.php │ ├── Attribute │ │ ├── WireCache.php │ │ └── WireComponent.php │ ├── WireComponent │ │ └── Broken.php │ └── WirePluginManager.php ├── Request.php ├── Response.php ├── StackMiddleware │ ├── HydrationMiddleware │ │ ├── CallHydrationHooks.php │ │ ├── CallPropertyHydrationHooks.php │ │ ├── HashDataPropertiesForDirtyDetection.php │ │ ├── HydratePublicProperties.php │ │ ├── HydrationMiddleware.php │ │ ├── NormalizeComponentPropertiesForJavaScript.php │ │ ├── NormalizeDataForJavaScript.php │ │ ├── NormalizeServerMemoSansDataForJavaScript.php │ │ ├── PerformAccessCheck.php │ │ ├── PerformActionCalls.php │ │ ├── PerformDataBindingUpdates.php │ │ ├── PerformEventEmissions.php │ │ ├── RenderView.php │ │ └── SecureHydrationWithChecksum.php │ └── RegisterWireMiddleware.php ├── TemporaryUploadedFile.php ├── Twig │ └── Extension │ │ └── WireTwigExtension.php ├── View.php ├── Wire.php ├── WireComponent.php ├── WireComponentBase.php ├── WireComponentInterface.php ├── WireComponentsRegistry.php ├── WireManager.php ├── Wireable.php ├── WithFileUploads.php └── WithPagination.php ├── wire.info.yml ├── wire.libraries.yml ├── wire.module ├── wire.routing.yml └── wire.services.yml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [wire-drupal] -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | **PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY.** 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you discover a security vulnerability within WireDrupal, please send an email at hugronaphor@gmail.com. All security vulnerabilities will be promptly addressed. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock 4 | node_modules 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright © Caleb Porzio 4 | 5 | The original codebase has been adapted to integrate with Drupal. 6 | 7 | Modifications made by Cornel Andreev are also licensed under the same terms and conditions as the original software. 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![WireDrupal](https://avatars.githubusercontent.com/u/126767236?s=200) 2 | # WireDrupal 3 | 4 | [![Packagist Version](https://img.shields.io/packagist/v/wire-drupal/wire)](https://packagist.org/packages/wire-drupal/wire) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/wire-drupal/wire)](https://packagist.org/packages/wire-drupal/wire) 6 | [![License](https://img.shields.io/packagist/l/wire-drupal/wire)](https://packagist.org/packages/wire-drupal/wire) 7 | 8 | Wire is a developer tool to build Dynamic interfaces for Drupal. 9 | 10 | --- 11 | All Docs available at: https://wire-drupal.com/docs 12 | 13 | Contributions are welcomed! 14 | 15 | Ideas, questions and bugs should be discussed in [Discussions](https://github.com/wire-drupal/wire/discussions). 16 | 17 | ## License 18 | 19 | Copyright © Cornel Andreev 20 | 21 | WireDrupal is open-sourced software licensed under the [MIT license](LICENSE.md). 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | edge: '18', 9 | }, 10 | }, 11 | ], 12 | ], 13 | plugins: [ 14 | "@babel/plugin-proposal-object-rest-spread", 15 | ], 16 | env: { 17 | test: { 18 | presets: [ 19 | [ 20 | '@babel/preset-env', 21 | { 22 | targets: { 23 | node: 'current', 24 | }, 25 | } 26 | ] 27 | ] 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wire-drupal/wire", 3 | "description": "Dynamic interfaces for Drupal.", 4 | "type": "drupal-module", 5 | "license": "MIT", 6 | "docs": "https://wire-drupal.com/docs", 7 | "security": "https://github.com/wire-drupal/wire/blob/main/.github/SECURITY.md", 8 | "authors": [ 9 | { 10 | "name": "Cornel Andreev", 11 | "email": "hugronaphor@gmail.com" 12 | } 13 | ], 14 | "repositories": [ 15 | { 16 | "type": "composer", 17 | "url": "https://packages.drupal.org/8" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.1", 22 | "ext-json": "*", 23 | "drupal/core": "^10.2 || ^11", 24 | "illuminate/support": "^10.6" 25 | }, 26 | "require-dev": { 27 | "slevomat/coding-standard": "~8.0", 28 | "php-parallel-lint/php-parallel-lint": "^1.3" 29 | }, 30 | "suggest": { 31 | "drush/drush": "To generate Wire components Drush ^12 is required" 32 | }, 33 | "autoload": { 34 | "files": [ 35 | "src/TemporaryUploadedFile.php" 36 | ] 37 | }, 38 | "config": { 39 | "allow-plugins": { 40 | "dealerdirect/phpcodesniffer-composer-installer": true 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /css/styles.css: -------------------------------------------------------------------------------- 1 | [wire\:loading], [wire\:loading\.delay], [wire\:loading\.inline-block], [wire\:loading\.inline], [wire\:loading\.block], [wire\:loading\.flex], [wire\:loading\.table], [wire\:loading\.grid], [wire\:loading\.inline-flex] { 2 | display: none; 3 | } 4 | 5 | [wire\:loading\.delay\.shortest], [wire\:loading\.delay\.shorter], [wire\:loading\.delay\.short], [wire\:loading\.delay\.long], [wire\:loading\.delay\.longer], [wire\:loading\.delay\.longest] { 6 | display:none; 7 | } 8 | 9 | [wire\:offline] { 10 | display: none; 11 | } 12 | 13 | [wire\:dirty]:not(textarea):not(input):not(select) { 14 | display: none; 15 | } 16 | 17 | input:-webkit-autofill, select:-webkit-autofill, textarea:-webkit-autofill { 18 | animation-duration: 50000s; 19 | animation-name: wireautofill; 20 | } 21 | 22 | @keyframes wireautofill { from {} } 23 | -------------------------------------------------------------------------------- /dist/init.js: -------------------------------------------------------------------------------- 1 | if (window.wire === undefined) { 2 | window.wire = new Wire(); 3 | window.Wire = window.wire; 4 | } 5 | 6 | if (window.Alpine) { 7 | document.addEventListener("DOMContentLoaded", function () { 8 | setTimeout(function () { 9 | console.warn("Wire: It looks like AlpineJS has already been loaded. Make sure Wire\'s scripts are loaded before Alpine.\\n\\n Reference docs for more info: https://wire-drupal.com/docs/alpine-js") 10 | }) 11 | }); 12 | } 13 | 14 | window.deferLoadingAlpine = function (callback) { 15 | window.addEventListener('wire:load', function () { 16 | callback(); 17 | }); 18 | }; 19 | 20 | var started = false; 21 | 22 | window.addEventListener('alpine:initializing', function () { 23 | if (!started) { 24 | window.wire.start(); 25 | 26 | started = true; 27 | } 28 | }); 29 | 30 | document.addEventListener("DOMContentLoaded", function () { 31 | if (!started) { 32 | window.wire.start(); 33 | 34 | started = true; 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /dist/manifest.json: -------------------------------------------------------------------------------- 1 | {"/wire.js":"/wire.js?id=485a635b2e59561b5604"} -------------------------------------------------------------------------------- /js/DirectiveManager.js: -------------------------------------------------------------------------------- 1 | import MessageBus from "./MessageBus" 2 | 3 | export default { 4 | directives: new MessageBus, 5 | 6 | register(name, callback) { 7 | if (this.has(name)) { 8 | throw `Wire: Directive already registered: [${name}]` 9 | } 10 | 11 | this.directives.register(name, callback) 12 | }, 13 | 14 | call(name, el, directive, component) { 15 | this.directives.call(name, el, directive, component) 16 | }, 17 | 18 | has(name) { 19 | return this.directives.has(name) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /js/HookManager.js: -------------------------------------------------------------------------------- 1 | import MessageBus from './MessageBus' 2 | 3 | export default { 4 | availableHooks: [ 5 | /** 6 | * Public Hooks 7 | */ 8 | 'component.initialized', 9 | 'element.initialized', 10 | 'element.updating', 11 | 'element.updated', 12 | 'element.removed', 13 | 'message.sent', 14 | 'message.failed', 15 | 'message.received', 16 | 'message.processed', 17 | 18 | /** 19 | * Private Hooks 20 | */ 21 | 'interceptWireModelSetValue', 22 | 'interceptWireModelAttachListener', 23 | 'beforeReplaceState', 24 | 'beforePushState', 25 | ], 26 | 27 | bus: new MessageBus(), 28 | 29 | register(name, callback) { 30 | if (! this.availableHooks.includes(name)) { 31 | throw `Wire: Referencing unknown hook: [${name}]` 32 | } 33 | 34 | this.bus.register(name, callback) 35 | }, 36 | 37 | call(name, ...params) { 38 | this.bus.call(name, ...params) 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /js/Message.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor(component, updateQueue) { 3 | this.component = component 4 | this.updateQueue = updateQueue 5 | } 6 | 7 | payload() { 8 | return { 9 | fingerprint: this.component.fingerprint, 10 | serverMemo: this.component.serverMemo, 11 | // This ensures only the type & payload properties only get sent over. 12 | updates: this.updateQueue.map(update => ({ 13 | type: update.type, 14 | payload: update.payload, 15 | })), 16 | } 17 | } 18 | 19 | shouldSkipWatcherForDataKey(dataKey) { 20 | // If the data is dirty, run the watcher. 21 | if (this.response.effects.dirty.includes(dataKey)) return false 22 | 23 | let compareBeforeFirstDot = (subject, value) => { 24 | if (typeof subject !== 'string' || typeof value !== 'string') return false 25 | 26 | return subject.split('.')[0] === value.split('.')[0] 27 | } 28 | 29 | // Otherwise see if there was a defered update for a data key. 30 | // In that case, we want to skip running the Wire watcher. 31 | return this.updateQueue 32 | .filter(update => compareBeforeFirstDot(update.name, dataKey)) 33 | .some(update => update.skipWatcher) 34 | } 35 | 36 | storeResponse(payload) { 37 | return (this.response = payload) 38 | } 39 | 40 | resolve() { 41 | let returns = this.response.effects.returns || [] 42 | 43 | this.updateQueue.forEach(update => { 44 | if (update.type !== 'callMethod') return 45 | 46 | update.resolve( 47 | returns[update.signature] !== undefined 48 | ? returns[update.signature] 49 | : (returns[update.method] !== undefined 50 | ? returns[update.method] 51 | : null) 52 | ) 53 | }) 54 | } 55 | 56 | reject() { 57 | this.updateQueue.forEach(update => { 58 | update.reject() 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /js/MessageBag.js: -------------------------------------------------------------------------------- 1 | 2 | export default class MessageBag { 3 | constructor() { 4 | this.bag = {} 5 | } 6 | 7 | add(name, thing) { 8 | if (! this.bag[name]) { 9 | this.bag[name] = [] 10 | } 11 | 12 | this.bag[name].push(thing) 13 | } 14 | 15 | push(name, thing) { 16 | this.add(name, thing) 17 | } 18 | 19 | first(name) { 20 | if (! this.bag[name]) return null 21 | 22 | return this.bag[name][0] 23 | } 24 | 25 | last(name) { 26 | return this.bag[name].slice(-1)[0] 27 | } 28 | 29 | get(name) { 30 | return this.bag[name] 31 | } 32 | 33 | shift(name) { 34 | return this.bag[name].shift() 35 | } 36 | 37 | call(name, ...params) { 38 | (this.listeners[name] || []).forEach(callback => { 39 | callback(...params) 40 | }) 41 | } 42 | 43 | has(name) { 44 | return Object.keys(this.listeners).includes(name) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /js/MessageBus.js: -------------------------------------------------------------------------------- 1 | 2 | export default class MessageBus { 3 | constructor() { 4 | this.listeners = {} 5 | } 6 | 7 | register(name, callback) { 8 | if (! this.listeners[name]) { 9 | this.listeners[name] = [] 10 | } 11 | 12 | this.listeners[name].push(callback) 13 | } 14 | 15 | call(name, ...params) { 16 | (this.listeners[name] || []).forEach(callback => { 17 | callback(...params) 18 | }) 19 | } 20 | 21 | has(name) { 22 | return Object.keys(this.listeners).includes(name) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /js/PrefetchMessage.js: -------------------------------------------------------------------------------- 1 | import Message from '@/Message' 2 | 3 | export default class extends Message { 4 | constructor(component, action) { 5 | super(component, [action]) 6 | } 7 | 8 | get prefetchId() { 9 | return this.updateQueue[0].toId() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /js/Store.js: -------------------------------------------------------------------------------- 1 | import EventAction from '@/action/event' 2 | import HookManager from '@/HookManager' 3 | import MessageBus from './MessageBus' 4 | import DirectiveManager from './DirectiveManager' 5 | 6 | const store = { 7 | componentsById: {}, 8 | listeners: new MessageBus(), 9 | initialRenderIsFinished: false, 10 | wireIsInBackground: false, 11 | wireIsOffline: false, 12 | sessionHasExpired: false, 13 | sessionHasExpiredCallback: undefined, 14 | directives: DirectiveManager, 15 | hooks: HookManager, 16 | onErrorCallback: () => { }, 17 | 18 | components() { 19 | return Object.keys(this.componentsById).map(key => { 20 | return this.componentsById[key] 21 | }) 22 | }, 23 | 24 | addComponent(component) { 25 | return (this.componentsById[component.id] = component) 26 | }, 27 | 28 | findComponent(id) { 29 | return this.componentsById[id] 30 | }, 31 | 32 | getComponentsByName(name) { 33 | return this.components().filter(component => { 34 | return component.name === name 35 | }) 36 | }, 37 | 38 | hasComponent(id) { 39 | return !!this.componentsById[id] 40 | }, 41 | 42 | tearDownComponents() { 43 | this.components().forEach(component => { 44 | this.removeComponent(component) 45 | }) 46 | }, 47 | 48 | on(event, callback) { 49 | this.listeners.register(event, callback) 50 | }, 51 | 52 | emit(event, ...params) { 53 | this.listeners.call(event, ...params) 54 | 55 | this.componentsListeningForEvent(event).forEach(component => 56 | component.addAction(new EventAction(event, params)) 57 | ) 58 | }, 59 | 60 | emitUp(el, event, ...params) { 61 | this.componentsListeningForEventThatAreTreeAncestors( 62 | el, 63 | event 64 | ).forEach(component => 65 | component.addAction(new EventAction(event, params)) 66 | ) 67 | }, 68 | 69 | emitSelf(componentId, event, ...params) { 70 | let component = this.findComponent(componentId) 71 | 72 | if (component.listeners.includes(event)) { 73 | component.addAction(new EventAction(event, params)) 74 | } 75 | }, 76 | 77 | emitTo(componentName, event, ...params) { 78 | let components = this.getComponentsByName(componentName) 79 | 80 | components.forEach(component => { 81 | if (component.listeners.includes(event)) { 82 | component.addAction(new EventAction(event, params)) 83 | } 84 | }) 85 | }, 86 | 87 | componentsListeningForEventThatAreTreeAncestors(el, event) { 88 | var parentIds = [] 89 | 90 | var parent = el.parentElement.closest('[wire\\:id]') 91 | 92 | while (parent) { 93 | parentIds.push(parent.getAttribute('wire:id')) 94 | 95 | parent = parent.parentElement.closest('[wire\\:id]') 96 | } 97 | 98 | return this.components().filter(component => { 99 | return ( 100 | component.listeners.includes(event) && 101 | parentIds.includes(component.id) 102 | ) 103 | }) 104 | }, 105 | 106 | componentsListeningForEvent(event) { 107 | return this.components().filter(component => { 108 | return component.listeners.includes(event) 109 | }) 110 | }, 111 | 112 | registerDirective(name, callback) { 113 | this.directives.register(name, callback) 114 | }, 115 | 116 | registerHook(name, callback) { 117 | this.hooks.register(name, callback) 118 | }, 119 | 120 | callHook(name, ...params) { 121 | this.hooks.call(name, ...params) 122 | }, 123 | 124 | changeComponentId(component, newId) { 125 | let oldId = component.id 126 | 127 | component.id = newId 128 | component.fingerprint.id = newId 129 | 130 | this.componentsById[newId] = component 131 | 132 | delete this.componentsById[oldId] 133 | 134 | // Now go through any parents of this component and change 135 | // the component's child id references. 136 | this.components().forEach(component => { 137 | let children = component.serverMemo.children || {} 138 | 139 | Object.entries(children).forEach(([key, { id, tagName }]) => { 140 | if (id === oldId) { 141 | children[key].id = newId 142 | } 143 | }) 144 | }) 145 | }, 146 | 147 | removeComponent(component) { 148 | // Remove event listeners attached to the DOM. 149 | component.tearDown() 150 | // Remove the component from the store. 151 | delete this.componentsById[component.id] 152 | }, 153 | 154 | onError(callback) { 155 | this.onErrorCallback = callback 156 | }, 157 | 158 | getClosestParentId(childId, subsetOfParentIds) { 159 | let distancesByParentId = {} 160 | 161 | subsetOfParentIds.forEach(parentId => { 162 | let distance = this.getDistanceToChild(parentId, childId) 163 | 164 | if (distance) distancesByParentId[parentId] = distance 165 | }) 166 | 167 | let smallestDistance = Math.min(...Object.values(distancesByParentId)) 168 | 169 | let closestParentId 170 | 171 | Object.entries(distancesByParentId).forEach(([parentId, distance]) => { 172 | if (distance === smallestDistance) closestParentId = parentId 173 | }) 174 | 175 | return closestParentId 176 | }, 177 | 178 | getDistanceToChild(parentId, childId, distanceMemo = 1) { 179 | let parentComponent = this.findComponent(parentId) 180 | 181 | if (! parentComponent) return 182 | 183 | let childIds = parentComponent.childIds 184 | 185 | if (childIds.includes(childId)) return distanceMemo 186 | 187 | for (let i = 0; i < childIds.length; i++) { 188 | let distance = this.getDistanceToChild(childIds[i], childId, distanceMemo + 1) 189 | 190 | if (distance) return distance 191 | } 192 | } 193 | } 194 | 195 | export default store 196 | -------------------------------------------------------------------------------- /js/action/deferred-model.js: -------------------------------------------------------------------------------- 1 | import Action from '.' 2 | 3 | export default class extends Action { 4 | constructor(name, value, el, skipWatcher = false) { 5 | super(el, skipWatcher) 6 | 7 | this.type = 'syncInput' 8 | this.name = name 9 | this.payload = { 10 | id: this.signature, 11 | name, 12 | value, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/action/event.js: -------------------------------------------------------------------------------- 1 | import Action from '.' 2 | 3 | export default class extends Action { 4 | constructor(event, params, el) { 5 | super(el) 6 | 7 | this.type = 'fireEvent' 8 | this.payload = { 9 | id: this.signature, 10 | event, 11 | params, 12 | } 13 | } 14 | 15 | // Overriding toId() because some EventActions don't have an "el" 16 | toId() { 17 | return btoa(encodeURIComponent(this.type, this.payload.event, JSON.stringify(this.payload.params))) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /js/action/index.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor(el, skipWatcher = false) { 3 | this.el = el 4 | this.skipWatcher = skipWatcher 5 | this.resolveCallback = () => { } 6 | this.rejectCallback = () => { } 7 | this.signature = (Math.random() + 1).toString(36).substring(8) 8 | } 9 | 10 | toId() { 11 | return btoa(encodeURIComponent(this.el.outerHTML)) 12 | } 13 | 14 | onResolve(callback) { 15 | this.resolveCallback = callback 16 | } 17 | 18 | onReject(callback) { 19 | this.rejectCallback = callback 20 | } 21 | 22 | resolve(thing) { 23 | this.resolveCallback(thing) 24 | } 25 | 26 | reject(thing) { 27 | this.rejectCallback(thing) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /js/action/method.js: -------------------------------------------------------------------------------- 1 | import Action from '.' 2 | 3 | export default class extends Action { 4 | constructor(method, params, el, skipWatcher = false) { 5 | super(el, skipWatcher) 6 | 7 | this.type = 'callMethod' 8 | this.method = method 9 | this.payload = { 10 | id: this.signature, 11 | method, 12 | params, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/action/model.js: -------------------------------------------------------------------------------- 1 | import Action from '.' 2 | 3 | export default class extends Action { 4 | constructor(name, value, el) { 5 | super(el) 6 | 7 | this.type = 'syncInput' 8 | this.name = name 9 | this.payload = { 10 | id: this.signature, 11 | name, 12 | value, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/component/DirtyStates.js: -------------------------------------------------------------------------------- 1 | import store from '@/Store' 2 | import DOM from '../dom/dom' 3 | import { wireDirectives } from '../util' 4 | 5 | export default function () { 6 | store.registerHook('component.initialized', component => { 7 | component.dirtyEls = [] 8 | }) 9 | 10 | store.registerHook('element.initialized', (el, component) => { 11 | if (wireDirectives(el).missing('dirty')) return 12 | 13 | component.dirtyEls.push(el) 14 | }) 15 | 16 | store.registerHook( 17 | 'interceptWireModelAttachListener', 18 | (directive, el, component) => { 19 | let property = directive.value 20 | 21 | el.addEventListener('input', () => { 22 | component.dirtyEls.forEach(dirtyEl => { 23 | let directives = wireDirectives(dirtyEl) 24 | if ( 25 | (directives.has('model') && 26 | directives.get('model').value === 27 | property) || 28 | (directives.has('target') && 29 | directives 30 | .get('target') 31 | .value.split(',') 32 | .map(s => s.trim()) 33 | .includes(property)) 34 | ) { 35 | let isDirty = DOM.valueFromInput(el, component) != component.get(property) 36 | 37 | setDirtyState(dirtyEl, isDirty) 38 | } 39 | }) 40 | }) 41 | } 42 | ) 43 | 44 | store.registerHook('message.received', (message, component) => { 45 | component.dirtyEls.forEach(element => { 46 | if (element.__wire_dirty_cleanup) { 47 | element.__wire_dirty_cleanup.forEach(cleanup => cleanup()) 48 | delete element.__wire_dirty_cleanup 49 | } 50 | }) 51 | }) 52 | 53 | store.registerHook('element.removed', (el, component) => { 54 | component.dirtyEls.forEach((element, index) => { 55 | if (element.isSameNode(el)) { 56 | component.dirtyEls.splice(index, 1) 57 | } 58 | }) 59 | }) 60 | } 61 | 62 | function setDirtyState(el, isDirty) { 63 | el.__wire_dirty_cleanup = []; 64 | 65 | // Process each of the 'dirty' directives for the element 66 | wireDirectives(el).all() 67 | .filter(directive => directive.type === 'dirty') 68 | .forEach(directive => { 69 | if (directive.modifiers.includes('class')) { 70 | const classes = directive.value.split(' ') 71 | if (directive.modifiers.includes('remove') !== isDirty) { 72 | el.classList.add(...classes) 73 | el.__wire_dirty_cleanup.push(() => el.classList.remove(...classes)) 74 | } else { 75 | el.classList.remove(...classes) 76 | el.__wire_dirty_cleanup.push(() => el.classList.add(...classes)) 77 | } 78 | } else if (directive.modifiers.includes('attr')) { 79 | if (directive.modifiers.includes('remove') !== isDirty) { 80 | el.setAttribute(directive.value, true) 81 | el.__wire_dirty_cleanup.push(() => el.removeAttribute(directive.value)) 82 | } else { 83 | el.removeAttribute(directive.value) 84 | el.__wire_dirty_cleanup.push(() => el.setAttribute(directive.value, true)) 85 | } 86 | } else if (!wireDirectives(el).get('model')) { 87 | el.style.display = isDirty ? 'inline-block' : 'none' 88 | el.__wire_dirty_cleanup.push(() => (el.style.display = isDirty ? 'none' : 'inline-block')) 89 | } 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /js/component/DisableForms.js: -------------------------------------------------------------------------------- 1 | import store from '@/Store' 2 | import { wireDirectives } from '../util' 3 | 4 | let cleanupStackByComponentId = {} 5 | 6 | export default function () { 7 | store.registerHook('element.initialized', (el, component) => { 8 | let directives = wireDirectives(el) 9 | 10 | if (directives.missing('submit')) return 11 | 12 | // Set a forms "disabled" state on inputs and buttons. 13 | // Wire will clean it all up automatically when the form 14 | // submission returns and the new DOM lacks these additions. 15 | el.addEventListener('submit', () => { 16 | cleanupStackByComponentId[component.id] = [] 17 | 18 | component.walk(node => { 19 | if (! el.contains(node)) return 20 | 21 | if (node.hasAttribute('wire:ignore')) return false 22 | 23 | if ( 24 | //