diff --git a/README.md b/README.md index 255b476ca..55268b3be 100644 --- a/README.md +++ b/README.md @@ -515,6 +515,22 @@ You can also use `$dispatch()` to trigger data updates for `x-model` bindings. F You can "watch" a component property with the `$watch` magic method. In the above example, when the button is clicked and `open` is changed, the provided callback will fire and `console.log` the new value. +Use dot-delimited paths to watch properties of nested objects like `foo.bar.baz`. For example: + +```html +
+ +
+``` + +You can also detect nested value changes inside objects "deep watching", By passing `{ deep: true }` as the options argument. For example: + +```html +
+ +
+``` + ## v3 Roadmap * Move from `x-ref` to `ref` for Vue parity diff --git a/dist/alpine-ie11.js b/dist/alpine-ie11.js index 4097a1d18..94af5e2d1 100644 --- a/dist/alpine-ie11.js +++ b/dist/alpine-ie11.js @@ -3200,17 +3200,42 @@ } }); - // `SameValue` abstract operation - // https://tc39.github.io/ecma262/#sec-samevalue - var sameValue = Object.is || function is(x, y) { - // eslint-disable-next-line no-self-compare - return x === y ? x !== 0 || 1 / x === 1 / y : x != x && y != y; - }; + var HAS_SPECIES_SUPPORT$2 = arrayMethodHasSpeciesSupport('slice'); + var USES_TO_LENGTH$5 = arrayMethodUsesToLength('slice', { ACCESSORS: true, 0: 0, 1: 2 }); - // `Object.is` method - // https://tc39.github.io/ecma262/#sec-object.is - _export({ target: 'Object', stat: true }, { - is: sameValue + var SPECIES$2 = wellKnownSymbol('species'); + var nativeSlice = [].slice; + var max$1 = Math.max; + + // `Array.prototype.slice` method + // https://tc39.github.io/ecma262/#sec-array.prototype.slice + // fallback for not array-like ES3 strings and DOM objects + _export({ target: 'Array', proto: true, forced: !HAS_SPECIES_SUPPORT$2 || !USES_TO_LENGTH$5 }, { + slice: function slice(start, end) { + var O = toIndexedObject(this); + var length = toLength(O.length); + var k = toAbsoluteIndex(start, length); + var fin = toAbsoluteIndex(end === undefined ? length : end, length); + // inline `ArraySpeciesCreate` for usage native `Array#slice` where it's possible + var Constructor, result, n; + if (isArray(O)) { + Constructor = O.constructor; + // cross-realm fallback + if (typeof Constructor == 'function' && (Constructor === Array || isArray(Constructor.prototype))) { + Constructor = undefined; + } else if (isObject(Constructor)) { + Constructor = Constructor[SPECIES$2]; + if (Constructor === null) Constructor = undefined; + } + if (Constructor === Array || Constructor === undefined) { + return nativeSlice.call(O, k, fin); + } + } + result = new (Constructor === undefined ? Array : Constructor)(max$1(fin - k, 0)); + for (n = 0; k < fin; k++, n++) if (k in O) createProperty(result, n, O[k]); + result.length = n; + return result; + } }); var FAILS_ON_PRIMITIVES = fails(function () { objectKeys(1); }); @@ -3235,6 +3260,46 @@ redefine(Object.prototype, 'toString', objectToString, { unsafe: true }); } + var propertyIsEnumerable = objectPropertyIsEnumerable.f; + + // `Object.{ entries, values }` methods implementation + var createMethod$4 = function (TO_ENTRIES) { + return function (it) { + var O = toIndexedObject(it); + var keys = objectKeys(O); + var length = keys.length; + var i = 0; + var result = []; + var key; + while (length > i) { + key = keys[i++]; + if (!descriptors || propertyIsEnumerable.call(O, key)) { + result.push(TO_ENTRIES ? [key, O[key]] : O[key]); + } + } + return result; + }; + }; + + var objectToArray = { + // `Object.entries` method + // https://tc39.github.io/ecma262/#sec-object.entries + entries: createMethod$4(true), + // `Object.values` method + // https://tc39.github.io/ecma262/#sec-object.values + values: createMethod$4(false) + }; + + var $values = objectToArray.values; + + // `Object.values` method + // https://tc39.github.io/ecma262/#sec-object.values + _export({ target: 'Object', stat: true }, { + values: function values(O) { + return $values(O); + } + }); + var nativePromiseConstructor = global_1.Promise; var redefineAll = function (target, src, options) { @@ -3242,14 +3307,14 @@ return target; }; - var SPECIES$2 = wellKnownSymbol('species'); + var SPECIES$3 = wellKnownSymbol('species'); var setSpecies = function (CONSTRUCTOR_NAME) { var Constructor = getBuiltIn(CONSTRUCTOR_NAME); var defineProperty = objectDefineProperty.f; - if (descriptors && Constructor && !Constructor[SPECIES$2]) { - defineProperty(Constructor, SPECIES$2, { + if (descriptors && Constructor && !Constructor[SPECIES$3]) { + defineProperty(Constructor, SPECIES$3, { configurable: true, get: function () { return this; } }); @@ -3301,14 +3366,14 @@ }; }); - var SPECIES$3 = wellKnownSymbol('species'); + var SPECIES$4 = wellKnownSymbol('species'); // `SpeciesConstructor` abstract operation // https://tc39.github.io/ecma262/#sec-speciesconstructor var speciesConstructor = function (O, defaultConstructor) { var C = anObject(O).constructor; var S; - return C === undefined || (S = anObject(C)[SPECIES$3]) == undefined ? defaultConstructor : aFunction$1(S); + return C === undefined || (S = anObject(C)[SPECIES$4]) == undefined ? defaultConstructor : aFunction$1(S); }; var engineIsIos = /(iphone|ipod|ipad).*applewebkit/i.test(engineUserAgent); @@ -3540,7 +3605,7 @@ - var SPECIES$4 = wellKnownSymbol('species'); + var SPECIES$5 = wellKnownSymbol('species'); var PROMISE = 'Promise'; var getInternalState$1 = internalState.get; var setInternalState$1 = internalState.set; @@ -3583,7 +3648,7 @@ exec(function () { /* empty */ }, function () { /* empty */ }); }; var constructor = promise.constructor = {}; - constructor[SPECIES$4] = FakePromise; + constructor[SPECIES$5] = FakePromise; return !(promise.then(function () { /* empty */ }) instanceof FakePromise); }); @@ -4016,44 +4081,6 @@ exec: regexpExec }); - var MATCH = wellKnownSymbol('match'); - - // `IsRegExp` abstract operation - // https://tc39.github.io/ecma262/#sec-isregexp - var isRegexp = function (it) { - var isRegExp; - return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : classofRaw(it) == 'RegExp'); - }; - - var notARegexp = function (it) { - if (isRegexp(it)) { - throw TypeError("The method doesn't accept regular expressions"); - } return it; - }; - - var MATCH$1 = wellKnownSymbol('match'); - - var correctIsRegexpLogic = function (METHOD_NAME) { - var regexp = /./; - try { - '/./'[METHOD_NAME](regexp); - } catch (e) { - try { - regexp[MATCH$1] = false; - return '/./'[METHOD_NAME](regexp); - } catch (f) { /* empty */ } - } return false; - }; - - // `String.prototype.includes` method - // https://tc39.github.io/ecma262/#sec-string.prototype.includes - _export({ target: 'String', proto: true, forced: !correctIsRegexpLogic('includes') }, { - includes: function includes(searchString /* , position = 0 */) { - return !!~String(requireObjectCoercible(this)) - .indexOf(notARegexp(searchString), arguments.length > 1 ? arguments[1] : undefined); - } - }); - // TODO: Remove from `core-js@4` since it's moved to entry points @@ -4062,7 +4089,7 @@ - var SPECIES$5 = wellKnownSymbol('species'); + var SPECIES$6 = wellKnownSymbol('species'); var REPLACE_SUPPORTS_NAMED_GROUPS = !fails(function () { // #replace needs built-in support for named groups. @@ -4125,7 +4152,7 @@ // RegExp[@@split] doesn't call the regex's exec method, but first creates // a new one. We need to return the patched regex when creating the new one. re.constructor = {}; - re.constructor[SPECIES$5] = function () { return re; }; + re.constructor[SPECIES$6] = function () { return re; }; re.flags = ''; re[SYMBOL] = /./[SYMBOL]; } @@ -4243,6 +4270,15 @@ ]; }); + var MATCH = wellKnownSymbol('match'); + + // `IsRegExp` abstract operation + // https://tc39.github.io/ecma262/#sec-isregexp + var isRegexp = function (it) { + var isRegExp; + return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : classofRaw(it) == 'RegExp'); + }; + var arrayPush = [].push; var min$2 = Math.min; var MAX_UINT32 = 0xFFFFFFFF; @@ -4366,6 +4402,57 @@ ]; }, !SUPPORTS_Y); + var notARegexp = function (it) { + if (isRegexp(it)) { + throw TypeError("The method doesn't accept regular expressions"); + } return it; + }; + + var MATCH$1 = wellKnownSymbol('match'); + + var correctIsRegexpLogic = function (METHOD_NAME) { + var regexp = /./; + try { + '/./'[METHOD_NAME](regexp); + } catch (e) { + try { + regexp[MATCH$1] = false; + return '/./'[METHOD_NAME](regexp); + } catch (f) { /* empty */ } + } return false; + }; + + var getOwnPropertyDescriptor$3 = objectGetOwnPropertyDescriptor.f; + + + + + + + var nativeStartsWith = ''.startsWith; + var min$3 = Math.min; + + var CORRECT_IS_REGEXP_LOGIC = correctIsRegexpLogic('startsWith'); + // https://github.com/zloirock/core-js/pull/702 + var MDN_POLYFILL_BUG = !CORRECT_IS_REGEXP_LOGIC && !!function () { + var descriptor = getOwnPropertyDescriptor$3(String.prototype, 'startsWith'); + return descriptor && !descriptor.writable; + }(); + + // `String.prototype.startsWith` method + // https://tc39.github.io/ecma262/#sec-string.prototype.startswith + _export({ target: 'String', proto: true, forced: !MDN_POLYFILL_BUG && !CORRECT_IS_REGEXP_LOGIC }, { + startsWith: function startsWith(searchString /* , position = 0 */) { + var that = String(requireObjectCoercible(this)); + notARegexp(searchString); + var index = toLength(min$3(arguments.length > 1 ? arguments[1] : undefined, that.length)); + var search = String(searchString); + return nativeStartsWith + ? nativeStartsWith.call(that, search, index) + : that.slice(index, index + search.length) === search; + } + }); + var IS_CONCAT_SPREADABLE = wellKnownSymbol('isConcatSpreadable'); var MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF; var MAXIMUM_ALLOWED_INDEX_EXCEEDED = 'Maximum allowed index exceeded'; @@ -4421,14 +4508,14 @@ var FIND = 'find'; var SKIPS_HOLES = true; - var USES_TO_LENGTH$5 = arrayMethodUsesToLength(FIND); + var USES_TO_LENGTH$6 = arrayMethodUsesToLength(FIND); // Shouldn't skip holes if (FIND in []) Array(1)[FIND](function () { SKIPS_HOLES = false; }); // `Array.prototype.find` method // https://tc39.github.io/ecma262/#sec-array.prototype.find - _export({ target: 'Array', proto: true, forced: SKIPS_HOLES || !USES_TO_LENGTH$5 }, { + _export({ target: 'Array', proto: true, forced: SKIPS_HOLES || !USES_TO_LENGTH$6 }, { find: function find(callbackfn /* , that = undefined */) { return $find(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); } @@ -4445,11 +4532,11 @@ var NEGATIVE_ZERO = !!nativeIndexOf && 1 / [1].indexOf(1, -0) < 0; var STRICT_METHOD$2 = arrayMethodIsStrict('indexOf'); - var USES_TO_LENGTH$6 = arrayMethodUsesToLength('indexOf', { ACCESSORS: true, 1: 0 }); + var USES_TO_LENGTH$7 = arrayMethodUsesToLength('indexOf', { ACCESSORS: true, 1: 0 }); // `Array.prototype.indexOf` method // https://tc39.github.io/ecma262/#sec-array.prototype.indexof - _export({ target: 'Array', proto: true, forced: NEGATIVE_ZERO || !STRICT_METHOD$2 || !USES_TO_LENGTH$6 }, { + _export({ target: 'Array', proto: true, forced: NEGATIVE_ZERO || !STRICT_METHOD$2 || !USES_TO_LENGTH$7 }, { indexOf: function indexOf(searchElement /* , fromIndex = 0 */) { return NEGATIVE_ZERO // convert -0 to +0 @@ -4471,18 +4558,18 @@ } }); - var HAS_SPECIES_SUPPORT$2 = arrayMethodHasSpeciesSupport('splice'); - var USES_TO_LENGTH$7 = arrayMethodUsesToLength('splice', { ACCESSORS: true, 0: 0, 1: 2 }); + var HAS_SPECIES_SUPPORT$3 = arrayMethodHasSpeciesSupport('splice'); + var USES_TO_LENGTH$8 = arrayMethodUsesToLength('splice', { ACCESSORS: true, 0: 0, 1: 2 }); - var max$1 = Math.max; - var min$3 = Math.min; + var max$2 = Math.max; + var min$4 = Math.min; var MAX_SAFE_INTEGER$1 = 0x1FFFFFFFFFFFFF; var MAXIMUM_ALLOWED_LENGTH_EXCEEDED = 'Maximum allowed length exceeded'; // `Array.prototype.splice` method // https://tc39.github.io/ecma262/#sec-array.prototype.splice // with adding support of @@species - _export({ target: 'Array', proto: true, forced: !HAS_SPECIES_SUPPORT$2 || !USES_TO_LENGTH$7 }, { + _export({ target: 'Array', proto: true, forced: !HAS_SPECIES_SUPPORT$3 || !USES_TO_LENGTH$8 }, { splice: function splice(start, deleteCount /* , ...items */) { var O = toObject(this); var len = toLength(O.length); @@ -4496,7 +4583,7 @@ actualDeleteCount = len - actualStart; } else { insertCount = argumentsLength - 2; - actualDeleteCount = min$3(max$1(toInteger(deleteCount), 0), len - actualStart); + actualDeleteCount = min$4(max$2(toInteger(deleteCount), 0), len - actualStart); } if (len + insertCount - actualDeleteCount > MAX_SAFE_INTEGER$1) { throw TypeError(MAXIMUM_ALLOWED_LENGTH_EXCEEDED); @@ -4577,7 +4664,7 @@ var rtrim = RegExp(whitespace + whitespace + '*$'); // `String.prototype.{ trim, trimStart, trimEnd, trimLeft, trimRight }` methods implementation - var createMethod$4 = function (TYPE) { + var createMethod$5 = function (TYPE) { return function ($this) { var string = String(requireObjectCoercible($this)); if (TYPE & 1) string = string.replace(ltrim, ''); @@ -4589,17 +4676,17 @@ var stringTrim = { // `String.prototype.{ trimLeft, trimStart }` methods // https://tc39.github.io/ecma262/#sec-string.prototype.trimstart - start: createMethod$4(1), + start: createMethod$5(1), // `String.prototype.{ trimRight, trimEnd }` methods // https://tc39.github.io/ecma262/#sec-string.prototype.trimend - end: createMethod$4(2), + end: createMethod$5(2), // `String.prototype.trim` method // https://tc39.github.io/ecma262/#sec-string.prototype.trim - trim: createMethod$4(3) + trim: createMethod$5(3) }; var getOwnPropertyNames = objectGetOwnPropertyNames.f; - var getOwnPropertyDescriptor$3 = objectGetOwnPropertyDescriptor.f; + var getOwnPropertyDescriptor$4 = objectGetOwnPropertyDescriptor.f; var defineProperty$3 = objectDefineProperty.f; var trim = stringTrim.trim; @@ -4658,7 +4745,7 @@ 'MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger' ).split(','), j = 0, key; keys$1.length > j; j++) { if (has(NativeNumber, key = keys$1[j]) && !has(NumberWrapper, key)) { - defineProperty$3(NumberWrapper, key, getOwnPropertyDescriptor$3(NativeNumber, key)); + defineProperty$3(NumberWrapper, key, getOwnPropertyDescriptor$4(NativeNumber, key)); } } NumberWrapper.prototype = NumberPrototype; @@ -4666,48 +4753,17 @@ redefine(global_1, NUMBER, NumberWrapper); } - var propertyIsEnumerable = objectPropertyIsEnumerable.f; - - // `Object.{ entries, values }` methods implementation - var createMethod$5 = function (TO_ENTRIES) { - return function (it) { - var O = toIndexedObject(it); - var keys = objectKeys(O); - var length = keys.length; - var i = 0; - var result = []; - var key; - while (length > i) { - key = keys[i++]; - if (!descriptors || propertyIsEnumerable.call(O, key)) { - result.push(TO_ENTRIES ? [key, O[key]] : O[key]); - } - } - return result; - }; - }; - - var objectToArray = { - // `Object.entries` method - // https://tc39.github.io/ecma262/#sec-object.entries - entries: createMethod$5(true), - // `Object.values` method - // https://tc39.github.io/ecma262/#sec-object.values - values: createMethod$5(false) - }; - - var $values = objectToArray.values; - - // `Object.values` method - // https://tc39.github.io/ecma262/#sec-object.values - _export({ target: 'Object', stat: true }, { - values: function values(O) { - return $values(O); + // `String.prototype.includes` method + // https://tc39.github.io/ecma262/#sec-string.prototype.includes + _export({ target: 'String', proto: true, forced: !correctIsRegexpLogic('includes') }, { + includes: function includes(searchString /* , position = 0 */) { + return !!~String(requireObjectCoercible(this)) + .indexOf(notARegexp(searchString), arguments.length > 1 ? arguments[1] : undefined); } }); - var max$2 = Math.max; - var min$4 = Math.min; + var max$3 = Math.max; + var min$5 = Math.min; var floor$1 = Math.floor; var SUBSTITUTION_SYMBOLS = /\$([$&'`]|\d\d?|<[^>]*>)/g; var SUBSTITUTION_SYMBOLS_NO_NAMED = /\$([$&'`]|\d\d?)/g; @@ -4772,7 +4828,7 @@ result = results[i]; var matched = String(result[0]); - var position = max$2(min$4(toInteger(result.index), S.length), 0); + var position = max$3(min$5(toInteger(result.index), S.length), 0); var captures = []; // NOTE: This is equivalent to // captures = result.slice(1).map(maybeToString) @@ -4832,37 +4888,6 @@ } }); - var getOwnPropertyDescriptor$4 = objectGetOwnPropertyDescriptor.f; - - - - - - - var nativeStartsWith = ''.startsWith; - var min$5 = Math.min; - - var CORRECT_IS_REGEXP_LOGIC = correctIsRegexpLogic('startsWith'); - // https://github.com/zloirock/core-js/pull/702 - var MDN_POLYFILL_BUG = !CORRECT_IS_REGEXP_LOGIC && !!function () { - var descriptor = getOwnPropertyDescriptor$4(String.prototype, 'startsWith'); - return descriptor && !descriptor.writable; - }(); - - // `String.prototype.startsWith` method - // https://tc39.github.io/ecma262/#sec-string.prototype.startswith - _export({ target: 'String', proto: true, forced: !MDN_POLYFILL_BUG && !CORRECT_IS_REGEXP_LOGIC }, { - startsWith: function startsWith(searchString /* , position = 0 */) { - var that = String(requireObjectCoercible(this)); - notARegexp(searchString); - var index = toLength(min$5(arguments.length > 1 ? arguments[1] : undefined, that.length)); - var search = String(searchString); - return nativeStartsWith - ? nativeStartsWith.call(that, search, index) - : that.slice(index, index + search.length) === search; - } - }); - var non = '\u200B\u0085\u180E'; // check that a method works with the correct list @@ -6351,10 +6376,15 @@ this.watchers = {}; this.unobservedData.$watch = function (property, callback) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + _newArrowCheck(this, _this); - if (!this.watchers[property]) this.watchers[property] = []; - this.watchers[property].push(callback); + if (!this.watchers[property]) { + this.watchers[property] = this.walkToConstructWatcher(property.split('.'), this.unobservedData, options); + } + + this.watchers[property].callbacks.push(callback); }.bind(this); this.showDirectiveStack = []; @@ -6405,50 +6435,27 @@ valueMutated: function valueMutated(target, key) { var _this3 = this; - if (self.watchers[key]) { - // If there's a watcher for this specific key, run it. - self.watchers[key].forEach(function (callback) { - _newArrowCheck(this, _this3); - - return callback(target[key]); - }.bind(this)); - } else { - // Let's walk through the watchers with "dot-notation" (foo.bar) and see - // if this mutation fits any of them. - Object.keys(self.watchers).filter(function (i) { - _newArrowCheck(this, _this3); + Object.values(self.watchers).forEach(function (watcher) { + var _this4 = this; - return i.includes('.'); - }.bind(this)).forEach(function (fullDotNotationKey) { - var _this4 = this; - - _newArrowCheck(this, _this3); - - var dotNotationParts = fullDotNotationKey.split('.'); // If this dot-notation watcher's last "part" doesn't match the current - // key, then skip it early for performance reasons. - - if (key !== dotNotationParts[dotNotationParts.length - 1]) return; // Now, walk through the dot-notation "parts" recursively to find - // a match, and call the watcher if one's found. - - dotNotationParts.reduce(function (comparisonData, part) { - var _this5 = this; + _newArrowCheck(this, _this3); + if (target === watcher.target && key === watcher.key) { + //Callback property watcher, return property value + watcher.callbacks.forEach(function (callback) { _newArrowCheck(this, _this4); - if (Object.is(target, comparisonData)) { - // Run the watchers. - self.watchers[fullDotNotationKey].forEach(function (callback) { - _newArrowCheck(this, _this5); - - return callback(target[key]); - }.bind(this)); - } - - return comparisonData[part]; - }.bind(this), self.getUnobservedData()); - }.bind(this)); - } // Don't react to data changes for cases like the `x-created` hook. + return callback(target[key]); + }.bind(this)); + } else if (watcher.deep && self.foundUnderObjectGraph(target, watcher.target, self)) { + //Callback structure watcher, return structure value + watcher.callbacks.forEach(function (callback) { + _newArrowCheck(this, _this4); + return callback(watcher.target); + }.bind(this)); + } + }.bind(this)); // Don't react to data changes for cases like the `x-created` hook. if (self.pauseReactivity) return; debounce(function () { @@ -6470,13 +6477,13 @@ }, { key: "walkAndSkipNestedComponents", value: function walkAndSkipNestedComponents(el, callback) { - var _this6 = this; + var _this5 = this; var initializeComponentCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () { - _newArrowCheck(this, _this6); + _newArrowCheck(this, _this5); }.bind(this); walk(el, function (el) { - _newArrowCheck(this, _this6); + _newArrowCheck(this, _this5); // We've hit a component. if (el.hasAttribute('x-data')) { @@ -6495,19 +6502,19 @@ }, { key: "initializeElements", value: function initializeElements(rootEl) { - var _this7 = this; + var _this6 = this; var extraVars = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () { - _newArrowCheck(this, _this7); + _newArrowCheck(this, _this6); }.bind(this); this.walkAndSkipNestedComponents(rootEl, function (el) { - _newArrowCheck(this, _this7); + _newArrowCheck(this, _this6); // Don't touch spawns from for loop if (el.__x_for_key !== undefined) return false; this.initializeElement(el, extraVars); }.bind(this), function (el) { - _newArrowCheck(this, _this7); + _newArrowCheck(this, _this6); el.__x = new Component(el); }.bind(this)); @@ -6532,19 +6539,19 @@ }, { key: "updateElements", value: function updateElements(rootEl) { - var _this8 = this; + var _this7 = this; var extraVars = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () { - _newArrowCheck(this, _this8); + _newArrowCheck(this, _this7); }.bind(this); this.walkAndSkipNestedComponents(rootEl, function (el) { - _newArrowCheck(this, _this8); + _newArrowCheck(this, _this7); // Don't touch spawns from for loop (and check if the root is actually a for loop in a parent, don't skip it.) if (el.__x_for_key !== undefined && !el.isSameNode(this.$el)) return false; this.updateElement(el, extraVars); }.bind(this), function (el) { - _newArrowCheck(this, _this8); + _newArrowCheck(this, _this7); el.__x = new Component(el); }.bind(this)); @@ -6557,45 +6564,45 @@ }, { key: "executeAndClearRemainingShowDirectiveStack", value: function executeAndClearRemainingShowDirectiveStack() { - var _this9 = this; + var _this8 = this; // The goal here is to start all the x-show transitions // and build a nested promise chain so that elements // only hide when the children are finished hiding. this.showDirectiveStack.reverse().map(function (thing) { - var _this10 = this; + var _this9 = this; - _newArrowCheck(this, _this9); + _newArrowCheck(this, _this8); return new Promise(function (resolve) { - var _this11 = this; + var _this10 = this; - _newArrowCheck(this, _this10); + _newArrowCheck(this, _this9); thing(function (finish) { - _newArrowCheck(this, _this11); + _newArrowCheck(this, _this10); resolve(finish); }.bind(this)); }.bind(this)); }.bind(this)).reduce(function (nestedPromise, promise) { - var _this12 = this; + var _this11 = this; - _newArrowCheck(this, _this9); + _newArrowCheck(this, _this8); return nestedPromise.then(function () { - var _this13 = this; + var _this12 = this; - _newArrowCheck(this, _this12); + _newArrowCheck(this, _this11); return promise.then(function (finish) { - _newArrowCheck(this, _this13); + _newArrowCheck(this, _this12); return finish(); }.bind(this)); }.bind(this)); }.bind(this), Promise.resolve(function () { - _newArrowCheck(this, _this9); + _newArrowCheck(this, _this8); }.bind(this))); // We've processed the handler stack. let's clear it. this.showDirectiveStack = []; @@ -6609,7 +6616,7 @@ }, { key: "registerListeners", value: function registerListeners(el, extraVars) { - var _this14 = this; + var _this13 = this; getXAttrs(el).forEach(function (_ref) { var type = _ref.type, @@ -6617,7 +6624,7 @@ modifiers = _ref.modifiers, expression = _ref.expression; - _newArrowCheck(this, _this14); + _newArrowCheck(this, _this13); switch (type) { case 'on': @@ -6633,20 +6640,20 @@ }, { key: "resolveBoundAttributes", value: function resolveBoundAttributes(el) { - var _this15 = this; + var _this14 = this; var initialUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var extraVars = arguments.length > 2 ? arguments[2] : undefined; var attrs = getXAttrs(el); attrs.forEach(function (_ref2) { - var _this16 = this; + var _this15 = this; var type = _ref2.type, value = _ref2.value, modifiers = _ref2.modifiers, expression = _ref2.expression; - _newArrowCheck(this, _this15); + _newArrowCheck(this, _this14); switch (type) { case 'model': @@ -6682,7 +6689,7 @@ // If this element also has x-for on it, don't process x-if. // We will let the "x-for" directive handle the "if"ing. if (attrs.filter(function (i) { - _newArrowCheck(this, _this16); + _newArrowCheck(this, _this15); return i.type === 'for'; }.bind(this)).length > 0) return; @@ -6703,10 +6710,10 @@ }, { key: "evaluateReturnExpression", value: function evaluateReturnExpression(el, expression) { - var _this17 = this; + var _this16 = this; var extraVars = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () { - _newArrowCheck(this, _this17); + _newArrowCheck(this, _this16); }.bind(this); return saferEval(expression, this.$data, _objectSpread2({}, extraVars(), { $dispatch: this.getDispatchFunction(el) @@ -6715,10 +6722,10 @@ }, { key: "evaluateCommandExpression", value: function evaluateCommandExpression(el, expression) { - var _this18 = this; + var _this17 = this; var extraVars = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () { - _newArrowCheck(this, _this18); + _newArrowCheck(this, _this17); }.bind(this); return saferEvalNoReturn(expression, this.$data, _objectSpread2({}, extraVars(), { $dispatch: this.getDispatchFunction(el) @@ -6727,12 +6734,12 @@ }, { key: "getDispatchFunction", value: function getDispatchFunction(el) { - var _this19 = this; + var _this18 = this; return function (event) { var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - _newArrowCheck(this, _this19); + _newArrowCheck(this, _this18); el.dispatchEvent(new CustomEvent(event, { detail: detail, @@ -6743,7 +6750,7 @@ }, { key: "listenForNewElementsToInitialize", value: function listenForNewElementsToInitialize() { - var _this20 = this; + var _this19 = this; var targetNode = this.$el; var observerOptions = { @@ -6752,9 +6759,9 @@ subtree: true }; var observer = new MutationObserver(function (mutations) { - var _this21 = this; + var _this20 = this; - _newArrowCheck(this, _this20); + _newArrowCheck(this, _this19); for (var i = 0; i < mutations.length; i++) { // Filter out mutations triggered from child components. @@ -6763,14 +6770,14 @@ if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') { (function () { - var _this22 = this; + var _this21 = this; var rawData = saferEval(mutations[i].target.getAttribute('x-data'), {}); Object.keys(rawData).forEach(function (key) { - _newArrowCheck(this, _this22); + _newArrowCheck(this, _this21); - if (_this21.$data[key] !== rawData[key]) { - _this21.$data[key] = rawData[key]; + if (_this20.$data[key] !== rawData[key]) { + _this20.$data[key] = rawData[key]; } }.bind(this)); })(); @@ -6778,7 +6785,7 @@ if (mutations[i].addedNodes.length > 0) { mutations[i].addedNodes.forEach(function (node) { - _newArrowCheck(this, _this21); + _newArrowCheck(this, _this20); if (node.nodeType !== 1 || node.__x_inserted_me) return; @@ -6797,7 +6804,7 @@ }, { key: "getRefsProxy", value: function getRefsProxy() { - var _this23 = this; + var _this22 = this; var self = this; var refObj = {}; @@ -6809,7 +6816,7 @@ // we just loop on the element, look for any x-ref and create a tmp property on a fake object. this.walkAndSkipNestedComponents(self.$el, function (el) { - _newArrowCheck(this, _this23); + _newArrowCheck(this, _this22); if (el.hasAttribute('x-ref')) { refObj[el.getAttribute('x-ref')] = true; @@ -6823,14 +6830,14 @@ return new Proxy(refObj, { get: function get(object, property) { - var _this24 = this; + var _this23 = this; if (property === '$isAlpineProxy') return true; var ref; // We can't just query the DOM because it's hard to filter out refs in // nested components. self.walkAndSkipNestedComponents(self.$el, function (el) { - _newArrowCheck(this, _this24); + _newArrowCheck(this, _this23); if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) { ref = el; @@ -6840,6 +6847,36 @@ } }); } + }, { + key: "walkToConstructWatcher", + value: function walkToConstructWatcher(path, parent, options) { + var child = parent[path[0]]; + + if (_typeof(child) === 'object' && path.length > 1) { + return this.walkToConstructWatcher(path.slice(1), child, options); + } + + var deep = options.deep && _typeof(child) === 'object'; + return { + target: deep ? child : parent, + key: path[0], + deep: deep, + callbacks: [] + }; + } + }, { + key: "foundUnderObjectGraph", + value: function foundUnderObjectGraph(needle, object, context) { + if (needle === object) return true; + + for (var key in object) { + if (object.hasOwnProperty(key) && _typeof(object[key]) === 'object' && !key.startsWith('$')) { + if (context.foundUnderObjectGraph(needle, object[key], context)) return true; + } + } + + return false; + } }]); return Component; diff --git a/dist/alpine.js b/dist/alpine.js index c67dab6f5..69ac393cb 100644 --- a/dist/alpine.js +++ b/dist/alpine.js @@ -1247,9 +1247,12 @@ this.watchers = {}; - this.unobservedData.$watch = (property, callback) => { - if (!this.watchers[property]) this.watchers[property] = []; - this.watchers[property].push(callback); + this.unobservedData.$watch = (property, callback, options = {}) => { + if (!this.watchers[property]) { + this.watchers[property] = this.walkToConstructWatcher(property.split('.'), this.unobservedData, options); + } + + this.watchers[property].callbacks.push(callback); }; this.showDirectiveStack = []; @@ -1291,30 +1294,15 @@ var self = this; let membrane = new ReactiveMembrane({ valueMutated(target, key) { - if (self.watchers[key]) { - // If there's a watcher for this specific key, run it. - self.watchers[key].forEach(callback => callback(target[key])); - } else { - // Let's walk through the watchers with "dot-notation" (foo.bar) and see - // if this mutation fits any of them. - Object.keys(self.watchers).filter(i => i.includes('.')).forEach(fullDotNotationKey => { - let dotNotationParts = fullDotNotationKey.split('.'); // If this dot-notation watcher's last "part" doesn't match the current - // key, then skip it early for performance reasons. - - if (key !== dotNotationParts[dotNotationParts.length - 1]) return; // Now, walk through the dot-notation "parts" recursively to find - // a match, and call the watcher if one's found. - - dotNotationParts.reduce((comparisonData, part) => { - if (Object.is(target, comparisonData)) { - // Run the watchers. - self.watchers[fullDotNotationKey].forEach(callback => callback(target[key])); - } - - return comparisonData[part]; - }, self.getUnobservedData()); - }); - } // Don't react to data changes for cases like the `x-created` hook. - + Object.values(self.watchers).forEach(watcher => { + if (target === watcher.target && key === watcher.key) { + //Callback property watcher, return property value + watcher.callbacks.forEach(callback => callback(target[key])); + } else if (watcher.deep && self.foundUnderObjectGraph(target, watcher.target, self)) { + //Callback structure watcher, return structure value + watcher.callbacks.forEach(callback => callback(watcher.target)); + } + }); // Don't react to data changes for cases like the `x-created` hook. if (self.pauseReactivity) return; debounce(() => { @@ -1576,6 +1564,34 @@ }); } + walkToConstructWatcher(path, parent, options) { + var child = parent[path[0]]; + + if (typeof child === 'object' && path.length > 1) { + return this.walkToConstructWatcher(path.slice(1), child, options); + } + + var deep = options.deep && typeof child === 'object'; + return { + target: deep ? child : parent, + key: path[0], + deep: deep, + callbacks: [] + }; + } + + foundUnderObjectGraph(needle, object, context) { + if (needle === object) return true; + + for (var key in object) { + if (object.hasOwnProperty(key) && typeof object[key] === 'object' && !key.startsWith('$')) { + if (context.foundUnderObjectGraph(needle, object[key], context)) return true; + } + } + + return false; + } + } const Alpine = { diff --git a/src/component.js b/src/component.js index 2b2963293..48d6621be 100644 --- a/src/component.js +++ b/src/component.js @@ -42,10 +42,12 @@ export default class Component { } this.watchers = {} - this.unobservedData.$watch = (property, callback) => { - if (! this.watchers[property]) this.watchers[property] = [] + this.unobservedData.$watch = (property, callback, options = {}) => { + if (! this.watchers[property]) { + this.watchers[property] = this.walkToConstructWatcher(property.split('.'), this.unobservedData, options) + } - this.watchers[property].push(callback) + this.watchers[property].callbacks.push(callback) } this.showDirectiveStack = [] @@ -93,32 +95,16 @@ export default class Component { let membrane = new ObservableMembrane({ valueMutated(target, key) { - if (self.watchers[key]) { - // If there's a watcher for this specific key, run it. - self.watchers[key].forEach(callback => callback(target[key])) - } else { - // Let's walk through the watchers with "dot-notation" (foo.bar) and see - // if this mutation fits any of them. - Object.keys(self.watchers) - .filter(i => i.includes('.')) - .forEach(fullDotNotationKey => { - let dotNotationParts = fullDotNotationKey.split('.') - - // If this dot-notation watcher's last "part" doesn't match the current - // key, then skip it early for performance reasons. - if (key !== dotNotationParts[dotNotationParts.length - 1]) return - - // Now, walk through the dot-notation "parts" recursively to find - // a match, and call the watcher if one's found. - dotNotationParts.reduce((comparisonData, part) => { - if (Object.is(target, comparisonData)) { - // Run the watchers. - self.watchers[fullDotNotationKey].forEach(callback => callback(target[key])) - } - return comparisonData[part] - }, self.getUnobservedData()) - }) - } + Object.values(self.watchers) + .forEach(watcher => { + if (target === watcher.target && key === watcher.key) { + //Callback property watcher, return property value + watcher.callbacks.forEach(callback => callback(target[key])) + } else if (watcher.deep && self.foundUnderObjectGraph(target, watcher.target, self)) { + //Callback structure watcher, return structure value + watcher.callbacks.forEach(callback => callback(watcher.target)) + } + }) // Don't react to data changes for cases like the `x-created` hook. if (self.pauseReactivity) return @@ -414,4 +400,33 @@ export default class Component { } }) } + + walkToConstructWatcher(path, parent, options) { + var child = parent[path[0]] + + if (typeof child === 'object' && path.length > 1) { + return this.walkToConstructWatcher(path.slice(1), child, options) + } + + var deep = (options.deep && typeof child === 'object') + + return { + target: deep ? child : parent, + key: path[0], + deep: deep, + callbacks: [] + } + } + + foundUnderObjectGraph(needle, object, context) { + if (needle === object) return true + + for (var key in object) { + if (object.hasOwnProperty(key) && typeof object[key] === 'object' && !key.startsWith('$')) { + if (context.foundUnderObjectGraph(needle, object[key], context)) return true + } + } + + return false + } } diff --git a/test/watch.spec.js b/test/watch.spec.js index 658bc9415..df5deb675 100644 --- a/test/watch.spec.js +++ b/test/watch.spec.js @@ -30,25 +30,62 @@ test('$watch', async () => { test('$watch nested properties', async () => { document.body.innerHTML = ` -
-

