├── livewire ├── __init__.py ├── migrations │ └── __init__.py ├── templates │ ├── livewire │ │ └── component.html │ ├── livewire_styles.html │ └── livewire_scripts.html ├── models.py ├── admin.py ├── static │ └── livewire │ │ └── src │ │ ├── connection │ │ ├── drivers │ │ │ ├── index.js │ │ │ └── http.js │ │ └── index.js │ │ ├── dom │ │ ├── morphdom │ │ │ ├── index.js │ │ │ ├── morphAttrs.js │ │ │ ├── util.js │ │ │ ├── specialElHandlers.js │ │ │ └── morphdom.js │ │ ├── polyfills │ │ │ ├── index.js │ │ │ └── modules │ │ │ │ ├── es.element.get-attribute-names.js │ │ │ │ ├── es.element.closest.js │ │ │ │ └── es.element.matches.js │ │ ├── directive_manager.js │ │ ├── dom.js │ │ ├── directive.js │ │ └── dom_element.js │ │ ├── action │ │ ├── model.js │ │ ├── index.js │ │ ├── method.js │ │ └── event.js │ │ ├── util │ │ ├── index.js │ │ ├── walk.js │ │ ├── getCsrfToken.js │ │ ├── dispatch.js │ │ ├── debounce.js │ │ └── query-string.js │ │ ├── DirectiveManager.js │ │ ├── MessageBus.js │ │ ├── PrefetchMessage.js │ │ ├── HookManager.js │ │ ├── MessageBag.js │ │ ├── component │ │ ├── PrefetchManager.js │ │ ├── FileUploads.js │ │ ├── OfflineStates.js │ │ ├── EchoManager.js │ │ ├── UpdateQueryString.js │ │ ├── DisableForms.js │ │ ├── Polling.js │ │ ├── DirtyStates.js │ │ ├── UploadManager.js │ │ ├── LoadingStates.js │ │ └── index.js │ │ ├── Message.js │ │ ├── index.js │ │ ├── Store.js │ │ └── node_initializer.js ├── apps.py ├── urls.py ├── tests.py ├── templatetags │ └── livewire_tags.py ├── utils.py └── views.py ├── setup.py ├── .coveragerc ├── README.md ├── updatelivewire.sh ├── babel.config.js ├── package.json ├── rollup.config.js └── .gitignore /livewire/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /livewire/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /livewire/templates/livewire/component.html: -------------------------------------------------------------------------------- 1 | {{ component|safe }} 2 | -------------------------------------------------------------------------------- /livewire/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /livewire/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/connection/drivers/index.js: -------------------------------------------------------------------------------- 1 | import http from './http' 2 | 3 | export default { http } 4 | -------------------------------------------------------------------------------- /livewire/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LivewireConfig(AppConfig): 5 | name = 'livewire' 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | setup( 5 | name="livewire", 6 | version="1.1", 7 | packages=find_packages(), 8 | ) 9 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/dom/morphdom/index.js: -------------------------------------------------------------------------------- 1 | import morphAttrs from './morphAttrs'; 2 | import morphdomFactory from './morphdom'; 3 | 4 | var morphdom = morphdomFactory(morphAttrs); 5 | 6 | export default morphdom; -------------------------------------------------------------------------------- /livewire/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.urls import path 3 | from .views import livewire_message 4 | 5 | urlpatterns = [ 6 | path('livewire/message/', livewire_message, name='livewire_app_url'), 7 | ] 8 | 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | source = . 4 | omit = */tests/*, */migrations/*, */urls.py, */settings/* 5 | */asgi.py, */wsgi.py, manage.py, fabfile.py, */apps.py, populate.py 6 | 7 | [report] 8 | show_missing = true 9 | skip_covered = true 10 | -------------------------------------------------------------------------------- /livewire/templates/livewire_styles.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django-livewire WIP!!! 2 | 3 | 4 | ![demo](http://g.recordit.co/nHeGUyucIi.gif) 5 | 6 | 7 | ## Render 8 | 9 | demo: https://github.com/zodman/django-livewire-demo/ 10 | 11 | Test URL: https://django-livewire.fly.dev/ 12 | 13 | 14 | ## Community 15 | 16 | 17 | https://t.me/django_livewire 18 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/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.payload = { 9 | name, 10 | value, 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/action/index.js: -------------------------------------------------------------------------------- 1 | 2 | export default class { 3 | constructor(el) { 4 | this.el = el 5 | } 6 | 7 | get ref() { 8 | return this.el ? this.el.ref : null 9 | } 10 | 11 | toId() { 12 | return btoa(encodeURIComponent(this.el.el.outerHTML)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/action/method.js: -------------------------------------------------------------------------------- 1 | import Action from '.' 2 | 3 | export default class extends Action { 4 | constructor(method, params, el) { 5 | super(el) 6 | 7 | this.type = 'callMethod' 8 | this.payload = { 9 | method, 10 | params, 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /updatelivewire.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | TMPDIR=$(mktemp -d ) 3 | DESTDIR=livewire/static/livewire 4 | wget https://github.com/livewire/livewire/archive/1.x.zip -O $TMPDIR/master.zip 5 | 6 | 7z x $TMPDIR/master.zip -o$TMPDIR 7 | rm -rf $DESTDIR/src/* 8 | cp -rf $TMPDIR/livewire-1.x/js/* $DESTDIR/src 9 | mkdir -p $DESTDIR/dist/ 10 | npm run build 11 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/util/index.js: -------------------------------------------------------------------------------- 1 | 2 | export * from './debounce' 3 | export * from './walk' 4 | export * from './dispatch' 5 | export * from './getCsrfToken' 6 | 7 | export function kebabCase(subject) { 8 | return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase() 9 | } 10 | 11 | export function tap(output, callback) { 12 | callback(output) 13 | 14 | return output 15 | } 16 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/util/walk.js: -------------------------------------------------------------------------------- 1 | 2 | // A little DOM-tree walker. 3 | // (TreeWalker won't do because I need to conditionaly ignore sub-trees using the callback) 4 | export function walk(root, callback) { 5 | if (callback(root) === false) return 6 | 7 | let node = root.firstElementChild 8 | 9 | while (node) { 10 | walk(node, callback) 11 | node = node.nextElementSibling 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /livewire/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.template import Template, Context 3 | 4 | 5 | class TestTT(TestCase): 6 | 7 | def test_rendered(self): 8 | template_to_render = Template( 9 | "{% load livewire_tags %}" 10 | "{% livewire_scripts %}" 11 | ) 12 | rendered_template = template_to_render.render(Context()) 13 | self.assertTrue("window.livewire" in rendered_template) 14 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/util/getCsrfToken.js: -------------------------------------------------------------------------------- 1 | export function getCsrfToken() { 2 | const tokenTag = document.head.querySelector('meta[name="csrf-token"]') 3 | let token 4 | 5 | if (!tokenTag) { 6 | if (!window.livewire_token) { 7 | throw new Error('Whoops, looks like you haven\'t added a "csrf-token" meta tag') 8 | } 9 | 10 | token = window.livewire_token 11 | } else { 12 | token = tokenTag.content 13 | } 14 | 15 | return token 16 | } 17 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/dom/polyfills/index.js: -------------------------------------------------------------------------------- 1 | import 'core-js/features/array/from'; 2 | import 'core-js/features/array/includes'; 3 | import 'core-js/features/array/flat'; 4 | import 'core-js/features/array/find'; 5 | import 'core-js/features/object/assign'; 6 | import 'core-js/features/promise'; 7 | import 'core-js/features/string/starts-with'; 8 | import 'whatwg-fetch' 9 | import './modules/es.element.get-attribute-names'; 10 | import './modules/es.element.matches'; 11 | import './modules/es.element.closest'; 12 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/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 | event, 10 | params, 11 | } 12 | } 13 | 14 | // Overriding toId() becuase some EventActions don't have an "el" 15 | toId() { 16 | return btoa(encodeURIComponent(this.type, this.payload.event, JSON.stringify(this.payload.params))) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/dom/polyfills/modules/es.element.get-attribute-names.js: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttributeNames#Polyfill 2 | if (Element.prototype.getAttributeNames == undefined) { 3 | Element.prototype.getAttributeNames = function () { 4 | var attributes = this.attributes; 5 | var length = attributes.length; 6 | var result = new Array(length); 7 | for (var i = 0; i < length; i++) { 8 | result[i] = attributes[i].name; 9 | } 10 | return result; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/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 `Livewire: 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 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/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 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/dom/polyfills/modules/es.element.closest.js: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill 2 | if (!Element.prototype.matches) { 3 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 4 | } 5 | 6 | if (!Element.prototype.closest) { 7 | Element.prototype.closest = function(s) { 8 | var el = this; 9 | 10 | do { 11 | if (el.matches(s)) return el; 12 | el = el.parentElement || el.parentNode; 13 | } while (el !== null && el.nodeType === 1); 14 | 15 | return null; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/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.actionQueue[0].toId() 10 | } 11 | 12 | payload() { 13 | return { 14 | fromPrefetch: this.prefetchId, 15 | ...super.payload() 16 | } 17 | } 18 | 19 | storeResponse(payload) { 20 | super.storeResponse(payload) 21 | 22 | this.response.fromPrefetch = payload.fromPrefetch 23 | 24 | return this.response 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /livewire/templatetags/livewire_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from livewire.views import LivewireComponent 3 | from livewire.utils import instance_class 4 | 5 | register = template.Library() 6 | 7 | @register.inclusion_tag("livewire_styles.html", takes_context=True) 8 | def livewire_styles(context): 9 | return context 10 | 11 | 12 | @register.inclusion_tag("livewire_scripts.html", takes_context=True) 13 | def livewire_scripts(context): 14 | return context 15 | 16 | 17 | @register.simple_tag(takes_context=True) 18 | def livewire(context, component, **kwargs): 19 | livewire_component = instance_class(component) 20 | return livewire_component.render_to_templatetag(**kwargs) 21 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/dom/polyfills/modules/es.element.matches.js: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill 2 | if (!Element.prototype.matches) { 3 | Element.prototype.matches = 4 | Element.prototype.matchesSelector || 5 | Element.prototype.mozMatchesSelector || 6 | Element.prototype.msMatchesSelector || 7 | Element.prototype.oMatchesSelector || 8 | Element.prototype.webkitMatchesSelector || 9 | function(s) { 10 | var matches = (this.document || this.ownerDocument).querySelectorAll(s), 11 | i = matches.length; 12 | while (--i >= 0 && matches.item(i) !== this) {} 13 | return i > -1; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | edge: '18', 9 | ie: "11", 10 | }, 11 | }, 12 | ], 13 | ], 14 | plugins: [ 15 | "@babel/plugin-proposal-object-rest-spread", 16 | ], 17 | env: { 18 | test: { 19 | presets: [ 20 | [ 21 | '@babel/preset-env', 22 | { 23 | targets: { 24 | node: 'current', 25 | }, 26 | } 27 | ] 28 | ] 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/HookManager.js: -------------------------------------------------------------------------------- 1 | import MessageBus from "./MessageBus" 2 | 3 | export default { 4 | availableHooks: [ 5 | 'componentInitialized', 6 | 'elementInitialized', 7 | 'elementRemoved', 8 | 'messageSent', 9 | 'messageFailed', 10 | 'responseReceived', 11 | 'beforeDomUpdate', 12 | 'beforeElementUpdate', 13 | 'afterElementUpdate', 14 | 'afterDomUpdate', 15 | 'interceptWireModelSetValue', 16 | 'interceptWireModelAttachListener', 17 | ], 18 | 19 | bus: new MessageBus, 20 | 21 | register(name, callback) { 22 | if (! this.availableHooks.includes(name)) { 23 | throw `Livewire: Referencing unknown hook: [${name}]` 24 | } 25 | 26 | this.bus.register(name, callback) 27 | }, 28 | 29 | call(name, ...params) { 30 | this.bus.call(name, ...params) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/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 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/component/PrefetchManager.js: -------------------------------------------------------------------------------- 1 | 2 | class PrefetchManager { 3 | constructor(component) { 4 | this.component = component 5 | this.prefetchMessagesByActionId = {} 6 | } 7 | 8 | addMessage(message) { 9 | this.prefetchMessagesByActionId[message.prefetchId] = message 10 | } 11 | 12 | storeResponseInMessageForPayload(payload) { 13 | const message = this.prefetchMessagesByActionId[payload.fromPrefetch] 14 | 15 | if (message) message.storeResponse(payload) 16 | } 17 | 18 | actionHasPrefetch(action) { 19 | return Object.keys(this.prefetchMessagesByActionId).includes(action.toId()) 20 | } 21 | 22 | actionPrefetchResponseHasBeenReceived(action) { 23 | return !! this.getPrefetchMessageByAction(action).response 24 | } 25 | 26 | getPrefetchMessageByAction(action) { 27 | return this.prefetchMessagesByActionId[action.toId()] 28 | } 29 | 30 | clearPrefetches() { 31 | this.prefetchMessagesByActionId = {} 32 | } 33 | } 34 | 35 | export default PrefetchManager 36 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/dom/directive_manager.js: -------------------------------------------------------------------------------- 1 | import ElementDirective from './directive'; 2 | 3 | export default class { 4 | constructor(el) { 5 | this.el = el 6 | this.directives = this.extractTypeModifiersAndValue() 7 | } 8 | 9 | all() { 10 | return this.directives 11 | } 12 | 13 | has(type) { 14 | return this.directives.map(directive => directive.type).includes(type) 15 | } 16 | 17 | missing(type) { 18 | return ! this.has(type) 19 | } 20 | 21 | get(type) { 22 | return this.directives.find(directive => directive.type === type) 23 | } 24 | 25 | extractTypeModifiersAndValue() { 26 | return Array.from(this.el.getAttributeNames() 27 | // Filter only the livewire directives. 28 | .filter(name => name.match(new RegExp('wire:'))) 29 | // Parse out the type, modifiers, and value from it. 30 | .map(name => { 31 | const [type, ...modifiers] = name.replace(new RegExp('wire:'), '').split('.') 32 | 33 | return new ElementDirective(type, modifiers, name, this.el) 34 | })) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/util/dispatch.js: -------------------------------------------------------------------------------- 1 | // I grabbed this from Turbolink's codebase. 2 | export function dispatch(eventName, { target, cancelable, data } = {}) { 3 | const event = document.createEvent("Events") 4 | event.initEvent(eventName, true, cancelable == true) 5 | event.data = data || {} 6 | 7 | // Fix setting `defaultPrevented` when `preventDefault()` is called 8 | // http://stackoverflow.com/questions/23349191/event-preventdefault-is-not-working-in-ie-11-for-custom-events 9 | if (event.cancelable && ! preventDefaultSupported) { 10 | const { preventDefault } = event 11 | event.preventDefault = function () { 12 | if (! this.defaultPrevented) { 13 | Object.defineProperty(this, "defaultPrevented", { get: () => true }) 14 | } 15 | preventDefault.call(this) 16 | } 17 | } 18 | 19 | (target || document).dispatchEvent(event) 20 | return event 21 | } 22 | 23 | const preventDefaultSupported = (() => { 24 | const event = document.createEvent("Events") 25 | event.initEvent("test", true, true) 26 | event.preventDefault() 27 | return event.defaultPrevented 28 | })() 29 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/connection/index.js: -------------------------------------------------------------------------------- 1 | import { dispatch } from '../util' 2 | import componentStore from '../Store'; 3 | 4 | export default class Connection { 5 | constructor(driver) { 6 | this.driver = driver 7 | 8 | this.driver.onMessage = (payload) => { 9 | this.onMessage(payload) 10 | } 11 | 12 | this.driver.onError = (payload, status) => { 13 | return this.onError(payload, status) 14 | } 15 | 16 | this.driver.init() 17 | } 18 | 19 | onMessage(payload) { 20 | if (payload.fromPrefetch) { 21 | componentStore.findComponent(payload.id).receivePrefetchMessage(payload) 22 | } else { 23 | let component = componentStore.findComponent(payload.id) 24 | 25 | if (! component) { 26 | console.warn(`Livewire: Component [${payload.name}] triggered an update, but not found on page.`) 27 | return 28 | } 29 | 30 | component.receiveMessage(payload) 31 | 32 | dispatch('livewire:update') 33 | } 34 | } 35 | 36 | onError(payloadThatFailedSending, status) { 37 | componentStore.findComponent(payloadThatFailedSending.id).messageSendFailed() 38 | 39 | return componentStore.onErrorCallback(status) 40 | } 41 | 42 | sendMessage(message) { 43 | this.driver.sendMessage(message.payload()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /livewire/utils.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.template.loader import render_to_string 3 | from django.utils.encoding import smart_str 4 | from django.utils.safestring import mark_safe 5 | from django.http import JsonResponse 6 | from django.conf import settings 7 | import random 8 | import string 9 | import importlib 10 | import re 11 | 12 | 13 | IGNORE = ("id", "template_name", "request") 14 | 15 | 16 | def get_vars(instance): 17 | props = set() 18 | for prop in dir(instance): 19 | if not callable(getattr(instance, prop)) and \ 20 | not "__" in prop and \ 21 | not prop in IGNORE \ 22 | and not prop.startswith("_LivewireComponent"): 23 | props.add(prop) 24 | return props 25 | 26 | 27 | def get_id(): 28 | return ''.join(random.choice(string.ascii_lowercase) for i in range(20)) 29 | 30 | 31 | def snakecase(name): 32 | name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 33 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() 34 | 35 | 36 | def get_component_name(name): 37 | name = name.replace("_", " ") 38 | return name.title().replace(" ", "") 39 | 40 | 41 | def instance_class(component_name, **kwargs): 42 | path = getattr(settings, "LIVEWIRE_COMPONENTS_PREFIX") 43 | if not path: 44 | assert False, "LIVEWIRE_COMPONENTS_PREFIX missing" 45 | module = importlib.import_module(path) 46 | class_name = get_component_name(component_name) 47 | class_livewire = getattr(module, '{}Livewire'.format(class_name)) 48 | inst = class_livewire() 49 | return inst 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "livewire/static/dist/livewire.js", 3 | "name": "laravel-livewire", 4 | "scripts": { 5 | "build": "npx rollup -c", 6 | "watch": "npx rollup -c -w", 7 | "test": "jest", 8 | "test:debug": "node --inspect node_modules/.bin/jest --runInBand" 9 | }, 10 | "author": "Caleb Porzio", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@babel/core": "^7.11.6", 14 | "@babel/plugin-proposal-object-rest-spread": "^7.11.0", 15 | "@babel/plugin-transform-runtime": "^7.11.5", 16 | "@babel/preset-env": "^7.11.5", 17 | "@rollup/plugin-alias": "^3.0.0", 18 | "@rollup/plugin-commonjs": "^15.0.0", 19 | "@rollup/plugin-node-resolve": "^9.0.0", 20 | "babel-jest": "^26.3.0", 21 | "core-js": "^3.6.4", 22 | "cross-env": "^7.0.2", 23 | "cross-spawn": "^7.0.3", 24 | "dom-testing-library": "^5.0.0", 25 | "false": "^0.0.4", 26 | "fs-extra": "^9.0.1", 27 | "get-value": "^3.0.1", 28 | "jest": "^26.4.2", 29 | "jest-dom": "^4.0.0", 30 | "jsdom-simulant": "^1.1.2", 31 | "md5": "^2.3.0", 32 | "mock-echo": "^1.1.1", 33 | "rollup": "^2.26.10", 34 | "rollup-plugin-babel": "^4.3.3", 35 | "rollup-plugin-commonjs": "^10.1.0", 36 | "rollup-plugin-filesize": "^9.0.2", 37 | "rollup-plugin-node-resolve": "^5.2.0", 38 | "rollup-plugin-output-manifest": "^1.0.2", 39 | "rollup-plugin-terser": "^7.0.2", 40 | "whatwg-fetch": "^3.4.0" 41 | }, 42 | "dependencies": { 43 | "eslint": "^7.8.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/component/FileUploads.js: -------------------------------------------------------------------------------- 1 | import store from '@/Store' 2 | 3 | export default function () { 4 | store.registerHook('interceptWireModelAttachListener', (el, directive, component) => { 5 | if (! (el.rawNode().tagName.toLowerCase() === 'input' && el.rawNode().type === 'file')) return 6 | 7 | let start = () => el.rawNode().dispatchEvent(new CustomEvent('livewire-upload-start', { bubbles: true })) 8 | let finish = () => el.rawNode().dispatchEvent(new CustomEvent('livewire-upload-finish', { bubbles: true })) 9 | let error = () => el.rawNode().dispatchEvent(new CustomEvent('livewire-upload-error', { bubbles: true })) 10 | let progress = (progressEvent) => { 11 | var percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ) 12 | 13 | el.rawNode().dispatchEvent( 14 | new CustomEvent('livewire-upload-progress', { 15 | bubbles: true, detail: { progress: percentCompleted } 16 | }) 17 | ) 18 | } 19 | 20 | let eventHandler = e => { 21 | if (e.target.files.length === 0) return 22 | 23 | start() 24 | 25 | if (e.target.multiple) { 26 | component.uploadMultiple(directive.value, e.target.files, finish, error, progress) 27 | } else { 28 | component.upload(directive.value, e.target.files[0], finish, error, progress) 29 | } 30 | } 31 | 32 | el.addEventListener('change', eventHandler) 33 | 34 | component.addListenerForTeardown(() => { 35 | el.removeEventListener('change', eventHandler) 36 | }) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/component/OfflineStates.js: -------------------------------------------------------------------------------- 1 | import store from '@/Store' 2 | 3 | var offlineEls = []; 4 | 5 | export default function () { 6 | store.registerHook('elementInitialized', el => { 7 | if (el.directives.missing('offline')) return 8 | 9 | offlineEls.push(el) 10 | }) 11 | 12 | window.addEventListener('offline', () => { 13 | store.livewireIsOffline = true 14 | 15 | offlineEls.forEach(el => { 16 | toggleOffline(el, true) 17 | }) 18 | }) 19 | 20 | window.addEventListener('online', () => { 21 | store.livewireIsOffline = false 22 | 23 | offlineEls.forEach(el => { 24 | toggleOffline(el, false) 25 | }) 26 | }) 27 | 28 | store.registerHook('elementRemoved', el => { 29 | offlineEls = offlineEls.filter(el => ! el.isSameNode(el)) 30 | }) 31 | } 32 | 33 | function toggleOffline(el, isOffline) { 34 | const directive = el.directives.get('offline') 35 | 36 | if (directive.modifiers.includes('class')) { 37 | const classes = directive.value.split(' ') 38 | if (directive.modifiers.includes('remove') !== isOffline) { 39 | el.rawNode().classList.add(...classes) 40 | } else { 41 | el.rawNode().classList.remove(...classes) 42 | } 43 | } else if (directive.modifiers.includes('attr')) { 44 | if (directive.modifiers.includes('remove') !== isOffline) { 45 | el.rawNode().setAttribute(directive.value, true) 46 | } else { 47 | el.rawNode().removeAttribute(directive.value) 48 | } 49 | } else if (! el.directives.get('model')) { 50 | el.rawNode().style.display = isOffline ? 'inline-block' : 'none' 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/dom/dom.js: -------------------------------------------------------------------------------- 1 | import DOMElement from './dom_element' 2 | 3 | /** 4 | * This is intended to isolate all native DOM operations. The operations that happen 5 | * one specific element will be instance methods, the operations you would normally 6 | * perform on the "document" (like "document.querySelector") will be static methods. 7 | */ 8 | export default class DOM { 9 | static rootComponentElements() { 10 | return Array.from(document.querySelectorAll(`[wire\\:id]`)) 11 | .map(el => new DOMElement(el)) 12 | } 13 | 14 | static rootComponentElementsWithNoParents() { 15 | // In CSS, it's simple to select all elements that DO have a certain ancestor. 16 | // However, it's not simple (kinda impossible) to select elements that DONT have 17 | // a certain ancestor. Therefore, we will flip the logic: select all roots that DO have 18 | // have a root ancestor, then select all roots that DONT, then diff the two. 19 | 20 | // Convert NodeLists to Arrays so we can use ".includes()". Ew. 21 | const allEls = Array.from(document.querySelectorAll(`[wire\\:id]`)) 22 | const onlyChildEls = Array.from(document.querySelectorAll(`[wire\\:id] [wire\\:id]`)) 23 | 24 | return allEls 25 | .filter(el => ! onlyChildEls.includes(el)) 26 | .map(el => new DOMElement(el)) 27 | } 28 | 29 | static allModelElementsInside(root) { 30 | return Array.from( 31 | root.querySelectorAll(`[wire\\:model]`) 32 | ).map(el => new DOMElement(el)) 33 | } 34 | 35 | static getByAttributeAndValue(attribute, value) { 36 | return new DOMElement(document.querySelector(`[wire\\:${attribute}="${value}"]`)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import md5 from 'md5' 2 | import fs from 'fs-extra' 3 | import babel from 'rollup-plugin-babel'; 4 | import alias from '@rollup/plugin-alias'; 5 | import filesize from 'rollup-plugin-filesize'; 6 | import { terser } from "rollup-plugin-terser"; 7 | import commonjs from '@rollup/plugin-commonjs'; 8 | import resolve from "rollup-plugin-node-resolve" 9 | import outputManifest from 'rollup-plugin-output-manifest'; 10 | 11 | export default { 12 | input: 'livewire/static/livewire/src/index.js', 13 | output: { 14 | format: 'umd', 15 | sourcemap: true, 16 | name: 'Livewire', 17 | file: 'livewire/static/livewire/dist/livewire.js', 18 | }, 19 | plugins: [ 20 | resolve(), 21 | commonjs({ 22 | // These npm packages still use common-js modules. Ugh. 23 | include: /node_modules\/(get-value|isobject|core-js)/, 24 | }), 25 | filesize(), 26 | terser({ 27 | mangle: false, 28 | compress: { 29 | drop_debugger: false, 30 | }, 31 | }), 32 | babel({ 33 | exclude: 'node_modules/**' 34 | }), 35 | alias({ 36 | entries: [ 37 | { find: '@', replacement: __dirname + '/livewire/static/livewire/src' }, 38 | ] 39 | }), 40 | // Mimic Laravel Mix's mix-manifest file for auto-cache-busting. 41 | outputManifest({ 42 | serialize() { 43 | const file = fs.readFileSync(__dirname + '/livewire/static/livewire/dist/livewire.js', 'utf8'); 44 | const hash = md5(file).substr(0, 20); 45 | 46 | return JSON.stringify({ 47 | '/livewire.js': '/livewire.js?id=' + hash, 48 | }) 49 | } 50 | }), 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /livewire/templates/livewire_scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 51 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/Message.js: -------------------------------------------------------------------------------- 1 | import store from '@/Store' 2 | 3 | export default class { 4 | constructor(component, actionQueue) { 5 | this.component = component 6 | this.actionQueue = actionQueue 7 | } 8 | 9 | get refs() { 10 | return this.actionQueue 11 | .map(action => { 12 | return action.ref 13 | }) 14 | .filter(ref => ref) 15 | } 16 | 17 | payload() { 18 | let payload = { 19 | id: this.component.id, 20 | data: this.component.data, 21 | name: this.component.name, 22 | checksum: this.component.checksum, 23 | locale: this.component.locale, 24 | children: this.component.children, 25 | actionQueue: this.actionQueue.map(action => { 26 | // This ensures only the type & payload properties only get sent over. 27 | return { 28 | type: action.type, 29 | payload: action.payload, 30 | } 31 | }), 32 | } 33 | 34 | if (Object.keys(this.component.errorBag).length > 0) { 35 | payload.errorBag = this.component.errorBag 36 | } 37 | 38 | return payload 39 | } 40 | 41 | storeResponse(payload) { 42 | return this.response = { 43 | id: payload.id, 44 | dom: payload.dom, 45 | checksum: payload.checksum, 46 | locale: payload.locale, 47 | children: payload.children, 48 | dirtyInputs: payload.dirtyInputs, 49 | eventQueue: payload.eventQueue, 50 | dispatchQueue: payload.dispatchQueue, 51 | events: payload.events, 52 | data: payload.data, 53 | redirectTo: payload.redirectTo, 54 | errorBag: payload.errorBag || {}, 55 | updatesQueryString: payload.updatesQueryString, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/util/debounce.js: -------------------------------------------------------------------------------- 1 | // This is kindof like a normal debouncer, except it behaves like both "immediate" and 2 | // "non-immediate" strategies. I'll try to visually demonstrate the differences: 3 | // [normal] = .......| 4 | // [immediate] = |....... 5 | // [both] = |......| 6 | 7 | // The reason I want it to fire on both ends of the debounce is for the following scenario: 8 | // - a user types a letter into an input 9 | // - the debouncer is waiting 200ms to send the ajax request 10 | // - in the meantime a user hits the enter key 11 | // - the debouncer is not up yet, so the "enter" request will get fired before the "key" request 12 | 13 | // Note: I also added a checker in here ("wasInterupted") for the the case of a user 14 | // only typing one key, but two ajax requests getting sent. 15 | 16 | export function debounceWithFiringOnBothEnds(func, wait) { 17 | var timeout; 18 | var timesInterupted = 0; 19 | 20 | return function() { 21 | var context = this, args = arguments; 22 | 23 | var callNow = ! timeout; 24 | 25 | if (timeout) { 26 | clearTimeout(timeout); 27 | timesInterupted++ 28 | } 29 | 30 | timeout = setTimeout(function () { 31 | timeout = null; 32 | if (timesInterupted > 0) { 33 | func.apply(context, args); 34 | timesInterupted = 0 35 | } 36 | }, wait); 37 | 38 | if (callNow) { 39 | func.apply(context, args); 40 | } 41 | }; 42 | }; 43 | 44 | export function debounce(func, wait, immediate) { 45 | var timeout 46 | return function () { 47 | var context = this, args = arguments 48 | var later = function () { 49 | timeout = null 50 | if (!immediate) func.apply(context, args) 51 | } 52 | var callNow = immediate && !timeout 53 | clearTimeout(timeout) 54 | timeout = setTimeout(later, wait) 55 | if (callNow) func.apply(context, args) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/component/EchoManager.js: -------------------------------------------------------------------------------- 1 | import store from '@/Store' 2 | 3 | class EchoManager { 4 | constructor(component) { 5 | this.component = component 6 | } 7 | 8 | registerListeners() { 9 | if (Array.isArray(this.component.events)) { 10 | this.component.events.forEach(event => { 11 | if (event.startsWith('echo')) { 12 | if (typeof Echo === 'undefined') { 13 | console.warn('Laravel Echo cannot be found') 14 | return 15 | } 16 | 17 | let event_parts = event.split(/(echo:|echo-)|:|,/) 18 | 19 | if (event_parts[1] == 'echo:') { 20 | event_parts.splice(2,0,'channel',undefined) 21 | } 22 | 23 | if (event_parts[2] == 'notification') { 24 | event_parts.push(undefined, undefined) 25 | } 26 | 27 | let [s1, signature, channel_type, s2, channel, s3, event_name] = event_parts 28 | 29 | if (['channel','private'].includes(channel_type)) { 30 | Echo[channel_type](channel).listen(event_name, (e) => { 31 | store.emit(event, e) 32 | }) 33 | } else if (channel_type == 'presence') { 34 | Echo.join(channel)[event_name]((e) => { 35 | store.emit(event, e) 36 | }) 37 | } else if (channel_type == 'notification') { 38 | Echo.private(channel).notification((notification) => { 39 | store.emit(event, notification) 40 | }) 41 | } else{ 42 | console.warn('Echo channel type not yet supported') 43 | } 44 | } 45 | }) 46 | } 47 | } 48 | } 49 | 50 | export default EchoManager 51 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/component/UpdateQueryString.js: -------------------------------------------------------------------------------- 1 | import store from '@/Store' 2 | import queryString from '@/util/query-string' 3 | 4 | export default function () { 5 | store.registerHook('responseReceived', (component, response) => { 6 | if (response.updatesQueryString === undefined) return 7 | 8 | var excepts = [] 9 | var dataDestinedForQueryString = {} 10 | 11 | if (Array.isArray(response.updatesQueryString)) { 12 | // User passed in a plain array to `$updatesQueryString` 13 | response.updatesQueryString.forEach(i => dataDestinedForQueryString[i] = component.data[i]) 14 | } else { 15 | // User specified an "except", and therefore made this an object. 16 | Object.keys(response.updatesQueryString).forEach(key => { 17 | if (isNaN(key)) { 18 | // If the key is non-numeric (presumably has an "except" key) 19 | dataDestinedForQueryString[key] = component.data[key] 20 | 21 | if (response.updatesQueryString[key].except !== undefined) { 22 | excepts.push({key: key, value: response.updatesQueryString[key].except}) 23 | } 24 | } else { 25 | // If key is numeric. 26 | const dataKey = response.updatesQueryString[key] 27 | dataDestinedForQueryString[dataKey] = component.data[dataKey] 28 | } 29 | }) 30 | } 31 | 32 | var queryData = { 33 | ...queryString.parse(window.location.search), 34 | ...dataDestinedForQueryString, 35 | } 36 | 37 | // Remove data items that are specified in the "except" key option. 38 | excepts.forEach(({ key, value }) => { 39 | if (queryData[key] == value) { 40 | delete queryData[key] 41 | } 42 | }) 43 | 44 | var stringifiedQueryString = queryString.stringify(queryData) 45 | 46 | history.replaceState({turbolinks: {}}, "", [window.location.pathname, stringifiedQueryString].filter(Boolean).join('?')) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /livewire/static/livewire/src/component/DisableForms.js: -------------------------------------------------------------------------------- 1 | import store from '@/Store' 2 | 3 | export default function () { 4 | let cleanupStackByComponentId = {} 5 | 6 | store.registerHook('elementInitialized', (el, component) => { 7 | if (el.directives.missing('submit')) return 8 | 9 | // Set a forms "disabled" state on inputs and buttons. 10 | // Livewire will clean it all up automatically when the form 11 | // submission returns and the new DOM lacks these additions. 12 | el.el.addEventListener('submit', () => { 13 | cleanupStackByComponentId[component.id] = [] 14 | 15 | component.walk(elem => { 16 | const node = elem.el 17 | 18 | if (! el.el.contains(node)) return 19 | 20 | if (elem.hasAttribute('ignore')) return false 21 | 22 | if ( 23 | //