@odoo/owl 2.0.0-alpha.2 → 2.0.0-beta-5

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
@@ -212,7 +212,8 @@
212
212
  }
213
213
  function makePropSetter(name) {
214
214
  return function setProp(value) {
215
- this[name] = value;
215
+ // support 0, fallback to empty string for other falsy values
216
+ this[name] = value === 0 ? 0 : value || "";
216
217
  };
217
218
  }
218
219
  function isProp(tag, key) {
@@ -266,10 +267,14 @@
266
267
  this[eventKey] = data;
267
268
  this.addEventListener(evName, listener, { capture });
268
269
  }
270
+ function remove() {
271
+ delete this[eventKey];
272
+ this.removeEventListener(evName, listener, { capture });
273
+ }
269
274
  function update(data) {
270
275
  this[eventKey] = data;
271
276
  }
272
- return { setup, update };
277
+ return { setup, update, remove };
273
278
  }
274
279
  // Synthetic handler: a form of event delegation that allows placing only one
275
280
  // listener per event type.
@@ -286,7 +291,10 @@
286
291
  _data[currentId] = data;
287
292
  this[eventKey] = _data;
288
293
  }
289
- return { setup, update: setup };
294
+ function remove() {
295
+ delete this[eventKey];
296
+ }
297
+ return { setup, update: setup, remove };
290
298
  }
291
299
  function nativeToSyntheticEvent(eventKey, event) {
292
300
  let dom = event.target;
@@ -511,7 +519,7 @@
511
519
  const characterDataSetData = getDescriptor$1(characterDataProto, "data").set;
512
520
  const nodeGetFirstChild = getDescriptor$1(nodeProto$2, "firstChild").get;
513
521
  const nodeGetNextSibling = getDescriptor$1(nodeProto$2, "nextSibling").get;
514
- const NO_OP$1 = () => { };
522
+ const NO_OP = () => { };
515
523
  const cache$1 = {};
516
524
  /**
517
525
  * Compiling blocks is a multi-step process:
@@ -815,7 +823,7 @@
815
823
  idx: info.idx,
816
824
  refIdx: info.refIdx,
817
825
  setData: makeRefSetter(index, ctx.refList),
818
- updateData: NO_OP$1,
826
+ updateData: NO_OP,
819
827
  });
820
828
  }
821
829
  }
@@ -1281,6 +1289,75 @@
1281
1289
  return new VHtml(str);
1282
1290
  }
1283
1291
 
1292
+ 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;
1302
+ class VCatcher {
1303
+ constructor(child, handlers) {
1304
+ this.afterNode = null;
1305
+ this.child = child;
1306
+ this.handlers = handlers;
1307
+ }
1308
+ mount(parent, afterNode) {
1309
+ this.parentEl = parent;
1310
+ this.afterNode = afterNode;
1311
+ this.child.mount(parent, afterNode);
1312
+ for (let i = 0; i < n; i++) {
1313
+ let origFn = this.handlers[i][0];
1314
+ const self = this;
1315
+ this.handlers[i][0] = function (ev) {
1316
+ const target = ev.target;
1317
+ let currentNode = self.child.firstNode();
1318
+ const afterNode = self.afterNode;
1319
+ while (currentNode !== afterNode) {
1320
+ if (currentNode.contains(target)) {
1321
+ return origFn.call(this, ev);
1322
+ }
1323
+ currentNode = currentNode.nextSibling;
1324
+ }
1325
+ };
1326
+ setupFns[i].call(parent, this.handlers[i]);
1327
+ }
1328
+ }
1329
+ moveBefore(other, afterNode) {
1330
+ this.afterNode = null;
1331
+ this.child.moveBefore(other ? other.child : null, afterNode);
1332
+ }
1333
+ patch(other, withBeforeRemove) {
1334
+ if (this === other) {
1335
+ return;
1336
+ }
1337
+ this.handlers = other.handlers;
1338
+ this.child.patch(other.child, withBeforeRemove);
1339
+ }
1340
+ beforeRemove() {
1341
+ this.child.beforeRemove();
1342
+ }
1343
+ remove() {
1344
+ for (let i = 0; i < n; i++) {
1345
+ removeFns[i].call(this.parentEl);
1346
+ }
1347
+ this.child.remove();
1348
+ }
1349
+ firstNode() {
1350
+ return this.child.firstNode();
1351
+ }
1352
+ toString() {
1353
+ return this.child.toString();
1354
+ }
1355
+ }
1356
+ return function (child, handlers) {
1357
+ return new VCatcher(child, handlers);
1358
+ };
1359
+ }
1360
+
1284
1361
  function mount$1(vnode, fixture, afterNode = null) {
1285
1362
  vnode.mount(fixture, afterNode);
1286
1363
  }
@@ -1294,143 +1371,464 @@
1294
1371
  vnode.remove();
1295
1372
  }
1296
1373
 
1374
+ const mainEventHandler = (data, ev, currentTarget) => {
1375
+ const { data: _data, modifiers } = filterOutModifiersFromData(data);
1376
+ data = _data;
1377
+ let stopped = false;
1378
+ if (modifiers.length) {
1379
+ let selfMode = false;
1380
+ const isSelf = ev.target === currentTarget;
1381
+ for (const mod of modifiers) {
1382
+ switch (mod) {
1383
+ case "self":
1384
+ selfMode = true;
1385
+ if (isSelf) {
1386
+ continue;
1387
+ }
1388
+ else {
1389
+ return stopped;
1390
+ }
1391
+ case "prevent":
1392
+ if ((selfMode && isSelf) || !selfMode)
1393
+ ev.preventDefault();
1394
+ continue;
1395
+ case "stop":
1396
+ if ((selfMode && isSelf) || !selfMode)
1397
+ ev.stopPropagation();
1398
+ stopped = true;
1399
+ continue;
1400
+ }
1401
+ }
1402
+ }
1403
+ // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
1404
+ // We check this rather than data[0] being truthy (or typeof function) so that it crashes
1405
+ // as expected when there is a handler expression that evaluates to a falsy value
1406
+ if (Object.hasOwnProperty.call(data, 0)) {
1407
+ const handler = data[0];
1408
+ if (typeof handler !== "function") {
1409
+ throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
1410
+ }
1411
+ let node = data[1] ? data[1].__owl__ : null;
1412
+ if (node ? node.status === 1 /* MOUNTED */ : true) {
1413
+ handler.call(node ? node.component : null, ev);
1414
+ }
1415
+ }
1416
+ return stopped;
1417
+ };
1418
+
1419
+ // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
1420
+ const TARGET = Symbol("Target");
1421
+ // Escape hatch to prevent reactivity system to turn something into a reactive
1422
+ const SKIP = Symbol("Skip");
1423
+ // Special key to subscribe to, to be notified of key creation/deletion
1424
+ const KEYCHANGES = Symbol("Key changes");
1425
+ const objectToString = Object.prototype.toString;
1426
+ const objectHasOwnProperty = Object.prototype.hasOwnProperty;
1427
+ const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
1428
+ const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
1297
1429
  /**
1298
- * Apply default props (only top level).
1430
+ * extract "RawType" from strings like "[object RawType]" => this lets us ignore
1431
+ * many native objects such as Promise (whose toString is [object Promise])
1432
+ * or Date ([object Date]), while also supporting collections without using
1433
+ * instanceof in a loop
1299
1434
  *
1300
- * Note that this method does modify in place the props
1435
+ * @param obj the object to check
1436
+ * @returns the raw type of the object
1301
1437
  */
