commit f0834bcfdbb0f0302ddfc3c9deff09d138e3021c Author: Samuel Štancl Date: Sun May 16 22:09:25 2021 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..4598b7c --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# alpine-reactive + +This package provides a reactivity layer for Alpine 2.x. + +## Problem + +When you create a component that uses a value defined outside of it, you can modify the value from Alpine, but not vice versa: + +```html +
+ + Click count: + +
+``` + +Clicking the buttons **will** update `window.clickCount`. However when `window.clickCount` is modified outside of the component, Alpine won't notice, and won't re-render the DOM. + +Only after something else triggers a re-render, Alpine will show the correct click count again. + +## Solution + +This package provides a reactive proxy wrapper for these objects. The syntax is very similar to Vue 3 — you just wrap the variable in `reactive()` and all changes will be tracked. + +One difference between this package's solution and Vue's `reactive()` is that Alpine requires the calls to be **component-specific**. Meaning, the `reactive()` helper also needs the component instance/element. To simplify that, the package also provides a magic Alpine property, `$reactive`. + +## Demo + +```html + + +
+ + Click count: + +
+``` + +Under the hood, this creates a proxy that forwards everything to `window.clickCount`, but + +## Full API + +### reactive(target, componentEl = null): Proxy for target + +This creates a reactive proxy for `object`. If `componentEl` is passed, all writes to this proxy will trigger `updateElements()` on `componentEl`'s Alpine instance. + +### ref(val): { value: val } + +This turns `foo` into `{ value: foo }`, which allows for `foo` — a primitive type in this example — to be used with proxies. + +### isRef(value): bool + +Checks if a value is a ref. + +### unRef(value) + +Syntactic sugar for `isRef(value) ? value.value : value`. + +### isReactiveProxy(proxy): bool + +Checks whether the passed variable is a value returned by `reactive()`. + +### watch(target, (key, value) => void) or watch(target, value => void), property) + +Watches a reactive proxy, or a property on the reactive proxy. + +Example: +```js +watch(window.counter, (key, value) => console.log(`${key} was changed to ${value}`)); // Watch proxy +watch(window.counter, count => console.log(count), 'count'); // Watch property +``` + +## Details + +The package provides a `$reactive` magic property for Alpine. That property is syntactic sugar for `reactive(target, $el)`. + +```diff +- counter: reactive(window.counter, $el) ++ counter: $reactive(window.counter) +``` + +The magic property is added using the exported `addMagicProperty()` function, which is called once Alpine is available by the `register()` function. `register()` is called automatically for friendly CDN imports. diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..c652ee2 --- /dev/null +++ b/demo.html @@ -0,0 +1,23 @@ + + + + + +
+ + Click count: + +
+ +Try using counter.count = 25 in the console. diff --git a/package.json b/package.json new file mode 100644 index 0000000..58d3079 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "alpine-reactive", + "version": "0.1.0", + "description": "Reactivity layer for Alpine 2.x", + "main": "src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/archtechx/alpine-reactive.git" + }, + "keywords": [ + "alpine.js", + "reactivity" + ], + "author": "Samuel Štancl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/archtechx/alpine-reactive/issues" + }, + "homepage": "https://github.com/archtechx/alpine-reactive#readme" +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..01832dd --- /dev/null +++ b/src/index.js @@ -0,0 +1,112 @@ +let proxies = window.__ALPINE_REACTIVE_PROPERTIES__ = new WeakMap +let refs = window.__ALPINE_REFS__ = new WeakSet + +let reactiveHandler = () => ({ + __alpine_componentEls: [], + __watchers: [], + + get (target, key, value, receiver) { + if (key === '__alpine_componentEls' || key === '__watchers') { + return this[key] + } + + let result = Reflect.get(target, key, value, receiver) + + return result + }, + + set(target, key, value, receiver) { + Reflect.set(target, key, value, receiver) + + for (const el of this.__alpine_componentEls) { + if (el && document.contains(el) && el.__x) { + setTimeout(() => el.__x.updateElements(el), 0) + } + } + + for (const watcher of this.__watchers) { + watcher(key, value) + } + + return true + }, +}) + +export function isRef(value) { + return refs.has(value) +} + +export function unRef(value) { + return isRef(value) ? value.value : value +} + +export function ref(value) { + const obj = { value } + + refs.add(obj) + + return obj +} + +export function isReactiveProxy(proxy) { + return proxy && proxy.__alpine_componentEls !== undefined +} + +export function reactive(value, componentEl = null) { + let result + + if (isReactiveProxy(value)) { + // The value is a reactive() proxy, so we use it immediately + result = value + } else if (proxies.has(value)) { + // The value is not a reactive() proxy, but we found that there + // is a registered proxy for the object in value, so we get it. + result = proxies.get(value) + } else { + // There's no proxy available, so we create one. + result = new Proxy(value, reactiveHandler()) + proxies.set(value, result) + } + + if (componentEl && ! result.__alpine_componentEls.includes(componentEl)) { + result.__alpine_componentEls.push(componentEl) + } + + return result +} + +export function watch(proxy, callback, key = null) { + if (! proxy || ! proxy.__watchers) { + return + } + + if (key) { + proxy.__watchers.push((k, v) => { + if (k === key) { + callback(v) + } + }) + } else { + proxy.__watchers.push(callback) + } +} + +export function addMagicProperty() { + window.Alpine.addMagicProperty('reactive', el => { + return function (property) { + return reactive(property, el) + } + }) +} + +export function register() { + const deferrer = window.deferLoadingAlpine || function (callback) { callback() } + + window.deferLoadingAlpine = function (callback) { + addMagicProperty() + + deferrer(callback) + } +} + +register()