Explorar o código

Let x-model, x-on, x-text. etc. listen to nested objects.

Use multi-level proxies to achieve this.
Pascal Thormeier %!s(int64=5) %!d(string=hai) anos
pai
achega
e411a3b515
Modificáronse 8 ficheiros con 212 adicións e 55 borrados
  1. 2 2
      dist/mix-manifest.json
  2. 49 16
      dist/project-x.js
  3. 0 0
      dist/project-x.min.js
  4. 77 32
      index.html
  5. 31 5
      src/component.js
  6. 13 0
      test/bind.spec.js
  7. 21 0
      test/model.spec.js
  8. 19 0
      test/on.spec.js

+ 2 - 2
dist/mix-manifest.json

@@ -1,4 +1,4 @@
 {
-    "/project-x.js": "/project-x.js?id=d08f25e3435288671f17",
-    "/project-x.min.js": "/project-x.min.js?id=10ff0e39dd2d314f3f25"
+    "/project-x.js": "/project-x.js?id=8ddea585824bef19cb31",
+    "/project-x.min.js": "/project-x.min.js?id=8f205394a2ed239a17a3"
 }

+ 49 - 16
dist/project-x.js

@@ -856,6 +856,8 @@ try {
 __webpack_require__.r(__webpack_exports__);
 /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Component; });
 /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./src/utils.js");
+function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
+
 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
 
 function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
@@ -881,18 +883,31 @@ function () {
     value: function wrapDataInObservable(data) {
       this.concernedData = [];
       var self = this;
-      return new Proxy(data, {
-        set: function set(obj, property, value) {
-          var setWasSuccessful = Reflect.set(obj, property, value);
 
-          if (self.concernedData.indexOf(property) === -1) {
-            self.concernedData.push(property);
+      var proxyHandler = function proxyHandler(keyPrefix) {
+        return {
+          set: function set(obj, property, value) {
+            var propertyName = keyPrefix + '.' + property;
+            var setWasSuccessful = Reflect.set(obj, property, value);
+
+            if (self.concernedData.indexOf(propertyName) === -1) {
+              self.concernedData.push(propertyName);
+            }
+
+            self.refresh();
+            return setWasSuccessful;
+          },
+          get: function get(target, key) {
+            if (_typeof(target[key]) === 'object' && target[key] !== null) {
+              return new Proxy(target[key], proxyHandler(keyPrefix + '.' + key));
+            }
+
+            return target[key];
           }
+        };
+      };
 
-          self.refresh();
-          return setWasSuccessful;
-        }
-      });
+      return new Proxy(data, proxyHandler());
     }
   }, {
     key: "initialize",
@@ -1111,12 +1126,30 @@ function () {
     key: "evaluateReturnExpression",
     value: function evaluateReturnExpression(expression) {
       var affectedDataKeys = [];
-      var proxiedData = new Proxy(this.data, {
-        get: function get(object, prop) {
-          affectedDataKeys.push(prop);
-          return object[prop];
-        }
-      });
+
+      var proxyHandler = function proxyHandler(prefix) {
+        return {
+          get: function get(object, prop) {
+            if (_typeof(object[prop]) === 'object' && object[prop] !== null && !Array.isArray(object[prop])) {
+              return new Proxy(object[prop], proxyHandler(prefix + '.' + prop));
+            }
+
+            if (typeof prop === 'string') {
+              affectedDataKeys.push(prefix + '.' + prop);
+            } else {
+              affectedDataKeys.push(prop);
+            }
+
+            if (_typeof(object[prop]) === 'object' && object[prop] !== null) {
+              return new Proxy(object[prop], proxyHandler(prefix + '.' + prop));
+            }
+
+            return object[prop];
+          }
+        };
+      };
+
+      var proxiedData = new Proxy(this.data, proxyHandler());
       var result = Object(_utils__WEBPACK_IMPORTED_MODULE_0__["saferEval"])(expression, proxiedData);
       return {
         output: result,
@@ -1461,4 +1494,4 @@ module.exports = __webpack_require__(/*! /Users/calebporzio/Documents/Code/sites
 /***/ })
 
 /******/ });
-});
+});

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
dist/project-x.min.js


+ 77 - 32
index.html

@@ -5,36 +5,44 @@
             [x-cloak] { display: none; }
         </style>
 
-        <script src="https://cdn.jsdelivr.net/gh/calebporzio/project-x@v0.4.0/dist/project-x.min.js" defer></script>
+
     </head>
     <body>
-        <div x-data="{ foo: 'bar' }">
-            <span x-text="foo"></span>
+        <div x-data="{ someText: 'bar' }">
+            <span x-text="someText"></span>
 
-            <div x-data="{ foo: 'bob' }">
+            <div x-data="{ someScopedText: 'bob' }">
                 <span x-ref="lob">hey</span>
                 <button x-on:click="console.log($refs.lob)">Something</button>
             </div>
         </div>
 
