فهرست منبع

Basic signal plugin implementation

Caleb Porzio 1 سال پیش
والد
کامیت
2d75fbc215

+ 7 - 6
index.html

@@ -6,15 +6,16 @@
     <script src="./packages/mask/dist/cdn.js"></script>
     <script src="./packages/ui/dist/cdn.js" defer></script> -->
     <script src="./packages/anchor/dist/cdn.js" defer></script>
+    <script src="./packages/signal/dist/cdn.js" defer></script>
     <script src="./packages/alpinejs/dist/cdn.js" defer></script>
     <!-- <script src="//cdn.tailwindcss.com"></script> -->
     <!-- <script src="//cdn.tailwindcss.com"></script> -->
 
-    <div x-data="{ val: true }"
-    >
-   <input type="text" x-model.boolean="val">
-   <input type="checkbox" x-model.boolean="val">
-   <input type="radio" name="foo" value="true" x-model.boolean="val">
-   <input type="radio" name="foo" value="false" x-model.boolean="val">
+    <div x-data="{ count: $signal(0) }">
+        <h1 x-text="count()"></h1>
+
+        <h1 x-effect="console.log(count())"></h1>
+
+        <button @click="count(count() + 1)">Increment</button>
     </div>
 </html>

+ 2 - 1
packages/alpinejs/src/alpine.js

@@ -1,4 +1,4 @@
-import { setReactivityEngine, disableEffectScheduling, reactive, effect, release, raw } from './reactivity'
+import { setReactivityEngine, disableEffectScheduling, reactive, effect, release, raw, engine } from './reactivity'
 import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed } from './directives'
 import { start, addRootSelector, addInitSelector, closestRoot, findClosest, initTree, destroyTree, interceptInit } from './lifecycle'
 import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMutations, flushAndStopDeferringMutations, startObservingMutations, stopObservingMutations } from './mutation'
@@ -21,6 +21,7 @@ import { bind } from './binds'
 import { data } from './datas'
 
 let Alpine = {
+    get engine() { return engine },
     get reactive() { return reactive },
     get release() { return release },
     get effect() { return effect },

+ 8 - 6
packages/alpinejs/src/reactivity.js

@@ -1,7 +1,7 @@
 
 import { scheduler } from './scheduler'
 
-let reactive, effect, release, raw
+let reactive, effect, release, raw, engine
 
 let shouldSchedule = true
 export function disableEffectScheduling(callback) {
@@ -12,17 +12,18 @@ export function disableEffectScheduling(callback) {
     shouldSchedule = true
 }
 
-export function setReactivityEngine(engine) {
-    reactive = engine.reactive
-    release = engine.release
-    effect = (callback) => engine.effect(callback, { scheduler: task => {
+export function setReactivityEngine(newEngine) {
+    engine = newEngine
+    reactive = newEngine.reactive
+    release = newEngine.release
+    effect = (callback) => newEngine.effect(callback, { scheduler: task => {
         if (shouldSchedule) {
             scheduler(task)
         } else {
             task()
         }
     } })
-    raw = engine.raw
+    raw = newEngine.raw
 }
 
 export function overrideEffect(override) { effect = override }
@@ -57,6 +58,7 @@ export function elementBoundEffect(el) {
 }
 
 export {
+    engine,
     release,
     reactive,
     effect,

+ 5 - 0
packages/signal/builds/cdn.js

@@ -0,0 +1,5 @@
+import signal from '../src/index.js'
+
+document.addEventListener('alpine:init', () => {
+    window.Alpine.plugin(signal)
+})

+ 3 - 0
packages/signal/builds/module.js

@@ -0,0 +1,3 @@
+import signal from '../src/index.js'
+
+export default signal

+ 17 - 0
packages/signal/package.json

@@ -0,0 +1,17 @@
+{
+    "name": "@alpinejs/signal",
+    "version": "3.13.3",
+    "description": "Use reactive signals for fine-grained-reactivity inside Alpine",
+    "homepage": "https://alpinejs.dev/plugins/signal",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/signal"
+    },
+    "author": "Caleb Porzio",
+    "license": "MIT",
+    "main": "dist/module.cjs.js",
+    "module": "dist/module.esm.js",
+    "unpkg": "dist/cdn.min.js",
+    "dependencies": {}
+}

+ 84 - 0
packages/signal/src/index.js

@@ -0,0 +1,84 @@
+
+export default function (Alpine) {
+    Alpine.magic('signal', el => initial => {
+        return signal(initial)
+    })
+
+    let vueEngine = Alpine.engine
+
+    Alpine.setReactivityEngine({
+        reactive: vueEngine.reactive,
+
+        effect: (callback, options) => {
+            let signalRef
+
+            let ref = vueEngine.effect(() => {
+                signalRef = signalEffect(() => {
+                    callback()
+                })
+            }, options)
+
+            ref.__signalEffect = signalRef
+
+            return ref
+        },
+
+        release: (effectReference) => {
+            if (effectReference.__signalEffect) {
+                signalRelease(effectReference.__signalEffect)
+            }
+
+            return vueEngine.release(effectReference)
+        },
+
+        raw: Alpine.raw,
+    })
+}
+
+let activeEffect = null;
+const effectsMap = new WeakMap(); // To keep track of which signals an effect is subscribed to
+
+export function signal(initialValue) {
+    let value = initialValue;
+    const subscribers = new Set();
+
+    const signalFunction = function(newValue) {
+        if (arguments.length === 0) {
+            if (activeEffect) {
+                subscribers.add(activeEffect);
+                let effects = effectsMap.get(activeEffect) || new Set();
+                effects.add(subscribers);
+                effectsMap.set(activeEffect, effects);
+            }
+            return value;
+        } else {
+            value = newValue;
+            subscribers.forEach(subscriber => subscriber());
+        }
+    };
+
+    // Ignore this inside Vue reactivity...
+    signalFunction.__v_skip = true
+
+    return signalFunction;
+}
+
+export function signalEffect(callback) {
+    const effectFn = () => {
+        activeEffect = effectFn;
+        callback();
+        activeEffect = null;
+    };
+    effectFn();
+    return effectFn;
+}
+
+export function signalRelease(effectFn) {
+    const subscribedSignals = effectsMap.get(effectFn);
+    if (subscribedSignals) {
+        subscribedSignals.forEach(subscribers => {
+            subscribers.delete(effectFn);
+        });
+    }
+    effectsMap.delete(effectFn);
+}

+ 1 - 0
scripts/build.js

@@ -11,6 +11,7 @@ let zlib = require('zlib');
     'intersect',
     'persist',
     'collapse',
+    'signal',
     'anchor',
     'morph',
     'focus',

+ 1 - 0
tests/cypress/spec.html

@@ -6,6 +6,7 @@
         <!-- This is where our test subjects will be injected. -->
     </blockquote>
 
+    <script src="/../../packages/signal/dist/cdn.js"></script>
     <script src="/../../packages/morph/dist/cdn.js"></script>
     <script src="/../../packages/persist/dist/cdn.js"></script>
     <script src="/../../packages/focus/dist/cdn.js"></script>