1302
- function applyDefaultProps(props, ComponentClass) {
1303
- const defaultProps = ComponentClass.defaultProps;
1304
- if (defaultProps) {
1305
- for (let propName in defaultProps) {
1306
- if (props[propName] === undefined) {
1307
- props[propName] = defaultProps[propName];
1308
- }
1309
- }
1438
+ function rawType(obj) {
1439
+ return objectToString.call(obj).slice(8, -1);
1440
+ }
1441
+ /**
1442
+ * Checks whether a given value can be made into a reactive object.
1443
+ *
1444
+ * @param value the value to check
1445
+ * @returns whether the value can be made reactive
1446
+ */
1447
+ function canBeMadeReactive(value) {
1448
+ if (typeof value !== "object") {
1449
+ return false;
1310
1450
  }
1451
+ return SUPPORTED_RAW_TYPES.has(rawType(value));
1311
1452
  }
1312
- //------------------------------------------------------------------------------
1313
- // Prop validation helper
1314
- //------------------------------------------------------------------------------
1315
- function getPropDescription(staticProps) {
1316
- if (staticProps instanceof Array) {
1317
- return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
1453
+ /**
1454
+ * Creates a reactive from the given object/callback if possible and returns it,
1455
+ * returns the original object otherwise.
1456
+ *
1457
+ * @param value the value make reactive
1458
+ * @returns a reactive for the given object when possible, the original otherwise
1459
+ */
1460
+ function possiblyReactive(val, cb) {
1461
+ return canBeMadeReactive(val) ? reactive(val, cb) : val;
1462
+ }
1463
+ /**
1464
+ * Mark an object or array so that it is ignored by the reactivity system
1465
+ *
1466
+ * @param value the value to mark
1467
+ * @returns the object itself
1468
+ */
1469
+ function markRaw(value) {
1470
+ value[SKIP] = true;
1471
+ return value;
1472
+ }
1473
+ /**
1474
+ * Given a reactive objet, return the raw (non reactive) underlying object
1475
+ *
1476
+ * @param value a reactive value
1477
+ * @returns the underlying value
1478
+ */
1479
+ function toRaw(value) {
1480
+ return value[TARGET] || value;
1481
+ }
1482
+ const targetToKeysToCallbacks = new WeakMap();
1483
+ /**
1484
+ * Observes a given key on a target with an callback. The callback will be
1485
+ * called when the given key changes on the target.
1486
+ *
1487
+ * @param target the target whose key should be observed
1488
+ * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
1489
+ * or deletion)
1490
+ * @param callback the function to call when the key changes
1491
+ */
1492
+ function observeTargetKey(target, key, callback) {
1493
+ if (!targetToKeysToCallbacks.get(target)) {
1494
+ targetToKeysToCallbacks.set(target, new Map());
1318
1495
  }
1319
- return staticProps || { "*": true };
1496
+ const keyToCallbacks = targetToKeysToCallbacks.get(target);
1497
+ if (!keyToCallbacks.get(key)) {
1498
+ keyToCallbacks.set(key, new Set());
1499
+ }
1500
+ keyToCallbacks.get(key).add(callback);
1501
+ if (!callbacksToTargets.has(callback)) {
1502
+ callbacksToTargets.set(callback, new Set());
1503
+ }
1504
+ callbacksToTargets.get(callback).add(target);
1320
1505
  }
1321
1506
  /**
1322
- * Validate the component props (or next props) against the (static) props
1323
- * description. This is potentially an expensive operation: it may needs to
1324
- * visit recursively the props and all the children to check if they are valid.
1325
- * This is why it is only done in 'dev' mode.
1507
+ * Notify Reactives that are observing a given target that a key has changed on
1508
+ * the target.
1509
+ *
1510
+ * @param target target whose Reactives should be notified that the target was
1511
+ * changed.
1512
+ * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
1513
+ * or deleted)
1326
1514
  */
1327
- function validateProps(name, props, parent) {
1328
- const ComponentClass = typeof name !== "string"
1329
- ? name
1330
- : parent.constructor.components[name];
1331
- if (!ComponentClass) {
1332
- // this is an error, wrong component. We silently return here instead so the
1333
- // error is triggered by the usual path ('component' function)
1515
+ function notifyReactives(target, key) {
1516
+ const keyToCallbacks = targetToKeysToCallbacks.get(target);
1517
+ if (!keyToCallbacks) {
1334
1518
  return;
1335
1519
  }
1336
- applyDefaultProps(props, ComponentClass);
1337
- const defaultProps = ComponentClass.defaultProps || {};
1338
- let propsDef = getPropDescription(ComponentClass.props);
1339
- const allowAdditionalProps = "*" in propsDef;
1340
- for (let propName in propsDef) {
1341
- if (propName === "*") {
1342
- continue;
1343
- }
1344
- const propDef = propsDef[propName];
1345
- let isMandatory = !!propDef;
1346
- if (typeof propDef === "object" && "optional" in propDef) {
1347
- isMandatory = !propDef.optional;
1348
- }
1349
- if (isMandatory && propName in defaultProps) {
1350
- throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
1351
- }
1352
- if (props[propName] === undefined) {
1353
- if (isMandatory) {
1354
- throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
1355
- }
1356
- else {
1357
- continue;
1358
- }
1359
- }
1360
- let isValid;
1361
- try {
1362
- isValid = isValidProp(props[propName], propDef);
1363
- }
1364
- catch (e) {
1365
- e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
1366
- throw e;
1367
- }
1368
- if (!isValid) {
1369
- throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
1370
- }
1520
+ const callbacks = keyToCallbacks.get(key);
1521
+ if (!callbacks) {
1522
+ return;
1371
1523
  }
1372
- if (!allowAdditionalProps) {
1373
- for (let propName in props) {
1374
- if (!(propName in propsDef)) {
1375
- throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
1376
- }
1377
- }
1524
+ // Loop on copy because clearReactivesForCallback will modify the set in place
1525
+ for (const callback of [...callbacks]) {
1526
+ clearReactivesForCallback(callback);
1527
+ callback();
1378
1528
  }
1379
1529
  }
1530
+ const callbacksToTargets = new WeakMap();
1380
1531
  /**
1381
- * Check if an invidual prop value matches its (static) prop definition
1532
+ * Clears all subscriptions of the Reactives associated with a given callback.
1533
+ *
1534
+ * @param callback the callback for which the reactives need to be cleared
1382
1535
  */
1383
- function isValidProp(prop, propDef) {
1384
- if (propDef === true) {
1385
- return true;
1536
+ function clearReactivesForCallback(callback) {
1537
+ const targetsToClear = callbacksToTargets.get(callback);
1538
+ if (!targetsToClear) {
1539
+ return;
1386
1540
  }
1387
- if (typeof propDef === "function") {
1388
- // Check if a value is constructed by some Constructor. Note that there is a
1389
- // slight abuse of language: we want to consider primitive values as well.
1390
- //
1391
- // So, even though 1 is not an instance of Number, we want to consider that
1392
- // it is valid.
1393
- if (typeof prop === "object") {
1394
- return prop instanceof propDef;
1541
+ for (const target of targetsToClear) {
1542
+ const observedKeys = targetToKeysToCallbacks.get(target);
1543
+ if (!observedKeys) {
1544
+ continue;
1395
1545
  }
1396
- return typeof prop === propDef.name.toLowerCase();
1397
- }
1398
- else if (propDef instanceof Array) {
1399
- // If this code is executed, this means that we want to check if a prop
1400
- // matches at least one of its descriptor.
1401
- let result = false;
1402
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
1403
- result = result || isValidProp(prop, propDef[i]);
1546
+ for (const callbacks of observedKeys.values()) {
1547
+ callbacks.delete(callback);
1404
1548
  }
1405
- return result;
1406
1549
  }
1407
- // propsDef is an object
1408
- if (propDef.optional && prop === undefined) {
1409
- return true;
1550
+ targetsToClear.clear();
1551
+ }
1552
+ function getSubscriptions(callback) {
1553
+ const targets = callbacksToTargets.get(callback) || [];
1554
+ return [...targets].map((target) => {
1555
+ const keysToCallbacks = targetToKeysToCallbacks.get(target);
1556
+ return {
1557
+ target,
1558
+ keys: keysToCallbacks ? [...keysToCallbacks.keys()] : [],
1559
+ };
1560
+ });
1561
+ }
1562
+ const reactiveCache = new WeakMap();
1563
+ /**
1564
+ * Creates a reactive proxy for an object. Reading data on the reactive object
1565
+ * subscribes to changes to the data. Writing data on the object will cause the
1566
+ * notify callback to be called if there are suscriptions to that data. Nested
1567
+ * objects and arrays are automatically made reactive as well.
1568
+ *
1569
+ * Whenever you are notified of a change, all subscriptions are cleared, and if
1570
+ * you would like to be notified of any further changes, you should go read
1571
+ * the underlying data again. We assume that if you don't go read it again after
1572
+ * being notified, it means that you are no longer interested in that data.
1573
+ *
1574
+ * Subscriptions:
1575
+ * + Reading a property on an object will subscribe you to changes in the value
1576
+ * of that property.
1577
+ * + Accessing an object keys (eg with Object.keys or with `for..in`) will
1578
+ * subscribe you to the creation/deletion of keys. Checking the presence of a
1579
+ * key on the object with 'in' has the same effect.
1580
+ * - getOwnPropertyDescriptor does not currently subscribe you to the property.
1581
+ * This is a choice that was made because changing a key's value will trigger
1582
+ * this trap and we do not want to subscribe by writes. This also means that
1583
+ * Object.hasOwnProperty doesn't subscribe as it goes through this trap.
1584
+ *
1585
+ * @param target the object for which to create a reactive proxy
1586
+ * @param callback the function to call when an observed property of the
1587
+ * reactive has changed
1588
+ * @returns a proxy that tracks changes to it
1589
+ */
1590
+ function reactive(target, callback = () => { }) {
1591
+ if (!canBeMadeReactive(target)) {
1592
+ throw new Error(`Cannot make the given value reactive`);
1410
1593
  }
1411
- let result = propDef.type ? isValidProp(prop, propDef.type) : true;
1412
- if (propDef.validate) {
1413
- result = result && propDef.validate(prop);
1594
+ if (SKIP in target) {
1595
+ return target;
1414
1596
  }
1415
- if (propDef.type === Array && propDef.element) {
1416
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
1417
- result = result && isValidProp(prop[i], propDef.element);
1418
- }
1597
+ const originalTarget = target[TARGET];
1598
+ if (originalTarget) {
1599
+ return reactive(originalTarget, callback);
1419
1600
  }
1420
- if (propDef.type === Object && propDef.shape) {
1421
- const shape = propDef.shape;
1422
- for (let key in shape) {
1423
- result = result && isValidProp(prop[key], shape[key]);
1424
- }
1425
- if (result) {
1426
- for (let propName in prop) {
1427
- if (!(propName in shape)) {
1428
- throw new Error(`unknown prop '${propName}'`);
1429
- }
1601
+ if (!reactiveCache.has(target)) {
1602
+ reactiveCache.set(target, new Map());
1603
+ }
1604
+ const reactivesForTarget = reactiveCache.get(target);
1605
+ if (!reactivesForTarget.has(callback)) {
1606
+ const targetRawType = rawType(target);
1607
+ const handler = COLLECTION_RAWTYPES.has(targetRawType)
1608
+ ? collectionsProxyHandler(target, callback, targetRawType)
1609
+ : basicProxyHandler(callback);
1610
+ const proxy = new Proxy(target, handler);
1611
+ reactivesForTarget.set(callback, proxy);
1612
+ }
1613
+ return reactivesForTarget.get(callback);
1614
+ }
1615
+ /**
1616
+ * Creates a basic proxy handler for regular objects and arrays.
1617
+ *
1618
+ * @param callback @see reactive
1619
+ * @returns a proxy handler object
1620
+ */
1621
+ function basicProxyHandler(callback) {
1622
+ return {
1623
+ get(target, key, proxy) {
1624
+ if (key === TARGET) {
1625
+ return target;
1626
+ }
1627
+ // non-writable non-configurable properties cannot be made reactive
1628
+ const desc = Object.getOwnPropertyDescriptor(target, key);
1629
+ if (desc && !desc.writable && !desc.configurable) {
1630
+ return Reflect.get(target, key, proxy);
1631
+ }
1632
+ observeTargetKey(target, key, callback);
1633
+ return possiblyReactive(Reflect.get(target, key, proxy), callback);
1634
+ },
1635
+ set(target, key, value, proxy) {
1636
+ const isNewKey = !objectHasOwnProperty.call(target, key);
1637
+ const originalValue = Reflect.get(target, key, proxy);
1638
+ const ret = Reflect.set(target, key, value, proxy);
1639
+ if (isNewKey) {
1640
+ notifyReactives(target, KEYCHANGES);
1641
+ }
1642
+ // While Array length may trigger the set trap, it's not actually set by this
1643
+ // method but is updated behind the scenes, and the trap is not called with the
1644
+ // new value. We disable the "same-value-optimization" for it because of that.
1645
+ if (originalValue !== value || (Array.isArray(target) && key === "length")) {
1646
+ notifyReactives(target, key);
1430
1647
  }
1648
+ return ret;
1649
+ },
1650
+ deleteProperty(target, key) {
1651
+ const ret = Reflect.deleteProperty(target, key);
1652
+ // TODO: only notify when something was actually deleted
1653
+ notifyReactives(target, KEYCHANGES);
1654
+ notifyReactives(target, key);
1655
+ return ret;
1656
+ },
1657
+ ownKeys(target) {
1658
+ observeTargetKey(target, KEYCHANGES, callback);
1659
+ return Reflect.ownKeys(target);
1660
+ },
1661
+ has(target, key) {
1662
+ // TODO: this observes all key changes instead of only the presence of the argument key
1663
+ // observing the key itself would observe value changes instead of presence changes
1664
+ // so we may need a finer grained system to distinguish observing value vs presence.
1665
+ observeTargetKey(target, KEYCHANGES, callback);
1666
+ return Reflect.has(target, key);
1667
+ },
1668
+ };
1669
+ }
1670
+ /**
1671
+ * Creates a function that will observe the key that is passed to it when called
1672
+ * and delegates to the underlying method.
1673
+ *
1674
+ * @param methodName name of the method to delegate to
1675
+ * @param target @see reactive
1676
+ * @param callback @see reactive
1677
+ */
1678
+ function makeKeyObserver(methodName, target, callback) {
1679
+ return (key) => {
1680
+ key = toRaw(key);
1681
+ observeTargetKey(target, key, callback);
1682
+ return possiblyReactive(target[methodName](key), callback);
1683
+ };
1684
+ }
1685
+ /**
1686
+ * Creates an iterable that will delegate to the underlying iteration method and
1687
+ * observe keys as necessary.
1688
+ *
1689
+ * @param methodName name of the method to delegate to
1690
+ * @param target @see reactive
1691
+ * @param callback @see reactive
1692
+ */
1693
+ function makeIteratorObserver(methodName, target, callback) {
1694
+ return function* () {
1695
+ observeTargetKey(target, KEYCHANGES, callback);
1696
+ const keys = target.keys();
1697
+ for (const item of target[methodName]()) {
1698
+ const key = keys.next().value;
1699
+ observeTargetKey(target, key, callback);
1700
+ yield possiblyReactive(item, callback);
1431
1701
  }
1432
- }
1433
- return result;
1702
+ };
1703
+ }
1704
+ /**
1705
+ * Creates a forEach function that will delegate to forEach on the underlying
1706
+ * collection while observing key changes, and keys as they're iterated over,
1707
+ * and making the passed keys/values reactive.
1708
+ *
1709
+ * @param target @see reactive
1710
+ * @param callback @see reactive
1711
+ */
1712
+ function makeForEachObserver(target, callback) {
1713
+ return function forEach(forEachCb, thisArg) {
1714
+ observeTargetKey(target, KEYCHANGES, callback);
1715
+ target.forEach(function (val, key, targetObj) {
1716
+ observeTargetKey(target, key, callback);
1717
+ forEachCb.call(thisArg, possiblyReactive(val, callback), possiblyReactive(key, callback), possiblyReactive(targetObj, callback));
1718
+ }, thisArg);
1719
+ };
1720
+ }
1721
+ /**
1722
+ * Creates a function that will delegate to an underlying method, and check if
1723
+ * that method has modified the presence or value of a key, and notify the
1724
+ * reactives appropriately.
1725
+ *
1726
+ * @param setterName name of the method to delegate to
1727
+ * @param getterName name of the method which should be used to retrieve the
1728
+ * value before calling the delegate method for comparison purposes
1729
+ * @param target @see reactive
1730
+ */
1731
+ function delegateAndNotify(setterName, getterName, target) {
1732
+ return (key, value) => {
1733
+ key = toRaw(key);
1734
+ const hadKey = target.has(key);
1735
+ const originalValue = target[getterName](key);
1736
+ const ret = target[setterName](key, value);
1737
+ const hasKey = target.has(key);
1738
+ if (hadKey !== hasKey) {
1739
+ notifyReactives(target, KEYCHANGES);
1740
+ }
1741
+ if (originalValue !== value) {
1742
+ notifyReactives(target, key);
1743
+ }
1744
+ return ret;
1745
+ };
1746
+ }
1747
+ /**
1748
+ * Creates a function that will clear the underlying collection and notify that
1749
+ * the keys of the collection have changed.
1750
+ *
1751
+ * @param target @see reactive
1752
+ */
1753
+ function makeClearNotifier(target) {
1754
+ return () => {
1755
+ const allKeys = [...target.keys()];
1756
+ target.clear();
1757
+ notifyReactives(target, KEYCHANGES);
1758
+ for (const key of allKeys) {
1759
+ notifyReactives(target, key);
1760
+ }
1761
+ };
1762
+ }
1763
+ /**
1764
+ * Maps raw type of an object to an object containing functions that can be used
1765
+ * to build an appropritate proxy handler for that raw type. Eg: when making a
1766
+ * reactive set, calling the has method should mark the key that is being
1767
+ * retrieved as observed, and calling the add or delete method should notify the
1768
+ * reactives that the key which is being added or deleted has been modified.
1769
+ */
1770
+ const rawTypeToFuncHandlers = {
1771
+ Set: (target, callback) => ({
1772
+ has: makeKeyObserver("has", target, callback),
1773
+ add: delegateAndNotify("add", "has", target),
1774
+ delete: delegateAndNotify("delete", "has", target),
1775
+ keys: makeIteratorObserver("keys", target, callback),
1776
+ values: makeIteratorObserver("values", target, callback),
1777
+ entries: makeIteratorObserver("entries", target, callback),
1778
+ [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
1779
+ forEach: makeForEachObserver(target, callback),
1780
+ clear: makeClearNotifier(target),
1781
+ get size() {
1782
+ observeTargetKey(target, KEYCHANGES, callback);
1783
+ return target.size;
1784
+ },
1785
+ }),
1786
+ Map: (target, callback) => ({
1787
+ has: makeKeyObserver("has", target, callback),
1788
+ get: makeKeyObserver("get", target, callback),
1789
+ set: delegateAndNotify("set", "get", target),
1790
+ delete: delegateAndNotify("delete", "has", target),
1791
+ keys: makeIteratorObserver("keys", target, callback),
1792
+ values: makeIteratorObserver("values", target, callback),
1793
+ entries: makeIteratorObserver("entries", target, callback),
1794
+ [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
1795
+ forEach: makeForEachObserver(target, callback),
1796
+ clear: makeClearNotifier(target),
1797
+ get size() {
1798
+ observeTargetKey(target, KEYCHANGES, callback);
1799
+ return target.size;
1800
+ },
1801
+ }),
1802
+ WeakMap: (target, callback) => ({
1803
+ has: makeKeyObserver("has", target, callback),
1804
+ get: makeKeyObserver("get", target, callback),
1805
+ set: delegateAndNotify("set", "get", target),
1806
+ delete: delegateAndNotify("delete", "has", target),
1807
+ }),
1808
+ };
1809
+ /**
1810
+ * Creates a proxy handler for collections (Set/Map/WeakMap)
1811
+ *
1812
+ * @param callback @see reactive
1813
+ * @param target @see reactive
1814
+ * @returns a proxy handler object
1815
+ */
1816
+ function collectionsProxyHandler(target, callback, targetRawType) {
1817
+ // TODO: if performance is an issue we can create the special handlers lazily when each
1818
+ // property is read.
1819
+ const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
1820
+ return Object.assign(basicProxyHandler(callback), {
1821
+ get(target, key) {
1822
+ if (key === TARGET) {
1823
+ return target;
1824
+ }
1825
+ if (objectHasOwnProperty.call(specialHandlers, key)) {
1826
+ return specialHandlers[key];
1827
+ }
1828
+ observeTargetKey(target, key, callback);
1829
+ return possiblyReactive(target[key], callback);
1830
+ },
1831
+ });
1434
1832
  }
1435
1833
 
1436
1834
  /**
@@ -1448,11 +1846,13 @@
1448
1846
  await Promise.resolve();
1449
1847
  if (!called) {
1450
1848
  called = true;
1451
- callback();
1452
1849
  // wait for all calls in this microtick to fall through before resetting "called"
1453
- // so that only the first call to the batched function calls the original callback
1454
- await Promise.resolve();
1455
- called = false;
1850
+ // so that only the first call to the batched function calls the original callback.
1851
+ // Schedule this before calling the callback so that calls to the batched function
1852
+ // within the callback will proceed only after resetting called to false, and have
1853
+ // a chance to execute the callback again
1854
+ Promise.resolve().then(() => (called = false));
1855
+ callback();
1456
1856
  }
1457
1857
  };
1458
1858
  }
@@ -1499,327 +1899,135 @@
1499
1899
  */
1500
1900
  function markup(value) {
1501
1901
  return new Markup(value);
1502
- }
1503
-
1504
- /**
1505
- * This file contains utility functions that will be injected in each template,
1506
- * to perform various useful tasks in the compiled code.
1507
- */
1508
- function withDefault(value, defaultValue) {
1509
- return value === undefined || value === null || value === false ? defaultValue : value;
1510
1902
  }
1511
- function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
1512
- key = key + "__slot_" + name;
1513
- const slots = (ctx.props && ctx.props.slots) || {};
1514
- const { __render, __ctx, __scope } = slots[name] || {};
1515
- const slotScope = Object.create(__ctx || {});
1516
- if (__scope) {
1517
- slotScope[__scope] = extra || {};
1903
+ // -----------------------------------------------------------------------------
1904
+ // xml tag helper
1905
+ // -----------------------------------------------------------------------------
1906
+ const globalTemplates = {};
1907
+ function xml(...args) {
1908
+ const name = `__template__${xml.nextId++}`;
1909
+ const value = String.raw(...args);
1910
+ globalTemplates[name] = value;
1911
+ return name;
1912
+ }
1913
+ xml.nextId = 1;
1914
+
1915
+ // Maps fibers to thrown errors
1916
+ const fibersInError = new WeakMap();
1917
+ const nodeErrorHandlers = new WeakMap();
1918
+ function _handleError(node, error) {
1919
+ if (!node) {
1920
+ return false;
1518
1921
  }
1519
- const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
1520
- if (defaultContent) {
1521
- let child1 = undefined;
1522
- let child2 = undefined;
1523
- if (slotBDom) {
1524
- child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
1922
+ const fiber = node.fiber;
1923
+ if (fiber) {
1924
+ fibersInError.set(fiber, error);
1925
+ }
1926
+ const errorHandlers = nodeErrorHandlers.get(node);
1927
+ if (errorHandlers) {
1928
+ let handled = false;
1929
+ // execute in the opposite order
1930
+ for (let i = errorHandlers.length - 1; i >= 0; i--) {
1931
+ try {
1932
+ errorHandlers[i](error);
1933
+ handled = true;
1934
+ break;
1935
+ }
1936
+ catch (e) {
1937
+ error = e;
1938
+ }
1525
1939
  }
1526
- else {
1527
- child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
1940
+ if (handled) {
1941
+ return true;
1528
1942
  }
1529
- return multi([child1, child2]);
1530
1943
  }
1531
- return slotBDom || text("");
1944
+ return _handleError(node.parent, error);
1532
1945
  }
1533
- function capture(ctx) {
1534
- const component = ctx.__owl__.component;
1535
- const result = Object.create(component);
1536
- for (let k in ctx) {
1537
- result[k] = ctx[k];
1946
+ function handleError(params) {
1947
+ const error = params.error;
1948
+ const node = "node" in params ? params.node : params.fiber.node;
1949
+ const fiber = "fiber" in params ? params.fiber : node.fiber;
1950
+ // resets the fibers on components if possible. This is important so that
1951
+ // new renderings can be properly included in the initial one, if any.
1952
+ let current = fiber;
1953
+ do {
1954
+ current.node.fiber = current;
1955
+ current = current.parent;
1956
+ } while (current);
1957
+ fibersInError.set(fiber.root, error);
1958
+ const handled = _handleError(node, error);
1959
+ if (!handled) {
1960
+ console.warn(`[Owl] Unhandled error. Destroying the root component`);
1961
+ try {
1962
+ node.app.destroy();
1963
+ }
1964
+ catch (e) {
1965
+ console.error(e);
1966
+ }
1538
1967
  }
1539
- return result;
1540
- }
1541
- function withKey(elem, k) {
1542
- elem.key = k;
1543
- return elem;
1968
+ }
1969
+
1970
+ function makeChildFiber(node, parent) {
1971
+ let current = node.fiber;
1972
+ if (current) {
1973
+ cancelFibers(current.children);
1974
+ current.root = null;
1975
+ }
1976
+ return new Fiber(node, parent);
1544
1977
  }
1545
- function prepareList(collection) {
1546
- let keys;
1547
- let values;
1548
- if (Array.isArray(collection)) {
1549
- keys = collection;
1550
- values = collection;
1978
+ function makeRootFiber(node) {
1979
+ let current = node.fiber;
1980
+ if (current) {
1981
+ let root = current.root;
1982
+ // lock root fiber because canceling children fibers may destroy components,
1983
+ // which means any arbitrary code can be run in onWillDestroy, which may
1984
+ // trigger new renderings
1985
+ root.locked = true;
1986
+ root.setCounter(root.counter + 1 - cancelFibers(current.children));
1987
+ root.locked = false;
1988
+ current.children = [];
1989
+ current.childrenMap = {};
1990
+ current.bdom = null;
1991
+ if (fibersInError.has(current)) {
1992
+ fibersInError.delete(current);
1993
+ fibersInError.delete(root);
1994
+ current.appliedToDom = false;
1995
+ }
1996
+ return current;
1551
1997
  }
1552
- else if (collection) {
1553
- values = Object.keys(collection);
1554
- keys = Object.values(collection);
1998
+ const fiber = new RootFiber(node, null);
1999
+ if (node.willPatch.length) {
2000
+ fiber.willPatch.push(fiber);
1555
2001
  }
1556
- else {
1557
- throw new Error("Invalid loop expression");
2002
+ if (node.patched.length) {
2003
+ fiber.patched.push(fiber);
1558
2004
  }
1559
- const n = values.length;
1560
- return [keys, values, n, new Array(n)];
1561
- }
1562
- const isBoundary = Symbol("isBoundary");
1563
- function setContextValue(ctx, key, value) {
1564
- const ctx0 = ctx;
1565
- while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
1566
- const newCtx = ctx.__proto__;
1567
- if (!newCtx) {
1568
- ctx = ctx0;
1569
- break;
1570
- }
1571
- ctx = newCtx;
1572
- }
1573
- ctx[key] = value;
1574
- }
1575
- function toNumber(val) {
1576
- const n = parseFloat(val);
1577
- return isNaN(n) ? val : n;
1578
- }
1579
- function shallowEqual$1(l1, l2) {
1580
- for (let i = 0, l = l1.length; i < l; i++) {
1581
- if (l1[i] !== l2[i]) {
1582
- return false;
1583
- }
1584
- }
1585
- return true;
1586
- }
1587
- class LazyValue {
1588
- constructor(fn, ctx, node) {
1589
- this.fn = fn;
1590
- this.ctx = capture(ctx);
1591
- this.node = node;
1592
- }
1593
- evaluate() {
1594
- return this.fn(this.ctx, this.node);
1595
- }
1596
- toString() {
1597
- return this.evaluate().toString();
1598
- }
1599
- }
1600
- /*
1601
- * Safely outputs `value` as a block depending on the nature of `value`
1602
- */
1603
- function safeOutput(value) {
1604
- if (!value) {
1605
- return value;
1606
- }
1607
- let safeKey;
1608
- let block;
1609
- if (value instanceof Markup) {
1610
- safeKey = `string_safe`;
1611
- block = html(value);
1612
- }
1613
- else if (value instanceof LazyValue) {
1614
- safeKey = `lazy_value`;
1615
- block = value.evaluate();
1616
- }
1617
- else if (value instanceof String || typeof value === "string") {
1618
- safeKey = "string_unsafe";
1619
- block = text(value);
1620
- }
1621
- else {
1622
- // Assuming it is a block
1623
- safeKey = "block_safe";
1624
- block = value;
1625
- }
1626
- return toggler(safeKey, block);
1627
- }
1628
- let boundFunctions = new WeakMap();
1629
- function bind(ctx, fn) {
1630
- let component = ctx.__owl__.component;
1631
- let boundFnMap = boundFunctions.get(component);
1632
- if (!boundFnMap) {
1633
- boundFnMap = new WeakMap();
1634
- boundFunctions.set(component, boundFnMap);
1635
- }
1636
- let boundFn = boundFnMap.get(fn);
1637
- if (!boundFn) {
1638
- boundFn = fn.bind(component);
1639
- boundFnMap.set(fn, boundFn);
1640
- }
1641
- return boundFn;
1642
- }
1643
- function multiRefSetter(refs, name) {
1644
- let count = 0;
1645
- return (el) => {
1646
- if (el) {
1647
- count++;
1648
- if (count > 1) {
1649
- throw new Error("Cannot have 2 elements with same ref name at the same time");
1650
- }
1651
- }
1652
- if (count === 0 || el) {
1653
- refs[name] = el;
1654
- }
1655
- };
1656
- }
1657
- const UTILS = {
1658
- withDefault,
1659
- zero: Symbol("zero"),
1660
- isBoundary,
1661
- callSlot,
1662
- capture,
1663
- withKey,
1664
- prepareList,
1665
- setContextValue,
1666
- multiRefSetter,
1667
- shallowEqual: shallowEqual$1,
1668
- toNumber,
1669
- validateProps,
1670
- LazyValue,
1671
- safeOutput,
1672
- bind,
1673
- };
1674
-
1675
- const mainEventHandler = (data, ev, currentTarget) => {
1676
- const { data: _data, modifiers } = filterOutModifiersFromData(data);
1677
- data = _data;
1678
- let stopped = false;
1679
- if (modifiers.length) {
1680
- let selfMode = false;
1681
- const isSelf = ev.target === currentTarget;
1682
- for (const mod of modifiers) {
1683
- switch (mod) {
1684
- case "self":
1685
- selfMode = true;
1686
- if (isSelf) {
1687
- continue;
1688
- }
1689
- else {
1690
- return stopped;
1691
- }
1692
- case "prevent":
1693
- if ((selfMode && isSelf) || !selfMode)
1694
- ev.preventDefault();
1695
- continue;
1696
- case "stop":
1697
- if ((selfMode && isSelf) || !selfMode)
1698
- ev.stopPropagation();
1699
- stopped = true;
1700
- continue;
1701
- }
1702
- }
1703
- }
1704
- // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
1705
- // We check this rather than data[0] being truthy (or typeof function) so that it crashes
1706
- // as expected when there is a handler expression that evaluates to a falsy value
1707
- if (Object.hasOwnProperty.call(data, 0)) {
1708
- const handler = data[0];
1709
- if (typeof handler !== "function") {
1710
- throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
1711
- }
1712
- let node = data[1] ? data[1].__owl__ : null;
1713
- if (node ? node.status === 1 /* MOUNTED */ : true) {
1714
- handler.call(node ? node.component : null, ev);
1715
- }
1716
- }
1717
- return stopped;
1718
- };
1719
-
1720
- // Maps fibers to thrown errors
1721
- const fibersInError = new WeakMap();
1722
- const nodeErrorHandlers = new WeakMap();
1723
- function _handleError(node, error, isFirstRound = false) {
1724
- if (!node) {
1725
- return false;
1726
- }
1727
- const fiber = node.fiber;
1728
- if (fiber) {
1729
- fibersInError.set(fiber, error);
1730
- }
1731
- const errorHandlers = nodeErrorHandlers.get(node);
1732
- if (errorHandlers) {
1733
- let stopped = false;
1734
- // execute in the opposite order
1735
- for (let i = errorHandlers.length - 1; i >= 0; i--) {
1736
- try {
1737
- errorHandlers[i](error);
1738
- stopped = true;
1739
- break;
1740
- }
1741
- catch (e) {
1742
- error = e;
1743
- }
1744
- }
1745
- if (stopped) {
1746
- if (isFirstRound && fiber && fiber.node.fiber) {
1747
- fiber.root.counter--;
1748
- }
1749
- return true;
1750
- }
1751
- }
1752
- return _handleError(node.parent, error);
1753
- }
1754
- function handleError(params) {
1755
- const error = params.error;
1756
- const node = "node" in params ? params.node : params.fiber.node;
1757
- const fiber = "fiber" in params ? params.fiber : node.fiber;
1758
- // resets the fibers on components if possible. This is important so that
1759
- // new renderings can be properly included in the initial one, if any.
1760
- let current = fiber;
1761
- do {
1762
- current.node.fiber = current;
1763
- current = current.parent;
1764
- } while (current);
1765
- fibersInError.set(fiber.root, error);
1766
- const handled = _handleError(node, error, true);
1767
- if (!handled) {
1768
- console.warn(`[Owl] Unhandled error. Destroying the root component`);
1769
- try {
1770
- node.app.destroy();
1771
- }
1772
- catch (e) {
1773
- console.error(e);
1774
- }
1775
- }
1776
- }
1777
-
1778
- function makeChildFiber(node, parent) {
1779
- let current = node.fiber;
1780
- if (current) {
1781
- let root = parent.root;
1782
- cancelFibers(root, current.children);
1783
- current.root = null;
1784
- }
1785
- return new Fiber(node, parent);
1786
- }
1787
- function makeRootFiber(node) {
1788
- let current = node.fiber;
1789
- if (current) {
1790
- let root = current.root;
1791
- root.counter -= cancelFibers(root, current.children);
1792
- current.children = [];
1793
- root.counter++;
1794
- current.bdom = null;
1795
- if (fibersInError.has(current)) {
1796
- fibersInError.delete(current);
1797
- fibersInError.delete(root);
1798
- current.appliedToDom = false;
1799
- }
1800
- return current;
1801
- }
1802
- const fiber = new RootFiber(node, null);
1803
- if (node.willPatch.length) {
1804
- fiber.willPatch.push(fiber);
1805
- }
1806
- if (node.patched.length) {
1807
- fiber.patched.push(fiber);
1808
- }
1809
- return fiber;
2005
+ return fiber;
1810
2006
  }
1811
2007
  /**
1812
2008
  * @returns number of not-yet rendered fibers cancelled
1813
2009
  */
1814
- function cancelFibers(root, fibers) {
2010
+ function cancelFibers(fibers) {
1815
2011
  let result = 0;
1816
2012
  for (let fiber of fibers) {
1817
- fiber.node.fiber = null;
1818
- fiber.root = root;
1819
- if (!fiber.bdom) {
2013
+ let node = fiber.node;
2014
+ if (node.status === 0 /* NEW */) {
2015
+ node.destroy();
2016
+ }
2017
+ node.fiber = null;
2018
+ if (fiber.bdom) {
2019
+ // if fiber has been rendered, this means that the component props have
2020
+ // been updated. however, this fiber will not be patched to the dom, so
2021
+ // it could happen that the next render compare the current props with
2022
+ // the same props, and skip the render completely. With the next line,
2023
+ // we kindly request the component code to force a render, so it works as
2024
+ // expected.
2025
+ node.forceNextRender = true;
2026
+ }
2027
+ else {
1820
2028
  result++;
1821
2029
  }
1822
- result += cancelFibers(root, fiber.children);
2030
+ result += cancelFibers(fiber.children);
1823
2031
  }
1824
2032
  return result;
1825
2033
  }
@@ -1828,11 +2036,14 @@
1828
2036
  this.bdom = null;
1829
2037
  this.children = [];
1830
2038
  this.appliedToDom = false;
2039
+ this.deep = false;
2040
+ this.childrenMap = {};
1831
2041
  this.node = node;
1832
2042
  this.parent = parent;
1833
2043
  if (parent) {
2044
+ this.deep = parent.deep;
1834
2045
  const root = parent.root;
1835
- root.counter++;
2046
+ root.setCounter(root.counter + 1);
1836
2047
  this.root = root;
1837
2048
  parent.children.push(this);
1838
2049
  }
@@ -1840,6 +2051,42 @@
1840
2051
  this.root = this;
1841
2052
  }
1842
2053
  }
2054
+ render() {
2055
+ // if some parent has a fiber => register in followup
2056
+ let prev = this.root.node;
2057
+ let scheduler = prev.app.scheduler;
2058
+ let current = prev.parent;
2059
+ while (current) {
2060
+ if (current.fiber) {
2061
+ let root = current.fiber.root;
2062
+ if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {
2063
+ current = root.node;
2064
+ }
2065
+ else {
2066
+ scheduler.delayedRenders.push(this);
2067
+ return;
2068
+ }
2069
+ }
2070
+ prev = current;
2071
+ current = current.parent;
2072
+ }
2073
+ // there are no current rendering from above => we can render
2074
+ this._render();
2075
+ }
2076
+ _render() {
2077
+ const node = this.node;
2078
+ const root = this.root;
2079
+ if (root) {
2080
+ try {
2081
+ this.bdom = true;
2082
+ this.bdom = node.renderFn();
2083
+ }
2084
+ catch (e) {
2085
+ handleError({ node, error: e });
2086
+ }
2087
+ root.setCounter(root.counter - 1);
2088
+ }
2089
+ }
1843
2090
  }
1844
2091
  class RootFiber extends Fiber {
1845
2092
  constructor() {
@@ -1873,7 +2120,7 @@
1873
2120
  }
1874
2121
  current = undefined;
1875
2122
  // Step 2: patching the dom
1876
- node.patch();
2123
+ node._patch();
1877
2124
  this.locked = false;
1878
2125
  // Step 4: calling all mounted lifecycle hooks
1879
2126
  let mountedFibers = this.mounted;
@@ -1901,6 +2148,12 @@
1901
2148
  handleError({ fiber: current || this, error: e });
1902
2149
  }
1903
2150
  }
2151
+ setCounter(newValue) {
2152
+ this.counter = newValue;
2153
+ if (newValue === 0) {
2154
+ this.node.app.scheduler.flush();
2155
+ }
2156
+ }
1904
2157
  }
1905
2158
  class MountFiber extends RootFiber {
1906
2159
  constructor(node, target, options = {}) {
@@ -1912,6 +2165,7 @@
1912
2165
  let current = this;
1913
2166
  try {
1914
2167
  const node = this.node;
2168
+ node.children = this.childrenMap;
1915
2169
  node.app.constructor.validateTarget(this.target);
1916
2170
  if (node.bdom) {
1917
2171
  // this is a complicated situation: if we mount a fiber with an existing
@@ -1950,9 +2204,148 @@
1950
2204
  }
1951
2205
  }
1952
2206
 
1953
- let currentNode = null;
1954
- function getCurrent() {
1955
- if (!currentNode) {
2207
+ /**
2208
+ * Apply default props (only top level).
2209
+ *
2210
+ * Note that this method does modify in place the props
2211
+ */
2212
+ function applyDefaultProps(props, ComponentClass) {
2213
+ const defaultProps = ComponentClass.defaultProps;
2214
+ if (defaultProps) {
2215
+ for (let propName in defaultProps) {
2216
+ if (props[propName] === undefined) {
2217
+ props[propName] = defaultProps[propName];
2218
+ }
2219
+ }
2220
+ }
2221
+ }
2222
+ //------------------------------------------------------------------------------
2223
+ // Prop validation helper
2224
+ //------------------------------------------------------------------------------
2225
+ function getPropDescription(staticProps) {
2226
+ if (staticProps instanceof Array) {
2227
+ return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
2228
+ }
2229
+ return staticProps || { "*": true };
2230
+ }
2231
+ /**
2232
+ * Validate the component props (or next props) against the (static) props
2233
+ * description. This is potentially an expensive operation: it may needs to
2234
+ * visit recursively the props and all the children to check if they are valid.
2235
+ * This is why it is only done in 'dev' mode.
2236
+ */
2237
+ function validateProps(name, props, parent) {
2238
+ const ComponentClass = typeof name !== "string"
2239
+ ? name
2240
+ : parent.constructor.components[name];
2241
+ if (!ComponentClass) {
2242
+ // this is an error, wrong component. We silently return here instead so the
2243
+ // error is triggered by the usual path ('component' function)
2244
+ return;
2245
+ }
2246
+ applyDefaultProps(props, ComponentClass);
2247
+ const defaultProps = ComponentClass.defaultProps || {};
2248
+ let propsDef = getPropDescription(ComponentClass.props);
2249
+ const allowAdditionalProps = "*" in propsDef;
2250
+ for (let propName in propsDef) {
2251
+ if (propName === "*") {
2252
+ continue;
2253
+ }
2254
+ const propDef = propsDef[propName];
2255
+ let isMandatory = !!propDef;
2256
+ if (typeof propDef === "object" && "optional" in propDef) {
2257
+ isMandatory = !propDef.optional;
2258
+ }
2259
+ if (isMandatory && propName in defaultProps) {
2260
+ throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
2261
+ }
2262
+ if (props[propName] === undefined) {
2263
+ if (isMandatory) {
2264
+ throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
2265
+ }
2266
+ else {
2267
+ continue;
2268
+ }
2269
+ }
2270
+ let isValid;
2271
+ try {
2272
+ isValid = isValidProp(props[propName], propDef);
2273
+ }
2274
+ catch (e) {
2275
+ e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
2276
+ throw e;
2277
+ }
2278
+ if (!isValid) {
2279
+ throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
2280
+ }
2281
+ }
2282
+ if (!allowAdditionalProps) {
2283
+ for (let propName in props) {
2284
+ if (!(propName in propsDef)) {
2285
+ throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
2286
+ }
2287
+ }
2288
+ }
2289
+ }
2290
+ /**
2291
+ * Check if an invidual prop value matches its (static) prop definition
2292
+ */
2293
+ function isValidProp(prop, propDef) {
2294
+ if (propDef === true) {
2295
+ return true;
2296
+ }
2297
+ if (typeof propDef === "function") {
2298
+ // Check if a value is constructed by some Constructor. Note that there is a
2299
+ // slight abuse of language: we want to consider primitive values as well.
2300
+ //
2301
+ // So, even though 1 is not an instance of Number, we want to consider that
2302
+ // it is valid.
2303
+ if (typeof prop === "object") {
2304
+ return prop instanceof propDef;
2305
+ }
2306
+ return typeof prop === propDef.name.toLowerCase();
2307
+ }
2308
+ else if (propDef instanceof Array) {
2309
+ // If this code is executed, this means that we want to check if a prop
2310
+ // matches at least one of its descriptor.
2311
+ let result = false;
2312
+ for (let i = 0, iLen = propDef.length; i < iLen; i++) {
2313
+ result = result || isValidProp(prop, propDef[i]);
2314
+ }
2315
+ return result;
2316
+ }
2317
+ // propsDef is an object
2318
+ if (propDef.optional && prop === undefined) {
2319
+ return true;
2320
+ }
2321
+ let result = propDef.type ? isValidProp(prop, propDef.type) : true;
2322
+ if (propDef.validate) {
2323
+ result = result && propDef.validate(prop);
2324
+ }
2325
+ if (propDef.type === Array && propDef.element) {
2326
+ for (let i = 0, iLen = prop.length; i < iLen; i++) {
2327
+ result = result && isValidProp(prop[i], propDef.element);
2328
+ }
2329
+ }
2330
+ if (propDef.type === Object && propDef.shape) {
2331
+ const shape = propDef.shape;
2332
+ for (let key in shape) {
2333
+ result = result && isValidProp(prop[key], shape[key]);
2334
+ }
2335
+ if (result) {
2336
+ for (let propName in prop) {
2337
+ if (!(propName in shape)) {
2338
+ throw new Error(`unknown prop '${propName}'`);
2339
+ }
2340
+ }
2341
+ }
2342
+ }
2343
+ return result;
2344
+ }
2345
+
2346
+ let currentNode = null;
2347
+ function getCurrent() {
2348
+ if (!currentNode) {
1956
2349
  throw new Error("No active component (a hook function should only be called in 'setup')");
1957
2350
  }
1958
2351
  return currentNode;
@@ -1960,24 +2353,61 @@
1960
2353
  function useComponent() {
1961
2354
  return currentNode.component;
1962
2355
  }
2356
+ // -----------------------------------------------------------------------------
2357
+ // Integration with reactivity system (useState)
2358
+ // -----------------------------------------------------------------------------
2359
+ const batchedRenderFunctions = new WeakMap();
2360
+ /**
2361
+ * Creates a reactive object that will be observed by the current component.
2362
+ * Reading data from the returned object (eg during rendering) will cause the
2363
+ * component to subscribe to that data and be rerendered when it changes.
2364
+ *
2365
+ * @param state the state to observe
2366
+ * @returns a reactive object that will cause the component to re-render on
2367
+ * relevant changes
2368
+ * @see reactive
2369
+ */
2370
+ function useState(state) {
2371
+ const node = getCurrent();
2372
+ let render = batchedRenderFunctions.get(node);
2373
+ if (!render) {
2374
+ render = batched(node.render.bind(node));
2375
+ batchedRenderFunctions.set(node, render);
2376
+ // manual implementation of onWillDestroy to break cyclic dependency
2377
+ node.willDestroy.push(clearReactivesForCallback.bind(null, render));
2378
+ }
2379
+ return reactive(state, render);
2380
+ }
2381
+ function arePropsDifferent(props1, props2) {
2382
+ for (let k in props1) {
2383
+ if (props1[k] !== props2[k]) {
2384
+ return true;
2385
+ }
2386
+ }
2387
+ return Object.keys(props1).length !== Object.keys(props2).length;
2388
+ }
1963
2389
  function component(name, props, key, ctx, parent) {
1964
2390
  let node = ctx.children[key];
1965
2391
  let isDynamic = typeof name !== "string";
1966
- if (node) {
1967
- if (node.status < 1 /* MOUNTED */) {
1968
- node.destroy();
1969
- node = undefined;
1970
- }
1971
- else if (node.status === 2 /* DESTROYED */) {
1972
- node = undefined;
1973
- }
2392
+ if (node && node.status === 2 /* DESTROYED */) {
2393
+ node = undefined;
1974
2394
  }
1975
2395
  if (isDynamic && node && node.component.constructor !== name) {
1976
2396
  node = undefined;
1977
2397
  }
1978
2398
  const parentFiber = ctx.fiber;
1979
2399
  if (node) {
1980
- node.updateAndRender(props, parentFiber);
2400
+ let shouldRender = node.forceNextRender;
2401
+ if (shouldRender) {
2402
+ node.forceNextRender = false;
2403
+ }
2404
+ else {
2405
+ const currentProps = node.component.props[TARGET];
2406
+ shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
2407
+ }
2408
+ if (shouldRender) {
2409
+ node.updateAndRender(props, parentFiber);
2410
+ }
1981
2411
  }
1982
2412
  else {
1983
2413
  // new component
@@ -1991,18 +2421,19 @@
1991
2421
  throw new Error(`Cannot find the definition of component "${name}"`);
1992
2422
  }
1993
2423
  }
1994
- node = new ComponentNode(C, props, ctx.app, ctx);
2424
+ node = new ComponentNode(C, props, ctx.app, ctx, key);
1995
2425
  ctx.children[key] = node;
1996
- const fiber = makeChildFiber(node, parentFiber);
1997
- node.initiateRender(fiber);
2426
+ node.initiateRender(new Fiber(node, parentFiber));
1998
2427
  }
2428
+ parentFiber.childrenMap[key] = node;
1999
2429
  return node;
2000
2430
  }
2001
2431
  class ComponentNode {
2002
- constructor(C, props, app, parent) {
2432
+ constructor(C, props, app, parent, parentKey) {
2003
2433
  this.fiber = null;
2004
2434
  this.bdom = null;
2005
2435
  this.status = 0 /* NEW */;
2436
+ this.forceNextRender = false;
2006
2437
  this.children = Object.create(null);
2007
2438
  this.refs = {};
2008
2439
  this.willStart = [];
@@ -2014,11 +2445,13 @@
2014
2445
  this.willDestroy = [];
2015
2446
  currentNode = this;
2016
2447
  this.app = app;
2017
- this.parent = parent || null;
2448
+ this.parent = parent;
2449
+ this.parentKey = parentKey;
2018
2450
  this.level = parent ? parent.level + 1 : 0;
2019
2451
  applyDefaultProps(props, C);
2020
2452
  const env = (parent && parent.childEnv) || app.env;
2021
2453
  this.childEnv = env;
2454
+ props = useState(props);
2022
2455
  this.component = new C(props, env, this);
2023
2456
  this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
2024
2457
  this.component.setup();
@@ -2043,23 +2476,32 @@
2043
2476
  return;
2044
2477
  }
2045
2478
  if (this.status === 0 /* NEW */ && this.fiber === fiber) {
2046
- this._render(fiber);
2479
+ fiber.render();
2047
2480
  }
2048
2481
  }
2049
- async render() {
2482
+ async render(deep = false) {
2050
2483
  let current = this.fiber;
2051
- if (current && current.root.locked) {
2484
+ if (current && (current.root.locked || current.bdom === true)) {
2052
2485
  await Promise.resolve();
2053
2486
  // situation may have changed after the microtask tick
2054
2487
  current = this.fiber;
2055
2488
  }
2056
- if (current && !current.bdom && !fibersInError.has(current)) {
2057
- return;
2489
+ if (current) {
2490
+ if (!current.bdom && !fibersInError.has(current)) {
2491
+ if (deep) {
2492
+ // we want the render from this point on to be with deep=true
2493
+ current.deep = deep;
2494
+ }
2495
+ return;
2496
+ }
2497
+ // if current rendering was with deep=true, we want this one to be the same
2498
+ deep = deep || current.deep;
2058
2499
  }
2059
- if (!this.bdom && !current) {
2500
+ else if (!this.bdom) {
2060
2501
  return;
2061
2502
  }
2062
2503
  const fiber = makeRootFiber(this);
2504
+ fiber.deep = deep;
2063
2505
  this.fiber = fiber;
2064
2506
  this.app.scheduler.addFiber(fiber);
2065
2507
  await Promise.resolve();
@@ -2078,16 +2520,7 @@
2078
2520
  // embedded in a rendering coming from above, so the fiber will be rendered
2079
2521
  // in the next microtick anyway, so we should not render it again.
2080
2522
  if (this.fiber === fiber && (current || !fiber.parent)) {
2081
- this._render(fiber);
2082
- }
2083
- }
2084
- _render(fiber) {
2085
- try {
2086
- fiber.bdom = this.renderFn();
2087
- fiber.root.counter--;
2088
- }
2089
- catch (e) {
2090
- handleError({ node: this, error: e });
2523
+ fiber.render();
2091
2524
  }
2092
2525
  }
2093
2526
  destroy() {
@@ -2107,8 +2540,15 @@
2107
2540
  for (let child of Object.values(this.children)) {
2108
2541
  child._destroy();
2109
2542
  }
2110
- for (let cb of this.willDestroy) {
2111
- cb.call(component);
2543
+ if (this.willDestroy.length) {
2544
+ try {
2545
+ for (let cb of this.willDestroy) {
2546
+ cb.call(component);
2547
+ }
2548
+ }
2549
+ catch (e) {
2550
+ handleError({ error: e, node: this });
2551
+ }
2112
2552
  }
2113
2553
  this.status = 2 /* DESTROYED */;
2114
2554
  }
@@ -2118,13 +2558,16 @@
2118
2558
  this.fiber = fiber;
2119
2559
  const component = this.component;
2120
2560
  applyDefaultProps(props, component.constructor);
2561
+ currentNode = this;
2562
+ props = useState(props);
2563
+ currentNode = null;
2121
2564
  const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
2122
2565
  await prom;
2123
2566
  if (fiber !== this.fiber) {
2124
2567
  return;
2125
2568
  }
2126
2569
  component.props = props;
2127
- this._render(fiber);
2570
+ fiber.render();
2128
2571
  const parentRoot = parentFiber.root;
2129
2572
  if (this.willPatch.length) {
2130
2573
  parentRoot.willPatch.push(fiber);
@@ -2171,17 +2614,24 @@
2171
2614
  bdom.mount(parent, anchor);
2172
2615
  this.status = 1 /* MOUNTED */;
2173
2616
  this.fiber.appliedToDom = true;
2617
+ this.children = this.fiber.childrenMap;
2174
2618
  this.fiber = null;
2175
2619
  }
2176
2620
  moveBefore(other, afterNode) {
2177
2621
  this.bdom.moveBefore(other ? other.bdom : null, afterNode);
2178
2622
  }
2179
2623
  patch() {
2624
+ if (this.fiber && this.fiber.parent) {
2625
+ // we only patch here renderings coming from above. renderings initiated
2626
+ // by the component will be patched independently in the appropriate
2627
+ // fiber.complete
2628
+ this._patch();
2629
+ }
2630
+ }
2631
+ _patch() {
2180
2632
  const hasChildren = Object.keys(this.children).length > 0;
2633
+ this.children = this.fiber.childrenMap;
2181
2634
  this.bdom.patch(this.fiber.bdom, hasChildren);
2182
- if (hasChildren) {
2183
- this.cleanOutdatedChildren();
2184
- }
2185
2635
  this.fiber.appliedToDom = true;
2186
2636
  this.fiber = null;
2187
2637
  }
@@ -2191,85 +2641,89 @@
2191
2641
  remove() {
2192
2642
  this.bdom.remove();
2193
2643
  }
2194
- cleanOutdatedChildren() {
2195
- const children = this.children;
2196
- for (const key in children) {
2197
- const node = children[key];
2198
- const status = node.status;
2199
- if (status !== 1 /* MOUNTED */) {
2200
- delete children[key];
2201
- if (status !== 2 /* DESTROYED */) {
2202
- node.destroy();
2203
- }
2204
- }
2205
- }
2644
+ // ---------------------------------------------------------------------------
2645
+ // Some debug helpers
2646
+ // ---------------------------------------------------------------------------
2647
+ get name() {
2648
+ return this.component.constructor.name;
2649
+ }
2650
+ get subscriptions() {
2651
+ const render = batchedRenderFunctions.get(this);
2652
+ return render ? getSubscriptions(render) : [];
2206
2653
  }
2207
2654
  }
2208
2655
 
2209
2656
  // -----------------------------------------------------------------------------
2210
- // hooks
2657
+ // Scheduler
2211
2658
  // -----------------------------------------------------------------------------
2212
- function onWillStart(fn) {
2213
- const node = getCurrent();
2214
- node.willStart.push(fn.bind(node.component));
2215
- }
2216
- function onWillUpdateProps(fn) {
2217
- const node = getCurrent();
2218
- node.willUpdateProps.push(fn.bind(node.component));
2219
- }
2220
- function onMounted(fn) {
2221
- const node = getCurrent();
2222
- node.mounted.push(fn.bind(node.component));
2223
- }
2224
- function onWillPatch(fn) {
2225
- const node = getCurrent();
2226
- node.willPatch.unshift(fn.bind(node.component));
2227
- }
2228
- function onPatched(fn) {
2229
- const node = getCurrent();
2230
- node.patched.push(fn.bind(node.component));
2231
- }
2232
- function onWillUnmount(fn) {
2233
- const node = getCurrent();
2234
- node.willUnmount.unshift(fn.bind(node.component));
2235
- }
2236
- function onWillDestroy(fn) {
2237
- const node = getCurrent();
2238
- node.willDestroy.push(fn.bind(node.component));
2239
- }
2240
- function onWillRender(fn) {
2241
- const node = getCurrent();
2242
- const renderFn = node.renderFn;
2243
- node.renderFn = () => {
2244
- fn.call(node.component);
2245
- return renderFn();
2246
- };
2247
- }
2248
- function onRendered(fn) {
2249
- const node = getCurrent();
2250
- const renderFn = node.renderFn;
2251
- node.renderFn = () => {
2252
- const result = renderFn();
2253
- fn.call(node.component);
2254
- return result;
2255
- };
2256
- }
2257
- function onError(callback) {
2258
- const node = getCurrent();
2259
- let handlers = nodeErrorHandlers.get(node);
2260
- if (!handlers) {
2261
- handlers = [];
2262
- nodeErrorHandlers.set(node, handlers);
2659
+ class Scheduler {
2660
+ constructor() {
2661
+ this.tasks = new Set();
2662
+ this.frame = 0;
2663
+ this.delayedRenders = [];
2664
+ this.requestAnimationFrame = Scheduler.requestAnimationFrame;
2263
2665
  }
2264
- handlers.push(callback.bind(node.component));
2265
- }
2266
-
2267
- /**
2268
- * Owl QWeb Expression Parser
2269
- *
2270
- * Owl needs in various contexts to be able to understand the structure of a
2271
- * string representing a javascript expression. The usual goal is to be able
2272
- * to rewrite some variables. For example, if a template has
2666
+ addFiber(fiber) {
2667
+ this.tasks.add(fiber.root);
2668
+ }
2669
+ /**
2670
+ * Process all current tasks. This only applies to the fibers that are ready.
2671
+ * Other tasks are left unchanged.
2672
+ */
2673
+ flush() {
2674
+ if (this.delayedRenders.length) {
2675
+ let renders = this.delayedRenders;
2676
+ this.delayedRenders = [];
2677
+ for (let f of renders) {
2678
+ if (f.root && f.node.status !== 2 /* DESTROYED */) {
2679
+ f.render();
2680
+ }
2681
+ }
2682
+ }
2683
+ if (this.frame === 0) {
2684
+ this.frame = this.requestAnimationFrame(() => {
2685
+ this.frame = 0;
2686
+ this.tasks.forEach((fiber) => this.processFiber(fiber));
2687
+ for (let task of this.tasks) {
2688
+ if (task.node.status === 2 /* DESTROYED */) {
2689
+ this.tasks.delete(task);
2690
+ }
2691
+ }
2692
+ });
2693
+ }
2694
+ }
2695
+ processFiber(fiber) {
2696
+ if (fiber.root !== fiber) {
2697
+ this.tasks.delete(fiber);
2698
+ return;
2699
+ }
2700
+ const hasError = fibersInError.has(fiber);
2701
+ if (hasError && fiber.counter !== 0) {
2702
+ this.tasks.delete(fiber);
2703
+ return;
2704
+ }
2705
+ if (fiber.node.status === 2 /* DESTROYED */) {
2706
+ this.tasks.delete(fiber);
2707
+ return;
2708
+ }
2709
+ if (fiber.counter === 0) {
2710
+ if (!hasError) {
2711
+ fiber.complete();
2712
+ }
2713
+ this.tasks.delete(fiber);
2714
+ }
2715
+ }
2716
+ }
2717
+ // capture the value of requestAnimationFrame as soon as possible, to avoid
2718
+ // interactions with other code, such as test frameworks that override them
2719
+ Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
2720
+
2721
+ /**
2722
+ * Owl QWeb Expression Parser
2723
+ *
2724
+ * Owl needs in various contexts to be able to understand the structure of a
2725
+ * string representing a javascript expression. The usual goal is to be able
2726
+ * to rewrite some variables. For example, if a template has
2273
2727
  *
2274
2728
  * ```xml
2275
2729
  * <t t-if="computeSomething({val: state.val})">...</t>
@@ -2416,24 +2870,31 @@
2416
2870
  function tokenize(expr) {
2417
2871
  const result = [];
2418
2872
  let token = true;
2419
- while (token) {
2420
- expr = expr.trim();
2421
- if (expr) {
2422
- for (let tokenizer of TOKENIZERS) {
2423
- token = tokenizer(expr);
2424
- if (token) {
2425
- result.push(token);
2426
- expr = expr.slice(token.size || token.value.length);
2427
- break;
2873
+ let error;
2874
+ let current = expr;
2875
+ try {
2876
+ while (token) {
2877
+ current = current.trim();
2878
+ if (current) {
2879
+ for (let tokenizer of TOKENIZERS) {
2880
+ token = tokenizer(current);
2881
+ if (token) {
2882
+ result.push(token);
2883
+ current = current.slice(token.size || token.value.length);
2884
+ break;
2885
+ }
2428
2886
  }
2429
2887
  }
2430
- }
2431
- else {
2432
- token = false;
2888
+ else {
2889
+ token = false;
2890
+ }
2433
2891
  }
2434
2892
  }
2435
- if (expr.length) {
2436
- throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
2893
+ catch (e) {
2894
+ error = e; // Silence all errors and throw a generic error below
2895
+ }
2896
+ if (current.length || error) {
2897
+ throw new Error(`Tokenizer error: could not tokenize \`${expr}\``);
2437
2898
  }
2438
2899
  return result;
2439
2900
  }
@@ -2638,7 +3099,7 @@
2638
3099
  }, params);
2639
3100
  }
2640
3101
  class CodeTarget {
2641
- constructor(name) {
3102
+ constructor(name, on) {
2642
3103
  this.indentLevel = 0;
2643
3104
  this.loopLevel = 0;
2644
3105
  this.code = [];
@@ -2649,6 +3110,7 @@
2649
3110
  this.refInfo = {};
2650
3111
  this.shouldProtectScope = false;
2651
3112
  this.name = name;
3113
+ this.on = on || null;
2652
3114
  }
2653
3115
  addLine(line, idx) {
2654
3116
  const prefix = new Array(this.indentLevel + 2).join(" ");
@@ -2698,7 +3160,7 @@
2698
3160
  this.targets = [];
2699
3161
  this.target = new CodeTarget("template");
2700
3162
  this.translatableAttributes = TRANSLATABLE_ATTRS;
2701
- this.staticCalls = [];
3163
+ this.staticDefs = [];
2702
3164
  this.helpers = new Set();
2703
3165
  this.translateFn = options.translateFn || ((s) => s);
2704
3166
  if (options.translatableAttributes) {
@@ -2741,8 +3203,8 @@
2741
3203
  if (this.templateName) {
2742
3204
  mainCode.push(`// Template name: "${this.templateName}"`);
2743
3205
  }
2744
- for (let { id, template } of this.staticCalls) {
2745
- mainCode.push(`const ${id} = getTemplate(${template});`);
3206
+ for (let { id, expr } of this.staticDefs) {
3207
+ mainCode.push(`const ${id} = ${expr};`);
2746
3208
  }
2747
3209
  // define all blocks
2748
3210
  if (this.blocks.length) {
@@ -2778,19 +3240,21 @@
2778
3240
  }
2779
3241
  return code;
2780
3242
  }
2781
- compileInNewTarget(prefix, ast, ctx) {
3243
+ compileInNewTarget(prefix, ast, ctx, on) {
2782
3244
  const name = this.generateId(prefix);
2783
3245
  const initialTarget = this.target;
2784
- const target = new CodeTarget(name);
3246
+ const target = new CodeTarget(name, on);
2785
3247
  this.targets.push(target);
2786
3248
  this.target = target;
2787
- const subCtx = createContext(ctx);
2788
- this.compileAST(ast, subCtx);
3249
+ this.compileAST(ast, createContext(ctx));
2789
3250
  this.target = initialTarget;
2790
3251
  return name;
2791
3252
  }
2792
- addLine(line) {
2793
- this.target.addLine(line);
3253
+ addLine(line, idx) {
3254
+ this.target.addLine(line, idx);
3255
+ }
3256
+ define(varName, expr) {
3257
+ this.addLine(`const ${varName} = ${expr};`);
2794
3258
  }
2795
3259
  generateId(prefix = "") {
2796
3260
  this.ids[prefix] = (this.ids[prefix] || 0) + 1;
@@ -2832,10 +3296,13 @@
2832
3296
  blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
2833
3297
  }
2834
3298
  if (block.isRoot && !ctx.preventRoot) {
3299
+ if (this.target.on) {
3300
+ blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
3301
+ }
2835
3302
  this.addLine(`return ${blockExpr};`);
2836
3303
  }
2837
3304
  else {
2838
- this.addLine(`let ${block.varName} = ${blockExpr};`);
3305
+ this.define(block.varName, blockExpr);
2839
3306
  }
2840
3307
  }
2841
3308
  /**
@@ -2863,7 +3330,7 @@
2863
3330
  if (!mapping.has(tok.varName)) {
2864
3331
  const varId = this.generateId("v");
2865
3332
  mapping.set(tok.varName, varId);
2866
- this.addLine(`const ${varId} = ${tok.value};`);
3333
+ this.define(varId, tok.value);
2867
3334
  }
2868
3335
  tok.value = mapping.get(tok.varName);
2869
3336
  }
@@ -3002,7 +3469,7 @@
3002
3469
  this.blocks.push(block);
3003
3470
  if (ast.dynamicTag) {
3004
3471
  const tagExpr = this.generateId("tag");
3005
- this.addLine(`let ${tagExpr} = ${compileExpr(ast.dynamicTag)};`);
3472
+ this.define(tagExpr, compileExpr(ast.dynamicTag));
3006
3473
  block.dynamicTagName = tagExpr;
3007
3474
  }
3008
3475
  }
@@ -3084,10 +3551,10 @@
3084
3551
  const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
3085
3552
  const baseExpression = compileExpr(baseExpr);
3086
3553
  const bExprId = this.generateId("bExpr");
3087
- this.addLine(`const ${bExprId} = ${baseExpression};`);
3554
+ this.define(bExprId, baseExpression);
3088
3555
  const expression = compileExpr(expr);
3089
3556
  const exprId = this.generateId("expr");
3090
- this.addLine(`const ${exprId} = ${expression};`);
3557
+ this.define(exprId, expression);
3091
3558
  const fullExpression = `${bExprId}[${exprId}]`;
3092
3559
  let idx;
3093
3560
  if (specialInitTargetAttr) {
@@ -3097,7 +3564,7 @@
3097
3564
  else if (hasDynamicChildren) {
3098
3565
  const bValueId = this.generateId("bValue");
3099
3566
  tModelSelectedExpr = `${bValueId}`;
3100
- this.addLine(`let ${tModelSelectedExpr} = ${fullExpression}`);
3567
+ this.define(tModelSelectedExpr, fullExpression);
3101
3568
  }
3102
3569
  else {
3103
3570
  idx = block.insertData(`${fullExpression}`, "attr");
@@ -3145,14 +3612,14 @@
3145
3612
  const children = block.children.slice();
3146
3613
  let current = children.shift();
3147
3614
  for (let i = codeIdx; i < code.length; i++) {
3148
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3149
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3615
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3616
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3150
3617
  current = children.shift();
3151
3618
  if (!current)
3152
3619
  break;
3153
3620
  }
3154
3621
  }
3155
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3622
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3156
3623
  }
3157
3624
  }
3158
3625
  }
@@ -3240,14 +3707,14 @@
3240
3707
  const children = block.children.slice();
3241
3708
  let current = children.shift();
3242
3709
  for (let i = codeIdx; i < code.length; i++) {
3243
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3244
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3710
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3711
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3245
3712
  current = children.shift();
3246
3713
  if (!current)
3247
3714
  break;
3248
3715
  }
3249
3716
  }
3250
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3717
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3251
3718
  }
3252
3719
  // note: this part is duplicated from end of compilemulti:
3253
3720
  const args = block.children.map((c) => c.varName).join(", ");
@@ -3268,10 +3735,10 @@
3268
3735
  const l = `l_block${block.id}`;
3269
3736
  const c = `c_block${block.id}`;
3270
3737
  this.helpers.add("prepareList");
3271
- this.addLine(`const [${keys}, ${vals}, ${l}, ${c}] = prepareList(${compileExpr(ast.collection)});`);
3738
+ this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);
3272
3739
  // Throw errors on duplicate keys in dev mode
3273
3740
  if (this.dev) {
3274
- this.addLine(`const keys${block.id} = new Set();`);
3741
+ this.define(`keys${block.id}`, `new Set()`);
3275
3742
  }
3276
3743
  this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
3277
3744
  this.target.indentLevel++;
@@ -3288,7 +3755,7 @@
3288
3755
  if (!ast.hasNoValue) {
3289
3756
  this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`);
3290
3757
  }
3291
- this.addLine(`let key${this.target.loopLevel} = ${ast.key ? compileExpr(ast.key) : loopVar};`);
3758
+ this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
3292
3759
  if (this.dev) {
3293
3760
  // Throw error on duplicate keys in dev mode
3294
3761
  this.addLine(`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new Error(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`);
@@ -3298,8 +3765,8 @@
3298
3765
  if (ast.memo) {
3299
3766
  this.target.hasCache = true;
3300
3767
  id = this.generateId();
3301
- this.addLine(`let memo${id} = ${compileExpr(ast.memo)}`);
3302
- this.addLine(`let vnode${id} = cache[key${this.target.loopLevel}];`);
3768
+ this.define(`memo${id}`, compileExpr(ast.memo));
3769
+ this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
3303
3770
  this.addLine(`if (vnode${id}) {`);
3304
3771
  this.target.indentLevel++;
3305
3772
  this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);
@@ -3327,7 +3794,7 @@
3327
3794
  }
3328
3795
  compileTKey(ast, ctx) {
3329
3796
  const tKeyExpr = this.generateId("tKey_");
3330
- this.addLine(`const ${tKeyExpr} = ${compileExpr(ast.expr)};`);
3797
+ this.define(tKeyExpr, compileExpr(ast.expr));
3331
3798
  ctx = createContext(ctx, {
3332
3799
  tKeyExpr,
3333
3800
  block: ctx.block,
@@ -3372,14 +3839,14 @@
3372
3839
  const children = block.children.slice();
3373
3840
  let current = children.shift();
3374
3841
  for (let i = codeIdx; i < code.length; i++) {
3375
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3376
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3842
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3843
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3377
3844
  current = children.shift();
3378
3845
  if (!current)
3379
3846
  break;
3380
3847
  }
3381
3848
  }
3382
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3849
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3383
3850
  }
3384
3851
  }
3385
3852
  const args = block.children.map((c) => c.varName).join(", ");
@@ -3410,7 +3877,7 @@
3410
3877
  const key = `key + \`${this.generateComponentKey()}\``;
3411
3878
  if (isDynamic) {
3412
3879
  const templateVar = this.generateId("template");
3413
- this.addLine(`const ${templateVar} = ${subTemplate};`);
3880
+ this.define(templateVar, subTemplate);
3414
3881
  block = this.createBlock(block, "multi", ctx);
3415
3882
  this.helpers.add("call");
3416
3883
  this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block, {
@@ -3421,7 +3888,7 @@
3421
3888
  else {
3422
3889
  const id = this.generateId(`callTemplate_`);
3423
3890
  this.helpers.add("getTemplate");
3424
- this.staticCalls.push({ id, template: subTemplate });
3891
+ this.staticDefs.push({ id, expr: `getTemplate(${subTemplate})` });
3425
3892
  block = this.createBlock(block, "multi", ctx);
3426
3893
  this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block, {
3427
3894
  ...ctx,
@@ -3515,27 +3982,29 @@
3515
3982
  compileComponent(ast, ctx) {
3516
3983
  let { block } = ctx;
3517
3984
  // props
3518
- const hasSlotsProp = "slots" in ast.props;
3985
+ const hasSlotsProp = "slots" in (ast.props || {});
3519
3986
  const props = [];
3520
- const propExpr = this.formatPropObject(ast.props);
3987
+ const propExpr = this.formatPropObject(ast.props || {});
3521
3988
  if (propExpr) {
3522
3989
  props.push(propExpr);
3523
3990
  }
3524
3991
  // slots
3525
- const hasSlot = !!Object.keys(ast.slots).length;
3526
3992
  let slotDef = "";
3527
- if (hasSlot) {
3993
+ if (ast.slots) {
3528
3994
  let ctxStr = "ctx";
3529
3995
  if (this.target.loopLevel || !this.hasSafeContext) {
3530
3996
  ctxStr = this.generateId("ctx");
3531
3997
  this.helpers.add("capture");
3532
- this.addLine(`const ${ctxStr} = capture(ctx);`);
3998
+ this.define(ctxStr, `capture(ctx)`);
3533
3999
  }
3534
4000
  let slotStr = [];
3535
4001
  for (let slotName in ast.slots) {
3536
- const slotAst = ast.slots[slotName].content;
3537
- const name = this.compileInNewTarget("slot", slotAst, ctx);
3538
- const params = [`__render: ${name}, __ctx: ${ctxStr}`];
4002
+ const slotAst = ast.slots[slotName];
4003
+ const params = [];
4004
+ if (slotAst.content) {
4005
+ const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
4006
+ params.push(`__render: ${name}, __ctx: ${ctxStr}`);
4007
+ }
3539
4008
  const scope = ast.slots[slotName].scope;
3540
4009
  if (scope) {
3541
4010
  params.push(`__scope: "${scope}"`);
@@ -3549,33 +4018,30 @@
3549
4018
  slotDef = `{${slotStr.join(", ")}}`;
3550
4019
  }
3551
4020
  if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
3552
- props.push(`slots: ${slotDef}`);
4021
+ this.helpers.add("markRaw");
4022
+ props.push(`slots: markRaw(${slotDef})`);
3553
4023
  }
3554
4024
  const propStr = `{${props.join(",")}}`;
3555
4025
  let propString = propStr;
3556
4026
  if (ast.dynamicProps) {
3557
- if (!props.length) {
3558
- propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`;
3559
- }
3560
- else {
3561
- propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
3562
- }
4027
+ propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${props.length ? ", " + propStr : ""})`;
3563
4028
  }
3564
4029
  let propVar;
3565
4030
  if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
3566
4031
  propVar = this.generateId("props");
3567
- this.addLine(`const ${propVar} = ${propString};`);
4032
+ this.define(propVar, propString);
3568
4033
  propString = propVar;
3569
4034
  }
3570
4035
  if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
3571
- this.addLine(`${propVar}.slots = Object.assign(${slotDef}, ${propVar}.slots)`);
4036
+ this.helpers.add("markRaw");
4037
+ this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
3572
4038
  }
3573
4039
  // cmap key
3574
4040
  const key = this.generateComponentKey();
3575
4041
  let expr;
3576
4042
  if (ast.isDynamic) {
3577
4043
  expr = this.generateId("Comp");
3578
- this.addLine(`let ${expr} = ${compileExpr(ast.name)};`);
4044
+ this.define(expr, compileExpr(ast.name));
3579
4045
  }
3580
4046
  else {
3581
4047
  expr = `\`${ast.name}\``;
@@ -3596,9 +4062,28 @@
3596
4062
  if (ast.isDynamic) {
3597
4063
  blockExpr = `toggler(${expr}, ${blockExpr})`;
3598
4064
  }
4065
+ // event handling
4066
+ if (ast.on) {
4067
+ blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);
4068
+ }
3599
4069
  block = this.createBlock(block, "multi", ctx);
3600
4070
  this.insertBlock(blockExpr, block, ctx);
3601
4071
  }
4072
+ wrapWithEventCatcher(expr, on) {
4073
+ this.helpers.add("createCatcher");
4074
+ let name = this.generateId("catcher");
4075
+ let spec = {};
4076
+ let handlers = [];
4077
+ for (let ev in on) {
4078
+ let handlerId = this.generateId("hdlr");
4079
+ let idx = handlers.push(handlerId) - 1;
4080
+ spec[ev] = idx;
4081
+ const handler = this.generateHandlerCode(ev, on[ev]);
4082
+ this.define(handlerId, handler);
4083
+ }
4084
+ this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });
4085
+ return `${name}(${expr}, [${handlers.join(",")}])`;
4086
+ }
3602
4087
  compileTSlot(ast, ctx) {
3603
4088
  this.helpers.add("callSlot");
3604
4089
  let { block } = ctx;
@@ -3620,13 +4105,17 @@
3620
4105
  else {
3621
4106
  if (dynamic) {
3622
4107
  let name = this.generateId("slot");
3623
- this.addLine(`const ${name} = ${slotName};`);
3624
- blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}), ${dynamic}, ${scope})`;
4108
+ this.define(name, slotName);
4109
+ blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}, ${dynamic}, ${scope}))`;
3625
4110
  }
3626
4111
  else {
3627
4112
  blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
3628
4113
  }
3629
4114
  }
4115
+ // event handling
4116
+ if (ast.on) {
4117
+ blockString = this.wrapWithEventCatcher(blockString, ast.on);
4118
+ }
3630
4119
  if (block) {
3631
4120
  this.insertAnchor(block);
3632
4121
  }
@@ -3647,7 +4136,7 @@
3647
4136
  if (this.target.loopLevel || !this.hasSafeContext) {
3648
4137
  ctxStr = this.generateId("ctx");
3649
4138
  this.helpers.add("capture");
3650
- this.addLine(`const ${ctxStr} = capture(ctx);`);
4139
+ this.define(ctxStr, `capture(ctx);`);
3651
4140
  }
3652
4141
  const blockString = `component(Portal, {target: ${ast.target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
3653
4142
  if (block) {
@@ -3768,6 +4257,9 @@
3768
4257
  if (tagName === "t" && !dynamicTag) {
3769
4258
  return null;
3770
4259
  }
4260
+ if (tagName.startsWith("block-")) {
4261
+ throw new Error(`Invalid tag name: '${tagName}'`);
4262
+ }
3771
4263
  ctx = Object.assign({}, ctx);
3772
4264
  if (tagName === "pre") {
3773
4265
  ctx.inPreTag = true;
@@ -3778,8 +4270,8 @@
3778
4270
  const ref = node.getAttribute("t-ref");
3779
4271
  node.removeAttribute("t-ref");
3780
4272
  const nodeAttrsNames = node.getAttributeNames();
3781
- const attrs = {};
3782
- const on = {};
4273
+ let attrs = null;
4274
+ let on = null;
3783
4275
  let model = null;
3784
4276
  for (let attr of nodeAttrsNames) {
3785
4277
  const value = node.getAttribute(attr);
@@ -3787,6 +4279,7 @@
3787
4279
  if (attr === "t-on") {
3788
4280
  throw new Error("Missing event name with t-on directive");
3789
4281
  }
4282
+ on = on || {};
3790
4283
  on[attr.slice(5)] = value;
3791
4284
  }
3792
4285
  else if (attr.startsWith("t-model")) {
@@ -3824,6 +4317,7 @@
3824
4317
  targetAttr: isCheckboxInput ? "checked" : "value",
3825
4318
  specialInitTargetAttr: isRadioInput ? "checked" : null,
3826
4319
  eventType,
4320
+ hasDynamicChildren: false,
3827
4321
  shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
3828
4322
  shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
3829
4323
  };
@@ -3833,6 +4327,9 @@
3833
4327
  ctx.tModelInfo = model;
3834
4328
  }
3835
4329
  }
4330
+ else if (attr.startsWith("block-")) {
4331
+ throw new Error(`Invalid attribute: '${attr}'`);
4332
+ }
3836
4333
  else if (attr !== "t-name") {
3837
4334
  if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
3838
4335
  throw new Error(`Unknown QWeb directive: '${attr}'`);
@@ -3841,6 +4338,7 @@
3841
4338
  if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
3842
4339
  tModel.hasDynamicChildren = true;
3843
4340
  }
4341
+ attrs = attrs || {};
3844
4342
  attrs[attr] = value;
3845
4343
  }
3846
4344
  }
@@ -3991,7 +4489,7 @@
3991
4489
  if (ast && ast.type === 11 /* TComponent */) {
3992
4490
  return {
3993
4491
  ...ast,
3994
- slots: { default: { content: tcall } },
4492
+ slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
3995
4493
  };
3996
4494
  }
3997
4495
  }
@@ -4075,7 +4573,6 @@
4075
4573
  // -----------------------------------------------------------------------------
4076
4574
  // Error messages when trying to use an unsupported directive on a component
4077
4575
  const directiveErrorMap = new Map([
4078
- ["t-on", "t-on is no longer supported on components. Consider passing a callback in props."],
4079
4576
  [
4080
4577
  "t-ref",
4081
4578
  "t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
@@ -4104,18 +4601,26 @@
4104
4601
  node.removeAttribute("t-props");
4105
4602
  const defaultSlotScope = node.getAttribute("t-slot-scope");
4106
4603
  node.removeAttribute("t-slot-scope");
4107
- const props = {};
4604
+ let on = null;
4605
+ let props = null;
4108
4606
  for (let name of node.getAttributeNames()) {
4109
4607
  const value = node.getAttribute(name);
4110
4608
  if (name.startsWith("t-")) {
4111
- const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
4112
- throw new Error(message || `unsupported directive on Component: ${name}`);
4609
+ if (name.startsWith("t-on-")) {
4610
+ on = on || {};
4611
+ on[name.slice(5)] = value;
4612
+ }
4613
+ else {
4614
+ const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
4615
+ throw new Error(message || `unsupported directive on Component: ${name}`);
4616
+ }
4113
4617
  }
4114
4618
  else {
4619
+ props = props || {};
4115
4620
  props[name] = value;
4116
4621
  }
4117
4622
  }
4118
- const slots = {};
4623
+ let slots = null;
4119
4624
  if (node.hasChildNodes()) {
4120
4625
  const clone = node.cloneNode(true);
4121
4626
  // named slots
@@ -4142,33 +4647,35 @@
4142
4647
  slotNode.removeAttribute("t-set-slot");
4143
4648
  slotNode.remove();
4144
4649
  const slotAst = parseNode(slotNode, ctx);
4145
- if (slotAst) {
4146
- const slotInfo = { content: slotAst };
4147
- const attrs = {};
4148
- for (let attributeName of slotNode.getAttributeNames()) {
4149
- const value = slotNode.getAttribute(attributeName);
4150
- if (attributeName === "t-slot-scope") {
4151
- slotInfo.scope = value;
4152
- continue;
4153
- }
4154
- attrs[attributeName] = value;
4650
+ let on = null;
4651
+ let attrs = null;
4652
+ let scope = null;
4653
+ for (let attributeName of slotNode.getAttributeNames()) {
4654
+ const value = slotNode.getAttribute(attributeName);
4655
+ if (attributeName === "t-slot-scope") {
4656
+ scope = value;
4657
+ continue;
4155
4658
  }
4156
- if (Object.keys(attrs).length) {
4157
- slotInfo.attrs = attrs;
4659
+ else if (attributeName.startsWith("t-on-")) {
4660
+ on = on || {};
4661
+ on[attributeName.slice(5)] = value;
4662
+ }
4663
+ else {
4664
+ attrs = attrs || {};
4665
+ attrs[attributeName] = value;
4158
4666
  }
4159
- slots[name] = slotInfo;
4160
4667
  }
4668
+ slots = slots || {};
4669
+ slots[name] = { content: slotAst, on, attrs, scope };
4161
4670
  }
4162
4671
  // default slot
4163
4672
  const defaultContent = parseChildNodes(clone, ctx);
4164
4673
  if (defaultContent) {
4165
- slots.default = { content: defaultContent };
4166
- if (defaultSlotScope) {
4167
- slots.default.scope = defaultSlotScope;
4168
- }
4674
+ slots = slots || {};
4675
+ slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
4169
4676
  }
4170
4677
  }
4171
- return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots };
4678
+ return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
4172
4679
  }
4173
4680
  // -----------------------------------------------------------------------------
4174
4681
  // Slots
@@ -4179,15 +4686,24 @@
4179
4686
  }
4180
4687
  const name = node.getAttribute("t-slot");
4181
4688
  node.removeAttribute("t-slot");
4182
- const attrs = {};
4689
+ let attrs = null;
4690
+ let on = null;
4183
4691
  for (let attributeName of node.getAttributeNames()) {
4184
4692
  const value = node.getAttribute(attributeName);
4185
- attrs[attributeName] = value;
4693
+ if (attributeName.startsWith("t-on-")) {
4694
+ on = on || {};
4695
+ on[attributeName.slice(5)] = value;
4696
+ }
4697
+ else {
4698
+ attrs = attrs || {};
4699
+ attrs[attributeName] = value;
4700
+ }
4186
4701
  }
4187
4702
  return {
4188
4703
  type: 14 /* TSlot */,
4189
4704
  name,
4190
4705
  attrs,
4706
+ on,
4191
4707
  defaultContent: parseChildNodes(node, ctx),
4192
4708
  };
4193
4709
  }
@@ -4381,108 +4897,112 @@
4381
4897
  return new Function("bdom, helpers", code);
4382
4898
  }
4383
4899
 
4384
- const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
4385
- const globalTemplates = {};
4386
- function parseXML(xml) {
4387
- const parser = new DOMParser();
4388
- const doc = parser.parseFromString(xml, "text/xml");
4389
- if (doc.getElementsByTagName("parsererror").length) {
4390
- let msg = "Invalid XML in template.";
4391
- const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
4392
- if (parsererrorText) {
4393
- msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
4394
- const re = /\d+/g;
4395
- const firstMatch = re.exec(parsererrorText);
4396
- if (firstMatch) {
4397
- const lineNumber = Number(firstMatch[0]);
4398
- const line = xml.split("\n")[lineNumber - 1];
4399
- const secondMatch = re.exec(parsererrorText);
4400
- if (line && secondMatch) {
4401
- const columnIndex = Number(secondMatch[0]) - 1;
4402
- if (line[columnIndex]) {
4403
- msg +=
4404
- `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
4405
- `${line}\n${"-".repeat(columnIndex - 1)}^`;
4406
- }
4900
+ const TIMEOUT = Symbol("timeout");
4901
+ function wrapError(fn, hookName) {
4902
+ const error = new Error(`The following error occurred in ${hookName}: `);
4903
+ const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
4904
+ const node = getCurrent();
4905
+ return (...args) => {
4906
+ try {
4907
+ const result = fn(...args);
4908
+ if (result instanceof Promise) {
4909
+ if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
4910
+ const fiber = node.fiber;
4911
+ Promise.race([
4912
+ result,
4913
+ new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
4914
+ ]).then((res) => {
4915
+ if (res === TIMEOUT && node.fiber === fiber) {
4916
+ console.warn(timeoutError);
4917
+ }
4918
+ });
4407
4919
  }
4920
+ return result.catch((cause) => {
4921
+ error.cause = cause;
4922
+ if (cause instanceof Error) {
4923
+ error.message += `"${cause.message}"`;
4924
+ }
4925
+ throw error;
4926
+ });
4408
4927
  }
4928
+ return result;
4409
4929
  }
4410
- throw new Error(msg);
4411
- }
4412
- return doc;
4413
- }
4414
- class TemplateSet {
4415
- constructor(config = {}) {
4416
- this.rawTemplates = Object.create(globalTemplates);
4417
- this.templates = {};
4418
- this.utils = Object.assign({}, UTILS, {
4419
- call: (owner, subTemplate, ctx, parent, key) => {
4420
- const template = this.getTemplate(subTemplate);
4421
- return toggler(subTemplate, template.call(owner, ctx, parent, key));
4422
- },
4423
- getTemplate: (name) => this.getTemplate(name),
4424
- });
4425
- this.dev = config.dev || false;
4426
- this.translateFn = config.translateFn;
4427
- this.translatableAttributes = config.translatableAttributes;
4428
- if (config.templates) {
4429
- this.addTemplates(config.templates);
4430
- }
4431
- }
4432
- addTemplate(name, template, options = {}) {
4433
- if (name in this.rawTemplates && !options.allowDuplicate) {
4434
- throw new Error(`Template ${name} already defined`);
4435
- }
4436
- this.rawTemplates[name] = template;
4437
- }
4438
- addTemplates(xml, options = {}) {
4439
- if (!xml) {
4440
- // empty string
4441
- return;
4442
- }
4443
- xml = xml instanceof Document ? xml : parseXML(xml);
4444
- for (const template of xml.querySelectorAll("[t-name]")) {
4445
- const name = template.getAttribute("t-name");
4446
- this.addTemplate(name, template, options);
4447
- }
4448
- }
4449
- getTemplate(name) {
4450
- if (!(name in this.templates)) {
4451
- const rawTemplate = this.rawTemplates[name];
4452
- if (rawTemplate === undefined) {
4453
- throw new Error(`Missing template: "${name}"`);
4930
+ catch (cause) {
4931
+ if (cause instanceof Error) {
4932
+ error.message += `"${cause.message}"`;
4454
4933
  }
4455
- const templateFn = this._compileTemplate(name, rawTemplate);
4456
- // first add a function to lazily get the template, in case there is a
4457
- // recursive call to the template name
4458
- const templates = this.templates;
4459
- this.templates[name] = function (context, parent) {
4460
- return templates[name].call(this, context, parent);
4461
- };
4462
- const template = templateFn(bdom, this.utils);
4463
- this.templates[name] = template;
4934
+ throw error;
4464
4935
  }
4465
- return this.templates[name];
4466
- }
4467
- _compileTemplate(name, template) {
4468
- return compile(template, {
4469
- name,
4470
- dev: this.dev,
4471
- translateFn: this.translateFn,
4472
- translatableAttributes: this.translatableAttributes,
4473
- });
4474
- }
4936
+ };
4475
4937
  }
4476
4938
  // -----------------------------------------------------------------------------
4477
- // xml tag helper
4939
+ // hooks
4478
4940
  // -----------------------------------------------------------------------------
4479
- function xml(...args) {
4480
- const name = `__template__${xml.nextId++}`;
4481
- const value = String.raw(...args);
4482
- globalTemplates[name] = value;
4483
- return name;
4941
+ function onWillStart(fn) {
4942
+ const node = getCurrent();
4943
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4944
+ node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
4484
4945
  }
4485
- xml.nextId = 1;
4946
+ function onWillUpdateProps(fn) {
4947
+ const node = getCurrent();
4948
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4949
+ node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
4950
+ }
4951
+ function onMounted(fn) {
4952
+ const node = getCurrent();
4953
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4954
+ node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
4955
+ }
4956
+ function onWillPatch(fn) {
4957
+ const node = getCurrent();
4958
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4959
+ node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
4960
+ }
4961
+ function onPatched(fn) {
4962
+ const node = getCurrent();
4963
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4964
+ node.patched.push(decorate(fn.bind(node.component), "onPatched"));
4965
+ }
4966
+ function onWillUnmount(fn) {
4967
+ const node = getCurrent();
4968
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4969
+ node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
4970
+ }
4971
+ function onWillDestroy(fn) {
4972
+ const node = getCurrent();
4973
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4974
+ node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
4975
+ }
4976
+ function onWillRender(fn) {
4977
+ const node = getCurrent();
4978
+ const renderFn = node.renderFn;
4979
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4980
+ fn = decorate(fn.bind(node.component), "onWillRender");
4981
+ node.renderFn = () => {
4982
+ fn();
4983
+ return renderFn();
4984
+ };
4985
+ }
4986
+ function onRendered(fn) {
4987
+ const node = getCurrent();
4988
+ const renderFn = node.renderFn;
4989
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4990
+ fn = decorate(fn.bind(node.component), "onRendered");
4991
+ node.renderFn = () => {
4992
+ const result = renderFn();
4993
+ fn();
4994
+ return result;
4995
+ };
4996
+ }
4997
+ function onError(callback) {
4998
+ const node = getCurrent();
4999
+ let handlers = nodeErrorHandlers.get(node);
5000
+ if (!handlers) {
5001
+ handlers = [];
5002
+ nodeErrorHandlers.set(node, handlers);
5003
+ }
5004
+ handlers.push(callback.bind(node.component));
5005
+ }
4486
5006
 
4487
5007
  class Component {
4488
5008
  constructor(props, env, node) {
@@ -4491,8 +5011,8 @@
4491
5011
  this.__owl__ = node;
4492
5012
  }
4493
5013
  setup() { }
4494
- render() {
4495
- this.__owl__.render();
5014
+ render(deep = false) {
5015
+ this.__owl__.render(deep);
4496
5016
  }
4497
5017
  }
4498
5018
  Component.template = "";
@@ -4561,75 +5081,293 @@
4561
5081
  slots: true,
4562
5082
  };
4563
5083
 
4564
- // -----------------------------------------------------------------------------
4565
- // Scheduler
4566
- // -----------------------------------------------------------------------------
4567
- class Scheduler {
4568
- constructor() {
4569
- this.tasks = new Set();
4570
- this.isRunning = false;
4571
- this.requestAnimationFrame = Scheduler.requestAnimationFrame;
5084
+ /**
5085
+ * This file contains utility functions that will be injected in each template,
5086
+ * to perform various useful tasks in the compiled code.
5087
+ */
5088
+ function withDefault(value, defaultValue) {
5089
+ return value === undefined || value === null || value === false ? defaultValue : value;
5090
+ }
5091
+ function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
5092
+ key = key + "__slot_" + name;
5093
+ const slots = ctx.props[TARGET].slots || {};
5094
+ const { __render, __ctx, __scope } = slots[name] || {};
5095
+ const slotScope = Object.create(__ctx || {});
5096
+ if (__scope) {
5097
+ slotScope[__scope] = extra;
5098
+ }
5099
+ const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
5100
+ if (defaultContent) {
5101
+ let child1 = undefined;
5102
+ let child2 = undefined;
5103
+ if (slotBDom) {
5104
+ child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
5105
+ }
5106
+ else {
5107
+ child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
5108
+ }
5109
+ return multi([child1, child2]);
5110
+ }
5111
+ return slotBDom || text("");
5112
+ }
5113
+ function capture(ctx) {
5114
+ const component = ctx.__owl__.component;
5115
+ const result = Object.create(component);
5116
+ for (let k in ctx) {
5117
+ result[k] = ctx[k];
5118
+ }
5119
+ return result;
5120
+ }
5121
+ function withKey(elem, k) {
5122
+ elem.key = k;
5123
+ return elem;
5124
+ }
5125
+ function prepareList(collection) {
5126
+ let keys;
5127
+ let values;
5128
+ if (Array.isArray(collection)) {
5129
+ keys = collection;
5130
+ values = collection;
5131
+ }
5132
+ else if (collection) {
5133
+ values = Object.keys(collection);
5134
+ keys = Object.values(collection);
4572
5135
  }
4573
- start() {
4574
- this.isRunning = true;
4575
- this.scheduleTasks();
5136
+ else {
5137
+ throw new Error("Invalid loop expression");
5138
+ }
5139
+ const n = values.length;
5140
+ return [keys, values, n, new Array(n)];
5141
+ }
5142
+ const isBoundary = Symbol("isBoundary");
5143
+ function setContextValue(ctx, key, value) {
5144
+ const ctx0 = ctx;
5145
+ while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
5146
+ const newCtx = ctx.__proto__;
5147
+ if (!newCtx) {
5148
+ ctx = ctx0;
5149
+ break;
5150
+ }
5151
+ ctx = newCtx;
5152
+ }
5153
+ ctx[key] = value;
5154
+ }
5155
+ function toNumber(val) {
5156
+ const n = parseFloat(val);
5157
+ return isNaN(n) ? val : n;
5158
+ }
5159
+ function shallowEqual(l1, l2) {
5160
+ for (let i = 0, l = l1.length; i < l; i++) {
5161
+ if (l1[i] !== l2[i]) {
5162
+ return false;
5163
+ }
5164
+ }
5165
+ return true;
5166
+ }
5167
+ class LazyValue {
5168
+ constructor(fn, ctx, node) {
5169
+ this.fn = fn;
5170
+ this.ctx = capture(ctx);
5171
+ this.node = node;
5172
+ }
5173
+ evaluate() {
5174
+ return this.fn(this.ctx, this.node);
5175
+ }
5176
+ toString() {
5177
+ return this.evaluate().toString();
5178
+ }
5179
+ }
5180
+ /*
5181
+ * Safely outputs `value` as a block depending on the nature of `value`
5182
+ */
5183
+ function safeOutput(value) {
5184
+ if (!value) {
5185
+ return value;
5186
+ }
5187
+ let safeKey;
5188
+ let block;
5189
+ if (value instanceof Markup) {
5190
+ safeKey = `string_safe`;
5191
+ block = html(value);
5192
+ }
5193
+ else if (value instanceof LazyValue) {
5194
+ safeKey = `lazy_value`;
5195
+ block = value.evaluate();
5196
+ }
5197
+ else if (value instanceof String || typeof value === "string") {
5198
+ safeKey = "string_unsafe";
5199
+ block = text(value);
5200
+ }
5201
+ else {
5202
+ // Assuming it is a block
5203
+ safeKey = "block_safe";
5204
+ block = value;
5205
+ }
5206
+ return toggler(safeKey, block);
5207
+ }
5208
+ let boundFunctions = new WeakMap();
5209
+ function bind(ctx, fn) {
5210
+ let component = ctx.__owl__.component;
5211
+ let boundFnMap = boundFunctions.get(component);
5212
+ if (!boundFnMap) {
5213
+ boundFnMap = new WeakMap();
5214
+ boundFunctions.set(component, boundFnMap);
5215
+ }
5216
+ let boundFn = boundFnMap.get(fn);
5217
+ if (!boundFn) {
5218
+ boundFn = fn.bind(component);
5219
+ boundFnMap.set(fn, boundFn);
5220
+ }
5221
+ return boundFn;
5222
+ }
5223
+ function multiRefSetter(refs, name) {
5224
+ let count = 0;
5225
+ return (el) => {
5226
+ if (el) {
5227
+ count++;
5228
+ if (count > 1) {
5229
+ throw new Error("Cannot have 2 elements with same ref name at the same time");
5230
+ }
5231
+ }
5232
+ if (count === 0 || el) {
5233
+ refs[name] = el;
5234
+ }
5235
+ };
5236
+ }
5237
+ const helpers = {
5238
+ withDefault,
5239
+ zero: Symbol("zero"),
5240
+ isBoundary,
5241
+ callSlot,
5242
+ capture,
5243
+ withKey,
5244
+ prepareList,
5245
+ setContextValue,
5246
+ multiRefSetter,
5247
+ shallowEqual,
5248
+ toNumber,
5249
+ validateProps,
5250
+ LazyValue,
5251
+ safeOutput,
5252
+ bind,
5253
+ createCatcher,
5254
+ };
5255
+
5256
+ const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
5257
+ function parseXML(xml) {
5258
+ const parser = new DOMParser();
5259
+ const doc = parser.parseFromString(xml, "text/xml");
5260
+ if (doc.getElementsByTagName("parsererror").length) {
5261
+ let msg = "Invalid XML in template.";
5262
+ const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
5263
+ if (parsererrorText) {
5264
+ msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
5265
+ const re = /\d+/g;
5266
+ const firstMatch = re.exec(parsererrorText);
5267
+ if (firstMatch) {
5268
+ const lineNumber = Number(firstMatch[0]);
5269
+ const line = xml.split("\n")[lineNumber - 1];
5270
+ const secondMatch = re.exec(parsererrorText);
5271
+ if (line && secondMatch) {
5272
+ const columnIndex = Number(secondMatch[0]) - 1;
5273
+ if (line[columnIndex]) {
5274
+ msg +=
5275
+ `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
5276
+ `${line}\n${"-".repeat(columnIndex - 1)}^`;
5277
+ }
5278
+ }
5279
+ }
5280
+ }
5281
+ throw new Error(msg);
5282
+ }
5283
+ return doc;
5284
+ }
5285
+ /**
5286
+ * Returns the helpers object that will be injected in each template closure
5287
+ * function
5288
+ */
5289
+ function makeHelpers(getTemplate) {
5290
+ return Object.assign({}, helpers, {
5291
+ Portal,
5292
+ markRaw,
5293
+ getTemplate,
5294
+ call: (owner, subTemplate, ctx, parent, key) => {
5295
+ const template = getTemplate(subTemplate);
5296
+ return toggler(subTemplate, template.call(owner, ctx, parent, key));
5297
+ },
5298
+ });
5299
+ }
5300
+ class TemplateSet {
5301
+ constructor(config = {}) {
5302
+ this.rawTemplates = Object.create(globalTemplates);
5303
+ this.templates = {};
5304
+ this.dev = config.dev || false;
5305
+ this.translateFn = config.translateFn;
5306
+ this.translatableAttributes = config.translatableAttributes;
5307
+ if (config.templates) {
5308
+ this.addTemplates(config.templates);
5309
+ }
5310
+ this.helpers = makeHelpers(this.getTemplate.bind(this));
4576
5311
  }
4577
- stop() {
4578
- this.isRunning = false;
5312
+ addTemplate(name, template, options = {}) {
5313
+ if (name in this.rawTemplates && !options.allowDuplicate) {
5314
+ throw new Error(`Template ${name} already defined`);
5315
+ }
5316
+ this.rawTemplates[name] = template;
4579
5317
  }
4580
- addFiber(fiber) {
4581
- this.tasks.add(fiber.root);
4582
- if (!this.isRunning) {
4583
- this.start();
5318
+ addTemplates(xml, options = {}) {
5319
+ if (!xml) {
5320
+ // empty string
5321
+ return;
5322
+ }
5323
+ xml = xml instanceof Document ? xml : parseXML(xml);
5324
+ for (const template of xml.querySelectorAll("[t-name]")) {
5325
+ const name = template.getAttribute("t-name");
5326
+ this.addTemplate(name, template, options);
4584
5327
  }
4585
5328
  }
4586
- /**
4587
- * Process all current tasks. This only applies to the fibers that are ready.
4588
- * Other tasks are left unchanged.
4589
- */
4590
- flush() {
4591
- this.tasks.forEach((fiber) => {
4592
- if (fiber.root !== fiber) {
4593
- this.tasks.delete(fiber);
4594
- return;
4595
- }
4596
- const hasError = fibersInError.has(fiber);
4597
- if (hasError && fiber.counter !== 0) {
4598
- this.tasks.delete(fiber);
4599
- return;
4600
- }
4601
- if (fiber.node.status === 2 /* DESTROYED */) {
4602
- this.tasks.delete(fiber);
4603
- return;
4604
- }
4605
- if (fiber.counter === 0) {
4606
- if (!hasError) {
4607
- fiber.complete();
5329
+ getTemplate(name) {
5330
+ if (!(name in this.templates)) {
5331
+ const rawTemplate = this.rawTemplates[name];
5332
+ if (rawTemplate === undefined) {
5333
+ let extraInfo = "";
5334
+ try {
5335
+ const componentName = getCurrent().component.constructor.name;
5336
+ extraInfo = ` (for component "${componentName}")`;
4608
5337
  }
4609
- this.tasks.delete(fiber);
5338
+ catch { }
5339
+ throw new Error(`Missing template: "${name}"${extraInfo}`);
4610
5340
  }
4611
- });
4612
- if (this.tasks.size === 0) {
4613
- this.stop();
5341
+ const templateFn = this._compileTemplate(name, rawTemplate);
5342
+ // first add a function to lazily get the template, in case there is a
5343
+ // recursive call to the template name
5344
+ const templates = this.templates;
5345
+ this.templates[name] = function (context, parent) {
5346
+ return templates[name].call(this, context, parent);
5347
+ };
5348
+ const template = templateFn(bdom, this.helpers);
5349
+ this.templates[name] = template;
4614
5350
  }
5351
+ return this.templates[name];
4615
5352
  }
4616
- scheduleTasks() {
4617
- this.requestAnimationFrame(() => {
4618
- this.flush();
4619
- if (this.isRunning) {
4620
- this.scheduleTasks();
4621
- }
5353
+ _compileTemplate(name, template) {
5354
+ return compile(template, {
5355
+ name,
5356
+ dev: this.dev,
5357
+ translateFn: this.translateFn,
5358
+ translatableAttributes: this.translatableAttributes,
4622
5359
  });
4623
5360
  }
4624
- }
4625
- // capture the value of requestAnimationFrame as soon as possible, to avoid
4626
- // interactions with other code, such as test frameworks that override them
4627
- Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
5361
+ }
4628
5362
 
4629
- const DEV_MSG = `Owl is running in 'dev' mode.
5363
+ let hasBeenLogged = false;
5364
+ const DEV_MSG = () => {
5365
+ const hash = window.owl ? window.owl.__info__.hash : "master";
5366
+ return `Owl is running in 'dev' mode.
4630
5367
 
4631
5368
  This is not suitable for production use.
4632
- See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
5369
+ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
5370
+ };
4633
5371
  class App extends TemplateSet {
4634
5372
  constructor(Root, config = {}) {
4635
5373
  super(config);
@@ -4639,11 +5377,13 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
4639
5377
  if (config.test) {
4640
5378
  this.dev = true;
4641
5379
  }
4642
- if (this.dev && !config.test) {
4643
- console.info(DEV_MSG);
5380
+ if (this.dev && !config.test && !hasBeenLogged) {
5381
+ console.info(DEV_MSG());
5382
+ hasBeenLogged = true;
4644
5383
  }
4645
- const descrs = Object.getOwnPropertyDescriptors(config.env || {});
4646
- this.env = Object.freeze(Object.defineProperties({}, descrs));
5384
+ const env = config.env || {};
5385
+ const descrs = Object.getOwnPropertyDescriptors(env);
5386
+ this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
4647
5387
  this.props = config.props || {};
4648
5388
  }
4649
5389
  mount(target, options) {
@@ -4654,7 +5394,7 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
4654
5394
  return prom;
4655
5395
  }
4656
5396
  makeNode(Component, props) {
4657
- return new ComponentNode(Component, props, this);
5397
+ return new ComponentNode(Component, props, this, null, null);
4658
5398
  }
4659
5399
  mountNode(node, target, options) {
4660
5400
  const promise = new Promise((resolve, reject) => {
@@ -4686,6 +5426,7 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
4686
5426
  }
4687
5427
  destroy() {
4688
5428
  if (this.root) {
5429
+ this.scheduler.flush();
4689
5430
  this.root.destroy();
4690
5431
  }
4691
5432
  }
@@ -4706,270 +5447,6 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
4706
5447
  }
4707
5448
  }
4708
5449
 
4709
- class Memo extends Component {
4710
- constructor(props, env, node) {
4711
- super(props, env, node);
4712
- // prevent patching process conditionally
4713
- let applyPatch = false;
4714
- const patchFn = node.patch;
4715
- node.patch = () => {
4716
- if (applyPatch) {
4717
- patchFn.call(node);
4718
- applyPatch = false;
4719
- }
4720
- };
4721
- // check props change, and render/apply patch if it changed
4722
- let prevProps = props;
4723
- const updateAndRender = node.updateAndRender;
4724
- node.updateAndRender = function (props, parentFiber) {
4725
- const shouldUpdate = !shallowEqual(prevProps, props);
4726
- if (shouldUpdate) {
4727
- prevProps = props;
4728
- updateAndRender.call(node, props, parentFiber);
4729
- applyPatch = true;
4730
- }
4731
- return Promise.resolve();
4732
- };
4733
- }
4734
- }
4735
- Memo.template = xml `<t t-slot="default"/>`;
4736
- /**
4737
- * we assume that each object have the same set of keys
4738
- */
4739
- function shallowEqual(p1, p2) {
4740
- for (let k in p1) {
4741
- if (k !== "slots" && p1[k] !== p2[k]) {
4742
- return false;
4743
- }
4744
- }
4745
- return true;
4746
- }
4747
-
4748
- // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
4749
- const TARGET = Symbol("Target");
4750
- // Escape hatch to prevent reactivity system to turn something into a reactive
4751
- const SKIP = Symbol("Skip");
4752
- // Special key to subscribe to, to be notified of key creation/deletion
4753
- const KEYCHANGES = Symbol("Key changes");
4754
- const objectToString = Object.prototype.toString;
4755
- /**
4756
- * Checks whether a given value can be made into a reactive object.
4757
- *
4758
- * @param value the value to check
4759
- * @returns whether the value can be made reactive
4760
- */
4761
- function canBeMadeReactive(value) {
4762
- if (typeof value !== "object") {
4763
- return false;
4764
- }
4765
- // extract "RawType" from strings like "[object RawType]" => this lets us
4766
- // ignore many native objects such as Promise (whose toString is [object Promise])
4767
- // or Date ([object Date]).
4768
- const rawType = objectToString.call(value).slice(8, -1);
4769
- return rawType === "Object" || rawType === "Array";
4770
- }
4771
- /**
4772
- * Mark an object or array so that it is ignored by the reactivity system
4773
- *
4774
- * @param value the value to mark
4775
- * @returns the object itself
4776
- */
4777
- function markRaw(value) {
4778
- value[SKIP] = true;
4779
- return value;
4780
- }
4781
- /**
4782
- * Given a reactive objet, return the raw (non reactive) underlying object
4783
- *
4784
- * @param value a reactive value
4785
- * @returns the underlying value
4786
- */
4787
- function toRaw(value) {
4788
- return value[TARGET];
4789
- }
4790
- const targetToKeysToCallbacks = new WeakMap();
4791
- /**
4792
- * Observes a given key on a target with an callback. The callback will be
4793
- * called when the given key changes on the target.
4794
- *
4795
- * @param target the target whose key should be observed
4796
- * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
4797
- * or deletion)
4798
- * @param callback the function to call when the key changes
4799
- */
4800
- function observeTargetKey(target, key, callback) {
4801
- if (!targetToKeysToCallbacks.get(target)) {
4802
- targetToKeysToCallbacks.set(target, new Map());
4803
- }
4804
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
4805
- if (!keyToCallbacks.get(key)) {
4806
- keyToCallbacks.set(key, new Set());
4807
- }
4808
- keyToCallbacks.get(key).add(callback);
4809
- if (!callbacksToTargets.has(callback)) {
4810
- callbacksToTargets.set(callback, new Set());
4811
- }
4812
- callbacksToTargets.get(callback).add(target);
4813
- }
4814
- /**
4815
- * Notify Reactives that are observing a given target that a key has changed on
4816
- * the target.
4817
- *
4818
- * @param target target whose Reactives should be notified that the target was
4819
- * changed.
4820
- * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
4821
- * or deleted)
4822
- */
4823
- function notifyReactives(target, key) {
4824
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
4825
- if (!keyToCallbacks) {
4826
- return;
4827
- }
4828
- const callbacks = keyToCallbacks.get(key);
4829
- if (!callbacks) {
4830
- return;
4831
- }
4832
- // Loop on copy because clearReactivesForCallback will modify the set in place
4833
- for (const callback of [...callbacks]) {
4834
- clearReactivesForCallback(callback);
4835
- callback();
4836
- }
4837
- }
4838
- const callbacksToTargets = new WeakMap();
4839
- /**
4840
- * Clears all subscriptions of the Reactives associated with a given callback.
4841
- *
4842
- * @param callback the callback for which the reactives need to be cleared
4843
- */
4844
- function clearReactivesForCallback(callback) {
4845
- const targetsToClear = callbacksToTargets.get(callback);
4846
- if (!targetsToClear) {
4847
- return;
4848
- }
4849
- for (const target of targetsToClear) {
4850
- const observedKeys = targetToKeysToCallbacks.get(target);
4851
- if (!observedKeys) {
4852
- continue;
4853
- }
4854
- for (const callbacks of observedKeys.values()) {
4855
- callbacks.delete(callback);
4856
- }
4857
- }
4858
- targetsToClear.clear();
4859
- }
4860
- const reactiveCache = new WeakMap();
4861
- /**
4862
- * Creates a reactive proxy for an object. Reading data on the reactive object
4863
- * subscribes to changes to the data. Writing data on the object will cause the
4864
- * notify callback to be called if there are suscriptions to that data. Nested
4865
- * objects and arrays are automatically made reactive as well.
4866
- *
4867
- * Whenever you are notified of a change, all subscriptions are cleared, and if
4868
- * you would like to be notified of any further changes, you should go read
4869
- * the underlying data again. We assume that if you don't go read it again after
4870
- * being notified, it means that you are no longer interested in that data.
4871
- *
4872
- * Subscriptions:
4873
- * + Reading a property on an object will subscribe you to changes in the value
4874
- * of that property.
4875
- * + Accessing an object keys (eg with Object.keys or with `for..in`) will
4876
- * subscribe you to the creation/deletion of keys. Checking the presence of a
4877
- * key on the object with 'in' has the same effect.
4878
- * - getOwnPropertyDescriptor does not currently subscribe you to the property.
4879
- * This is a choice that was made because changing a key's value will trigger
4880
- * this trap and we do not want to subscribe by writes. This also means that
4881
- * Object.hasOwnProperty doesn't subscribe as it goes through this trap.
4882
- *
4883
- * @param target the object for which to create a reactive proxy
4884
- * @param callback the function to call when an observed property of the
4885
- * reactive has changed
4886
- * @returns a proxy that tracks changes to it
4887
- */
4888
- function reactive(target, callback = () => { }) {
4889
- if (!canBeMadeReactive(target)) {
4890
- throw new Error(`Cannot make the given value reactive`);
4891
- }
4892
- if (SKIP in target) {
4893
- return target;
4894
- }
4895
- const originalTarget = target[TARGET];
4896
- if (originalTarget) {
4897
- return reactive(originalTarget, callback);
4898
- }
4899
- if (!reactiveCache.has(target)) {
4900
- reactiveCache.set(target, new Map());
4901
- }
4902
- const reactivesForTarget = reactiveCache.get(target);
4903
- if (!reactivesForTarget.has(callback)) {
4904
- const proxy = new Proxy(target, {
4905
- get(target, key, proxy) {
4906
- if (key === TARGET) {
4907
- return target;
4908
- }
4909
- observeTargetKey(target, key, callback);
4910
- const value = Reflect.get(target, key, proxy);
4911
- if (!canBeMadeReactive(value)) {
4912
- return value;
4913
- }
4914
- return reactive(value, callback);
4915
- },
4916
- set(target, key, value, proxy) {
4917
- const isNewKey = !Object.hasOwnProperty.call(target, key);
4918
- const originalValue = Reflect.get(target, key, proxy);
4919
- const ret = Reflect.set(target, key, value, proxy);
4920
- if (isNewKey) {
4921
- notifyReactives(target, KEYCHANGES);
4922
- }
4923
- // While Array length may trigger the set trap, it's not actually set by this
4924
- // method but is updated behind the scenes, and the trap is not called with the
4925
- // new value. We disable the "same-value-optimization" for it because of that.
4926
- if (originalValue !== value || (Array.isArray(target) && key === "length")) {
4927
- notifyReactives(target, key);
4928
- }
4929
- return ret;
4930
- },
4931
- deleteProperty(target, key) {
4932
- const ret = Reflect.deleteProperty(target, key);
4933
- notifyReactives(target, KEYCHANGES);
4934
- notifyReactives(target, key);
4935
- return ret;
4936
- },
4937
- ownKeys(target) {
4938
- observeTargetKey(target, KEYCHANGES, callback);
4939
- return Reflect.ownKeys(target);
4940
- },
4941
- has(target, key) {
4942
- // TODO: this observes all key changes instead of only the presence of the argument key
4943
- observeTargetKey(target, KEYCHANGES, callback);
4944
- return Reflect.has(target, key);
4945
- },
4946
- });
4947
- reactivesForTarget.set(callback, proxy);
4948
- }
4949
- return reactivesForTarget.get(callback);
4950
- }
4951
- const batchedRenderFunctions = new WeakMap();
4952
- /**
4953
- * Creates a reactive object that will be observed by the current component.
4954
- * Reading data from the returned object (eg during rendering) will cause the
4955
- * component to subscribe to that data and be rerendered when it changes.
4956
- *
4957
- * @param state the state to observe
4958
- * @returns a reactive object that will cause the component to re-render on
4959
- * relevant changes
4960
- * @see reactive
4961
- */
4962
- function useState(state) {
4963
- const node = getCurrent();
4964
- if (!batchedRenderFunctions.has(node)) {
4965
- batchedRenderFunctions.set(node, batched(() => node.render()));
4966
- onWillDestroy(() => clearReactivesForCallback(render));
4967
- }
4968
- const render = batchedRenderFunctions.get(node);
4969
- const reactiveState = reactive(state, render);
4970
- return reactiveState;
4971
- }
4972
-
4973
5450
  // -----------------------------------------------------------------------------
4974
5451
  // useRef
4975
5452
  // -----------------------------------------------------------------------------
@@ -5015,10 +5492,6 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
5015
5492
  const node = getCurrent();
5016
5493
  node.childEnv = extendEnv(node.childEnv, envExtension);
5017
5494
  }
5018
- // -----------------------------------------------------------------------------
5019
- // useEffect
5020
- // -----------------------------------------------------------------------------
5021
- const NO_OP = () => { };
5022
5495
  /**
5023
5496
  * This hook will run a callback when a component is mounted and patched, and
5024
5497
  * will run a cleanup function before patching and before unmounting the
@@ -5036,18 +5509,20 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
5036
5509
  let dependencies;
5037
5510
  onMounted(() => {
5038
5511
  dependencies = computeDependencies();
5039
- cleanup = effect(...dependencies) || NO_OP;
5512
+ cleanup = effect(...dependencies);
5040
5513
  });
5041
5514
  onPatched(() => {
5042
5515
  const newDeps = computeDependencies();
5043
5516
  const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
5044
5517
  if (shouldReapply) {
5045
5518
  dependencies = newDeps;
5046
- cleanup();
5047
- cleanup = effect(...dependencies) || NO_OP;
5519
+ if (cleanup) {
5520
+ cleanup();
5521
+ }
5522
+ cleanup = effect(...dependencies);
5048
5523
  }
5049
5524
  });
5050
- onWillUnmount(() => cleanup());
5525
+ onWillUnmount(() => cleanup && cleanup());
5051
5526
  }
5052
5527
  // -----------------------------------------------------------------------------
5053
5528
  // useExternalListener
@@ -5074,7 +5549,6 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
5074
5549
 
5075
5550
  config.shouldNormalizeDom = false;
5076
5551
  config.mainEventHandler = mainEventHandler;
5077
- UTILS.Portal = Portal;
5078
5552
  const blockDom = {
5079
5553
  config,
5080
5554
  // bdom entry points
@@ -5095,7 +5569,6 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
5095
5569
  exports.App = App;
5096
5570
  exports.Component = Component;
5097
5571
  exports.EventBus = EventBus;
5098
- exports.Memo = Memo;
5099
5572
  exports.__info__ = __info__;
5100
5573
  exports.blockDom = blockDom;
5101
5574
  exports.loadFile = loadFile;
@@ -5129,9 +5602,9 @@ See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for mor
5129
5602
  Object.defineProperty(exports, '__esModule', { value: true });
5130
5603
 
5131
5604
 
5132
- __info__.version = '2.0.0-alpha.2';
5133
- __info__.date = '2022-02-14T12:42:47.468Z';
5134
- __info__.hash = '4a922ed';
5605
+ __info__.version = '2.0.0-beta-5';
5606
+ __info__.date = '2022-04-07T13:36:37.300Z';
5607
+ __info__.hash = '1179e84';
5135
5608
  __info__.url = 'https://github.com/odoo/owl';
5136
5609
 
5137
5610