-        <div x-data="{ foo: 'bar' }">
-            <div x-on:click="foo = 'baz'">
-                <button x-on:click.stop></button>
+        <hr>
+
+        <div x-data="{ someText: 'bar' }">
+            <div x-on:click="someText = 'baz'">
+                <button x-on:click.stop>Change to baz</button>
             </div>
 
-            <span x-text="foo"></span>
+            <span x-text="someText"></span>
         </div>
 
+        <hr>
+
         <div x-data="{ count: 0 }">
             <span x-text="count"></span>
         </div>
 
-        <div x-data="{ foo: 'bar' }">
-            <input x-model="foo"></input>
+        <hr>
+
+        <div x-data="{ someText: 'bar' }">
+            <input x-model="someText"></input>
 
-            <button x-on:click="foo = 'baz'"></button>
+            <button x-on:click="someText = 'baz'">Change to baz</button>
         </div>
 
+        <hr>
+
         <div x-data="{ open: false }">
             <button x-on:click="open = true">Open Dropdown</button>
 
@@ -47,51 +55,86 @@
             </ul>
         </div>
 
-        <div>
-            <div id="goHere">
-                some nested thing
-            </div>
-        </div>
-        <div x-data="{ foo: ['bar', 'baz'] }">
-            <select x-model="foo" multiple>
+        <hr>
+
+        <div x-data="{ choices: ['bar', 'baz'] }">
+            <select x-model="choices" multiple>
                 <option disabled value="">Please select one</option>
                 <option>bar</option>
                 <option>baz</option>
             </select>
 
-            <span x-text="foo"></span>
+            <span x-text="choices"></span>
         </div>
-        <div x-data="{ currentTab: 'foo' }">
-            <button x-bind:class="{ 'active': currentTab === 'foo' }" x-on:click="currentTab = 'foo'">Foo</button>
-            <button x-bind:class="{ 'active': currentTab === 'bar' }" x-on:click="currentTab = 'bar'">Bar</button>
 
-            <div x-bind:class="{ 'hidden': currentTab !== 'foo' }">Tab Foo</div>
-            <div class="hidden" x-bind:class="{ 'hidden': currentTab !== 'bar' }">Tab Bar</div>
+        <hr>
+
+        <div x-data="{ currentTab: 'tab1' }">
+            <button x-bind:class="{ 'active': currentTab === 'tab1' }" x-on:click="currentTab = 'tab1'">Foo</button>
+            <button x-bind:class="{ 'active': currentTab === 'tab2' }" x-on:click="currentTab = 'tab2'">Bar</button>
+
+            <div x-bind:class="{ 'hidden': currentTab !== 'tab1' }">Tab Foo</div>
+            <div class="hidden" x-bind:class="{ 'hidden': currentTab !== 'tab2' }">Tab Bar</div>
         </div>
 
-        <div x-data="{ foo: 'bar' }">
-            <input type="radio" x-model="foo" value="bar"></input>
-            <input type="radio" x-model="foo" value="baz"></input>
+        <hr>
 
-            <span x-text="foo"></span>
+        <div x-data="{ someArr: 'bar' }">
+            <input type="radio" x-model="someArr" value="bar"></input>
+            <input type="radio" x-model="someArr" value="baz"></input>
+
+            <span x-text="someArr"></span>
         </div>
 
+        <hr>
 
         <div x-data="{ name: 1 }">
             <input type="text" x-model.number="name">
 
             <span x-text="name"></span>
         </div>
-        <div x-data="{ foo: [] }">
-            <input type="checkbox" x-model="foo" value="bar"></input>
-            <input type="checkbox" x-model="foo" value="baz"></input>
 
-            <span x-text="foo"></span>
+        <hr>
+
+        <div id="goHere">
+            Click me.
+        </div>
+
+        <hr>
+
+        <div x-data="{ someArr: [] }">
+            <input type="checkbox" x-model="someArr" value="bar"></input>
+            <input type="checkbox" x-model="someArr" value="baz"></input>
+
+            <span x-text="someArr"></span>
         </div>
+
+        <hr>
+
         <div x-data="{}">
             <span class="" x-bind:class="['hey']"></span>
         </div>
 
