commit ff59f31cb4680c9494a7a03e809433625dc2ff23 Author: Samuel Štancl Date: Sat Jul 10 21:31:39 2021 +0200 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..504afef --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b6f8bd --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# vue-petite driver for Livewire + +This package provides a layer for + +## Installation + +Currently it's only possible to use this library using a ` +``` + +The imported `createApp` automatically includes a bit of global state and a `v-livewire` directive. If you'd like to do this manually, you can use: + +```html + +``` + +## Usage + +The package provides a `v-livewire` directive that lets you configure bi-directional links between Vue state and Livewire state. + +For example, if you had a `messages` property in Vue and an `items` property in Livewire, you could do: + +```html +
+``` + +Note that you **always need to have the property in Vue as well**. You need some initial state, and your template must work with the empty state. In our case, an empty state for messages is just `{}`. + +If the properties are named the same, you can simply pass an array: + +```html +
+``` + +If you'd like to defer value changes, i.e. have reactive state in Vue but only update Livewire backend state when a Livewire action is executed, you can use the `.defer` modifier: + +```html +
+``` + +```html +
+ You can use Vue-only state in the component: {{ foo }} + + + +
+``` + +## Things to note + +Vue uses templates which contain `{{ these }}` things, and that doesn't pair with Livewire as well as Alpine. + +For that reason, the library automatically adds `wire:ignore` to the root element of each petite-vue component. diff --git a/package.json b/package.json new file mode 100644 index 0000000..ba4be60 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "livewire-petite-vue", + "version": "0.1.0", + "description": "petite-vue driver for Livewire", + "files": [ + "src/index.js" + ], + "main": "src/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/archtechx/livewire-petite-vue.git" + }, + "keywords": [ + "livewire", + "vue", + "vuejs" + ], + "author": "Samuel Štancl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/archtechx/livewire-petite-vue/issues" + }, + "homepage": "https://github.com/archtechx/livewire-petite-vue#readme", + "devDependencies": { + "laravel-mix": "^6.0.25", + "ts-loader": "^9.2.3", + "typescript": "^4.3.5" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..e0a568f --- /dev/null +++ b/src/index.js @@ -0,0 +1,120 @@ +import { createApp, nextTick, reactive } from 'https://unpkg.com/petite-vue?module' + +let wireProxies = new WeakMap + +// Vue-tracked object that changes after every LW request +const __livewireMemo = reactive({ + previous: 'none', + checksum: 'start', +}) + +Livewire.hook('message.received', (data) => __livewireMemo.checksum = data.response.serverMemo.checksum) + +// Vue changes -> LW +let wireProperty = (livewire, prop, defer = false) => new Proxy(livewire.get(prop), { + set(target, property, value) { + livewire.set(prop + '.' + property, value, defer) + + return true + }, + + get(target, property, val, receiver) { + if (property === Symbol.toStringTag) { + return 'WireProperty' + } + + let value = livewire.get(prop + '.' + property); + + if (typeof value === 'object') { + return wireProperty(livewire, prop + '.' + property, defer) + } + + return value + } +}) + +// Vue changes -> LW +let wireProxy = (livewire, defer = false) => { + if (wireProxies.has(livewire) && ! defer) { + return wireProxies.get(livewire) + } + + let proxy = new Proxy(__livewireMemo, { + set(target, property, value) { + livewire.set(property, value, defer) + + return true + }, + + get(target, property) { + if (property === 'deferred') { + return wireProxy(livewire, true) + } + + if (property === Symbol.toStringTag) { + return 'WireProxy' + } + + let value = livewire.get(property); + + if (value === undefined && ! property.startsWith('__v')) { + return (...args) => livewire.call(property, ...args); + } + + if (typeof value === 'object') { + return wireProperty(livewire, property, defer) + } + + return value + } + }) + + if (! defer) { + wireProxies.set(livewire, proxy) + } + + return proxy +} + +const directive = ctx => { + ctx.el.setAttribute('wire:ignore', true) + + const wire = wireProxy(ctx.el.closest('[wire\\:id]').__livewire); + + const state = ctx.ctx.scope; + + state.wire = wire + + ctx.effect(() => { + if (state.__livewireMemo.checksum !== state.__livewireMemo.previous) { + let data = ctx.get() + + if (Array.isArray(data)) { + data = data.reduce((map, property) => { + map[property] = property + + return map + }, {}) + } + + for (const [property, livewire] of Object.entries(data)) { + // LW changes -> Vue + if (ctx.modifiers && ctx.modifiers.defer) { + state[property] = wire.deferred[livewire] + } else { + state[property] = wire[livewire] + } + } + } + }); + + return () => {} +} + +const state = { + __livewireMemo, +}; + +const create = (data) => createApp({ ...state, ...data }).directive('livewire', directive) + +export { state, directive, create as createApp, nextTick, reactive };