@odoo/owl 2.0.0-beta-8 → 2.0.0-beta-11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/owl.iife.js CHANGED
@@ -170,6 +170,10 @@
170
170
  for (let key in expr) {
171
171
  const value = expr[key];
172
172
  if (value) {
173
+ key = trim.call(key);
174
+ if (!key) {
175
+ continue;
176
+ }
173
177
  const words = split.call(key, wordRegexp);
174
178
  for (let word of words) {
175
179
  result[word] = value;
@@ -1290,29 +1294,35 @@
1290
1294
  }
1291
1295
 
1292
1296
  function createCatcher(eventsSpec) {
1293
- let setupFns = [];
1294
- let removeFns = [];
1295
- for (let name in eventsSpec) {
1296
- let index = eventsSpec[name];
1297
- let { setup, remove } = createEventHandler(name);
1298
- setupFns[index] = setup;
1299
- removeFns[index] = remove;
1300
- }
1301
- let n = setupFns.length;
1297
+ const n = Object.keys(eventsSpec).length;
1302
1298
  class VCatcher {
1303
1299
  constructor(child, handlers) {
1300
+ this.handlerFns = [];
1304
1301
  this.afterNode = null;
1305
1302
  this.child = child;
1306
- this.handlers = handlers;
1303
+ this.handlerData = handlers;
1307
1304
  }
1308
1305
  mount(parent, afterNode) {
1309
1306
  this.parentEl = parent;
1310
- this.afterNode = afterNode;
1311
1307
  this.child.mount(parent, afterNode);
1308
+ this.afterNode = document.createTextNode("");
1309
+ parent.insertBefore(this.afterNode, afterNode);
1310
+ this.wrapHandlerData();
1311
+ for (let name in eventsSpec) {
1312
+ const index = eventsSpec[name];
1313
+ const handler = createEventHandler(name);
1314
+ this.handlerFns[index] = handler;
1315
+ handler.setup.call(parent, this.handlerData[index]);
1316
+ }
1317
+ }
1318
+ wrapHandlerData() {
1312
1319
  for (let i = 0; i < n; i++) {
1313
- let origFn = this.handlers[i][0];
1320
+ let handler = this.handlerData[i];
1321
+ // handler = [...mods, fn, comp], so we need to replace second to last elem
1322
+ let idx = handler.length - 2;
1323
+ let origFn = handler[idx];
1314
1324
  const self = this;
1315
- this.handlers[i][0] = function (ev) {
1325
+ handler[idx] = function (ev) {
1316
1326
  const target = ev.target;
1317
1327
  let currentNode = self.child.firstNode();
1318
1328
  const afterNode = self.afterNode;
@@ -1323,18 +1333,21 @@
1323
1333
  currentNode = currentNode.nextSibling;
1324
1334
  }
1325
1335
  };
1326
- setupFns[i].call(parent, this.handlers[i]);
1327
1336
  }
1328
1337
  }
1329
1338
  moveBefore(other, afterNode) {
1330
- this.afterNode = null;
1331
1339
  this.child.moveBefore(other ? other.child : null, afterNode);
1340
+ this.parentEl.insertBefore(this.afterNode, afterNode);
1332
1341
  }
1333
1342
  patch(other, withBeforeRemove) {
1334
1343
  if (this === other) {
1335
1344
  return;
1336
1345
  }
1337
- this.handlers = other.handlers;
1346
+ this.handlerData = other.handlerData;
1347
+ this.wrapHandlerData();
1348
+ for (let i = 0; i < n; i++) {
1349
+ this.handlerFns[i].update.call(this.parentEl, this.handlerData[i]);
1350
+ }
1338
1351
  this.child.patch(other.child, withBeforeRemove);
1339
1352
  }
1340
1353
  beforeRemove() {
@@ -1342,9 +1355,10 @@
1342
1355
  }
1343
1356
  remove() {
1344
1357
  for (let i = 0; i < n; i++) {
1345
- removeFns[i].call(this.parentEl);
1358
+ this.handlerFns[i].remove.call(this.parentEl);
1346
1359
  }
1347
1360
  this.child.remove();
1361
+ this.afterNode.remove();
1348
1362
  }
1349
1363
  firstNode() {
1350
1364
  return this.child.firstNode();
@@ -1371,798 +1385,786 @@
1371
1385
  vnode.remove();
1372
1386
  }
1373
1387
 
1374
- // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
1375
- const TARGET = Symbol("Target");
1376
- // Escape hatch to prevent reactivity system to turn something into a reactive
1377
- const SKIP = Symbol("Skip");
1378
- // Special key to subscribe to, to be notified of key creation/deletion
1379
- const KEYCHANGES = Symbol("Key changes");
1380
- const objectToString = Object.prototype.toString;
1381
- const objectHasOwnProperty = Object.prototype.hasOwnProperty;
1382
- const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
1383
- const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
1384
- /**
1385
- * extract "RawType" from strings like "[object RawType]" => this lets us ignore
1386
- * many native objects such as Promise (whose toString is [object Promise])
1387
- * or Date ([object Date]), while also supporting collections without using
1388
- * instanceof in a loop
1389
- *
1390
- * @param obj the object to check
1391
- * @returns the raw type of the object
1392
- */
1393
- function rawType(obj) {
1394
- return objectToString.call(obj).slice(8, -1);
1395
- }
1396
- /**
1397
- * Checks whether a given value can be made into a reactive object.
1398
- *
1399
- * @param value the value to check
1400
- * @returns whether the value can be made reactive
1401
- */
1402
- function canBeMadeReactive(value) {
1403
- if (typeof value !== "object") {
1388
+ // Maps fibers to thrown errors
1389
+ const fibersInError = new WeakMap();
1390
+ const nodeErrorHandlers = new WeakMap();
1391
+ function _handleError(node, error) {
1392
+ if (!node) {
1404
1393
  return false;
1405
1394
  }
1406
- return SUPPORTED_RAW_TYPES.has(rawType(value));
1407
- }
1408
- /**
1409
- * Creates a reactive from the given object/callback if possible and returns it,
1410
- * returns the original object otherwise.
1411
- *
1412
- * @param value the value make reactive
1413
- * @returns a reactive for the given object when possible, the original otherwise
1414
- */
1415
- function possiblyReactive(val, cb) {
1416
- return canBeMadeReactive(val) ? reactive(val, cb) : val;
1417
- }
1418
- /**
1419
- * Mark an object or array so that it is ignored by the reactivity system
1420
- *
1421
- * @param value the value to mark
1422
- * @returns the object itself
1423
- */
1424
- function markRaw(value) {
1425
- value[SKIP] = true;
1426
- return value;
1427
- }
1428
- /**
1429
- * Given a reactive objet, return the raw (non reactive) underlying object
1430
- *
1431
- * @param value a reactive value
1432
- * @returns the underlying value
1433
- */
1434
- function toRaw(value) {
1435
- return value[TARGET] || value;
1436
- }
1437
- const targetToKeysToCallbacks = new WeakMap();
1438
- /**
1439
- * Observes a given key on a target with an callback. The callback will be
1440
- * called when the given key changes on the target.
1441
- *
1442
- * @param target the target whose key should be observed
1443
- * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
1444
- * or deletion)
1445
- * @param callback the function to call when the key changes
1446
- */
1447
- function observeTargetKey(target, key, callback) {
1448
- if (!targetToKeysToCallbacks.get(target)) {
1449
- targetToKeysToCallbacks.set(target, new Map());
1395
+ const fiber = node.fiber;
1396
+ if (fiber) {
1397
+ fibersInError.set(fiber, error);
1450
1398
  }
1451
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
1452
- if (!keyToCallbacks.get(key)) {
1453
- keyToCallbacks.set(key, new Set());
1399
+ const errorHandlers = nodeErrorHandlers.get(node);
1400
+ if (errorHandlers) {
1401
+ let handled = false;
1402
+ // execute in the opposite order
1403
+ for (let i = errorHandlers.length - 1; i >= 0; i--) {
1404
+ try {
1405
+ errorHandlers[i](error);
1406
+ handled = true;
1407
+ break;
1408
+ }
1409
+ catch (e) {
1410
+ error = e;
1411
+ }
1412
+ }
1413
+ if (handled) {
1414
+ return true;
1415
+ }
1454
1416
  }
1455
- keyToCallbacks.get(key).add(callback);
1456
- if (!callbacksToTargets.has(callback)) {
1457
- callbacksToTargets.set(callback, new Set());
1417
+ return _handleError(node.parent, error);
1418
+ }
1419
+ function handleError(params) {
1420
+ const error = params.error;
1421
+ const node = "node" in params ? params.node : params.fiber.node;
1422
+ const fiber = "fiber" in params ? params.fiber : node.fiber;
1423
+ // resets the fibers on components if possible. This is important so that
1424
+ // new renderings can be properly included in the initial one, if any.
1425
+ let current = fiber;
1426
+ do {
1427
+ current.node.fiber = current;
1428
+ current = current.parent;
1429
+ } while (current);
1430
+ fibersInError.set(fiber.root, error);
1431
+ const handled = _handleError(node, error);
1432
+ if (!handled) {
1433
+ console.warn(`[Owl] Unhandled error. Destroying the root component`);
1434
+ try {
1435
+ node.app.destroy();
1436
+ }
1437
+ catch (e) {
1438
+ console.error(e);
1439
+ }
1458
1440
  }
1459
- callbacksToTargets.get(callback).add(target);
1441
+ }
1442
+
1443
+ function makeChildFiber(node, parent) {
1444
+ let current = node.fiber;
1445
+ if (current) {
1446
+ cancelFibers(current.children);
1447
+ current.root = null;
1448
+ }
1449
+ return new Fiber(node, parent);
1460
1450
  }
1461
- /**
1462
- * Notify Reactives that are observing a given target that a key has changed on
1463
- * the target.
1464
- *
1465
- * @param target target whose Reactives should be notified that the target was
1466
- * changed.
1467
- * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
1468
- * or deleted)
1469
- */
1470
- function notifyReactives(target, key) {
1471
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
1472
- if (!keyToCallbacks) {
1473
- return;
1451
+ function makeRootFiber(node) {
1452
+ let current = node.fiber;
1453
+ if (current) {
1454
+ let root = current.root;
1455
+ // lock root fiber because canceling children fibers may destroy components,
1456
+ // which means any arbitrary code can be run in onWillDestroy, which may
1457
+ // trigger new renderings
1458
+ root.locked = true;
1459
+ root.setCounter(root.counter + 1 - cancelFibers(current.children));
1460
+ root.locked = false;
1461
+ current.children = [];
1462
+ current.childrenMap = {};
1463
+ current.bdom = null;
1464
+ if (fibersInError.has(current)) {
1465
+ fibersInError.delete(current);
1466
+ fibersInError.delete(root);
1467
+ current.appliedToDom = false;
1468
+ }
1469
+ return current;
1474
1470
  }
1475
- const callbacks = keyToCallbacks.get(key);
1476
- if (!callbacks) {
1477
- return;
1471
+ const fiber = new RootFiber(node, null);
1472
+ if (node.willPatch.length) {
1473
+ fiber.willPatch.push(fiber);
1478
1474
  }
1479
- // Loop on copy because clearReactivesForCallback will modify the set in place
1480
- for (const callback of [...callbacks]) {
1481
- clearReactivesForCallback(callback);
1482
- callback();
1475
+ if (node.patched.length) {
1476
+ fiber.patched.push(fiber);
1483
1477
  }
1478
+ return fiber;
1479
+ }
1480
+ function throwOnRender() {
1481
+ throw new Error("Attempted to render cancelled fiber");
1484
1482
  }
1485
- const callbacksToTargets = new WeakMap();
1486
1483
  /**
1487
- * Clears all subscriptions of the Reactives associated with a given callback.
1488
- *
1489
- * @param callback the callback for which the reactives need to be cleared
1484
+ * @returns number of not-yet rendered fibers cancelled
1490
1485
  */
1491
- function clearReactivesForCallback(callback) {
1492
- const targetsToClear = callbacksToTargets.get(callback);
1493
- if (!targetsToClear) {
1494
- return;
1495
- }
1496
- for (const target of targetsToClear) {
1497
- const observedKeys = targetToKeysToCallbacks.get(target);
1498
- if (!observedKeys) {
1499
- continue;
1486
+ function cancelFibers(fibers) {
1487
+ let result = 0;
1488
+ for (let fiber of fibers) {
1489
+ let node = fiber.node;
1490
+ fiber.render = throwOnRender;
1491
+ if (node.status === 0 /* NEW */) {
1492
+ node.destroy();
1493
+ delete node.parent.children[node.parentKey];
1500
1494
  }
1501
- for (const callbacks of observedKeys.values()) {
1502
- callbacks.delete(callback);
1495
+ node.fiber = null;
1496
+ if (fiber.bdom) {
1497
+ // if fiber has been rendered, this means that the component props have
1498
+ // been updated. however, this fiber will not be patched to the dom, so
1499
+ // it could happen that the next render compare the current props with
1500
+ // the same props, and skip the render completely. With the next line,
1501
+ // we kindly request the component code to force a render, so it works as
1502
+ // expected.
1503
+ node.forceNextRender = true;
1504
+ }
1505
+ else {
1506
+ result++;
1503
1507
  }
1508
+ result += cancelFibers(fiber.children);
1504
1509
  }
1505
- targetsToClear.clear();
1506
- }
1507
- function getSubscriptions(callback) {
1508
- const targets = callbacksToTargets.get(callback) || [];
1509
- return [...targets].map((target) => {
1510
- const keysToCallbacks = targetToKeysToCallbacks.get(target);
1511
- return {
1512
- target,
1513
- keys: keysToCallbacks ? [...keysToCallbacks.keys()] : [],
1514
- };
1515
- });
1510
+ return result;
1516
1511
  }
1517
- const reactiveCache = new WeakMap();
1518
- /**
1519
- * Creates a reactive proxy for an object. Reading data on the reactive object
1520
- * subscribes to changes to the data. Writing data on the object will cause the
1521
- * notify callback to be called if there are suscriptions to that data. Nested
1522
- * objects and arrays are automatically made reactive as well.
1523
- *
1524
- * Whenever you are notified of a change, all subscriptions are cleared, and if
1525
- * you would like to be notified of any further changes, you should go read
1526
- * the underlying data again. We assume that if you don't go read it again after
1527
- * being notified, it means that you are no longer interested in that data.
1528
- *
1529
- * Subscriptions:
1530
- * + Reading a property on an object will subscribe you to changes in the value
1531
- * of that property.
1532
- * + Accessing an object keys (eg with Object.keys or with `for..in`) will
1533
- * subscribe you to the creation/deletion of keys. Checking the presence of a
1534
- * key on the object with 'in' has the same effect.
1535
- * - getOwnPropertyDescriptor does not currently subscribe you to the property.
1536
- * This is a choice that was made because changing a key's value will trigger
1537
- * this trap and we do not want to subscribe by writes. This also means that
1538
- * Object.hasOwnProperty doesn't subscribe as it goes through this trap.
1539
- *
1540
- * @param target the object for which to create a reactive proxy
1541
- * @param callback the function to call when an observed property of the
1542
- * reactive has changed
1543
- * @returns a proxy that tracks changes to it
1544
- */
1545
- function reactive(target, callback = () => { }) {
1546
- if (!canBeMadeReactive(target)) {
1547
- throw new Error(`Cannot make the given value reactive`);
1512
+ class Fiber {
1513
+ constructor(node, parent) {
1514
+ this.bdom = null;
1515
+ this.children = [];
1516
+ this.appliedToDom = false;
1517
+ this.deep = false;
1518
+ this.childrenMap = {};
1519
+ this.node = node;
1520
+ this.parent = parent;
1521
+ if (parent) {
1522
+ this.deep = parent.deep;
1523
+ const root = parent.root;
1524
+ root.setCounter(root.counter + 1);
1525
+ this.root = root;
1526
+ parent.children.push(this);
1527
+ }
1528
+ else {
1529
+ this.root = this;
1530
+ }
1548
1531
  }
1549
- if (SKIP in target) {
1550
- return target;
1532
+ render() {
1533
+ // if some parent has a fiber => register in followup
1534
+ let prev = this.root.node;
1535
+ let scheduler = prev.app.scheduler;
1536
+ let current = prev.parent;
1537
+ while (current) {
1538
+ if (current.fiber) {
1539
+ let root = current.fiber.root;
1540
+ if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {
1541
+ current = root.node;
1542
+ }
1543
+ else {
1544
+ scheduler.delayedRenders.push(this);
1545
+ return;
1546
+ }
1547
+ }
1548
+ prev = current;
1549
+ current = current.parent;
1550
+ }
1551
+ // there are no current rendering from above => we can render
1552
+ this._render();
1551
1553
  }
1552
- const originalTarget = target[TARGET];
1553
- if (originalTarget) {
1554
- return reactive(originalTarget, callback);
1554
+ _render() {
1555
+ const node = this.node;
1556
+ const root = this.root;
1557
+ if (root) {
1558
+ try {
1559
+ this.bdom = true;
1560
+ this.bdom = node.renderFn();
1561
+ }
1562
+ catch (e) {
1563
+ handleError({ node, error: e });
1564
+ }
1565
+ root.setCounter(root.counter - 1);
1566
+ }
1555
1567
  }
1556
- if (!reactiveCache.has(target)) {
1557
- reactiveCache.set(target, new WeakMap());
1568
+ }
1569
+ class RootFiber extends Fiber {
1570
+ constructor() {
1571
+ super(...arguments);
1572
+ this.counter = 1;
1573
+ // only add stuff in this if they have registered some hooks
1574
+ this.willPatch = [];
1575
+ this.patched = [];
1576
+ this.mounted = [];
1577
+ // A fiber is typically locked when it is completing and the patch has not, or is being applied.
1578
+ // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
1579
+ this.locked = false;
1558
1580
  }
1559
- const reactivesForTarget = reactiveCache.get(target);
1560
- if (!reactivesForTarget.has(callback)) {
1561
- const targetRawType = rawType(target);
1562
- const handler = COLLECTION_RAWTYPES.has(targetRawType)
1563
- ? collectionsProxyHandler(target, callback, targetRawType)
1564
- : basicProxyHandler(callback);
1565
- const proxy = new Proxy(target, handler);
1566
- reactivesForTarget.set(callback, proxy);
1581
+ complete() {
1582
+ const node = this.node;
1583
+ this.locked = true;
1584
+ let current = undefined;
1585
+ try {
1586
+ // Step 1: calling all willPatch lifecycle hooks
1587
+ for (current of this.willPatch) {
1588
+ // because of the asynchronous nature of the rendering, some parts of the
1589
+ // UI may have been rendered, then deleted in a followup rendering, and we
1590
+ // do not want to call onWillPatch in that case.
1591
+ let node = current.node;
1592
+ if (node.fiber === current) {
1593
+ const component = node.component;
1594
+ for (let cb of node.willPatch) {
1595
+ cb.call(component);
1596
+ }
1597
+ }
1598
+ }
1599
+ current = undefined;
1600
+ // Step 2: patching the dom
1601
+ node._patch();
1602
+ this.locked = false;
1603
+ // Step 4: calling all mounted lifecycle hooks
1604
+ let mountedFibers = this.mounted;
1605
+ while ((current = mountedFibers.pop())) {
1606
+ current = current;
1607
+ if (current.appliedToDom) {
1608
+ for (let cb of current.node.mounted) {
1609
+ cb();
1610
+ }
1611
+ }
1612
+ }
1613
+ // Step 5: calling all patched hooks
1614
+ let patchedFibers = this.patched;
1615
+ while ((current = patchedFibers.pop())) {
1616
+ current = current;
1617
+ if (current.appliedToDom) {
1618
+ for (let cb of current.node.patched) {
1619
+ cb();
1620
+ }
1621
+ }
1622
+ }
1623
+ }
1624
+ catch (e) {
1625
+ this.locked = false;
1626
+ handleError({ fiber: current || this, error: e });
1627
+ }
1628
+ }
1629
+ setCounter(newValue) {
1630
+ this.counter = newValue;
1631
+ if (newValue === 0) {
1632
+ this.node.app.scheduler.flush();
1633
+ }
1567
1634
  }
1568
- return reactivesForTarget.get(callback);
1569
1635
  }
1570
- /**
1571
- * Creates a basic proxy handler for regular objects and arrays.
1572
- *
1573
- * @param callback @see reactive
1574
- * @returns a proxy handler object
1575
- */
1576
- function basicProxyHandler(callback) {
1577
- return {
1578
- get(target, key, proxy) {
1579
- if (key === TARGET) {
1580
- return target;
1581
- }
1582
- // non-writable non-configurable properties cannot be made reactive
1583
- const desc = Object.getOwnPropertyDescriptor(target, key);
1584
- if (desc && !desc.writable && !desc.configurable) {
1585
- return Reflect.get(target, key, proxy);
1636
+ class MountFiber extends RootFiber {
1637
+ constructor(node, target, options = {}) {
1638
+ super(node, null);
1639
+ this.target = target;
1640
+ this.position = options.position || "last-child";
1641
+ }
1642
+ complete() {
1643
+ let current = this;
1644
+ try {
1645
+ const node = this.node;
1646
+ node.children = this.childrenMap;
1647
+ node.app.constructor.validateTarget(this.target);
1648
+ if (node.bdom) {
1649
+ // this is a complicated situation: if we mount a fiber with an existing
1650
+ // bdom, this means that this same fiber was already completed, mounted,
1651
+ // but a crash occurred in some mounted hook. Then, it was handled and
1652
+ // the new rendering is being applied.
1653
+ node.updateDom();
1586
1654
  }
1587
- observeTargetKey(target, key, callback);
1588
- return possiblyReactive(Reflect.get(target, key, proxy), callback);
1589
- },
1590
- set(target, key, value, proxy) {
1591
- const isNewKey = !objectHasOwnProperty.call(target, key);
1592
- const originalValue = Reflect.get(target, key, proxy);
1593
- const ret = Reflect.set(target, key, value, proxy);
1594
- if (isNewKey) {
1595
- notifyReactives(target, KEYCHANGES);
1655
+ else {
1656
+ node.bdom = this.bdom;
1657
+ if (this.position === "last-child" || this.target.childNodes.length === 0) {
1658
+ mount$1(node.bdom, this.target);
1659
+ }
1660
+ else {
1661
+ const firstChild = this.target.childNodes[0];
1662
+ mount$1(node.bdom, this.target, firstChild);
1663
+ }
1596
1664
  }
1597
- // While Array length may trigger the set trap, it's not actually set by this
1598
- // method but is updated behind the scenes, and the trap is not called with the
1599
- // new value. We disable the "same-value-optimization" for it because of that.
1600
- if (originalValue !== value || (Array.isArray(target) && key === "length")) {
1601
- notifyReactives(target, key);
1665
+ // unregistering the fiber before mounted since it can do another render
1666
+ // and that the current rendering is obviously completed
1667
+ node.fiber = null;
1668
+ node.status = 1 /* MOUNTED */;
1669
+ this.appliedToDom = true;
1670
+ let mountedFibers = this.mounted;
1671
+ while ((current = mountedFibers.pop())) {
1672
+ if (current.appliedToDom) {
1673
+ for (let cb of current.node.mounted) {
1674
+ cb();
1675
+ }
1676
+ }
1602
1677
  }
1603
- return ret;
1604
- },
1605
- deleteProperty(target, key) {
1606
- const ret = Reflect.deleteProperty(target, key);
1607
- // TODO: only notify when something was actually deleted
1608
- notifyReactives(target, KEYCHANGES);
1609
- notifyReactives(target, key);
1610
- return ret;
1611
- },
1612
- ownKeys(target) {
1613
- observeTargetKey(target, KEYCHANGES, callback);
1614
- return Reflect.ownKeys(target);
1615
- },
1616
- has(target, key) {
1617
- // TODO: this observes all key changes instead of only the presence of the argument key
1618
- // observing the key itself would observe value changes instead of presence changes
1619
- // so we may need a finer grained system to distinguish observing value vs presence.
1620
- observeTargetKey(target, KEYCHANGES, callback);
1621
- return Reflect.has(target, key);
1622
- },
1623
- };
1624
- }
1678
+ }
1679
+ catch (e) {
1680
+ handleError({ fiber: current, error: e });
1681
+ }
1682
+ }
1683
+ }
1684
+
1685
+ // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
1686
+ const TARGET = Symbol("Target");
1687
+ // Escape hatch to prevent reactivity system to turn something into a reactive
1688
+ const SKIP = Symbol("Skip");
1689
+ // Special key to subscribe to, to be notified of key creation/deletion
1690
+ const KEYCHANGES = Symbol("Key changes");
1691
+ const objectToString = Object.prototype.toString;
1692
+ const objectHasOwnProperty = Object.prototype.hasOwnProperty;
1693
+ const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
1694
+ const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
1625
1695
  /**
1626
- * Creates a function that will observe the key that is passed to it when called
1627
- * and delegates to the underlying method.
1696
+ * extract "RawType" from strings like "[object RawType]" => this lets us ignore
1697
+ * many native objects such as Promise (whose toString is [object Promise])
1698
+ * or Date ([object Date]), while also supporting collections without using
1699
+ * instanceof in a loop
1628
1700
  *
1629
- * @param methodName name of the method to delegate to
1630
- * @param target @see reactive
1631
- * @param callback @see reactive
1701
+ * @param obj the object to check
1702
+ * @returns the raw type of the object
1632
1703
  */
1633
- function makeKeyObserver(methodName, target, callback) {
1634
- return (key) => {
1635
- key = toRaw(key);
1636
- observeTargetKey(target, key, callback);
1637
- return possiblyReactive(target[methodName](key), callback);
1638
- };
1704
+ function rawType(obj) {
1705
+ return objectToString.call(obj).slice(8, -1);
1639
1706
  }
1640
1707
  /**
1641
- * Creates an iterable that will delegate to the underlying iteration method and
1642
- * observe keys as necessary.
1708
+ * Checks whether a given value can be made into a reactive object.
1643
1709
  *
1644
- * @param methodName name of the method to delegate to
1645
- * @param target @see reactive
1646
- * @param callback @see reactive
1710
+ * @param value the value to check
1711
+ * @returns whether the value can be made reactive
1647
1712
  */
1648
- function makeIteratorObserver(methodName, target, callback) {
1649
- return function* () {
1650
- observeTargetKey(target, KEYCHANGES, callback);
1651
- const keys = target.keys();
1652
- for (const item of target[methodName]()) {
1653
- const key = keys.next().value;
1654
- observeTargetKey(target, key, callback);
1655
- yield possiblyReactive(item, callback);
1656
- }
1657
- };
1713
+ function canBeMadeReactive(value) {
1714
+ if (typeof value !== "object") {
1715
+ return false;
1716
+ }
1717
+ return SUPPORTED_RAW_TYPES.has(rawType(value));
1658
1718
  }
1659
1719
  /**
1660
- * Creates a forEach function that will delegate to forEach on the underlying
1661
- * collection while observing key changes, and keys as they're iterated over,
1662
- * and making the passed keys/values reactive.
1720
+ * Creates a reactive from the given object/callback if possible and returns it,
1721
+ * returns the original object otherwise.
1663
1722
  *
1664
- * @param target @see reactive
1665
- * @param callback @see reactive
1723
+ * @param value the value make reactive
1724
+ * @returns a reactive for the given object when possible, the original otherwise
1666
1725
  */
1667
- function makeForEachObserver(target, callback) {
1668
- return function forEach(forEachCb, thisArg) {
1669
- observeTargetKey(target, KEYCHANGES, callback);
1670
- target.forEach(function (val, key, targetObj) {
1671
- observeTargetKey(target, key, callback);
1672
- forEachCb.call(thisArg, possiblyReactive(val, callback), possiblyReactive(key, callback), possiblyReactive(targetObj, callback));
1673
- }, thisArg);
1674
- };
1726
+ function possiblyReactive(val, cb) {
1727
+ return canBeMadeReactive(val) ? reactive(val, cb) : val;
1675
1728
  }
1676
1729
  /**
1677
- * Creates a function that will delegate to an underlying method, and check if
1678
- * that method has modified the presence or value of a key, and notify the
1679
- * reactives appropriately.
1730
+ * Mark an object or array so that it is ignored by the reactivity system
1680
1731
  *
1681
- * @param setterName name of the method to delegate to
1682
- * @param getterName name of the method which should be used to retrieve the
1683
- * value before calling the delegate method for comparison purposes
1684
- * @param target @see reactive
1732
+ * @param value the value to mark
1733
+ * @returns the object itself
1685
1734
  */
1686
- function delegateAndNotify(setterName, getterName, target) {
1687
- return (key, value) => {
1688
- key = toRaw(key);
1689
- const hadKey = target.has(key);
1690
- const originalValue = target[getterName](key);
1691
- const ret = target[setterName](key, value);
1692
- const hasKey = target.has(key);
1693
- if (hadKey !== hasKey) {
1694
- notifyReactives(target, KEYCHANGES);
1695
- }
1696
- if (originalValue !== value) {
1697
- notifyReactives(target, key);
1698
- }
1699
- return ret;
1700
- };
1735
+ function markRaw(value) {
1736
+ value[SKIP] = true;
1737
+ return value;
1701
1738
  }
1702
1739
  /**
1703
- * Creates a function that will clear the underlying collection and notify that
1704
- * the keys of the collection have changed.
1740
+ * Given a reactive objet, return the raw (non reactive) underlying object
1705
1741
  *
1706
- * @param target @see reactive
1742
+ * @param value a reactive value
1743
+ * @returns the underlying value
1707
1744
  */
1708
- function makeClearNotifier(target) {
1709
- return () => {
1710
- const allKeys = [...target.keys()];
1711
- target.clear();
1712
- notifyReactives(target, KEYCHANGES);
1713
- for (const key of allKeys) {
1714
- notifyReactives(target, key);
1715
- }
1716
- };
1745
+ function toRaw(value) {
1746
+ return value[TARGET] || value;
1717
1747
  }
1748
+ const targetToKeysToCallbacks = new WeakMap();
1718
1749
  /**
1719
- * Maps raw type of an object to an object containing functions that can be used
1720
- * to build an appropritate proxy handler for that raw type. Eg: when making a
1721
- * reactive set, calling the has method should mark the key that is being
1722
- * retrieved as observed, and calling the add or delete method should notify the
1723
- * reactives that the key which is being added or deleted has been modified.
1724
- */
1725
- const rawTypeToFuncHandlers = {
1726
- Set: (target, callback) => ({
1727
- has: makeKeyObserver("has", target, callback),
1728
- add: delegateAndNotify("add", "has", target),
1729
- delete: delegateAndNotify("delete", "has", target),
1730
- keys: makeIteratorObserver("keys", target, callback),
1731
- values: makeIteratorObserver("values", target, callback),
1732
- entries: makeIteratorObserver("entries", target, callback),
1733
- [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
1734
- forEach: makeForEachObserver(target, callback),
1735
- clear: makeClearNotifier(target),
1736
- get size() {
1737
- observeTargetKey(target, KEYCHANGES, callback);
1738
- return target.size;
1739
- },
1740
- }),
1741
- Map: (target, callback) => ({
1742
- has: makeKeyObserver("has", target, callback),
1743
- get: makeKeyObserver("get", target, callback),
1744
- set: delegateAndNotify("set", "get", target),
1745
- delete: delegateAndNotify("delete", "has", target),
1746
- keys: makeIteratorObserver("keys", target, callback),
1747
- values: makeIteratorObserver("values", target, callback),
1748
- entries: makeIteratorObserver("entries", target, callback),
1749
- [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
1750
- forEach: makeForEachObserver(target, callback),
1751
- clear: makeClearNotifier(target),
1752
- get size() {
1753
- observeTargetKey(target, KEYCHANGES, callback);
1754
- return target.size;
1755
- },
1756
- }),
1757
- WeakMap: (target, callback) => ({
1758
- has: makeKeyObserver("has", target, callback),
1759
- get: makeKeyObserver("get", target, callback),
1760
- set: delegateAndNotify("set", "get", target),
1761
- delete: delegateAndNotify("delete", "has", target),
1762
- }),
1763
- };
1764
- /**
1765
- * Creates a proxy handler for collections (Set/Map/WeakMap)
1766
- *
1767
- * @param callback @see reactive
1768
- * @param target @see reactive
1769
- * @returns a proxy handler object
1770
- */
1771
- function collectionsProxyHandler(target, callback, targetRawType) {
1772
- // TODO: if performance is an issue we can create the special handlers lazily when each
1773
- // property is read.
1774
- const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
1775
- return Object.assign(basicProxyHandler(callback), {
1776
- get(target, key) {
1777
- if (key === TARGET) {
1778
- return target;
1779
- }
1780
- if (objectHasOwnProperty.call(specialHandlers, key)) {
1781
- return specialHandlers[key];
1782
- }
1783
- observeTargetKey(target, key, callback);
1784
- return possiblyReactive(target[key], callback);
1785
- },
1786
- });
1787
- }
1788
-
1789
- /**
1790
- * Creates a batched version of a callback so that all calls to it in the same
1791
- * microtick will only call the original callback once.
1750
+ * Observes a given key on a target with an callback. The callback will be
1751
+ * called when the given key changes on the target.
1792
1752
  *
1793
- * @param callback the callback to batch
1794
- * @returns a batched version of the original callback
1753
+ * @param target the target whose key should be observed
1754
+ * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
1755
+ * or deletion)
1756
+ * @param callback the function to call when the key changes
1795
1757
  */
1796
- function batched(callback) {
1797
- let called = false;
1798
- return async () => {
1799
- // This await blocks all calls to the callback here, then releases them sequentially
1800
- // in the next microtick. This line decides the granularity of the batch.
1801
- await Promise.resolve();
1802
- if (!called) {
1803
- called = true;
1804
- // wait for all calls in this microtick to fall through before resetting "called"
1805
- // so that only the first call to the batched function calls the original callback.
1806
- // Schedule this before calling the callback so that calls to the batched function
1807
- // within the callback will proceed only after resetting called to false, and have
1808
- // a chance to execute the callback again
1809
- Promise.resolve().then(() => (called = false));
1810
- callback();
1811
- }
1812
- };
1813
- }
1814
- function validateTarget(target) {
1815
- if (!(target instanceof HTMLElement)) {
1816
- throw new Error("Cannot mount component: the target is not a valid DOM element");
1758
+ function observeTargetKey(target, key, callback) {
1759
+ if (!targetToKeysToCallbacks.get(target)) {
1760
+ targetToKeysToCallbacks.set(target, new Map());
1817
1761
  }
1818
- if (!document.body.contains(target)) {
1819
- throw new Error("Cannot mount a component on a detached dom node");
1762
+ const keyToCallbacks = targetToKeysToCallbacks.get(target);
1763
+ if (!keyToCallbacks.get(key)) {
1764
+ keyToCallbacks.set(key, new Set());
1765
+ }
1766
+ keyToCallbacks.get(key).add(callback);
1767
+ if (!callbacksToTargets.has(callback)) {
1768
+ callbacksToTargets.set(callback, new Set());
1820
1769
  }
1770
+ callbacksToTargets.get(callback).add(target);
1821
1771
  }
1822
- class EventBus extends EventTarget {
1823
- trigger(name, payload) {
1824
- this.dispatchEvent(new CustomEvent(name, { detail: payload }));
1772
+ /**
1773
+ * Notify Reactives that are observing a given target that a key has changed on
1774
+ * the target.
1775
+ *
1776
+ * @param target target whose Reactives should be notified that the target was
1777
+ * changed.
1778
+ * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
1779
+ * or deleted)
1780
+ */
1781
+ function notifyReactives(target, key) {
1782
+ const keyToCallbacks = targetToKeysToCallbacks.get(target);
1783
+ if (!keyToCallbacks) {
1784
+ return;
1785
+ }
1786
+ const callbacks = keyToCallbacks.get(key);
1787
+ if (!callbacks) {
1788
+ return;
1789
+ }
1790
+ // Loop on copy because clearReactivesForCallback will modify the set in place
1791
+ for (const callback of [...callbacks]) {
1792
+ clearReactivesForCallback(callback);
1793
+ callback();
1825
1794
  }
1826
1795
  }
1827
- function whenReady(fn) {
1828
- return new Promise(function (resolve) {
1829
- if (document.readyState !== "loading") {
1830
- resolve(true);
1796
+ const callbacksToTargets = new WeakMap();
1797
+ /**
1798
+ * Clears all subscriptions of the Reactives associated with a given callback.
1799
+ *
1800
+ * @param callback the callback for which the reactives need to be cleared
1801
+ */
1802
+ function clearReactivesForCallback(callback) {
1803
+ const targetsToClear = callbacksToTargets.get(callback);
1804
+ if (!targetsToClear) {
1805
+ return;
1806
+ }
1807
+ for (const target of targetsToClear) {
1808
+ const observedKeys = targetToKeysToCallbacks.get(target);
1809
+ if (!observedKeys) {
1810
+ continue;
1831
1811
  }
1832
- else {
1833
- document.addEventListener("DOMContentLoaded", resolve, false);
1812
+ for (const callbacks of observedKeys.values()) {
1813
+ callbacks.delete(callback);
1834
1814
  }
1835
- }).then(fn || function () { });
1836
- }
1837
- async function loadFile(url) {
1838
- const result = await fetch(url);
1839
- if (!result.ok) {
1840
- throw new Error("Error while fetching xml templates");
1841
1815
  }
1842
- return await result.text();
1816
+ targetsToClear.clear();
1843
1817
  }
1844
- /*
1845
- * This class just transports the fact that a string is safe
1846
- * to be injected as HTML. Overriding a JS primitive is quite painful though
1847
- * so we need to redfine toString and valueOf.
1848
- */
1849
- class Markup extends String {
1818
+ function getSubscriptions(callback) {
1819
+ const targets = callbacksToTargets.get(callback) || [];
1820
+ return [...targets].map((target) => {
1821
+ const keysToCallbacks = targetToKeysToCallbacks.get(target);
1822
+ return {
1823
+ target,
1824
+ keys: keysToCallbacks ? [...keysToCallbacks.keys()] : [],
1825
+ };
1826
+ });
1850
1827
  }
1851
- /*
1852
- * Marks a value as safe, that is, a value that can be injected as HTML directly.
1853
- * It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
1828
+ const reactiveCache = new WeakMap();
1829
+ /**
1830
+ * Creates a reactive proxy for an object. Reading data on the reactive object
1831
+ * subscribes to changes to the data. Writing data on the object will cause the
1832
+ * notify callback to be called if there are suscriptions to that data. Nested
1833
+ * objects and arrays are automatically made reactive as well.
1834
+ *
1835
+ * Whenever you are notified of a change, all subscriptions are cleared, and if
1836
+ * you would like to be notified of any further changes, you should go read
1837
+ * the underlying data again. We assume that if you don't go read it again after
1838
+ * being notified, it means that you are no longer interested in that data.
1839
+ *
1840
+ * Subscriptions:
1841
+ * + Reading a property on an object will subscribe you to changes in the value
1842
+ * of that property.
1843
+ * + Accessing an object keys (eg with Object.keys or with `for..in`) will
1844
+ * subscribe you to the creation/deletion of keys. Checking the presence of a
1845
+ * key on the object with 'in' has the same effect.
1846
+ * - getOwnPropertyDescriptor does not currently subscribe you to the property.
1847
+ * This is a choice that was made because changing a key's value will trigger
1848
+ * this trap and we do not want to subscribe by writes. This also means that
1849
+ * Object.hasOwnProperty doesn't subscribe as it goes through this trap.
1850
+ *
1851
+ * @param target the object for which to create a reactive proxy
1852
+ * @param callback the function to call when an observed property of the
1853
+ * reactive has changed
1854
+ * @returns a proxy that tracks changes to it
1854
1855
  */
1855
- function markup(value) {
1856
- return new Markup(value);
1857
- }
1858
-
1859
- class Component {
1860
- constructor(props, env, node) {
1861
- this.props = props;
1862
- this.env = env;
1863
- this.__owl__ = node;
1856
+ function reactive(target, callback = () => { }) {
1857
+ if (!canBeMadeReactive(target)) {
1858
+ throw new Error(`Cannot make the given value reactive`);
1864
1859
  }
1865
- setup() { }
1866
- render(deep = false) {
1867
- this.__owl__.render(deep === true);
1860
+ if (SKIP in target) {
1861
+ return target;
1868
1862
  }
1869
- }
1870
- Component.template = "";
1871
-
1872
- // Maps fibers to thrown errors
1873
- const fibersInError = new WeakMap();
1874
- const nodeErrorHandlers = new WeakMap();
1875
- function _handleError(node, error) {
1876
- if (!node) {
1877
- return false;
1863
+ const originalTarget = target[TARGET];
1864
+ if (originalTarget) {
1865
+ return reactive(originalTarget, callback);
1878
1866
  }
1879
- const fiber = node.fiber;
1880
- if (fiber) {
1881
- fibersInError.set(fiber, error);
1867
+ if (!reactiveCache.has(target)) {
1868
+ reactiveCache.set(target, new WeakMap());
1882
1869
  }
1883
- const errorHandlers = nodeErrorHandlers.get(node);
1884
- if (errorHandlers) {
1885
- let handled = false;
1886
- // execute in the opposite order
1887
- for (let i = errorHandlers.length - 1; i >= 0; i--) {
1888
- try {
1889
- errorHandlers[i](error);
1890
- handled = true;
1891
- break;
1892
- }
1893
- catch (e) {
1894
- error = e;
1895
- }
1896
- }
1897
- if (handled) {
1898
- return true;
1899
- }
1870
+ const reactivesForTarget = reactiveCache.get(target);
1871
+ if (!reactivesForTarget.has(callback)) {
1872
+ const targetRawType = rawType(target);
1873
+ const handler = COLLECTION_RAWTYPES.has(targetRawType)
1874
+ ? collectionsProxyHandler(target, callback, targetRawType)
1875
+ : basicProxyHandler(callback);
1876
+ const proxy = new Proxy(target, handler);
1877
+ reactivesForTarget.set(callback, proxy);
1900
1878
  }
1901
- return _handleError(node.parent, error);
1879
+ return reactivesForTarget.get(callback);
1902
1880
  }
1903
- function handleError(params) {
1904
- const error = params.error;
1905
- const node = "node" in params ? params.node : params.fiber.node;
1906
- const fiber = "fiber" in params ? params.fiber : node.fiber;
1907
- // resets the fibers on components if possible. This is important so that
1908
- // new renderings can be properly included in the initial one, if any.
1909
- let current = fiber;
1910
- do {
1911
- current.node.fiber = current;
1912
- current = current.parent;
1913
- } while (current);
1914
- fibersInError.set(fiber.root, error);
1915
- const handled = _handleError(node, error);
1916
- if (!handled) {
1917
- console.warn(`[Owl] Unhandled error. Destroying the root component`);
1918
- try {
1919
- node.app.destroy();
1920
- }
1921
- catch (e) {
1922
- console.error(e);
1923
- }
1924
- }
1925
- }
1926
-
1927
- function makeChildFiber(node, parent) {
1928
- let current = node.fiber;
1929
- if (current) {
1930
- cancelFibers(current.children);
1931
- current.root = null;
1932
- }
1933
- return new Fiber(node, parent);
1881
+ /**
1882
+ * Creates a basic proxy handler for regular objects and arrays.
1883
+ *
1884
+ * @param callback @see reactive
1885
+ * @returns a proxy handler object
1886
+ */
1887
+ function basicProxyHandler(callback) {
1888
+ return {
1889
+ get(target, key, proxy) {
1890
+ if (key === TARGET) {
1891
+ return target;
1892
+ }
1893
+ // non-writable non-configurable properties cannot be made reactive
1894
+ const desc = Object.getOwnPropertyDescriptor(target, key);
1895
+ if (desc && !desc.writable && !desc.configurable) {
1896
+ return Reflect.get(target, key, proxy);
1897
+ }
1898
+ observeTargetKey(target, key, callback);
1899
+ return possiblyReactive(Reflect.get(target, key, proxy), callback);
1900
+ },
1901
+ set(target, key, value, proxy) {
1902
+ const isNewKey = !objectHasOwnProperty.call(target, key);
1903
+ const originalValue = Reflect.get(target, key, proxy);
1904
+ const ret = Reflect.set(target, key, value, proxy);
1905
+ if (isNewKey) {
1906
+ notifyReactives(target, KEYCHANGES);
1907
+ }
1908
+ // While Array length may trigger the set trap, it's not actually set by this
1909
+ // method but is updated behind the scenes, and the trap is not called with the
1910
+ // new value. We disable the "same-value-optimization" for it because of that.
1911
+ if (originalValue !== value || (Array.isArray(target) && key === "length")) {
1912
+ notifyReactives(target, key);
1913
+ }
1914
+ return ret;
1915
+ },
1916
+ deleteProperty(target, key) {
1917
+ const ret = Reflect.deleteProperty(target, key);
1918
+ // TODO: only notify when something was actually deleted
1919
+ notifyReactives(target, KEYCHANGES);
1920
+ notifyReactives(target, key);
1921
+ return ret;
1922
+ },
1923
+ ownKeys(target) {
1924
+ observeTargetKey(target, KEYCHANGES, callback);
1925
+ return Reflect.ownKeys(target);
1926
+ },
1927
+ has(target, key) {
1928
+ // TODO: this observes all key changes instead of only the presence of the argument key
1929
+ // observing the key itself would observe value changes instead of presence changes
1930
+ // so we may need a finer grained system to distinguish observing value vs presence.
1931
+ observeTargetKey(target, KEYCHANGES, callback);
1932
+ return Reflect.has(target, key);
1933
+ },
1934
+ };
1934
1935
  }
1935
- function makeRootFiber(node) {
1936
- let current = node.fiber;
1937
- if (current) {
1938
- let root = current.root;
1939
- // lock root fiber because canceling children fibers may destroy components,
1940
- // which means any arbitrary code can be run in onWillDestroy, which may
1941
- // trigger new renderings
1942
- root.locked = true;
1943
- root.setCounter(root.counter + 1 - cancelFibers(current.children));
1944
- root.locked = false;
1945
- current.children = [];
1946
- current.childrenMap = {};
1947
- current.bdom = null;
1948
- if (fibersInError.has(current)) {
1949
- fibersInError.delete(current);
1950
- fibersInError.delete(root);
1951
- current.appliedToDom = false;
1936
+ /**
1937
+ * Creates a function that will observe the key that is passed to it when called
1938
+ * and delegates to the underlying method.
1939
+ *
1940
+ * @param methodName name of the method to delegate to
1941
+ * @param target @see reactive
1942
+ * @param callback @see reactive
1943
+ */
1944
+ function makeKeyObserver(methodName, target, callback) {
1945
+ return (key) => {
1946
+ key = toRaw(key);
1947
+ observeTargetKey(target, key, callback);
1948
+ return possiblyReactive(target[methodName](key), callback);
1949
+ };
1950
+ }
1951
+ /**
1952
+ * Creates an iterable that will delegate to the underlying iteration method and
1953
+ * observe keys as necessary.
1954
+ *
1955
+ * @param methodName name of the method to delegate to
1956
+ * @param target @see reactive
1957
+ * @param callback @see reactive
1958
+ */
1959
+ function makeIteratorObserver(methodName, target, callback) {
1960
+ return function* () {
1961
+ observeTargetKey(target, KEYCHANGES, callback);
1962
+ const keys = target.keys();
1963
+ for (const item of target[methodName]()) {
1964
+ const key = keys.next().value;
1965
+ observeTargetKey(target, key, callback);
1966
+ yield possiblyReactive(item, callback);
1952
1967
  }
1953
- return current;
1954
- }
1955
- const fiber = new RootFiber(node, null);
1956
- if (node.willPatch.length) {
1957
- fiber.willPatch.push(fiber);
1958
- }
1959
- if (node.patched.length) {
1960
- fiber.patched.push(fiber);
1961
- }
1962
- return fiber;
1968
+ };
1963
1969
  }
1964
- function throwOnRender() {
1965
- throw new Error("Attempted to render cancelled fiber");
1970
+ /**
1971
+ * Creates a forEach function that will delegate to forEach on the underlying
1972
+ * collection while observing key changes, and keys as they're iterated over,
1973
+ * and making the passed keys/values reactive.
1974
+ *
1975
+ * @param target @see reactive
1976
+ * @param callback @see reactive
1977
+ */
1978
+ function makeForEachObserver(target, callback) {
1979
+ return function forEach(forEachCb, thisArg) {
1980
+ observeTargetKey(target, KEYCHANGES, callback);
1981
+ target.forEach(function (val, key, targetObj) {
1982
+ observeTargetKey(target, key, callback);
1983
+ forEachCb.call(thisArg, possiblyReactive(val, callback), possiblyReactive(key, callback), possiblyReactive(targetObj, callback));
1984
+ }, thisArg);
1985
+ };
1966
1986
  }
1967
1987
  /**
1968
- * @returns number of not-yet rendered fibers cancelled
1988
+ * Creates a function that will delegate to an underlying method, and check if
1989
+ * that method has modified the presence or value of a key, and notify the
1990
+ * reactives appropriately.
1991
+ *
1992
+ * @param setterName name of the method to delegate to
1993
+ * @param getterName name of the method which should be used to retrieve the
1994
+ * value before calling the delegate method for comparison purposes
1995
+ * @param target @see reactive
1969
1996
  */
1970
- function cancelFibers(fibers) {
1971
- let result = 0;
1972
- for (let fiber of fibers) {
1973
- let node = fiber.node;
1974
- fiber.render = throwOnRender;
1975
- if (node.status === 0 /* NEW */) {
1976
- node.destroy();
1977
- }
1978
- node.fiber = null;
1979
- if (fiber.bdom) {
1980
- // if fiber has been rendered, this means that the component props have
1981
- // been updated. however, this fiber will not be patched to the dom, so
1982
- // it could happen that the next render compare the current props with
1983
- // the same props, and skip the render completely. With the next line,
1984
- // we kindly request the component code to force a render, so it works as
1985
- // expected.
1986
- node.forceNextRender = true;
1997
+ function delegateAndNotify(setterName, getterName, target) {
1998
+ return (key, value) => {
1999
+ key = toRaw(key);
2000
+ const hadKey = target.has(key);
2001
+ const originalValue = target[getterName](key);
2002
+ const ret = target[setterName](key, value);
2003
+ const hasKey = target.has(key);
2004
+ if (hadKey !== hasKey) {
2005
+ notifyReactives(target, KEYCHANGES);
1987
2006
  }
1988
- else {
1989
- result++;
2007
+ if (originalValue !== value) {
2008
+ notifyReactives(target, key);
1990
2009
  }
1991
- result += cancelFibers(fiber.children);
1992
- }
1993
- return result;
2010
+ return ret;
2011
+ };
1994
2012
  }
1995
- class Fiber {
1996
- constructor(node, parent) {
1997
- this.bdom = null;
1998
- this.children = [];
1999
- this.appliedToDom = false;
2000
- this.deep = false;
2001
- this.childrenMap = {};
2002
- this.node = node;
2003
- this.parent = parent;
2004
- if (parent) {
2005
- this.deep = parent.deep;
2006
- const root = parent.root;
2007
- root.setCounter(root.counter + 1);
2008
- this.root = root;
2009
- parent.children.push(this);
2010
- }
2011
- else {
2012
- this.root = this;
2013
- }
2014
- }
2015
- render() {
2016
- // if some parent has a fiber => register in followup
2017
- let prev = this.root.node;
2018
- let scheduler = prev.app.scheduler;
2019
- let current = prev.parent;
2020
- while (current) {
2021
- if (current.fiber) {
2022
- let root = current.fiber.root;
2023
- if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {
2024
- current = root.node;
2025
- }
2026
- else {
2027
- scheduler.delayedRenders.push(this);
2028
- return;
2029
- }
2030
- }
2031
- prev = current;
2032
- current = current.parent;
2033
- }
2034
- // there are no current rendering from above => we can render
2035
- this._render();
2036
- }
2037
- _render() {
2038
- const node = this.node;
2039
- const root = this.root;
2040
- if (root) {
2041
- try {
2042
- this.bdom = true;
2043
- this.bdom = node.renderFn();
2044
- }
2045
- catch (e) {
2046
- handleError({ node, error: e });
2047
- }
2048
- root.setCounter(root.counter - 1);
2013
+ /**
2014
+ * Creates a function that will clear the underlying collection and notify that
2015
+ * the keys of the collection have changed.
2016
+ *
2017
+ * @param target @see reactive
2018
+ */
2019
+ function makeClearNotifier(target) {
2020
+ return () => {
2021
+ const allKeys = [...target.keys()];
2022
+ target.clear();
2023
+ notifyReactives(target, KEYCHANGES);
2024
+ for (const key of allKeys) {
2025
+ notifyReactives(target, key);
2049
2026
  }
2050
- }
2027
+ };
2051
2028
  }
2052
- class RootFiber extends Fiber {
2053
- constructor() {
2054
- super(...arguments);
2055
- this.counter = 1;
2056
- // only add stuff in this if they have registered some hooks
2057
- this.willPatch = [];
2058
- this.patched = [];
2059
- this.mounted = [];
2060
- // A fiber is typically locked when it is completing and the patch has not, or is being applied.
2061
- // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
2062
- this.locked = false;
2063
- }
2064
- complete() {
2065
- const node = this.node;
2066
- this.locked = true;
2067
- let current = undefined;
2068
- try {
2069
- // Step 1: calling all willPatch lifecycle hooks
2070
- for (current of this.willPatch) {
2071
- // because of the asynchronous nature of the rendering, some parts of the
2072
- // UI may have been rendered, then deleted in a followup rendering, and we
2073
- // do not want to call onWillPatch in that case.
2074
- let node = current.node;
2075
- if (node.fiber === current) {
2076
- const component = node.component;
2077
- for (let cb of node.willPatch) {
2078
- cb.call(component);
2079
- }
2080
- }
2081
- }
2082
- current = undefined;
2083
- // Step 2: patching the dom
2084
- node._patch();
2085
- this.locked = false;
2086
- // Step 4: calling all mounted lifecycle hooks
2087
- let mountedFibers = this.mounted;
2088
- while ((current = mountedFibers.pop())) {
2089
- current = current;
2090
- if (current.appliedToDom) {
2091
- for (let cb of current.node.mounted) {
2092
- cb();
2093
- }
2094
- }
2029
+ /**
2030
+ * Maps raw type of an object to an object containing functions that can be used
2031
+ * to build an appropritate proxy handler for that raw type. Eg: when making a
2032
+ * reactive set, calling the has method should mark the key that is being
2033
+ * retrieved as observed, and calling the add or delete method should notify the
2034
+ * reactives that the key which is being added or deleted has been modified.
2035
+ */
2036
+ const rawTypeToFuncHandlers = {
2037
+ Set: (target, callback) => ({
2038
+ has: makeKeyObserver("has", target, callback),
2039
+ add: delegateAndNotify("add", "has", target),
2040
+ delete: delegateAndNotify("delete", "has", target),
2041
+ keys: makeIteratorObserver("keys", target, callback),
2042
+ values: makeIteratorObserver("values", target, callback),
2043
+ entries: makeIteratorObserver("entries", target, callback),
2044
+ [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
2045
+ forEach: makeForEachObserver(target, callback),
2046
+ clear: makeClearNotifier(target),
2047
+ get size() {
2048
+ observeTargetKey(target, KEYCHANGES, callback);
2049
+ return target.size;
2050
+ },
2051
+ }),
2052
+ Map: (target, callback) => ({
2053
+ has: makeKeyObserver("has", target, callback),
2054
+ get: makeKeyObserver("get", target, callback),
2055
+ set: delegateAndNotify("set", "get", target),
2056
+ delete: delegateAndNotify("delete", "has", target),
2057
+ keys: makeIteratorObserver("keys", target, callback),
2058
+ values: makeIteratorObserver("values", target, callback),
2059
+ entries: makeIteratorObserver("entries", target, callback),
2060
+ [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
2061
+ forEach: makeForEachObserver(target, callback),
2062
+ clear: makeClearNotifier(target),
2063
+ get size() {
2064
+ observeTargetKey(target, KEYCHANGES, callback);
2065
+ return target.size;
2066
+ },
2067
+ }),
2068
+ WeakMap: (target, callback) => ({
2069
+ has: makeKeyObserver("has", target, callback),
2070
+ get: makeKeyObserver("get", target, callback),
2071
+ set: delegateAndNotify("set", "get", target),
2072
+ delete: delegateAndNotify("delete", "has", target),
2073
+ }),
2074
+ };
2075
+ /**
2076
+ * Creates a proxy handler for collections (Set/Map/WeakMap)
2077
+ *
2078
+ * @param callback @see reactive
2079
+ * @param target @see reactive
2080
+ * @returns a proxy handler object
2081
+ */
2082
+ function collectionsProxyHandler(target, callback, targetRawType) {
2083
+ // TODO: if performance is an issue we can create the special handlers lazily when each
2084
+ // property is read.
2085
+ const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
2086
+ return Object.assign(basicProxyHandler(callback), {
2087
+ get(target, key) {
2088
+ if (key === TARGET) {
2089
+ return target;
2095
2090
  }
2096
- // Step 5: calling all patched hooks
2097
- let patchedFibers = this.patched;
2098
- while ((current = patchedFibers.pop())) {
2099
- current = current;
2100
- if (current.appliedToDom) {
2101
- for (let cb of current.node.patched) {
2102
- cb();
2103
- }
2104
- }
2091
+ if (objectHasOwnProperty.call(specialHandlers, key)) {
2092
+ return specialHandlers[key];
2105
2093
  }
2094
+ observeTargetKey(target, key, callback);
2095
+ return possiblyReactive(target[key], callback);
2096
+ },
2097
+ });
2098
+ }
2099
+
2100
+ /**
2101
+ * Creates a batched version of a callback so that all calls to it in the same
2102
+ * microtick will only call the original callback once.
2103
+ *
2104
+ * @param callback the callback to batch
2105
+ * @returns a batched version of the original callback
2106
+ */
2107
+ function batched(callback) {
2108
+ let called = false;
2109
+ return async () => {
2110
+ // This await blocks all calls to the callback here, then releases them sequentially
2111
+ // in the next microtick. This line decides the granularity of the batch.
2112
+ await Promise.resolve();
2113
+ if (!called) {
2114
+ called = true;
2115
+ // wait for all calls in this microtick to fall through before resetting "called"
2116
+ // so that only the first call to the batched function calls the original callback.
2117
+ // Schedule this before calling the callback so that calls to the batched function
2118
+ // within the callback will proceed only after resetting called to false, and have
2119
+ // a chance to execute the callback again
2120
+ Promise.resolve().then(() => (called = false));
2121
+ callback();
2106
2122
  }
2107
- catch (e) {
2108
- this.locked = false;
2109
- handleError({ fiber: current || this, error: e });
2110
- }
2123
+ };
2124
+ }
2125
+ function validateTarget(target) {
2126
+ if (!(target instanceof HTMLElement)) {
2127
+ throw new Error("Cannot mount component: the target is not a valid DOM element");
2111
2128
  }
2112
- setCounter(newValue) {
2113
- this.counter = newValue;
2114
- if (newValue === 0) {
2115
- this.node.app.scheduler.flush();
2116
- }
2129
+ if (!document.body.contains(target)) {
2130
+ throw new Error("Cannot mount a component on a detached dom node");
2117
2131
  }
2118
2132
  }
2119
- class MountFiber extends RootFiber {
2120
- constructor(node, target, options = {}) {
2121
- super(node, null);
2122
- this.target = target;
2123
- this.position = options.position || "last-child";
2133
+ class EventBus extends EventTarget {
2134
+ trigger(name, payload) {
2135
+ this.dispatchEvent(new CustomEvent(name, { detail: payload }));
2124
2136
  }
2125
- complete() {
2126
- let current = this;
2127
- try {
2128
- const node = this.node;
2129
- node.children = this.childrenMap;
2130
- node.app.constructor.validateTarget(this.target);
2131
- if (node.bdom) {
2132
- // this is a complicated situation: if we mount a fiber with an existing
2133
- // bdom, this means that this same fiber was already completed, mounted,
2134
- // but a crash occurred in some mounted hook. Then, it was handled and
2135
- // the new rendering is being applied.
2136
- node.updateDom();
2137
- }
2138
- else {
2139
- node.bdom = this.bdom;
2140
- if (this.position === "last-child" || this.target.childNodes.length === 0) {
2141
- mount$1(node.bdom, this.target);
2142
- }
2143
- else {
2144
- const firstChild = this.target.childNodes[0];
2145
- mount$1(node.bdom, this.target, firstChild);
2146
- }
2147
- }
2148
- // unregistering the fiber before mounted since it can do another render
2149
- // and that the current rendering is obviously completed
2150
- node.fiber = null;
2151
- node.status = 1 /* MOUNTED */;
2152
- this.appliedToDom = true;
2153
- let mountedFibers = this.mounted;
2154
- while ((current = mountedFibers.pop())) {
2155
- if (current.appliedToDom) {
2156
- for (let cb of current.node.mounted) {
2157
- cb();
2158
- }
2159
- }
2160
- }
2137
+ }
2138
+ function whenReady(fn) {
2139
+ return new Promise(function (resolve) {
2140
+ if (document.readyState !== "loading") {
2141
+ resolve(true);
2161
2142
  }
2162
- catch (e) {
2163
- handleError({ fiber: current, error: e });
2143
+ else {
2144
+ document.addEventListener("DOMContentLoaded", resolve, false);
2164
2145
  }
2146
+ }).then(fn || function () { });
2147
+ }
2148
+ async function loadFile(url) {
2149
+ const result = await fetch(url);
2150
+ if (!result.ok) {
2151
+ throw new Error("Error while fetching xml templates");
2165
2152
  }
2153
+ return await result.text();
2154
+ }
2155
+ /*
2156
+ * This class just transports the fact that a string is safe
2157
+ * to be injected as HTML. Overriding a JS primitive is quite painful though
2158
+ * so we need to redfine toString and valueOf.
2159
+ */
2160
+ class Markup extends String {
2161
+ }
2162
+ /*
2163
+ * Marks a value as safe, that is, a value that can be injected as HTML directly.
2164
+ * It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
2165
+ */
2166
+ function markup(value) {
2167
+ return new Markup(value);
2166
2168
  }
2167
2169
 
2168
2170
  let currentNode = null;
@@ -2177,8 +2179,6 @@
2177
2179
  }
2178
2180
  /**
2179
2181
  * Apply default props (only top level).
2180
- *
2181
- * Note that this method does modify in place the props
2182
2182
  */
2183
2183
  function applyDefaultProps(props, defaultProps) {
2184
2184
  for (let propName in defaultProps) {
@@ -2212,61 +2212,6 @@
2212
2212
  }
2213
2213
  return reactive(state, render);
2214
2214
  }
2215
- function arePropsDifferent(props1, props2) {
2216
- for (let k in props1) {
2217
- const prop1 = props1[k] && typeof props1[k] === "object" ? toRaw(props1[k]) : props1[k];
2218
- const prop2 = props2[k] && typeof props2[k] === "object" ? toRaw(props2[k]) : props2[k];
2219
- if (prop1 !== prop2) {
2220
- return true;
2221
- }
2222
- }
2223
- return Object.keys(props1).length !== Object.keys(props2).length;
2224
- }
2225
- function component(name, props, key, ctx, parent) {
2226
- let node = ctx.children[key];
2227
- let isDynamic = typeof name !== "string";
2228
- if (node && node.status === 2 /* DESTROYED */) {
2229
- node = undefined;
2230
- }
2231
- if (isDynamic && node && node.component.constructor !== name) {
2232
- node = undefined;
2233
- }
2234
- const parentFiber = ctx.fiber;
2235
- if (node) {
2236
- let shouldRender = node.forceNextRender;
2237
- if (shouldRender) {
2238
- node.forceNextRender = false;
2239
- }
2240
- else {
2241
- const currentProps = node.component.props;
2242
- shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
2243
- }
2244
- if (shouldRender) {
2245
- node.updateAndRender(props, parentFiber);
2246
- }
2247
- }
2248
- else {
2249
- // new component
2250
- let C;
2251
- if (isDynamic) {
2252
- C = name;
2253
- }
2254
- else {
2255
- C = parent.constructor.components[name];
2256
- if (!C) {
2257
- throw new Error(`Cannot find the definition of component "${name}"`);
2258
- }
2259
- else if (!(C.prototype instanceof Component)) {
2260
- throw new Error(`"${name}" is not a Component. It must inherit from the Component class`);
2261
- }
2262
- }
2263
- node = new ComponentNode(C, props, ctx.app, ctx, key);
2264
- ctx.children[key] = node;
2265
- node.initiateRender(new Fiber(node, parentFiber));
2266
- }
2267
- parentFiber.childrenMap[key] = node;
2268
- return node;
2269
- }
2270
2215
  class ComponentNode {
2271
2216
  constructor(C, props, app, parent, parentKey) {
2272
2217
  this.fiber = null;
@@ -2285,9 +2230,11 @@
2285
2230
  currentNode = this;
2286
2231
  this.app = app;
2287
2232
  this.parent = parent;
2233
+ this.props = props;
2288
2234
  this.parentKey = parentKey;
2289
2235
  this.level = parent ? parent.level + 1 : 0;
2290
2236
  const defaultProps = C.defaultProps;
2237
+ props = Object.assign({}, props);
2291
2238
  if (defaultProps) {
2292
2239
  applyDefaultProps(props, defaultProps);
2293
2240
  }
@@ -2400,6 +2347,8 @@
2400
2347
  this.status = 2 /* DESTROYED */;
2401
2348
  }
2402
2349
  async updateAndRender(props, parentFiber) {
2350
+ const rawProps = props;
2351
+ props = Object.assign({}, props);
2403
2352
  // update
2404
2353
  const fiber = makeChildFiber(this, parentFiber);
2405
2354
  this.fiber = fiber;
@@ -2422,6 +2371,7 @@
2422
2371
  return;
2423
2372
  }
2424
2373
  component.props = props;
2374
+ this.props = rawProps;
2425
2375
  fiber.render();
2426
2376
  const parentRoot = parentFiber.root;
2427
2377
  if (this.willPatch.length) {
@@ -2484,10 +2434,15 @@
2484
2434
  }
2485
2435
  }
2486
2436
  _patch() {
2487
- const hasChildren = Object.keys(this.children).length > 0;
2488
- this.children = this.fiber.childrenMap;
2489
- this.bdom.patch(this.fiber.bdom, hasChildren);
2490
- this.fiber.appliedToDom = true;
2437
+ let hasChildren = false;
2438
+ for (let _k in this.children) {
2439
+ hasChildren = true;
2440
+ break;
2441
+ }
2442
+ const fiber = this.fiber;
2443
+ this.children = fiber.childrenMap;
2444
+ this.bdom.patch(fiber.bdom, hasChildren);
2445
+ fiber.appliedToDom = true;
2491
2446
  this.fiber = null;
2492
2447
  }
2493
2448
  beforeRemove() {
@@ -2615,6 +2570,19 @@
2615
2570
  handlers.push(callback.bind(node.component));
2616
2571
  }
2617
2572
 
2573
+ class Component {
2574
+ constructor(props, env, node) {
2575
+ this.props = props;
2576
+ this.env = env;
2577
+ this.__owl__ = node;
2578
+ }
2579
+ setup() { }
2580
+ render(deep = false) {
2581
+ this.__owl__.render(deep === true);
2582
+ }
2583
+ }
2584
+ Component.template = "";
2585
+
2618
2586
  const VText = text("").constructor;
2619
2587
  class VPortal extends VText {
2620
2588
  constructor(selector, realBDom) {
@@ -2693,6 +2661,7 @@
2693
2661
  // -----------------------------------------------------------------------------
2694
2662
  const isUnionType = (t) => Array.isArray(t);
2695
2663
  const isBaseType = (t) => typeof t !== "object";
2664
+ const isValueType = (t) => typeof t === "object" && t && "value" in t;
2696
2665
  function isOptional(t) {
2697
2666
  return typeof t === "object" && "optional" in t ? t.optional || false : false;
2698
2667
  }
@@ -2706,6 +2675,9 @@
2706
2675
  else if (isUnionType(info)) {
2707
2676
  return info.map(describe).join(" or ");
2708
2677
  }
2678
+ else if (isValueType(info)) {
2679
+ return String(info.value);
2680
+ }
2709
2681
  if ("element" in info) {
2710
2682
  return `list of ${describe({ type: info.element, optional: false })}s`;
2711
2683
  }
@@ -2791,6 +2763,9 @@
2791
2763
  else if (isBaseType(descr)) {
2792
2764
  return validateBaseType(key, value, descr);
2793
2765
  }
2766
+ else if (isValueType(descr)) {
2767
+ return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;
2768
+ }
2794
2769
  else if (isUnionType(descr)) {
2795
2770
  let validDescr = descr.find((p) => !validateType(key, value, p));
2796
2771
  return validDescr ? null : `'${key}' is not a ${describe(descr)}`;
@@ -2819,6 +2794,7 @@
2819
2794
  return result;
2820
2795
  }
2821
2796
 
2797
+ const ObjectCreate = Object.create;
2822
2798
  /**
2823
2799
  * This file contains utility functions that will be injected in each template,
2824
2800
  * to perform various useful tasks in the compiled code.
@@ -2830,7 +2806,7 @@
2830
2806
  key = key + "__slot_" + name;
2831
2807
  const slots = ctx.props.slots || {};
2832
2808
  const { __render, __ctx, __scope } = slots[name] || {};
2833
- const slotScope = Object.create(__ctx || {});
2809
+ const slotScope = ObjectCreate(__ctx || {});
2834
2810
  if (__scope) {
2835
2811
  slotScope[__scope] = extra;
2836
2812
  }
@@ -2850,7 +2826,7 @@
2850
2826
  }
2851
2827
  function capture(ctx) {
2852
2828
  const component = ctx.__owl__.component;
2853
- const result = Object.create(component);
2829
+ const result = ObjectCreate(component);
2854
2830
  for (let k in ctx) {
2855
2831
  result[k] = ctx[k];
2856
2832
  }
@@ -2903,13 +2879,14 @@
2903
2879
  return true;
2904
2880
  }
2905
2881
  class LazyValue {
2906
- constructor(fn, ctx, node) {
2882
+ constructor(fn, ctx, component, node) {
2907
2883
  this.fn = fn;
2908
2884
  this.ctx = capture(ctx);
2885
+ this.component = component;
2909
2886
  this.node = node;
2910
2887
  }
2911
2888
  evaluate() {
2912
- return this.fn(this.ctx, this.node);
2889
+ return this.fn.call(this.component, this.ctx, this.node);
2913
2890
  }
2914
2891
  toString() {
2915
2892
  return this.evaluate().toString();
@@ -2918,43 +2895,56 @@
2918
2895
  /*
2919
2896
  * Safely outputs `value` as a block depending on the nature of `value`
2920
2897
  */
2921
- function safeOutput(value) {
2922
- if (!value) {
2923
- return value;
2898
+ function safeOutput(value, defaultValue) {
2899
+ if (value === undefined) {
2900
+ return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
2924
2901
  }
2925
2902
  let safeKey;
2926
2903
  let block;
2927
- if (value instanceof Markup) {
2928
- safeKey = `string_safe`;
2929
- block = html(value);
2930
- }
2931
- else if (value instanceof LazyValue) {
2932
- safeKey = `lazy_value`;
2933
- block = value.evaluate();
2934
- }
2935
- else if (value instanceof String || typeof value === "string") {
2936
- safeKey = "string_unsafe";
2937
- block = text(value);
2938
- }
2939
- else {
2940
- // Assuming it is a block
2941
- safeKey = "block_safe";
2942
- block = value;
2904
+ switch (typeof value) {
2905
+ case "object":
2906
+ if (value instanceof Markup) {
2907
+ safeKey = `string_safe`;
2908
+ block = html(value);
2909
+ }
2910
+ else if (value instanceof LazyValue) {
2911
+ safeKey = `lazy_value`;
2912
+ block = value.evaluate();
2913
+ }
2914
+ else if (value instanceof String) {
2915
+ safeKey = "string_unsafe";
2916
+ block = text(value);
2917
+ }
2918
+ else {
2919
+ // Assuming it is a block
2920
+ safeKey = "block_safe";
2921
+ block = value;
2922
+ }
2923
+ break;
2924
+ case "string":
2925
+ safeKey = "string_unsafe";
2926
+ block = text(value);
2927
+ break;
2928
+ default:
2929
+ safeKey = "string_unsafe";
2930
+ block = text(String(value));
2943
2931
  }
2944
2932
  return toggler(safeKey, block);
2945
2933
  }
2946
2934
  let boundFunctions = new WeakMap();
2935
+ const WeakMapGet = WeakMap.prototype.get;
2936
+ const WeakMapSet = WeakMap.prototype.set;
2947
2937
  function bind(ctx, fn) {
2948
2938
  let component = ctx.__owl__.component;
2949
- let boundFnMap = boundFunctions.get(component);
2939
+ let boundFnMap = WeakMapGet.call(boundFunctions, component);
2950
2940
  if (!boundFnMap) {
2951
2941
  boundFnMap = new WeakMap();
2952
- boundFunctions.set(component, boundFnMap);
2942
+ WeakMapSet.call(boundFunctions, component, boundFnMap);
2953
2943
  }
2954
- let boundFn = boundFnMap.get(fn);
2944
+ let boundFn = WeakMapGet.call(boundFnMap, fn);
2955
2945
  if (!boundFn) {
2956
2946
  boundFn = fn.bind(component);
2957
- boundFnMap.set(fn, boundFn);
2947
+ WeakMapSet.call(boundFnMap, fn, boundFn);
2958
2948
  }
2959
2949
  return boundFn;
2960
2950
  }
@@ -3030,7 +3020,7 @@
3030
3020
  markRaw,
3031
3021
  };
3032
3022
 
3033
- const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
3023
+ const bdom = { text, createBlock, list, multi, html, toggler, comment };
3034
3024
  function parseXML$1(xml) {
3035
3025
  const parser = new DOMParser();
3036
3026
  const doc = parser.parseFromString(xml, "text/xml");
@@ -3193,7 +3183,7 @@
3193
3183
  });
3194
3184
  // note that the space after typeof is relevant. It makes sure that the formatted
3195
3185
  // expression has a space after typeof. Currently we don't support delete and void
3196
- const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ".split(",");
3186
+ const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~".split(",");
3197
3187
  let tokenizeString = function (expr) {
3198
3188
  let s = expr[0];
3199
3189
  let start = s;
@@ -3441,15 +3431,17 @@
3441
3431
  .map((t) => paddedValues.get(t.value) || t.value)
3442
3432
  .join("");
3443
3433
  }
3444
- const INTERP_REGEXP = /\{\{.*?\}\}/g;
3445
- const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
3446
- function interpolate(s) {
3434
+ const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
3435
+ function replaceDynamicParts(s, replacer) {
3447
3436
  let matches = s.match(INTERP_REGEXP);
3448
3437
  if (matches && matches[0].length === s.length) {
3449
- return `(${compileExpr(s.slice(2, -2))})`;
3438
+ return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
3450
3439
  }
3451
- let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
3440
+ let r = s.replace(INTERP_REGEXP, (s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}");
3452
3441
  return "`" + r + "`";
3442
+ }
3443
+ function interpolate(s) {
3444
+ return replaceDynamicParts(s, compileExpr);
3453
3445
  }
3454
3446
 
3455
3447
  // using a non-html document so that <inner/outer>HTML serializes as XML instead
@@ -3624,9 +3616,7 @@
3624
3616
  tKeyExpr: null,
3625
3617
  });
3626
3618
  // define blocks and utility functions
3627
- let mainCode = [
3628
- ` let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;`,
3629
- ];
3619
+ let mainCode = [` let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];
3630
3620
  if (this.helpers.size) {
3631
3621
  mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
3632
3622
  }
@@ -3642,6 +3632,7 @@
3642
3632
  for (let block of this.blocks) {
3643
3633
  if (block.dom) {
3644
3634
  let xmlString = block.asXmlString();
3635
+ xmlString = xmlString.replace(/`/g, "\\`");
3645
3636
  if (block.dynamicTagName) {
3646
3637
  xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
3647
3638
  xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
@@ -3949,8 +3940,8 @@
3949
3940
  this.target.hasRef = true;
3950
3941
  const isDynamic = INTERP_REGEXP.test(ast.ref);
3951
3942
  if (isDynamic) {
3952
- const str = ast.ref.replace(INTERP_REGEXP, (expr) => "${" + this.captureExpression(expr.slice(2, -2), true) + "}");
3953
- const idx = block.insertData(`(el) => refs[\`${str}\`] = el`, "ref");
3943
+ const str = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
3944
+ const idx = block.insertData(`(el) => refs[${str}] = el`, "ref");
3954
3945
  attrs["block-ref"] = String(idx);
3955
3946
  }
3956
3947
  else {
@@ -4079,16 +4070,24 @@
4079
4070
  this.insertAnchor(block);
4080
4071
  }
4081
4072
  block = this.createBlock(block, "html", ctx);
4082
- this.helpers.add(ast.expr === "0" ? "zero" : "safeOutput");
4083
- let expr = ast.expr === "0" ? "ctx[zero]" : `safeOutput(${compileExpr(ast.expr)})`;
4084
- if (ast.body) {
4085
- const nextId = BlockDescription.nextBlockId;
4073
+ let blockStr;
4074
+ if (ast.expr === "0") {
4075
+ this.helpers.add("zero");
4076
+ blockStr = `ctx[zero]`;
4077
+ }
4078
+ else if (ast.body) {
4079
+ let bodyValue = null;
4080
+ bodyValue = BlockDescription.nextBlockId;
4086
4081
  const subCtx = createContext(ctx);
4087
4082
  this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);
4088
- this.helpers.add("withDefault");
4089
- expr = `withDefault(${expr}, b${nextId})`;
4083
+ this.helpers.add("safeOutput");
4084
+ blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;
4085
+ }
4086
+ else {
4087
+ this.helpers.add("safeOutput");
4088
+ blockStr = `safeOutput(${compileExpr(ast.expr)})`;
4090
4089
  }
4091
- this.insertBlock(`${expr}`, block, ctx);
4090
+ this.insertBlock(blockStr, block, ctx);
4092
4091
  }
4093
4092
  compileTIf(ast, ctx, nextNode) {
4094
4093
  let { block, forceNewBlock, index } = ctx;
@@ -4281,16 +4280,21 @@
4281
4280
  }
4282
4281
  compileTCall(ast, ctx) {
4283
4282
  let { block, forceNewBlock } = ctx;
4283
+ let ctxVar = ctx.ctxVar || "ctx";
4284
+ if (ast.context) {
4285
+ ctxVar = generateId("ctx");
4286
+ this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
4287
+ }
4284
4288
  if (ast.body) {
4285
- this.addLine(`ctx = Object.create(ctx);`);
4286
- this.addLine(`ctx[isBoundary] = 1;`);
4289
+ this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
4290
+ this.addLine(`${ctxVar}[isBoundary] = 1;`);
4287
4291
  this.helpers.add("isBoundary");
4288
4292
  const nextId = BlockDescription.nextBlockId;
4289
- const subCtx = createContext(ctx, { preventRoot: true });
4293
+ const subCtx = createContext(ctx, { preventRoot: true, ctxVar });
4290
4294
  this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);
4291
4295
  if (nextId !== BlockDescription.nextBlockId) {
4292
4296
  this.helpers.add("zero");
4293
- this.addLine(`ctx[zero] = b${nextId};`);
4297
+ this.addLine(`${ctxVar}[zero] = b${nextId};`);
4294
4298
  }
4295
4299
  }
4296
4300
  const isDynamic = INTERP_REGEXP.test(ast.name);
@@ -4308,7 +4312,7 @@
4308
4312
  }
4309
4313
  this.define(templateVar, subTemplate);
4310
4314
  block = this.createBlock(block, "multi", ctx);
4311
- this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block, {
4315
+ this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block, {
4312
4316
  ...ctx,
4313
4317
  forceNewBlock: !block,
4314
4318
  });
@@ -4317,13 +4321,13 @@
4317
4321
  const id = generateId(`callTemplate_`);
4318
4322
  this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
4319
4323
  block = this.createBlock(block, "multi", ctx);
4320
- this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block, {
4324
+ this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block, {
4321
4325
  ...ctx,
4322
4326
  forceNewBlock: !block,
4323
4327
  });
4324
4328
  }
4325
4329
  if (ast.body && !ctx.isLast) {
4326
- this.addLine(`ctx = ctx.__proto__;`);
4330
+ this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`);
4327
4331
  }
4328
4332
  }
4329
4333
  compileTCallBlock(ast, ctx) {
@@ -4344,7 +4348,7 @@
4344
4348
  this.helpers.add("LazyValue");
4345
4349
  const bodyAst = { type: 3 /* Multi */, content: ast.body };
4346
4350
  const name = this.compileInNewTarget("value", bodyAst, ctx);
4347
- let value = `new LazyValue(${name}, ctx, node)`;
4351
+ let value = `new LazyValue(${name}, ctx, this, node)`;
4348
4352
  value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
4349
4353
  this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
4350
4354
  }
@@ -4362,7 +4366,7 @@
4362
4366
  value = expr;
4363
4367
  }
4364
4368
  this.helpers.add("setContextValue");
4365
- this.addLine(`setContextValue(ctx, "${ast.name}", ${value});`);
4369
+ this.addLine(`setContextValue(${ctx.ctxVar || "ctx"}, "${ast.name}", ${value});`);
4366
4370
  }
4367
4371
  }
4368
4372
  generateComponentKey() {
@@ -4400,21 +4404,20 @@
4400
4404
  return `${name}: ${value || undefined}`;
4401
4405
  }
4402
4406
  formatPropObject(obj) {
4403
- const params = [];
4404
- for (const [n, v] of Object.entries(obj)) {
4405
- params.push(this.formatProp(n, v));
4407
+ return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
4408
+ }
4409
+ getPropString(props, dynProps) {
4410
+ let propString = `{${props.join(",")}}`;
4411
+ if (dynProps) {
4412
+ propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? ", " + propString : ""})`;
4406
4413
  }
4407
- return params.join(", ");
4414
+ return propString;
4408
4415
  }
4409
4416
  compileComponent(ast, ctx) {
4410
4417
  let { block } = ctx;
4411
4418
  // props
4412
4419
  const hasSlotsProp = "slots" in (ast.props || {});
4413
- const props = [];
4414
- const propExpr = this.formatPropObject(ast.props || {});
4415
- if (propExpr) {
4416
- props.push(propExpr);
4417
- }
4420
+ const props = ast.props ? this.formatPropObject(ast.props) : [];
4418
4421
  // slots
4419
4422
  let slotDef = "";
4420
4423
  if (ast.slots) {
@@ -4437,7 +4440,7 @@
4437
4440
  params.push(`__scope: "${scope}"`);
4438
4441
  }
4439
4442
  if (ast.slots[slotName].attrs) {
4440
- params.push(this.formatPropObject(ast.slots[slotName].attrs));
4443
+ params.push(...this.formatPropObject(ast.slots[slotName].attrs));
4441
4444
  }
4442
4445
  const slotInfo = `{${params.join(", ")}}`;
4443
4446
  slotStr.push(`'${slotName}': ${slotInfo}`);
@@ -4448,11 +4451,7 @@
4448
4451
  this.helpers.add("markRaw");
4449
4452
  props.push(`slots: markRaw(${slotDef})`);
4450
4453
  }
4451
- const propStr = `{${props.join(",")}}`;
4452
- let propString = propStr;
4453
- if (ast.dynamicProps) {
4454
- propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${props.length ? ", " + propStr : ""})`;
4455
- }
4454
+ let propString = this.getPropString(props, ast.dynamicProps);
4456
4455
  let propVar;
4457
4456
  if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
4458
4457
  propVar = generateId("props");
@@ -4484,8 +4483,12 @@
4484
4483
  if (ctx.tKeyExpr) {
4485
4484
  keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
4486
4485
  }
4487
- const blockArgs = `${expr}, ${propString}, ${keyArg}, node, ctx`;
4488
- let blockExpr = `component(${blockArgs})`;
4486
+ let id = generateId("comp");
4487
+ this.staticDefs.push({
4488
+ id,
4489
+ expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, ${!ast.props && !ast.dynamicProps})`,
4490
+ });
4491
+ let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;
4489
4492
  if (ast.isDynamic) {
4490
4493
  blockExpr = `toggler(${expr}, ${blockExpr})`;
4491
4494
  }
@@ -4524,7 +4527,12 @@
4524
4527
  else {
4525
4528
  slotName = "'" + ast.name + "'";
4526
4529
  }
4527
- const scope = ast.attrs ? `{${this.formatPropObject(ast.attrs)}}` : null;
4530
+ const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
4531
+ if (ast.attrs) {
4532
+ delete ast.attrs["t-props"];
4533
+ }
4534
+ const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
4535
+ const scope = this.getPropString(props, dynProps);
4528
4536
  if (ast.defaultContent) {
4529
4537
  const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
4530
4538
  blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
@@ -4565,10 +4573,15 @@
4565
4573
  if (this.target.loopLevel || !this.hasSafeContext) {
4566
4574
  ctxStr = generateId("ctx");
4567
4575
  this.helpers.add("capture");
4568
- this.define(ctxStr, `capture(ctx);`);
4576
+ this.define(ctxStr, `capture(ctx)`);
4569
4577
  }
4578
+ let id = generateId("comp");
4579
+ this.staticDefs.push({
4580
+ id,
4581
+ expr: `app.createComponent(null, false, true, false, false)`,
4582
+ });
4570
4583
  const target = compileExpr(ast.target);
4571
- const blockString = `component(Portal, {target: ${target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
4584
+ const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
4572
4585
  if (block) {
4573
4586
  this.insertAnchor(block);
4574
4587
  }
@@ -4908,10 +4921,12 @@
4908
4921
  return null;
4909
4922
  }
4910
4923
  const subTemplate = node.getAttribute("t-call");
4924
+ const context = node.getAttribute("t-call-context");
4911
4925
  node.removeAttribute("t-call");
4926
+ node.removeAttribute("t-call-context");
4912
4927
  if (node.tagName !== "t") {
4913
4928
  const ast = parseNode(node, ctx);
4914
- const tcall = { type: 7 /* TCall */, name: subTemplate, body: null };
4929
+ const tcall = { type: 7 /* TCall */, name: subTemplate, body: null, context };
4915
4930
  if (ast && ast.type === 2 /* DomNode */) {
4916
4931
  ast.content = [tcall];
4917
4932
  return ast;
@@ -4928,6 +4943,7 @@
4928
4943
  type: 7 /* TCall */,
4929
4944
  name: subTemplate,
4930
4945
  body: body.length ? body : null,
4946
+ context,
4931
4947
  };
4932
4948
  }
4933
4949
  // -----------------------------------------------------------------------------
@@ -5511,6 +5527,55 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
5511
5527
  this.root.destroy();
5512
5528
  }
5513
5529
  }
5530
+ createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, hasNoProp) {
5531
+ const isDynamic = !isStatic;
5532
+ function _arePropsDifferent(props1, props2) {
5533
+ for (let k in props1) {
5534
+ if (props1[k] !== props2[k]) {
5535
+ return true;
5536
+ }
5537
+ }
5538
+ return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length;
5539
+ }
5540
+ const arePropsDifferent = hasSlotsProp
5541
+ ? (_1, _2) => true
5542
+ : hasNoProp
5543
+ ? (_1, _2) => false
5544
+ : _arePropsDifferent;
5545
+ const updateAndRender = ComponentNode.prototype.updateAndRender;
5546
+ const initiateRender = ComponentNode.prototype.initiateRender;
5547
+ return (props, key, ctx, parent, C) => {
5548
+ let children = ctx.children;
5549
+ let node = children[key];
5550
+ if (isDynamic && node && node.component.constructor !== C) {
5551
+ node = undefined;
5552
+ }
5553
+ const parentFiber = ctx.fiber;
5554
+ if (node) {
5555
+ if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {
5556
+ node.forceNextRender = false;
5557
+ updateAndRender.call(node, props, parentFiber);
5558
+ }
5559
+ }
5560
+ else {
5561
+ // new component
5562
+ if (isStatic) {
5563
+ C = parent.constructor.components[name];
5564
+ if (!C) {
5565
+ throw new Error(`Cannot find the definition of component "${name}"`);
5566
+ }
5567
+ else if (!(C.prototype instanceof Component)) {
5568
+ throw new Error(`"${name}" is not a Component. It must inherit from the Component class`);
5569
+ }
5570
+ }
5571
+ node = new ComponentNode(C, props, this, ctx, key);
5572
+ children[key] = node;
5573
+ initiateRender.call(node, new Fiber(node, parentFiber));
5574
+ }
5575
+ parentFiber.childrenMap[key] = node;
5576
+ return node;
5577
+ };
5578
+ }
5514
5579
  }
5515
5580
  App.validateTarget = validateTarget;
5516
5581
  async function mount(C, target, config = {}) {
@@ -5693,9 +5758,9 @@ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration
5693
5758
  Object.defineProperty(exports, '__esModule', { value: true });
5694
5759
 
5695
5760
 
5696
- __info__.version = '2.0.0-beta-8';
5697
- __info__.date = '2022-05-31T12:25:19.319Z';
5698
- __info__.hash = 'b56a9c2';
5761
+ __info__.version = '2.0.0-beta-11';
5762
+ __info__.date = '2022-06-28T13:28:26.775Z';
5763
+ __info__.hash = '76c389a';
5699
5764
  __info__.url = 'https://github.com/odoo/owl';
5700
5765
 
5701
5766