+        <hr>
+
+        <div x-data="{ nested: { deeperNested: false, evenDeeper: { someFlag: false } } }">
+            <!-- Doesn't work, the class doesn't get attached on click. -->
+            <p x-bind:class="{ 'works': nested.deeperNested }" x-on:click="nested.deeperNested = true">
+                Append nested class
+            </p>
+
+            <p x-bind:class="{ 'works': nested.evenDeeper.someFlag }" x-on:click="nested.evenDeeper.someFlag = true">
+                Append even deeper nested class
+            </p>
+        </div>
+
+        <hr>
+
+        <div x-data="{ some: { nested: { obj: 'Some nested model' } }}">
+            <input type="text" x-model="some.nested.obj">
+            <span x-text="some.nested.obj"></span>
+        </div>
+
         <script>
             const thing = document.querySelector('#goHere')
             const handler = (e) => {
@@ -105,5 +148,7 @@
 
             var listener = thing.addEventListener('click', handler)
         </script>
+
+        <script src="./dist/project-x.js"></script>
     </body>
 </html>

+ 31 - 5
src/component.js

@@ -15,19 +15,31 @@ export default class Component {
         this.concernedData = []
 
         var self = this
-        return new Proxy(data, {
+
+        const proxyHandler = keyPrefix => ({
             set(obj, property, value) {
+                const propertyName = keyPrefix + '.' + property
+
                 const setWasSuccessful = Reflect.set(obj, property, value)
 
-                if (self.concernedData.indexOf(property) === -1) {
-                    self.concernedData.push(property)
+                if (self.concernedData.indexOf(propertyName) === -1) {
+                    self.concernedData.push(propertyName)
                 }
 
                 self.refresh()
 
                 return setWasSuccessful
+            },
+            get(target, key) {
+                if (typeof target[key] === 'object' && target[key] !== null) {
+                    return new Proxy(target[key], proxyHandler(keyPrefix + '.' + key))
+                }
+
+                return target[key]
             }
         })
+
+        return new Proxy(data, proxyHandler())
     }
 
     initialize() {
@@ -199,14 +211,28 @@ export default class Component {
     evaluateReturnExpression(expression) {
         var affectedDataKeys = []
 
-        const proxiedData = new Proxy(this.data, {
+        const proxyHandler = prefix => ({
             get(object, prop) {
-                affectedDataKeys.push(prop)
+                if (typeof object[prop] === 'object' && object[prop] !== null && !Array.isArray(object[prop])) {
+                    return new Proxy(object[prop], proxyHandler(prefix + '.' + prop))
+                }
+
+                if (typeof prop === 'string') {
+                    affectedDataKeys.push(prefix + '.' + prop)
+                } else {
+                    affectedDataKeys.push(prop)
+                }
+
+                if (typeof object[prop] === 'object' && object[prop] !== null) {
+                    return new Proxy(object[prop], proxyHandler(prefix + '.' + prop))
+                }
 
                 return object[prop]
             }
         })
 
+        const proxiedData = new Proxy(this.data, proxyHandler())
+
         const result = saferEval(expression, proxiedData)
 
         return {

+ 13 - 0
test/bind.spec.js

@@ -40,6 +40,18 @@ test('class attribute bindings are added by object syntax', async () => {
     expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
 })
 
+test('class attribute bindings are added by nested object syntax', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ nested: { isOn: true } }">
+            <span x-bind:class="{ 'foo': nested.isOn }"></span>
+        </div>
+    `
+
+    projectX.start()
+
+    expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+})
+
 test('class attribute bindings are removed by array syntax', async () => {
     document.body.innerHTML = `
         <div x-data="{}">
@@ -99,3 +111,4 @@ test('boolean attributes set to true are added to element', async () => {
     expect(document.querySelectorAll('input')[2].required).toBeTruthy()
     expect(document.querySelectorAll('input')[3].readOnly).toBeTruthy()
 })
+

+ 21 - 0
test/model.spec.js

@@ -220,3 +220,24 @@ test('x-model binds multiple select dropdown', async () => {
         expect(document.querySelector('span').innerText).toEqual(['bar', 'baz'])
     })
 })
+
+test('x-model binds nested keys', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ some: { nested: { key: 'foo' } } }">
+            <input type="text" x-model="some.nested.key">
+            <span x-text="some.nested.key"></span>
+        </div>
+    `
+
+    projectX.start()
+
+    expect(document.querySelector('input').value).toEqual('foo')
+    expect(document.querySelector('span').innerText).toEqual('foo')
+
+    fireEvent.input(document.querySelector('input'), { target: { value: 'bar' }})
+
+    await wait(() => {
+        expect(document.querySelector('input').value).toEqual('bar')
+        expect(document.querySelector('span').innerText).toEqual('bar')
+    })
+})

+ 19 - 0
test/on.spec.js

@@ -23,6 +23,25 @@ test('data modified in event listener updates effected attribute bindings', asyn
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
 })
 
+test('nested data modified in event listener updates effected attribute bindings', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ nested: { foo: 'bar' } }">
+            <button x-on:click="nested.foo = 'baz'"></button>
+
+            <span x-bind:foo="nested.foo"></span>
+        </div>
+    `
+
+    projectX.start()
+
+    expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
+})
+
+
 test('.stop modifier', async () => {
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio