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