-

+

+

+ + - +

+

+ +
` Alpine.start() - expect(document.querySelector('h1').innerText).toEqual('baz') + expect(document.querySelector('h1').innerText).toEqual('bar') expect(document.querySelector('h2').innerText).toEqual('lob') + expect(document.querySelector('h3').innerText).toEqual('quux') + expect(document.querySelector('h4').innerText).toEqual('corge') - document.querySelector('button').click() + document.querySelectorAll('button')[0].click() await wait(() => { - expect(document.querySelector('h1').innerText).toEqual('law') - expect(document.querySelector('h2').innerText).toEqual('law') + expect(document.querySelector('h1').innerText).toEqual('baz') + expect(document.querySelector('h2').innerText).toEqual('baz') + expect(document.querySelector('h3').innerText).toEqual('quux') + expect(document.querySelector('h4').innerText).toEqual('corge') }) + + document.querySelectorAll('button')[1].click() + + await wait(() => { + expect(document.querySelector('h1').innerText).toEqual('baz') + expect(document.querySelector('h2').innerText).toEqual('baz') + expect(document.querySelector('h3').innerText).toEqual('biz') + expect(document.querySelector('h4').innerText).toEqual('biz') + }) +}) + +test('$watch deep', async () => { + window.bob = [ {foo: ''} ] + + document.body.innerHTML = ` +
+ +
+ ` + + Alpine.start() + + expect(window.bob[0].foo).toEqual('') + + document.querySelector('button').click() + + expect(window.bob[0].foo).toEqual('corge') })