@odoo/owl 2.0.0-alpha.1 → 2.0.0-beta-4

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:
@@ -811,13 +819,12 @@ function updateCtx(ctx, tree) {
811
819
  break;
812
820
  }
813
821
  case "ref":
814
- const index = ctx.refList.push(NO_OP$1) - 1;
815
- ctx.cbRefs.push(info.idx);
822
+ const index = ctx.cbRefs.push(info.idx) - 1;
816
823
  ctx.locations.push({
817
824
  idx: info.idx,
818
825
  refIdx: info.refIdx,
819
826
  setData: makeRefSetter(index, ctx.refList),
820
- updateData: NO_OP$1,
827
+ updateData: NO_OP,
821
828
  });
822
829
  }
823
830
  }
@@ -830,10 +837,12 @@ function buildBlock(template, ctx) {
830
837
  if (ctx.cbRefs.length) {
831
838
  const cbRefs = ctx.cbRefs;
832
839
  const refList = ctx.refList;
840
+ let cbRefsNumber = cbRefs.length;
833
841
  B = class extends B {
834
842
  mount(parent, afterNode) {
843
+ refList.push(new Array(cbRefsNumber));
835
844
  super.mount(parent, afterNode);
836
- for (let cbRef of refList) {
845
+ for (let cbRef of refList.pop()) {
837
846
  cbRef();
838
847
  }
839
848
  }
@@ -995,7 +1004,7 @@ function setText(value) {
995
1004
  }
996
1005
  function makeRefSetter(index, refs) {
997
1006
  return function setRef(fn) {
998
- refs[index] = () => fn(this);
1007
+ refs[refs.length - 1][index] = () => fn(this);
999
1008
  };
1000
1009
  }
1001
1010
 
@@ -1281,6 +1290,75 @@ function html(str) {
1281
1290
  return new VHtml(str);
1282
1291
  }
1283
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
+
1284
1362
  function mount$1(vnode, fixture, afterNode = null) {
1285
1363
  vnode.mount(fixture, afterNode);
1286
1364
  }
@@ -1294,143 +1372,464 @@ function remove(vnode, withBeforeRemove = false) {
1294
1372
  vnode.remove();
1295
1373
  }
1296
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"]);
1297
1430
  /**
1298
- * 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
1299
1435
  *
1300
- * 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
1301
1438
  */
1302
- function applyDefaultProps(props, ComponentClass) {
1303
- const defaultProps = ComponentClass.defaultProps;
1304
- if (defaultProps) {
1305
- for (let propName in defaultProps) {
1306
- if (props[propName] === undefined) {
1307
- props[propName] = defaultProps[propName];
1308
- }
1309
- }
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;
1310
1451
  }
1452
+ return SUPPORTED_RAW_TYPES.has(rawType(value));
1311
1453
  }
1312
- //------------------------------------------------------------------------------
1313
- // Prop validation helper
1314
- //------------------------------------------------------------------------------
1315
- function getPropDescription(staticProps) {
1316
- if (staticProps instanceof Array) {
1317
- return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
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());
1318
1496
  }
1319
- 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);
1320
1506
  }
1321
1507
  /**
1322
- * Validate the component props (or next props) against the (static) props
1323
- * description. This is potentially an expensive operation: it may needs to
1324
- * visit recursively the props and all the children to check if they are valid.
1325
- * This is why it is only done in 'dev' mode.
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)
1326
1515
  */
1327
- function validateProps(name, props, parent) {
1328
- const ComponentClass = typeof name !== "string"
1329
- ? name
1330
- : parent.constructor.components[name];
1331
- if (!ComponentClass) {
1332
- // this is an error, wrong component. We silently return here instead so the
1333
- // error is triggered by the usual path ('component' function)
1516
+ function notifyReactives(target, key) {
1517
+ const keyToCallbacks = targetToKeysToCallbacks.get(target);
1518
+ if (!keyToCallbacks) {
1334
1519
  return;
1335
1520
  }
1336
- applyDefaultProps(props, ComponentClass);
1337
- const defaultProps = ComponentClass.defaultProps || {};
1338
- let propsDef = getPropDescription(ComponentClass.props);
1339
- const allowAdditionalProps = "*" in propsDef;
1340
- for (let propName in propsDef) {
1341
- if (propName === "*") {
1342
- continue;
1343
- }
1344
- const propDef = propsDef[propName];
1345
- let isMandatory = !!propDef;
1346
- if (typeof propDef === "object" && "optional" in propDef) {
1347
- isMandatory = !propDef.optional;
1348
- }
1349
- if (isMandatory && propName in defaultProps) {
1350
- throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
1351
- }
1352
- if (props[propName] === undefined) {
1353
- if (isMandatory) {
1354
- throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
1355
- }
1356
- else {
1357
- continue;
1358
- }
1359
- }
1360
- let isValid;
1361
- try {
1362
- isValid = isValidProp(props[propName], propDef);
1363
- }
1364
- catch (e) {
1365
- e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
1366
- throw e;
1367
- }
1368
- if (!isValid) {
1369
- throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
1370
- }
1521
+ const callbacks = keyToCallbacks.get(key);
1522
+ if (!callbacks) {
1523
+ return;
1371
1524
  }
1372
- if (!allowAdditionalProps) {
1373
- for (let propName in props) {
1374
- if (!(propName in propsDef)) {
1375
- throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
1376
- }
1377
- }
1525
+ // Loop on copy because clearReactivesForCallback will modify the set in place
1526
+ for (const callback of [...callbacks]) {
1527
+ clearReactivesForCallback(callback);
1528
+ callback();
1378
1529
  }
1379
1530
  }
1531
+ const callbacksToTargets = new WeakMap();
1380
1532
  /**
1381
- * 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
1382
1536
  */
1383
- function isValidProp(prop, propDef) {
1384
- if (propDef === true) {
1385
- return true;
1537
+ function clearReactivesForCallback(callback) {
1538
+ const targetsToClear = callbacksToTargets.get(callback);
1539
+ if (!targetsToClear) {
1540
+ return;
1386
1541
  }
1387
- if (typeof propDef === "function") {
1388
- // Check if a value is constructed by some Constructor. Note that there is a
1389
- // slight abuse of language: we want to consider primitive values as well.
1390
- //
1391
- // So, even though 1 is not an instance of Number, we want to consider that
1392
- // it is valid.
1393
- if (typeof prop === "object") {
1394
- return prop instanceof propDef;
1542
+ for (const target of targetsToClear) {
1543
+ const observedKeys = targetToKeysToCallbacks.get(target);
1544
+ if (!observedKeys) {
1545
+ continue;
1395
1546
  }
1396
- return typeof prop === propDef.name.toLowerCase();
1397
- }
1398
- else if (propDef instanceof Array) {
1399
- // If this code is executed, this means that we want to check if a prop
1400
- // matches at least one of its descriptor.
1401
- let result = false;
1402
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
1403
- result = result || isValidProp(prop, propDef[i]);
1547
+ for (const callbacks of observedKeys.values()) {
1548
+ callbacks.delete(callback);
1404
1549
  }
1405
- return result;
1406
1550
  }
1407
- // propsDef is an object
1408
- if (propDef.optional && prop === undefined) {
1409
- 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`);
1410
1594
  }
1411
- let result = propDef.type ? isValidProp(prop, propDef.type) : true;
1412
- if (propDef.validate) {
1413
- result = result && propDef.validate(prop);
1595
+ if (SKIP in target) {
1596
+ return target;
1414
1597
  }
1415
- if (propDef.type === Array && propDef.element) {
1416
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
1417
- result = result && isValidProp(prop[i], propDef.element);
1418
- }
1598
+ const originalTarget = target[TARGET];
1599
+ if (originalTarget) {
1600
+ return reactive(originalTarget, callback);
1419
1601
  }
1420
- if (propDef.type === Object && propDef.shape) {
1421
- const shape = propDef.shape;
1422
- for (let key in shape) {
1423
- result = result && isValidProp(prop[key], shape[key]);
1424
- }
1425
- if (result) {
1426
- for (let propName in prop) {
1427
- if (!(propName in shape)) {
1428
- throw new Error(`unknown prop '${propName}'`);
1429
- }
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);
1430
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);
1431
1702
  }
1432
- }
1433
- 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
+ });
1434
1833
  }
1435
1834
 
1436
1835
  /**
@@ -1448,11 +1847,13 @@ function batched(callback) {
1448
1847
  await Promise.resolve();
1449
1848
  if (!called) {
1450
1849
  called = true;
1451
- callback();
1452
1850
  // wait for all calls in this microtick to fall through before resetting "called"
1453
- // so that only the first call to the batched function calls the original callback
1454
- await Promise.resolve();
1455
- called = false;
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();
1456
1857
  }
1457
1858
  };
1458
1859
  }
@@ -1499,223 +1900,18 @@ class Markup extends String {
1499
1900
  */
1500
1901
  function markup(value) {
1501
1902
  return new Markup(value);
1502
- }
1503
-
1504
- /**
1505
- * This file contains utility functions that will be injected in each template,
1506
- * to perform various useful tasks in the compiled code.
1507
- */
1508
- function withDefault(value, defaultValue) {
1509
- return value === undefined || value === null || value === false ? defaultValue : value;
1510
- }
1511
- function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
1512
- key = key + "__slot_" + name;
1513
- const slots = (ctx.props && ctx.props.slots) || {};
1514
- const { __render, __ctx, __scope } = slots[name] || {};
1515
- const slotScope = Object.create(__ctx || {});
1516
- if (__scope) {
1517
- slotScope[__scope] = extra || {};
1518
- }
1519
- const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
1520
- if (defaultContent) {
1521
- let child1 = undefined;
1522
- let child2 = undefined;
1523
- if (slotBDom) {
1524
- child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
1525
- }
1526
- else {
1527
- child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
1528
- }
1529
- return multi([child1, child2]);
1530
- }
1531
- return slotBDom || text("");
1532
- }
1533
- function capture(ctx) {
1534
- const component = ctx.__owl__.component;
1535
- const result = Object.create(component);
1536
- for (let k in ctx) {
1537
- result[k] = ctx[k];
1538
- }
1539
- return result;
1540
- }
1541
- function withKey(elem, k) {
1542
- elem.key = k;
1543
- return elem;
1544
- }
1545
- function prepareList(collection) {
1546
- let keys;
1547
- let values;
1548
- if (Array.isArray(collection)) {
1549
- keys = collection;
1550
- values = collection;
1551
- }
1552
- else if (collection) {
1553
- values = Object.keys(collection);
1554
- keys = Object.values(collection);
1555
- }
1556
- else {
1557
- throw new Error("Invalid loop expression");
1558
- }
1559
- const n = values.length;
1560
- return [keys, values, n, new Array(n)];
1561
1903
  }
1562
- const isBoundary = Symbol("isBoundary");
1563
- function setContextValue(ctx, key, value) {
1564
- const ctx0 = ctx;
1565
- while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
1566
- const newCtx = ctx.__proto__;
1567
- if (!newCtx) {
1568
- ctx = ctx0;
1569
- break;
1570
- }
1571
- ctx = newCtx;
1572
- }
1573
- ctx[key] = value;
1574
- }
1575
- function toNumber(val) {
1576
- const n = parseFloat(val);
1577
- return isNaN(n) ? val : n;
1578
- }
1579
- function shallowEqual$1(l1, l2) {
1580
- for (let i = 0, l = l1.length; i < l; i++) {
1581
- if (l1[i] !== l2[i]) {
1582
- return false;
1583
- }
1584
- }
1585
- return true;
1586
- }
1587
- class LazyValue {
1588
- constructor(fn, ctx, node) {
1589
- this.fn = fn;
1590
- this.ctx = capture(ctx);
1591
- this.node = node;
1592
- }
1593
- evaluate() {
1594
- return this.fn(this.ctx, this.node);
1595
- }
1596
- toString() {
1597
- return this.evaluate().toString();
1598
- }
1599
- }
1600
- /*
1601
- * Safely outputs `value` as a block depending on the nature of `value`
1602
- */
1603
- function safeOutput(value) {
1604
- if (!value) {
1605
- return value;
1606
- }
1607
- let safeKey;
1608
- let block;
1609
- if (value instanceof Markup) {
1610
- safeKey = `string_safe`;
1611
- block = html(value);
1612
- }
1613
- else if (value instanceof LazyValue) {
1614
- safeKey = `lazy_value`;
1615
- block = value.evaluate();
1616
- }
1617
- else if (typeof value === "string") {
1618
- safeKey = "string_unsafe";
1619
- block = text(value);
1620
- }
1621
- else {
1622
- // Assuming it is a block
1623
- safeKey = "block_safe";
1624
- block = value;
1625
- }
1626
- return toggler(safeKey, block);
1627
- }
1628
- let boundFunctions = new WeakMap();
1629
- function bind(ctx, fn) {
1630
- let component = ctx.__owl__.component;
1631
- let boundFnMap = boundFunctions.get(component);
1632
- if (!boundFnMap) {
1633
- boundFnMap = new WeakMap();
1634
- boundFunctions.set(component, boundFnMap);
1635
- }
1636
- let boundFn = boundFnMap.get(fn);
1637
- if (!boundFn) {
1638
- boundFn = fn.bind(component);
1639
- boundFnMap.set(fn, boundFn);
1640
- }
1641
- return boundFn;
1642
- }
1643
- function multiRefSetter(refs, name) {
1644
- let count = 0;
1645
- return (el) => {
1646
- if (el) {
1647
- count++;
1648
- if (count > 1) {
1649
- throw new Error("Cannot have 2 elements with same ref name at the same time");
1650
- }
1651
- }
1652
- if (count === 0 || el) {
1653
- refs[name] = el;
1654
- }
1655
- };
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;
1656
1913
  }
1657
- const UTILS = {
1658
- withDefault,
1659
- zero: Symbol("zero"),
1660
- isBoundary,
1661
- callSlot,
1662
- capture,
1663
- withKey,
1664
- prepareList,
1665
- setContextValue,
1666
- multiRefSetter,
1667
- shallowEqual: shallowEqual$1,
1668
- toNumber,
1669
- validateProps,
1670
- LazyValue,
1671
- safeOutput,
1672
- bind,
1673
- };
1674
-
1675
- const mainEventHandler = (data, ev, currentTarget) => {
1676
- const { data: _data, modifiers } = filterOutModifiersFromData(data);
1677
- data = _data;
1678
- let stopped = false;
1679
- if (modifiers.length) {
1680
- let selfMode = false;
1681
- const isSelf = ev.target === currentTarget;
1682
- for (const mod of modifiers) {
1683
- switch (mod) {
1684
- case "self":
1685
- selfMode = true;
1686
- if (isSelf) {
1687
- continue;
1688
- }
1689
- else {
1690
- return stopped;
1691
- }
1692
- case "prevent":
1693
- if ((selfMode && isSelf) || !selfMode)
1694
- ev.preventDefault();
1695
- continue;
1696
- case "stop":
1697
- if ((selfMode && isSelf) || !selfMode)
1698
- ev.stopPropagation();
1699
- stopped = true;
1700
- continue;
1701
- }
1702
- }
1703
- }
1704
- // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
1705
- // We check this rather than data[0] being truthy (or typeof function) so that it crashes
1706
- // as expected when there is a handler expression that evaluates to a falsy value
1707
- if (Object.hasOwnProperty.call(data, 0)) {
1708
- const handler = data[0];
1709
- if (typeof handler !== "function") {
1710
- throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
1711
- }
1712
- let node = data[1] ? data[1].__owl__ : null;
1713
- if (node ? node.status === 1 /* MOUNTED */ : true) {
1714
- handler.call(node ? node.component : null, ev);
1715
- }
1716
- }
1717
- return stopped;
1718
- };
1914
+ xml.nextId = 1;
1719
1915
 
1720
1916
  // Maps fibers to thrown errors
1721
1917
  const fibersInError = new WeakMap();
@@ -1744,7 +1940,8 @@ function _handleError(node, error, isFirstRound = false) {
1744
1940
  }
1745
1941
  if (stopped) {
1746
1942
  if (isFirstRound && fiber && fiber.node.fiber) {
1747
- fiber.root.counter--;
1943
+ const root = fiber.root;
1944
+ root.setCounter(root.counter - 1);
1748
1945
  }
1749
1946
  return true;
1750
1947
  }
@@ -1778,9 +1975,12 @@ function handleError(params) {
1778
1975
  function makeChildFiber(node, parent) {
1779
1976
  let current = node.fiber;
1780
1977
  if (current) {
1781
- let root = parent.root;
1782
- cancelFibers(root, current.children);
1978
+ cancelFibers(current.children);
1783
1979
  current.root = null;
1980
+ if (current instanceof RootFiber && current.delayedRenders.length) {
1981
+ let root = parent.root;
1982
+ root.delayedRenders = root.delayedRenders.concat(current.delayedRenders);
1983
+ }
1784
1984
  }
1785
1985
  return new Fiber(node, parent);
1786
1986
  }
@@ -1788,10 +1988,12 @@ function makeRootFiber(node) {
1788
1988
  let current = node.fiber;
1789
1989
  if (current) {
1790
1990
  let root = current.root;
1791
- root.counter -= cancelFibers(root, current.children);
1991
+ root.setCounter(root.counter + 1 - cancelFibers(current.children));
1792
1992
  current.children = [];
1793
- root.counter++;
1794
1993
  current.bdom = null;
1994
+ if (current === root) {
1995
+ root.reachedChildren = new WeakSet();
1996
+ }
1795
1997
  if (fibersInError.has(current)) {
1796
1998
  fibersInError.delete(current);
1797
1999
  fibersInError.delete(root);
@@ -1811,15 +2013,23 @@ function makeRootFiber(node) {
1811
2013
  /**
1812
2014
  * @returns number of not-yet rendered fibers cancelled
1813
2015
  */
1814
- function cancelFibers(root, fibers) {
2016
+ function cancelFibers(fibers) {
1815
2017
  let result = 0;
1816
2018
  for (let fiber of fibers) {
1817
2019
  fiber.node.fiber = null;
1818
- fiber.root = root;
1819
- if (!fiber.bdom) {
2020
+ if (fiber.bdom) {
2021
+ // if fiber has been rendered, this means that the component props have
2022
+ // been updated. however, this fiber will not be patched to the dom, so
2023
+ // it could happen that the next render compare the current props with
2024
+ // the same props, and skip the render completely. With the next line,
2025
+ // we kindly request the component code to force a render, so it works as
2026
+ // expected.
2027
+ fiber.node.forceNextRender = true;
2028
+ }
2029
+ else {
1820
2030
  result++;
1821
2031
  }
1822
- result += cancelFibers(root, fiber.children);
2032
+ result += cancelFibers(fiber.children);
1823
2033
  }
1824
2034
  return result;
1825
2035
  }
@@ -1828,11 +2038,13 @@ class Fiber {
1828
2038
  this.bdom = null;
1829
2039
  this.children = [];
1830
2040
  this.appliedToDom = false;
2041
+ this.deep = false;
1831
2042
  this.node = node;
1832
2043
  this.parent = parent;
1833
2044
  if (parent) {
2045
+ this.deep = parent.deep;
1834
2046
  const root = parent.root;
1835
- root.counter++;
2047
+ root.setCounter(root.counter + 1);
1836
2048
  this.root = root;
1837
2049
  parent.children.push(this);
1838
2050
  }
@@ -1840,6 +2052,45 @@ class Fiber {
1840
2052
  this.root = this;
1841
2053
  }
1842
2054
  }
2055
+ render() {
2056
+ // if some parent has a fiber => register in followup
2057
+ let prev = this.root.node;
2058
+ let current = prev.parent;
2059
+ while (current) {
2060
+ if (current.fiber) {
2061
+ let root = current.fiber.root;
2062
+ if (root.counter) {
2063
+ root.delayedRenders.push(this);
2064
+ return;
2065
+ }
2066
+ else {
2067
+ if (!root.reachedChildren.has(prev)) {
2068
+ // is dead
2069
+ this.node.app.scheduler.shouldClear = true;
2070
+ return;
2071
+ }
2072
+ current = root.node;
2073
+ }
2074
+ }
2075
+ prev = current;
2076
+ current = current.parent;
2077
+ }
2078
+ // there are no current rendering from above => we can render
2079
+ this._render();
2080
+ }
2081
+ _render() {
2082
+ const node = this.node;
2083
+ const root = this.root;
2084
+ if (root) {
2085
+ try {
2086
+ this.bdom = node.renderFn();
2087
+ root.setCounter(root.counter - 1);
2088
+ }
2089
+ catch (e) {
2090
+ handleError({ node, error: e });
2091
+ }
2092
+ }
2093
+ }
1843
2094
  }
1844
2095
  class RootFiber extends Fiber {
1845
2096
  constructor() {
@@ -1852,6 +2103,8 @@ class RootFiber extends Fiber {
1852
2103
  // A fiber is typically locked when it is completing and the patch has not, or is being applied.
1853
2104
  // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
1854
2105
  this.locked = false;
2106
+ this.delayedRenders = [];
2107
+ this.reachedChildren = new WeakSet();
1855
2108
  }
1856
2109
  complete() {
1857
2110
  const node = this.node;
@@ -1873,7 +2126,7 @@ class RootFiber extends Fiber {
1873
2126
  }
1874
2127
  current = undefined;
1875
2128
  // Step 2: patching the dom
1876
- node.patch();
2129
+ node._patch();
1877
2130
  this.locked = false;
1878
2131
  // Step 4: calling all mounted lifecycle hooks
1879
2132
  let mountedFibers = this.mounted;
@@ -1901,6 +2154,20 @@ class RootFiber extends Fiber {
1901
2154
  handleError({ fiber: current || this, error: e });
1902
2155
  }
1903
2156
  }
2157
+ setCounter(newValue) {
2158
+ this.counter = newValue;
2159
+ if (newValue === 0) {
2160
+ if (this.delayedRenders.length) {
2161
+ for (let f of this.delayedRenders) {
2162
+ if (f.root) {
2163
+ f.render();
2164
+ }
2165
+ }
2166
+ this.delayedRenders = [];
2167
+ }
2168
+ this.node.app.scheduler.flush();
2169
+ }
2170
+ }
1904
2171
  }
1905
2172
  class MountFiber extends RootFiber {
1906
2173
  constructor(node, target, options = {}) {
@@ -1944,10 +2211,149 @@ class MountFiber extends RootFiber {
1944
2211
  }
1945
2212
  }
1946
2213
  }
1947
- catch (e) {
1948
- handleError({ fiber: current, error: e });
1949
- }
2214
+ catch (e) {
2215
+ handleError({ fiber: current, error: e });
2216
+ }
2217
+ }
2218
+ }
2219
+
2220
+ /**
2221
+ * Apply default props (only top level).
2222
+ *
2223
+ * Note that this method does modify in place the props
2224
+ */
2225
+ function applyDefaultProps(props, ComponentClass) {
2226
+ const defaultProps = ComponentClass.defaultProps;
2227
+ if (defaultProps) {
2228
+ for (let propName in defaultProps) {
2229
+ if (props[propName] === undefined) {
2230
+ props[propName] = defaultProps[propName];
2231
+ }
2232
+ }
2233
+ }
2234
+ }
2235
+ //------------------------------------------------------------------------------
2236
+ // Prop validation helper
2237
+ //------------------------------------------------------------------------------
2238
+ function getPropDescription(staticProps) {
2239
+ if (staticProps instanceof Array) {
2240
+ return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
2241
+ }
2242
+ return staticProps || { "*": true };
2243
+ }
2244
+ /**
2245
+ * Validate the component props (or next props) against the (static) props
2246
+ * description. This is potentially an expensive operation: it may needs to
2247
+ * visit recursively the props and all the children to check if they are valid.
2248
+ * This is why it is only done in 'dev' mode.
2249
+ */
2250
+ function validateProps(name, props, parent) {
2251
+ const ComponentClass = typeof name !== "string"
2252
+ ? name
2253
+ : parent.constructor.components[name];
2254
+ if (!ComponentClass) {
2255
+ // this is an error, wrong component. We silently return here instead so the
2256
+ // error is triggered by the usual path ('component' function)
2257
+ return;
2258
+ }
2259
+ applyDefaultProps(props, ComponentClass);
2260
+ const defaultProps = ComponentClass.defaultProps || {};
2261
+ let propsDef = getPropDescription(ComponentClass.props);
2262
+ const allowAdditionalProps = "*" in propsDef;
2263
+ for (let propName in propsDef) {
2264
+ if (propName === "*") {
2265
+ continue;
2266
+ }
2267
+ const propDef = propsDef[propName];
2268
+ let isMandatory = !!propDef;
2269
+ if (typeof propDef === "object" && "optional" in propDef) {
2270
+ isMandatory = !propDef.optional;
2271
+ }
2272
+ if (isMandatory && propName in defaultProps) {
2273
+ throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
2274
+ }
2275
+ if (props[propName] === undefined) {
2276
+ if (isMandatory) {
2277
+ throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
2278
+ }
2279
+ else {
2280
+ continue;
2281
+ }
2282
+ }
2283
+ let isValid;
2284
+ try {
2285
+ isValid = isValidProp(props[propName], propDef);
2286
+ }
2287
+ catch (e) {
2288
+ e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
2289
+ throw e;
2290
+ }
2291
+ if (!isValid) {
2292
+ throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
2293
+ }
2294
+ }
2295
+ if (!allowAdditionalProps) {
2296
+ for (let propName in props) {
2297
+ if (!(propName in propsDef)) {
2298
+ throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
2299
+ }
2300
+ }
2301
+ }
2302
+ }
2303
+ /**
2304
+ * Check if an invidual prop value matches its (static) prop definition
2305
+ */
2306
+ function isValidProp(prop, propDef) {
2307
+ if (propDef === true) {
2308
+ return true;
2309
+ }
2310
+ if (typeof propDef === "function") {
2311
+ // Check if a value is constructed by some Constructor. Note that there is a
2312
+ // slight abuse of language: we want to consider primitive values as well.
2313
+ //
2314
+ // So, even though 1 is not an instance of Number, we want to consider that
2315
+ // it is valid.
2316
+ if (typeof prop === "object") {
2317
+ return prop instanceof propDef;
2318
+ }
2319
+ return typeof prop === propDef.name.toLowerCase();
2320
+ }
2321
+ else if (propDef instanceof Array) {
2322
+ // If this code is executed, this means that we want to check if a prop
2323
+ // matches at least one of its descriptor.
2324
+ let result = false;
2325
+ for (let i = 0, iLen = propDef.length; i < iLen; i++) {
2326
+ result = result || isValidProp(prop, propDef[i]);
2327
+ }
2328
+ return result;
2329
+ }
2330
+ // propsDef is an object
2331
+ if (propDef.optional && prop === undefined) {
2332
+ return true;
2333
+ }
2334
+ let result = propDef.type ? isValidProp(prop, propDef.type) : true;
2335
+ if (propDef.validate) {
2336
+ result = result && propDef.validate(prop);
2337
+ }
2338
+ if (propDef.type === Array && propDef.element) {
2339
+ for (let i = 0, iLen = prop.length; i < iLen; i++) {
2340
+ result = result && isValidProp(prop[i], propDef.element);
2341
+ }
2342
+ }
2343
+ if (propDef.type === Object && propDef.shape) {
2344
+ const shape = propDef.shape;
2345
+ for (let key in shape) {
2346
+ result = result && isValidProp(prop[key], shape[key]);
2347
+ }
2348
+ if (result) {
2349
+ for (let propName in prop) {
2350
+ if (!(propName in shape)) {
2351
+ throw new Error(`unknown prop '${propName}'`);
2352
+ }
2353
+ }
2354
+ }
1950
2355
  }
2356
+ return result;
1951
2357
  }
1952
2358
 
1953
2359
  let currentNode = null;
@@ -1960,6 +2366,39 @@ function getCurrent() {
1960
2366
  function useComponent() {
1961
2367
  return currentNode.component;
1962
2368
  }
2369
+ // -----------------------------------------------------------------------------
2370
+ // Integration with reactivity system (useState)
2371
+ // -----------------------------------------------------------------------------
2372
+ const batchedRenderFunctions = new WeakMap();
2373
+ /**
2374
+ * Creates a reactive object that will be observed by the current component.
2375
+ * Reading data from the returned object (eg during rendering) will cause the
2376
+ * component to subscribe to that data and be rerendered when it changes.
2377
+ *
2378
+ * @param state the state to observe
2379
+ * @returns a reactive object that will cause the component to re-render on
2380
+ * relevant changes
2381
+ * @see reactive
2382
+ */
2383
+ function useState(state) {
2384
+ const node = getCurrent();
2385
+ let render = batchedRenderFunctions.get(node);
2386
+ if (!render) {
2387
+ render = batched(node.render.bind(node));
2388
+ batchedRenderFunctions.set(node, render);
2389
+ // manual implementation of onWillDestroy to break cyclic dependency
2390
+ node.willDestroy.push(clearReactivesForCallback.bind(null, render));
2391
+ }
2392
+ return reactive(state, render);
2393
+ }
2394
+ function arePropsDifferent(props1, props2) {
2395
+ for (let k in props1) {
2396
+ if (props1[k] !== props2[k]) {
2397
+ return true;
2398
+ }
2399
+ }
2400
+ return Object.keys(props1).length !== Object.keys(props2).length;
2401
+ }
1963
2402
  function component(name, props, key, ctx, parent) {
1964
2403
  let node = ctx.children[key];
1965
2404
  let isDynamic = typeof name !== "string";
@@ -1977,7 +2416,17 @@ function component(name, props, key, ctx, parent) {
1977
2416
  }
1978
2417
  const parentFiber = ctx.fiber;
1979
2418
  if (node) {
1980
- node.updateAndRender(props, parentFiber);
2419
+ let shouldRender = node.forceNextRender;
2420
+ if (shouldRender) {
2421
+ node.forceNextRender = false;
2422
+ }
2423
+ else {
2424
+ const currentProps = node.component.props[TARGET];
2425
+ shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
2426
+ }
2427
+ if (shouldRender) {
2428
+ node.updateAndRender(props, parentFiber);
2429
+ }
1981
2430
  }
1982
2431
  else {
1983
2432
  // new component
@@ -1993,9 +2442,9 @@ function component(name, props, key, ctx, parent) {
1993
2442
  }
1994
2443
  node = new ComponentNode(C, props, ctx.app, ctx);
1995
2444
  ctx.children[key] = node;
1996
- const fiber = makeChildFiber(node, parentFiber);
1997
- node.initiateRender(fiber);
2445
+ node.initiateRender(new Fiber(node, parentFiber));
1998
2446
  }
2447
+ parentFiber.root.reachedChildren.add(node);
1999
2448
  return node;
2000
2449
  }
2001
2450
  class ComponentNode {
@@ -2003,6 +2452,7 @@ class ComponentNode {
2003
2452
  this.fiber = null;
2004
2453
  this.bdom = null;
2005
2454
  this.status = 0 /* NEW */;
2455
+ this.forceNextRender = false;
2006
2456
  this.children = Object.create(null);
2007
2457
  this.refs = {};
2008
2458
  this.willStart = [];
@@ -2019,6 +2469,7 @@ class ComponentNode {
2019
2469
  applyDefaultProps(props, C);
2020
2470
  const env = (parent && parent.childEnv) || app.env;
2021
2471
  this.childEnv = env;
2472
+ props = useState(props);
2022
2473
  this.component = new C(props, env, this);
2023
2474
  this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
2024
2475
  this.component.setup();
@@ -2043,23 +2494,32 @@ class ComponentNode {
2043
2494
  return;
2044
2495
  }
2045
2496
  if (this.status === 0 /* NEW */ && this.fiber === fiber) {
2046
- this._render(fiber);
2497
+ fiber.render();
2047
2498
  }
2048
2499
  }
2049
- async render() {
2500
+ async render(deep = false) {
2050
2501
  let current = this.fiber;
2051
2502
  if (current && current.root.locked) {
2052
2503
  await Promise.resolve();
2053
2504
  // situation may have changed after the microtask tick
2054
2505
  current = this.fiber;
2055
2506
  }
2056
- if (current && !current.bdom && !fibersInError.has(current)) {
2057
- return;
2507
+ if (current) {
2508
+ if (!current.bdom && !fibersInError.has(current)) {
2509
+ if (deep) {
2510
+ // we want the render from this point on to be with deep=true
2511
+ current.deep = deep;
2512
+ }
2513
+ return;
2514
+ }
2515
+ // if current rendering was with deep=true, we want this one to be the same
2516
+ deep = deep || current.deep;
2058
2517
  }
2059
- if (!this.bdom && !current) {
2518
+ else if (!this.bdom) {
2060
2519
  return;
2061
2520
  }
2062
2521
  const fiber = makeRootFiber(this);
2522
+ fiber.deep = deep;
2063
2523
  this.fiber = fiber;
2064
2524
  this.app.scheduler.addFiber(fiber);
2065
2525
  await Promise.resolve();
@@ -2078,16 +2538,7 @@ class ComponentNode {
2078
2538
  // embedded in a rendering coming from above, so the fiber will be rendered
2079
2539
  // in the next microtick anyway, so we should not render it again.
2080
2540
  if (this.fiber === fiber && (current || !fiber.parent)) {
2081
- this._render(fiber);
2082
- }
2083
- }
2084
- _render(fiber) {
2085
- try {
2086
- fiber.bdom = this.renderFn();
2087
- fiber.root.counter--;
2088
- }
2089
- catch (e) {
2090
- handleError({ node: this, error: e });
2541
+ fiber.render();
2091
2542
  }
2092
2543
  }
2093
2544
  destroy() {
@@ -2118,13 +2569,16 @@ class ComponentNode {
2118
2569
  this.fiber = fiber;
2119
2570
  const component = this.component;
2120
2571
  applyDefaultProps(props, component.constructor);
2572
+ currentNode = this;
2573
+ props = useState(props);
2574
+ currentNode = null;
2121
2575
  const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
2122
2576
  await prom;
2123
2577
  if (fiber !== this.fiber) {
2124
2578
  return;
2125
2579
  }
2126
2580
  component.props = props;
2127
- this._render(fiber);
2581
+ fiber.render();
2128
2582
  const parentRoot = parentFiber.root;
2129
2583
  if (this.willPatch.length) {
2130
2584
  parentRoot.willPatch.push(fiber);
@@ -2177,6 +2631,14 @@ class ComponentNode {
2177
2631
  this.bdom.moveBefore(other ? other.bdom : null, afterNode);
2178
2632
  }
2179
2633
  patch() {
2634
+ if (this.fiber && this.fiber.parent) {
2635
+ // we only patch here renderings coming from above. renderings initiated
2636
+ // by the component will be patched independently in the appropriate
2637
+ // fiber.complete
2638
+ this._patch();
2639
+ }
2640
+ }
2641
+ _patch() {
2180
2642
  const hasChildren = Object.keys(this.children).length > 0;
2181
2643
  this.bdom.patch(this.fiber.bdom, hasChildren);
2182
2644
  if (hasChildren) {
@@ -2204,65 +2666,76 @@ class ComponentNode {
2204
2666
  }
2205
2667
  }
2206
2668
  }
2669
+ // ---------------------------------------------------------------------------
2670
+ // Some debug helpers
2671
+ // ---------------------------------------------------------------------------
2672
+ get name() {
2673
+ return this.component.constructor.name;
2674
+ }
2675
+ get subscriptions() {
2676
+ const render = batchedRenderFunctions.get(this);
2677
+ return render ? getSubscriptions(render) : [];
2678
+ }
2207
2679
  }
2208
2680
 
2209
2681
  // -----------------------------------------------------------------------------
2210
- // hooks
2682
+ // Scheduler
2211
2683
  // -----------------------------------------------------------------------------
2212
- function onWillStart(fn) {
2213
- const node = getCurrent();
2214
- node.willStart.push(fn.bind(node.component));
2215
- }
2216
- function onWillUpdateProps(fn) {
2217
- const node = getCurrent();
2218
- node.willUpdateProps.push(fn.bind(node.component));
2219
- }
2220
- function onMounted(fn) {
2221
- const node = getCurrent();
2222
- node.mounted.push(fn.bind(node.component));
2223
- }
2224
- function onWillPatch(fn) {
2225
- const node = getCurrent();
2226
- node.willPatch.unshift(fn.bind(node.component));
2227
- }
2228
- function onPatched(fn) {
2229
- const node = getCurrent();
2230
- node.patched.push(fn.bind(node.component));
2231
- }
2232
- function onWillUnmount(fn) {
2233
- const node = getCurrent();
2234
- node.willUnmount.unshift(fn.bind(node.component));
2235
- }
2236
- function onWillDestroy(fn) {
2237
- const node = getCurrent();
2238
- node.willDestroy.push(fn.bind(node.component));
2239
- }
2240
- function onWillRender(fn) {
2241
- const node = getCurrent();
2242
- const renderFn = node.renderFn;
2243
- node.renderFn = () => {
2244
- fn.call(node.component);
2245
- return renderFn();
2246
- };
2247
- }
2248
- function onRendered(fn) {
2249
- const node = getCurrent();
2250
- const renderFn = node.renderFn;
2251
- node.renderFn = () => {
2252
- const result = renderFn();
2253
- fn.call(node.component);
2254
- return result;
2255
- };
2256
- }
2257
- function onError(callback) {
2258
- const node = getCurrent();
2259
- let handlers = nodeErrorHandlers.get(node);
2260
- if (!handlers) {
2261
- handlers = [];
2262
- nodeErrorHandlers.set(node, handlers);
2684
+ class Scheduler {
2685
+ constructor() {
2686
+ this.tasks = new Set();
2687
+ this.frame = 0;
2688
+ this.shouldClear = false;
2689
+ this.requestAnimationFrame = Scheduler.requestAnimationFrame;
2263
2690
  }
2264
- handlers.push(callback.bind(node.component));
2265
- }
2691
+ addFiber(fiber) {
2692
+ this.tasks.add(fiber.root);
2693
+ }
2694
+ /**
2695
+ * Process all current tasks. This only applies to the fibers that are ready.
2696
+ * Other tasks are left unchanged.
2697
+ */
2698
+ flush() {
2699
+ if (this.frame === 0) {
2700
+ this.frame = this.requestAnimationFrame(() => {
2701
+ this.frame = 0;
2702
+ this.tasks.forEach((fiber) => this.processFiber(fiber));
2703
+ if (this.shouldClear) {
2704
+ this.shouldClear = false;
2705
+ for (let task of this.tasks) {
2706
+ if (task.node.status === 2 /* DESTROYED */) {
2707
+ this.tasks.delete(task);
2708
+ }
2709
+ }
2710
+ }
2711
+ });
2712
+ }
2713
+ }
2714
+ processFiber(fiber) {
2715
+ if (fiber.root !== fiber) {
2716
+ this.tasks.delete(fiber);
2717
+ return;
2718
+ }
2719
+ const hasError = fibersInError.has(fiber);
2720
+ if (hasError && fiber.counter !== 0) {
2721
+ this.tasks.delete(fiber);
2722
+ return;
2723
+ }
2724
+ if (fiber.node.status === 2 /* DESTROYED */) {
2725
+ this.tasks.delete(fiber);
2726
+ return;
2727
+ }
2728
+ if (fiber.counter === 0) {
2729
+ if (!hasError) {
2730
+ fiber.complete();
2731
+ }
2732
+ this.tasks.delete(fiber);
2733
+ }
2734
+ }
2735
+ }
2736
+ // capture the value of requestAnimationFrame as soon as possible, to avoid
2737
+ // interactions with other code, such as test frameworks that override them
2738
+ Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
2266
2739
 
2267
2740
  /**
2268
2741
  * Owl QWeb Expression Parser
@@ -2416,24 +2889,31 @@ const TOKENIZERS = [
2416
2889
  function tokenize(expr) {
2417
2890
  const result = [];
2418
2891
  let token = true;
2419
- while (token) {
2420
- expr = expr.trim();
2421
- if (expr) {
2422
- for (let tokenizer of TOKENIZERS) {
2423
- token = tokenizer(expr);
2424
- if (token) {
2425
- result.push(token);
2426
- expr = expr.slice(token.size || token.value.length);
2427
- break;
2892
+ let error;
2893
+ let current = expr;
2894
+ try {
2895
+ while (token) {
2896
+ current = current.trim();
2897
+ if (current) {
2898
+ for (let tokenizer of TOKENIZERS) {
2899
+ token = tokenizer(current);
2900
+ if (token) {
2901
+ result.push(token);
2902
+ current = current.slice(token.size || token.value.length);
2903
+ break;
2904
+ }
2428
2905
  }
2429
2906
  }
2430
- }
2431
- else {
2432
- token = false;
2907
+ else {
2908
+ token = false;
2909
+ }
2433
2910
  }
2434
2911
  }
2435
- if (expr.length) {
2436
- throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
2912
+ catch (e) {
2913
+ error = e; // Silence all errors and throw a generic error below
2914
+ }
2915
+ if (current.length || error) {
2916
+ throw new Error(`Tokenizer error: could not tokenize \`${expr}\``);
2437
2917
  }
2438
2918
  return result;
2439
2919
  }
@@ -2638,7 +3118,7 @@ function createContext(parentCtx, params) {
2638
3118
  }, params);
2639
3119
  }
2640
3120
  class CodeTarget {
2641
- constructor(name) {
3121
+ constructor(name, on) {
2642
3122
  this.indentLevel = 0;
2643
3123
  this.loopLevel = 0;
2644
3124
  this.code = [];
@@ -2649,6 +3129,7 @@ class CodeTarget {
2649
3129
  this.refInfo = {};
2650
3130
  this.shouldProtectScope = false;
2651
3131
  this.name = name;
3132
+ this.on = on || null;
2652
3133
  }
2653
3134
  addLine(line, idx) {
2654
3135
  const prefix = new Array(this.indentLevel + 2).join(" ");
@@ -2698,7 +3179,7 @@ class CodeGenerator {
2698
3179
  this.targets = [];
2699
3180
  this.target = new CodeTarget("template");
2700
3181
  this.translatableAttributes = TRANSLATABLE_ATTRS;
2701
- this.staticCalls = [];
3182
+ this.staticDefs = [];
2702
3183
  this.helpers = new Set();
2703
3184
  this.translateFn = options.translateFn || ((s) => s);
2704
3185
  if (options.translatableAttributes) {
@@ -2741,8 +3222,8 @@ class CodeGenerator {
2741
3222
  if (this.templateName) {
2742
3223
  mainCode.push(`// Template name: "${this.templateName}"`);
2743
3224
  }
2744
- for (let { id, template } of this.staticCalls) {
2745
- mainCode.push(`const ${id} = getTemplate(${template});`);
3225
+ for (let { id, expr } of this.staticDefs) {
3226
+ mainCode.push(`const ${id} = ${expr};`);
2746
3227
  }
2747
3228
  // define all blocks
2748
3229
  if (this.blocks.length) {
@@ -2778,19 +3259,21 @@ class CodeGenerator {
2778
3259
  }
2779
3260
  return code;
2780
3261
  }
2781
- compileInNewTarget(prefix, ast, ctx) {
3262
+ compileInNewTarget(prefix, ast, ctx, on) {
2782
3263
  const name = this.generateId(prefix);
2783
3264
  const initialTarget = this.target;
2784
- const target = new CodeTarget(name);
3265
+ const target = new CodeTarget(name, on);
2785
3266
  this.targets.push(target);
2786
3267
  this.target = target;
2787
- const subCtx = createContext(ctx);
2788
- this.compileAST(ast, subCtx);
3268
+ this.compileAST(ast, createContext(ctx));
2789
3269
  this.target = initialTarget;
2790
3270
  return name;
2791
3271
  }
2792
- addLine(line) {
2793
- this.target.addLine(line);
3272
+ addLine(line, idx) {
3273
+ this.target.addLine(line, idx);
3274
+ }
3275
+ define(varName, expr) {
3276
+ this.addLine(`const ${varName} = ${expr};`);
2794
3277
  }
2795
3278
  generateId(prefix = "") {
2796
3279
  this.ids[prefix] = (this.ids[prefix] || 0) + 1;
@@ -2832,10 +3315,13 @@ class CodeGenerator {
2832
3315
  blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
2833
3316
  }
2834
3317
  if (block.isRoot && !ctx.preventRoot) {
3318
+ if (this.target.on) {
3319
+ blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
3320
+ }
2835
3321
  this.addLine(`return ${blockExpr};`);
2836
3322
  }
2837
3323
  else {
2838
- this.addLine(`let ${block.varName} = ${blockExpr};`);
3324
+ this.define(block.varName, blockExpr);
2839
3325
  }
2840
3326
  }
2841
3327
  /**
@@ -2863,7 +3349,7 @@ class CodeGenerator {
2863
3349
  if (!mapping.has(tok.varName)) {
2864
3350
  const varId = this.generateId("v");
2865
3351
  mapping.set(tok.varName, varId);
2866
- this.addLine(`const ${varId} = ${tok.value};`);
3352
+ this.define(varId, tok.value);
2867
3353
  }
2868
3354
  tok.value = mapping.get(tok.varName);
2869
3355
  }
@@ -3002,7 +3488,7 @@ class CodeGenerator {
3002
3488
  this.blocks.push(block);
3003
3489
  if (ast.dynamicTag) {
3004
3490
  const tagExpr = this.generateId("tag");
3005
- this.addLine(`let ${tagExpr} = ${compileExpr(ast.dynamicTag)};`);
3491
+ this.define(tagExpr, compileExpr(ast.dynamicTag));
3006
3492
  block.dynamicTagName = tagExpr;
3007
3493
  }
3008
3494
  }
@@ -3084,10 +3570,10 @@ class CodeGenerator {
3084
3570
  const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
3085
3571
  const baseExpression = compileExpr(baseExpr);
3086
3572
  const bExprId = this.generateId("bExpr");
3087
- this.addLine(`const ${bExprId} = ${baseExpression};`);
3573
+ this.define(bExprId, baseExpression);
3088
3574
  const expression = compileExpr(expr);
3089
3575
  const exprId = this.generateId("expr");
3090
- this.addLine(`const ${exprId} = ${expression};`);
3576
+ this.define(exprId, expression);
3091
3577
  const fullExpression = `${bExprId}[${exprId}]`;
3092
3578
  let idx;
3093
3579
  if (specialInitTargetAttr) {
@@ -3097,7 +3583,7 @@ class CodeGenerator {
3097
3583
  else if (hasDynamicChildren) {
3098
3584
  const bValueId = this.generateId("bValue");
3099
3585
  tModelSelectedExpr = `${bValueId}`;
3100
- this.addLine(`let ${tModelSelectedExpr} = ${fullExpression}`);
3586
+ this.define(tModelSelectedExpr, fullExpression);
3101
3587
  }
3102
3588
  else {
3103
3589
  idx = block.insertData(`${fullExpression}`, "attr");
@@ -3145,14 +3631,14 @@ class CodeGenerator {
3145
3631
  const children = block.children.slice();
3146
3632
  let current = children.shift();
3147
3633
  for (let i = codeIdx; i < code.length; i++) {
3148
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3149
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3634
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3635
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3150
3636
  current = children.shift();
3151
3637
  if (!current)
3152
3638
  break;
3153
3639
  }
3154
3640
  }
3155
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3641
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3156
3642
  }
3157
3643
  }
3158
3644
  }
@@ -3240,14 +3726,14 @@ class CodeGenerator {
3240
3726
  const children = block.children.slice();
3241
3727
  let current = children.shift();
3242
3728
  for (let i = codeIdx; i < code.length; i++) {
3243
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3244
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3729
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3730
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3245
3731
  current = children.shift();
3246
3732
  if (!current)
3247
3733
  break;
3248
3734
  }
3249
3735
  }
3250
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3736
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3251
3737
  }
3252
3738
  // note: this part is duplicated from end of compilemulti:
3253
3739
  const args = block.children.map((c) => c.varName).join(", ");
@@ -3268,10 +3754,10 @@ class CodeGenerator {
3268
3754
  const l = `l_block${block.id}`;
3269
3755
  const c = `c_block${block.id}`;
3270
3756
  this.helpers.add("prepareList");
3271
- this.addLine(`const [${keys}, ${vals}, ${l}, ${c}] = prepareList(${compileExpr(ast.collection)});`);
3757
+ this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);
3272
3758
  // Throw errors on duplicate keys in dev mode
3273
3759
  if (this.dev) {
3274
- this.addLine(`const keys${block.id} = new Set();`);
3760
+ this.define(`keys${block.id}`, `new Set()`);
3275
3761
  }
3276
3762
  this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
3277
3763
  this.target.indentLevel++;
@@ -3288,7 +3774,7 @@ class CodeGenerator {
3288
3774
  if (!ast.hasNoValue) {
3289
3775
  this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`);
3290
3776
  }
3291
- this.addLine(`let key${this.target.loopLevel} = ${ast.key ? compileExpr(ast.key) : loopVar};`);
3777
+ this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
3292
3778
  if (this.dev) {
3293
3779
  // Throw error on duplicate keys in dev mode
3294
3780
  this.addLine(`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new Error(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`);
@@ -3298,8 +3784,8 @@ class CodeGenerator {
3298
3784
  if (ast.memo) {
3299
3785
  this.target.hasCache = true;
3300
3786
  id = this.generateId();
3301
- this.addLine(`let memo${id} = ${compileExpr(ast.memo)}`);
3302
- this.addLine(`let vnode${id} = cache[key${this.target.loopLevel}];`);
3787
+ this.define(`memo${id}`, compileExpr(ast.memo));
3788
+ this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
3303
3789
  this.addLine(`if (vnode${id}) {`);
3304
3790
  this.target.indentLevel++;
3305
3791
  this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);
@@ -3327,7 +3813,7 @@ class CodeGenerator {
3327
3813
  }
3328
3814
  compileTKey(ast, ctx) {
3329
3815
  const tKeyExpr = this.generateId("tKey_");
3330
- this.addLine(`const ${tKeyExpr} = ${compileExpr(ast.expr)};`);
3816
+ this.define(tKeyExpr, compileExpr(ast.expr));
3331
3817
  ctx = createContext(ctx, {
3332
3818
  tKeyExpr,
3333
3819
  block: ctx.block,
@@ -3372,14 +3858,14 @@ class CodeGenerator {
3372
3858
  const children = block.children.slice();
3373
3859
  let current = children.shift();
3374
3860
  for (let i = codeIdx; i < code.length; i++) {
3375
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3376
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3861
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3862
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3377
3863
  current = children.shift();
3378
3864
  if (!current)
3379
3865
  break;
3380
3866
  }
3381
3867
  }
3382
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3868
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3383
3869
  }
3384
3870
  }
3385
3871
  const args = block.children.map((c) => c.varName).join(", ");
@@ -3410,7 +3896,7 @@ class CodeGenerator {
3410
3896
  const key = `key + \`${this.generateComponentKey()}\``;
3411
3897
  if (isDynamic) {
3412
3898
  const templateVar = this.generateId("template");
3413
- this.addLine(`const ${templateVar} = ${subTemplate};`);
3899
+ this.define(templateVar, subTemplate);
3414
3900
  block = this.createBlock(block, "multi", ctx);
3415
3901
  this.helpers.add("call");
3416
3902
  this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block, {
@@ -3421,7 +3907,7 @@ class CodeGenerator {
3421
3907
  else {
3422
3908
  const id = this.generateId(`callTemplate_`);
3423
3909
  this.helpers.add("getTemplate");
3424
- this.staticCalls.push({ id, template: subTemplate });
3910
+ this.staticDefs.push({ id, expr: `getTemplate(${subTemplate})` });
3425
3911
  block = this.createBlock(block, "multi", ctx);
3426
3912
  this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block, {
3427
3913
  ...ctx,
@@ -3515,26 +4001,25 @@ class CodeGenerator {
3515
4001
  compileComponent(ast, ctx) {
3516
4002
  let { block } = ctx;
3517
4003
  // props
3518
- const hasSlotsProp = "slots" in ast.props;
4004
+ const hasSlotsProp = "slots" in (ast.props || {});
3519
4005
  const props = [];
3520
- const propExpr = this.formatPropObject(ast.props);
4006
+ const propExpr = this.formatPropObject(ast.props || {});
3521
4007
  if (propExpr) {
3522
4008
  props.push(propExpr);
3523
4009
  }
3524
4010
  // slots
3525
- const hasSlot = !!Object.keys(ast.slots).length;
3526
4011
  let slotDef = "";
3527
- if (hasSlot) {
4012
+ if (ast.slots) {
3528
4013
  let ctxStr = "ctx";
3529
4014
  if (this.target.loopLevel || !this.hasSafeContext) {
3530
4015
  ctxStr = this.generateId("ctx");
3531
4016
  this.helpers.add("capture");
3532
- this.addLine(`const ${ctxStr} = capture(ctx);`);
4017
+ this.define(ctxStr, `capture(ctx)`);
3533
4018
  }
3534
4019
  let slotStr = [];
3535
4020
  for (let slotName in ast.slots) {
3536
- const slotAst = ast.slots[slotName].content;
3537
- const name = this.compileInNewTarget("slot", slotAst, ctx);
4021
+ const slotAst = ast.slots[slotName];
4022
+ const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
3538
4023
  const params = [`__render: ${name}, __ctx: ${ctxStr}`];
3539
4024
  const scope = ast.slots[slotName].scope;
3540
4025
  if (scope) {
@@ -3549,33 +4034,30 @@ class CodeGenerator {
3549
4034
  slotDef = `{${slotStr.join(", ")}}`;
3550
4035
  }
3551
4036
  if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
3552
- props.push(`slots: ${slotDef}`);
4037
+ this.helpers.add("markRaw");
4038
+ props.push(`slots: markRaw(${slotDef})`);
3553
4039
  }
3554
4040
  const propStr = `{${props.join(",")}}`;
3555
4041
  let propString = propStr;
3556
4042
  if (ast.dynamicProps) {
3557
- if (!props.length) {
3558
- propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`;
3559
- }
3560
- else {
3561
- propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`;
3562
- }
4043
+ propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${props.length ? ", " + propStr : ""})`;
3563
4044
  }
3564
4045
  let propVar;
3565
4046
  if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
3566
4047
  propVar = this.generateId("props");
3567
- this.addLine(`const ${propVar} = ${propString};`);
4048
+ this.define(propVar, propString);
3568
4049
  propString = propVar;
3569
4050
  }
3570
4051
  if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
3571
- this.addLine(`${propVar}.slots = Object.assign(${slotDef}, ${propVar}.slots)`);
4052
+ this.helpers.add("markRaw");
4053
+ this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
3572
4054
  }
3573
4055
  // cmap key
3574
4056
  const key = this.generateComponentKey();
3575
4057
  let expr;
3576
4058
  if (ast.isDynamic) {
3577
4059
  expr = this.generateId("Comp");
3578
- this.addLine(`let ${expr} = ${compileExpr(ast.name)};`);
4060
+ this.define(expr, compileExpr(ast.name));
3579
4061
  }
3580
4062
  else {
3581
4063
  expr = `\`${ast.name}\``;
@@ -3596,9 +4078,28 @@ class CodeGenerator {
3596
4078
  if (ast.isDynamic) {
3597
4079
  blockExpr = `toggler(${expr}, ${blockExpr})`;
3598
4080
  }
4081
+ // event handling
4082
+ if (ast.on) {
4083
+ blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);
4084
+ }
3599
4085
  block = this.createBlock(block, "multi", ctx);
3600
4086
  this.insertBlock(blockExpr, block, ctx);
3601
4087
  }
4088
+ wrapWithEventCatcher(expr, on) {
4089
+ this.helpers.add("createCatcher");
4090
+ let name = this.generateId("catcher");
4091
+ let spec = {};
4092
+ let handlers = [];
4093
+ for (let ev in on) {
4094
+ let handlerId = this.generateId("hdlr");
4095
+ let idx = handlers.push(handlerId) - 1;
4096
+ spec[ev] = idx;
4097
+ const handler = this.generateHandlerCode(ev, on[ev]);
4098
+ this.define(handlerId, handler);
4099
+ }
4100
+ this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });
4101
+ return `${name}(${expr}, [${handlers.join(",")}])`;
4102
+ }
3602
4103
  compileTSlot(ast, ctx) {
3603
4104
  this.helpers.add("callSlot");
3604
4105
  let { block } = ctx;
@@ -3620,13 +4121,17 @@ class CodeGenerator {
3620
4121
  else {
3621
4122
  if (dynamic) {
3622
4123
  let name = this.generateId("slot");
3623
- this.addLine(`const ${name} = ${slotName};`);
4124
+ this.define(name, slotName);
3624
4125
  blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}), ${dynamic}, ${scope})`;
3625
4126
  }
3626
4127
  else {
3627
4128
  blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
3628
4129
  }
3629
4130
  }
4131
+ // event handling
4132
+ if (ast.on) {
4133
+ blockString = this.wrapWithEventCatcher(blockString, ast.on);
4134
+ }
3630
4135
  if (block) {
3631
4136
  this.insertAnchor(block);
3632
4137
  }
@@ -3647,7 +4152,7 @@ class CodeGenerator {
3647
4152
  if (this.target.loopLevel || !this.hasSafeContext) {
3648
4153
  ctxStr = this.generateId("ctx");
3649
4154
  this.helpers.add("capture");
3650
- this.addLine(`const ${ctxStr} = capture(ctx);`);
4155
+ this.define(ctxStr, `capture(ctx);`);
3651
4156
  }
3652
4157
  const blockString = `component(Portal, {target: ${ast.target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
3653
4158
  if (block) {
@@ -3768,6 +4273,9 @@ function parseDOMNode(node, ctx) {
3768
4273
  if (tagName === "t" && !dynamicTag) {
3769
4274
  return null;
3770
4275
  }
4276
+ if (tagName.startsWith("block-")) {
4277
+ throw new Error(`Invalid tag name: '${tagName}'`);
4278
+ }
3771
4279
  ctx = Object.assign({}, ctx);
3772
4280
  if (tagName === "pre") {
3773
4281
  ctx.inPreTag = true;
@@ -3778,8 +4286,8 @@ function parseDOMNode(node, ctx) {
3778
4286
  const ref = node.getAttribute("t-ref");
3779
4287
  node.removeAttribute("t-ref");
3780
4288
  const nodeAttrsNames = node.getAttributeNames();
3781
- const attrs = {};
3782
- const on = {};
4289
+ let attrs = null;
4290
+ let on = null;
3783
4291
  let model = null;
3784
4292
  for (let attr of nodeAttrsNames) {
3785
4293
  const value = node.getAttribute(attr);
@@ -3787,6 +4295,7 @@ function parseDOMNode(node, ctx) {
3787
4295
  if (attr === "t-on") {
3788
4296
  throw new Error("Missing event name with t-on directive");
3789
4297
  }
4298
+ on = on || {};
3790
4299
  on[attr.slice(5)] = value;
3791
4300
  }
3792
4301
  else if (attr.startsWith("t-model")) {
@@ -3824,6 +4333,7 @@ function parseDOMNode(node, ctx) {
3824
4333
  targetAttr: isCheckboxInput ? "checked" : "value",
3825
4334
  specialInitTargetAttr: isRadioInput ? "checked" : null,
3826
4335
  eventType,
4336
+ hasDynamicChildren: false,
3827
4337
  shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
3828
4338
  shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
3829
4339
  };
@@ -3833,6 +4343,9 @@ function parseDOMNode(node, ctx) {
3833
4343
  ctx.tModelInfo = model;
3834
4344
  }
3835
4345
  }
4346
+ else if (attr.startsWith("block-")) {
4347
+ throw new Error(`Invalid attribute: '${attr}'`);
4348
+ }
3836
4349
  else if (attr !== "t-name") {
3837
4350
  if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
3838
4351
  throw new Error(`Unknown QWeb directive: '${attr}'`);
@@ -3841,6 +4354,7 @@ function parseDOMNode(node, ctx) {
3841
4354
  if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
3842
4355
  tModel.hasDynamicChildren = true;
3843
4356
  }
4357
+ attrs = attrs || {};
3844
4358
  attrs[attr] = value;
3845
4359
  }
3846
4360
  }
@@ -3991,7 +4505,7 @@ function parseTCall(node, ctx) {
3991
4505
  if (ast && ast.type === 11 /* TComponent */) {
3992
4506
  return {
3993
4507
  ...ast,
3994
- slots: { default: { content: tcall } },
4508
+ slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
3995
4509
  };
3996
4510
  }
3997
4511
  }
@@ -4075,7 +4589,6 @@ function parseTSetNode(node, ctx) {
4075
4589
  // -----------------------------------------------------------------------------
4076
4590
  // Error messages when trying to use an unsupported directive on a component
4077
4591
  const directiveErrorMap = new Map([
4078
- ["t-on", "t-on is no longer supported on components. Consider passing a callback in props."],
4079
4592
  [
4080
4593
  "t-ref",
4081
4594
  "t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
@@ -4104,18 +4617,26 @@ function parseComponent(node, ctx) {
4104
4617
  node.removeAttribute("t-props");
4105
4618
  const defaultSlotScope = node.getAttribute("t-slot-scope");
4106
4619
  node.removeAttribute("t-slot-scope");
4107
- const props = {};
4620
+ let on = null;
4621
+ let props = null;
4108
4622
  for (let name of node.getAttributeNames()) {
4109
4623
  const value = node.getAttribute(name);
4110
4624
  if (name.startsWith("t-")) {
4111
- const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
4112
- throw new Error(message || `unsupported directive on Component: ${name}`);
4625
+ if (name.startsWith("t-on-")) {
4626
+ on = on || {};
4627
+ on[name.slice(5)] = value;
4628
+ }
4629
+ else {
4630
+ const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
4631
+ throw new Error(message || `unsupported directive on Component: ${name}`);
4632
+ }
4113
4633
  }
4114
4634
  else {
4635
+ props = props || {};
4115
4636
  props[name] = value;
4116
4637
  }
4117
4638
  }
4118
- const slots = {};
4639
+ let slots = null;
4119
4640
  if (node.hasChildNodes()) {
4120
4641
  const clone = node.cloneNode(true);
4121
4642
  // named slots
@@ -4143,32 +4664,36 @@ function parseComponent(node, ctx) {
4143
4664
  slotNode.remove();
4144
4665
  const slotAst = parseNode(slotNode, ctx);
4145
4666
  if (slotAst) {
4146
- const slotInfo = { content: slotAst };
4147
- const attrs = {};
4667
+ let on = null;
4668
+ let attrs = null;
4669
+ let scope = null;
4148
4670
  for (let attributeName of slotNode.getAttributeNames()) {
4149
4671
  const value = slotNode.getAttribute(attributeName);
4150
4672
  if (attributeName === "t-slot-scope") {
4151
- slotInfo.scope = value;
4673
+ scope = value;
4152
4674
  continue;
4153
4675
  }
4154
- attrs[attributeName] = value;
4155
- }
4156
- if (Object.keys(attrs).length) {
4157
- slotInfo.attrs = attrs;
4676
+ else if (attributeName.startsWith("t-on-")) {
4677
+ on = on || {};
4678
+ on[attributeName.slice(5)] = value;
4679
+ }
4680
+ else {
4681
+ attrs = attrs || {};
4682
+ attrs[attributeName] = value;
4683
+ }
4158
4684
  }
4159
- slots[name] = slotInfo;
4685
+ slots = slots || {};
4686
+ slots[name] = { content: slotAst, on, attrs, scope };
4160
4687
  }
4161
4688
  }
4162
4689
  // default slot
4163
4690
  const defaultContent = parseChildNodes(clone, ctx);
4164
4691
  if (defaultContent) {
4165
- slots.default = { content: defaultContent };
4166
- if (defaultSlotScope) {
4167
- slots.default.scope = defaultSlotScope;
4168
- }
4692
+ slots = slots || {};
4693
+ slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
4169
4694
  }
4170
4695
  }
4171
- return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots };
4696
+ return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
4172
4697
  }
4173
4698
  // -----------------------------------------------------------------------------
4174
4699
  // Slots
@@ -4179,15 +4704,24 @@ function parseTSlot(node, ctx) {
4179
4704
  }
4180
4705
  const name = node.getAttribute("t-slot");
4181
4706
  node.removeAttribute("t-slot");
4182
- const attrs = {};
4707
+ let attrs = null;
4708
+ let on = null;
4183
4709
  for (let attributeName of node.getAttributeNames()) {
4184
4710
  const value = node.getAttribute(attributeName);
4185
- attrs[attributeName] = value;
4711
+ if (attributeName.startsWith("t-on-")) {
4712
+ on = on || {};
4713
+ on[attributeName.slice(5)] = value;
4714
+ }
4715
+ else {
4716
+ attrs = attrs || {};
4717
+ attrs[attributeName] = value;
4718
+ }
4186
4719
  }
4187
4720
  return {
4188
4721
  type: 14 /* TSlot */,
4189
4722
  name,
4190
4723
  attrs,
4724
+ on,
4191
4725
  defaultContent: parseChildNodes(node, ctx),
4192
4726
  };
4193
4727
  }
@@ -4381,108 +4915,112 @@ function compile(template, options = {}) {
4381
4915
  return new Function("bdom, helpers", code);
4382
4916
  }
4383
4917
 
4384
- const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
4385
- const globalTemplates = {};
4386
- function parseXML(xml) {
4387
- const parser = new DOMParser();
4388
- const doc = parser.parseFromString(xml, "text/xml");
4389
- if (doc.getElementsByTagName("parsererror").length) {
4390
- let msg = "Invalid XML in template.";
4391
- const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
4392
- if (parsererrorText) {
4393
- msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
4394
- const re = /\d+/g;
4395
- const firstMatch = re.exec(parsererrorText);
4396
- if (firstMatch) {
4397
- const lineNumber = Number(firstMatch[0]);
4398
- const line = xml.split("\n")[lineNumber - 1];
4399
- const secondMatch = re.exec(parsererrorText);
4400
- if (line && secondMatch) {
4401
- const columnIndex = Number(secondMatch[0]) - 1;
4402
- if (line[columnIndex]) {
4403
- msg +=
4404
- `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
4405
- `${line}\n${"-".repeat(columnIndex - 1)}^`;
4406
- }
4918
+ const TIMEOUT = Symbol("timeout");
4919
+ function wrapError(fn, hookName) {
4920
+ const error = new Error(`The following error occurred in ${hookName}: `);
4921
+ const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
4922
+ const node = getCurrent();
4923
+ return (...args) => {
4924
+ try {
4925
+ const result = fn(...args);
4926
+ if (result instanceof Promise) {
4927
+ if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
4928
+ const fiber = node.fiber;
4929
+ Promise.race([
4930
+ result,
4931
+ new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
4932
+ ]).then((res) => {
4933
+ if (res === TIMEOUT && node.fiber === fiber) {
4934
+ console.warn(timeoutError);
4935
+ }
4936
+ });
4407
4937
  }
4938
+ return result.catch((cause) => {
4939
+ error.cause = cause;
4940
+ if (cause instanceof Error) {
4941
+ error.message += `"${cause.message}"`;
4942
+ }
4943
+ throw error;
4944
+ });
4408
4945
  }
4946
+ return result;
4409
4947
  }
4410
- throw new Error(msg);
4411
- }
4412
- return doc;
4413
- }
4414
- class TemplateSet {
4415
- constructor(config = {}) {
4416
- this.rawTemplates = Object.create(globalTemplates);
4417
- this.templates = {};
4418
- this.utils = Object.assign({}, UTILS, {
4419
- call: (owner, subTemplate, ctx, parent, key) => {
4420
- const template = this.getTemplate(subTemplate);
4421
- return toggler(subTemplate, template.call(owner, ctx, parent, key));
4422
- },
4423
- getTemplate: (name) => this.getTemplate(name),
4424
- });
4425
- this.dev = config.dev || false;
4426
- this.translateFn = config.translateFn;
4427
- this.translatableAttributes = config.translatableAttributes;
4428
- if (config.templates) {
4429
- this.addTemplates(config.templates);
4430
- }
4431
- }
4432
- addTemplate(name, template, options = {}) {
4433
- if (name in this.rawTemplates && !options.allowDuplicate) {
4434
- throw new Error(`Template ${name} already defined`);
4435
- }
4436
- this.rawTemplates[name] = template;
4437
- }
4438
- addTemplates(xml, options = {}) {
4439
- if (!xml) {
4440
- // empty string
4441
- return;
4442
- }
4443
- xml = xml instanceof Document ? xml : parseXML(xml);
4444
- for (const template of xml.querySelectorAll("[t-name]")) {
4445
- const name = template.getAttribute("t-name");
4446
- this.addTemplate(name, template, options);
4447
- }
4448
- }
4449
- getTemplate(name) {
4450
- if (!(name in this.templates)) {
4451
- const rawTemplate = this.rawTemplates[name];
4452
- if (rawTemplate === undefined) {
4453
- throw new Error(`Missing template: "${name}"`);
4948
+ catch (cause) {
4949
+ if (cause instanceof Error) {
4950
+ error.message += `"${cause.message}"`;
4454
4951
  }
4455
- const templateFn = this._compileTemplate(name, rawTemplate);
4456
- // first add a function to lazily get the template, in case there is a
4457
- // recursive call to the template name
4458
- const templates = this.templates;
4459
- this.templates[name] = function (context, parent) {
4460
- return templates[name].call(this, context, parent);
4461
- };
4462
- const template = templateFn(bdom, this.utils);
4463
- this.templates[name] = template;
4952
+ throw error;
4464
4953
  }
4465
- return this.templates[name];
4466
- }
4467
- _compileTemplate(name, template) {
4468
- return compile(template, {
4469
- name,
4470
- dev: this.dev,
4471
- translateFn: this.translateFn,
4472
- translatableAttributes: this.translatableAttributes,
4473
- });
4474
- }
4954
+ };
4955
+ }
4956
+ // -----------------------------------------------------------------------------
4957
+ // hooks
4958
+ // -----------------------------------------------------------------------------
4959
+ function onWillStart(fn) {
4960
+ const node = getCurrent();
4961
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4962
+ node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
4963
+ }
4964
+ function onWillUpdateProps(fn) {
4965
+ const node = getCurrent();
4966
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4967
+ node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
4968
+ }
4969
+ function onMounted(fn) {
4970
+ const node = getCurrent();
4971
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4972
+ node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
4973
+ }
4974
+ function onWillPatch(fn) {
4975
+ const node = getCurrent();
4976
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4977
+ node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
4475
4978
  }
4476
- // -----------------------------------------------------------------------------
4477
- // xml tag helper
4478
- // -----------------------------------------------------------------------------
4479
- function xml(...args) {
4480
- const name = `__template__${xml.nextId++}`;
4481
- const value = String.raw(...args);
4482
- globalTemplates[name] = value;
4483
- return name;
4979
+ function onPatched(fn) {
4980
+ const node = getCurrent();
4981
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4982
+ node.patched.push(decorate(fn.bind(node.component), "onPatched"));
4484
4983
  }
4485
- xml.nextId = 1;
4984
+ function onWillUnmount(fn) {
4985
+ const node = getCurrent();
4986
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4987
+ node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
4988
+ }
4989
+ function onWillDestroy(fn) {
4990
+ const node = getCurrent();
4991
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4992
+ node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
4993
+ }
4994
+ function onWillRender(fn) {
4995
+ const node = getCurrent();
4996
+ const renderFn = node.renderFn;
4997
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4998
+ fn = decorate(fn.bind(node.component), "onWillRender");
4999
+ node.renderFn = () => {
5000
+ fn();
5001
+ return renderFn();
5002
+ };
5003
+ }
5004
+ function onRendered(fn) {
5005
+ const node = getCurrent();
5006
+ const renderFn = node.renderFn;
5007
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
5008
+ fn = decorate(fn.bind(node.component), "onRendered");
5009
+ node.renderFn = () => {
5010
+ const result = renderFn();
5011
+ fn();
5012
+ return result;
5013
+ };
5014
+ }
5015
+ function onError(callback) {
5016
+ const node = getCurrent();
5017
+ let handlers = nodeErrorHandlers.get(node);
5018
+ if (!handlers) {
5019
+ handlers = [];
5020
+ nodeErrorHandlers.set(node, handlers);
5021
+ }
5022
+ handlers.push(callback.bind(node.component));
5023
+ }
4486
5024
 
4487
5025
  class Component {
4488
5026
  constructor(props, env, node) {
@@ -4491,8 +5029,8 @@ class Component {
4491
5029
  this.__owl__ = node;
4492
5030
  }
4493
5031
  setup() { }
4494
- render() {
4495
- this.__owl__.render();
5032
+ render(deep = false) {
5033
+ this.__owl__.render(deep);
4496
5034
  }
4497
5035
  }
4498
5036
  Component.template = "";
@@ -4561,75 +5099,293 @@ Portal.props = {
4561
5099
  slots: true,
4562
5100
  };
4563
5101
 
4564
- // -----------------------------------------------------------------------------
4565
- // Scheduler
4566
- // -----------------------------------------------------------------------------
4567
- class Scheduler {
4568
- constructor() {
4569
- this.tasks = new Set();
4570
- this.isRunning = false;
4571
- this.requestAnimationFrame = Scheduler.requestAnimationFrame;
5102
+ /**
5103
+ * This file contains utility functions that will be injected in each template,
5104
+ * to perform various useful tasks in the compiled code.
5105
+ */
5106
+ function withDefault(value, defaultValue) {
5107
+ return value === undefined || value === null || value === false ? defaultValue : value;
5108
+ }
5109
+ function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
5110
+ key = key + "__slot_" + name;
5111
+ const slots = ctx.props[TARGET].slots || {};
5112
+ const { __render, __ctx, __scope } = slots[name] || {};
5113
+ const slotScope = Object.create(__ctx || {});
5114
+ if (__scope) {
5115
+ slotScope[__scope] = extra;
5116
+ }
5117
+ const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
5118
+ if (defaultContent) {
5119
+ let child1 = undefined;
5120
+ let child2 = undefined;
5121
+ if (slotBDom) {
5122
+ child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
5123
+ }
5124
+ else {
5125
+ child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
5126
+ }
5127
+ return multi([child1, child2]);
5128
+ }
5129
+ return slotBDom || text("");
5130
+ }
5131
+ function capture(ctx) {
5132
+ const component = ctx.__owl__.component;
5133
+ const result = Object.create(component);
5134
+ for (let k in ctx) {
5135
+ result[k] = ctx[k];
5136
+ }
5137
+ return result;
5138
+ }
5139
+ function withKey(elem, k) {
5140
+ elem.key = k;
5141
+ return elem;
5142
+ }
5143
+ function prepareList(collection) {
5144
+ let keys;
5145
+ let values;
5146
+ if (Array.isArray(collection)) {
5147
+ keys = collection;
5148
+ values = collection;
5149
+ }
5150
+ else if (collection) {
5151
+ values = Object.keys(collection);
5152
+ keys = Object.values(collection);
5153
+ }
5154
+ else {
5155
+ throw new Error("Invalid loop expression");
5156
+ }
5157
+ const n = values.length;
5158
+ return [keys, values, n, new Array(n)];
5159
+ }
5160
+ const isBoundary = Symbol("isBoundary");
5161
+ function setContextValue(ctx, key, value) {
5162
+ const ctx0 = ctx;
5163
+ while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
5164
+ const newCtx = ctx.__proto__;
5165
+ if (!newCtx) {
5166
+ ctx = ctx0;
5167
+ break;
5168
+ }
5169
+ ctx = newCtx;
5170
+ }
5171
+ ctx[key] = value;
5172
+ }
5173
+ function toNumber(val) {
5174
+ const n = parseFloat(val);
5175
+ return isNaN(n) ? val : n;
5176
+ }
5177
+ function shallowEqual(l1, l2) {
5178
+ for (let i = 0, l = l1.length; i < l; i++) {
5179
+ if (l1[i] !== l2[i]) {
5180
+ return false;
5181
+ }
5182
+ }
5183
+ return true;
5184
+ }
5185
+ class LazyValue {
5186
+ constructor(fn, ctx, node) {
5187
+ this.fn = fn;
5188
+ this.ctx = capture(ctx);
5189
+ this.node = node;
5190
+ }
5191
+ evaluate() {
5192
+ return this.fn(this.ctx, this.node);
5193
+ }
5194
+ toString() {
5195
+ return this.evaluate().toString();
5196
+ }
5197
+ }
5198
+ /*
5199
+ * Safely outputs `value` as a block depending on the nature of `value`
5200
+ */
5201
+ function safeOutput(value) {
5202
+ if (!value) {
5203
+ return value;
5204
+ }
5205
+ let safeKey;
5206
+ let block;
5207
+ if (value instanceof Markup) {
5208
+ safeKey = `string_safe`;
5209
+ block = html(value);
5210
+ }
5211
+ else if (value instanceof LazyValue) {
5212
+ safeKey = `lazy_value`;
5213
+ block = value.evaluate();
5214
+ }
5215
+ else if (value instanceof String || typeof value === "string") {
5216
+ safeKey = "string_unsafe";
5217
+ block = text(value);
5218
+ }
5219
+ else {
5220
+ // Assuming it is a block
5221
+ safeKey = "block_safe";
5222
+ block = value;
5223
+ }
5224
+ return toggler(safeKey, block);
5225
+ }
5226
+ let boundFunctions = new WeakMap();
5227
+ function bind(ctx, fn) {
5228
+ let component = ctx.__owl__.component;
5229
+ let boundFnMap = boundFunctions.get(component);
5230
+ if (!boundFnMap) {
5231
+ boundFnMap = new WeakMap();
5232
+ boundFunctions.set(component, boundFnMap);
5233
+ }
5234
+ let boundFn = boundFnMap.get(fn);
5235
+ if (!boundFn) {
5236
+ boundFn = fn.bind(component);
5237
+ boundFnMap.set(fn, boundFn);
5238
+ }
5239
+ return boundFn;
5240
+ }
5241
+ function multiRefSetter(refs, name) {
5242
+ let count = 0;
5243
+ return (el) => {
5244
+ if (el) {
5245
+ count++;
5246
+ if (count > 1) {
5247
+ throw new Error("Cannot have 2 elements with same ref name at the same time");
5248
+ }
5249
+ }
5250
+ if (count === 0 || el) {
5251
+ refs[name] = el;
5252
+ }
5253
+ };
5254
+ }
5255
+ const helpers = {
5256
+ withDefault,
5257
+ zero: Symbol("zero"),
5258
+ isBoundary,
5259
+ callSlot,
5260
+ capture,
5261
+ withKey,
5262
+ prepareList,
5263
+ setContextValue,
5264
+ multiRefSetter,
5265
+ shallowEqual,
5266
+ toNumber,
5267
+ validateProps,
5268
+ LazyValue,
5269
+ safeOutput,
5270
+ bind,
5271
+ createCatcher,
5272
+ };
5273
+
5274
+ const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
5275
+ function parseXML(xml) {
5276
+ const parser = new DOMParser();
5277
+ const doc = parser.parseFromString(xml, "text/xml");
5278
+ if (doc.getElementsByTagName("parsererror").length) {
5279
+ let msg = "Invalid XML in template.";
5280
+ const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
5281
+ if (parsererrorText) {
5282
+ msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
5283
+ const re = /\d+/g;
5284
+ const firstMatch = re.exec(parsererrorText);
5285
+ if (firstMatch) {
5286
+ const lineNumber = Number(firstMatch[0]);
5287
+ const line = xml.split("\n")[lineNumber - 1];
5288
+ const secondMatch = re.exec(parsererrorText);
5289
+ if (line && secondMatch) {
5290
+ const columnIndex = Number(secondMatch[0]) - 1;
5291
+ if (line[columnIndex]) {
5292
+ msg +=
5293
+ `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
5294
+ `${line}\n${"-".repeat(columnIndex - 1)}^`;
5295
+ }
5296
+ }
5297
+ }
5298
+ }
5299
+ throw new Error(msg);
4572
5300
  }
4573
- start() {
4574
- this.isRunning = true;
4575
- this.scheduleTasks();
5301
+ return doc;
5302
+ }
5303
+ /**
5304
+ * Returns the helpers object that will be injected in each template closure
5305
+ * function
5306
+ */
5307
+ function makeHelpers(getTemplate) {
5308
+ return Object.assign({}, helpers, {
5309
+ Portal,
5310
+ markRaw,
5311
+ getTemplate,
5312
+ call: (owner, subTemplate, ctx, parent, key) => {
5313
+ const template = getTemplate(subTemplate);
5314
+ return toggler(subTemplate, template.call(owner, ctx, parent, key));
5315
+ },
5316
+ });
5317
+ }
5318
+ class TemplateSet {
5319
+ constructor(config = {}) {
5320
+ this.rawTemplates = Object.create(globalTemplates);
5321
+ this.templates = {};
5322
+ this.dev = config.dev || false;
5323
+ this.translateFn = config.translateFn;
5324
+ this.translatableAttributes = config.translatableAttributes;
5325
+ if (config.templates) {
5326
+ this.addTemplates(config.templates);
5327
+ }
5328
+ this.helpers = makeHelpers(this.getTemplate.bind(this));
4576
5329
  }
4577
- stop() {
4578
- this.isRunning = false;
5330
+ addTemplate(name, template, options = {}) {
5331
+ if (name in this.rawTemplates && !options.allowDuplicate) {
5332
+ throw new Error(`Template ${name} already defined`);
5333
+ }
5334
+ this.rawTemplates[name] = template;
4579
5335
  }
4580
- addFiber(fiber) {
4581
- this.tasks.add(fiber.root);
4582
- if (!this.isRunning) {
4583
- this.start();
5336
+ addTemplates(xml, options = {}) {
5337
+ if (!xml) {
5338
+ // empty string
5339
+ return;
5340
+ }
5341
+ xml = xml instanceof Document ? xml : parseXML(xml);
5342
+ for (const template of xml.querySelectorAll("[t-name]")) {
5343
+ const name = template.getAttribute("t-name");
5344
+ this.addTemplate(name, template, options);
4584
5345
  }
4585
5346
  }
4586
- /**
4587
- * Process all current tasks. This only applies to the fibers that are ready.
4588
- * Other tasks are left unchanged.
4589
- */
4590
- flush() {
4591
- this.tasks.forEach((fiber) => {
4592
- if (fiber.root !== fiber) {
4593
- this.tasks.delete(fiber);
4594
- return;
4595
- }
4596
- const hasError = fibersInError.has(fiber);
4597
- if (hasError && fiber.counter !== 0) {
4598
- this.tasks.delete(fiber);
4599
- return;
4600
- }
4601
- if (fiber.node.status === 2 /* DESTROYED */) {
4602
- this.tasks.delete(fiber);
4603
- return;
4604
- }
4605
- if (fiber.counter === 0) {
4606
- if (!hasError) {
4607
- fiber.complete();
5347
+ getTemplate(name) {
5348
+ if (!(name in this.templates)) {
5349
+ const rawTemplate = this.rawTemplates[name];
5350
+ if (rawTemplate === undefined) {
5351
+ let extraInfo = "";
5352
+ try {
5353
+ const componentName = getCurrent().component.constructor.name;
5354
+ extraInfo = ` (for component "${componentName}")`;
4608
5355
  }
4609
- this.tasks.delete(fiber);
5356
+ catch { }
5357
+ throw new Error(`Missing template: "${name}"${extraInfo}`);
4610
5358
  }
4611
- });
4612
- if (this.tasks.size === 0) {
4613
- this.stop();
5359
+ const templateFn = this._compileTemplate(name, rawTemplate);
5360
+ // first add a function to lazily get the template, in case there is a
5361
+ // recursive call to the template name
5362
+ const templates = this.templates;
5363
+ this.templates[name] = function (context, parent) {
5364
+ return templates[name].call(this, context, parent);
5365
+ };
5366
+ const template = templateFn(bdom, this.helpers);
5367
+ this.templates[name] = template;
4614
5368
  }
5369
+ return this.templates[name];
4615
5370
  }
4616
- scheduleTasks() {
4617
- this.requestAnimationFrame(() => {
4618
- this.flush();
4619
- if (this.isRunning) {
4620
- this.scheduleTasks();
4621
- }
5371
+ _compileTemplate(name, template) {
5372
+ return compile(template, {
5373
+ name,
5374
+ dev: this.dev,
5375
+ translateFn: this.translateFn,
5376
+ translatableAttributes: this.translatableAttributes,
4622
5377
  });
4623
5378
  }
4624
- }
4625
- // capture the value of requestAnimationFrame as soon as possible, to avoid
4626
- // interactions with other code, such as test frameworks that override them
4627
- Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
5379
+ }
4628
5380
 
4629
- const DEV_MSG = `Owl is running in 'dev' mode.
5381
+ let hasBeenLogged = false;
5382
+ const DEV_MSG = () => {
5383
+ const hash = window.owl ? window.owl.__info__.hash : "master";
5384
+ return `Owl is running in 'dev' mode.
4630
5385
 
4631
5386
  This is not suitable for production use.
4632
- See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
5387
+ See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
5388
+ };
4633
5389
  class App extends TemplateSet {
4634
5390
  constructor(Root, config = {}) {
4635
5391
  super(config);
@@ -4639,11 +5395,13 @@ class App extends TemplateSet {
4639
5395
  if (config.test) {
4640
5396
  this.dev = true;
4641
5397
  }
4642
- if (this.dev && !config.test) {
4643
- console.info(DEV_MSG);
5398
+ if (this.dev && !config.test && !hasBeenLogged) {
5399
+ console.info(DEV_MSG());
5400
+ hasBeenLogged = true;
4644
5401
  }
4645
- const descrs = Object.getOwnPropertyDescriptors(config.env || {});
4646
- this.env = Object.freeze(Object.defineProperties({}, descrs));
5402
+ const env = config.env || {};
5403
+ const descrs = Object.getOwnPropertyDescriptors(env);
5404
+ this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
4647
5405
  this.props = config.props || {};
4648
5406
  }
4649
5407
  mount(target, options) {
@@ -4686,6 +5444,7 @@ class App extends TemplateSet {
4686
5444
  }
4687
5445
  destroy() {
4688
5446
  if (this.root) {
5447
+ this.scheduler.flush();
4689
5448
  this.root.destroy();
4690
5449
  }
4691
5450
  }
@@ -4706,270 +5465,6 @@ function status(component) {
4706
5465
  }
4707
5466
  }
4708
5467
 
4709
- class Memo extends Component {
4710
- constructor(props, env, node) {
4711
- super(props, env, node);
4712
- // prevent patching process conditionally
4713
- let applyPatch = false;
4714
- const patchFn = node.patch;
4715
- node.patch = () => {
4716
- if (applyPatch) {
4717
- patchFn.call(node);
4718
- applyPatch = false;
4719
- }
4720
- };
4721
- // check props change, and render/apply patch if it changed
4722
- let prevProps = props;
4723
- const updateAndRender = node.updateAndRender;
4724
- node.updateAndRender = function (props, parentFiber) {
4725
- const shouldUpdate = !shallowEqual(prevProps, props);
4726
- if (shouldUpdate) {
4727
- prevProps = props;
4728
- updateAndRender.call(node, props, parentFiber);
4729
- applyPatch = true;
4730
- }
4731
- return Promise.resolve();
4732
- };
4733
- }
4734
- }
4735
- Memo.template = xml `<t t-slot="default"/>`;
4736
- /**
4737
- * we assume that each object have the same set of keys
4738
- */
4739
- function shallowEqual(p1, p2) {
4740
- for (let k in p1) {
4741
- if (k !== "slots" && p1[k] !== p2[k]) {
4742
- return false;
4743
- }
4744
- }
4745
- return true;
4746
- }
4747
-
4748
- // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
4749
- const TARGET = Symbol("Target");
4750
- // Escape hatch to prevent reactivity system to turn something into a reactive
4751
- const SKIP = Symbol("Skip");
4752
- // Special key to subscribe to, to be notified of key creation/deletion
4753
- const KEYCHANGES = Symbol("Key changes");
4754
- const objectToString = Object.prototype.toString;
4755
- /**
4756
- * Checks whether a given value can be made into a reactive object.
4757
- *
4758
- * @param value the value to check
4759
- * @returns whether the value can be made reactive
4760
- */
4761
- function canBeMadeReactive(value) {
4762
- if (typeof value !== "object") {
4763
- return false;
4764
- }
4765
- // extract "RawType" from strings like "[object RawType]" => this lets us
4766
- // ignore many native objects such as Promise (whose toString is [object Promise])
4767
- // or Date ([object Date]).
4768
- const rawType = objectToString.call(value).slice(8, -1);
4769
- return rawType === "Object" || rawType === "Array";
4770
- }
4771
- /**
4772
- * Mark an object or array so that it is ignored by the reactivity system
4773
- *
4774
- * @param value the value to mark
4775
- * @returns the object itself
4776
- */
4777
- function markRaw(value) {
4778
- value[SKIP] = true;
4779
- return value;
4780
- }
4781
- /**
4782
- * Given a reactive objet, return the raw (non reactive) underlying object
4783
- *
4784
- * @param value a reactive value
4785
- * @returns the underlying value
4786
- */
4787
- function toRaw(value) {
4788
- return value[TARGET];
4789
- }
4790
- const targetToKeysToCallbacks = new WeakMap();
4791
- /**
4792
- * Observes a given key on a target with an callback. The callback will be
4793
- * called when the given key changes on the target.
4794
- *
4795
- * @param target the target whose key should be observed
4796
- * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
4797
- * or deletion)
4798
- * @param callback the function to call when the key changes
4799
- */
4800
- function observeTargetKey(target, key, callback) {
4801
- if (!targetToKeysToCallbacks.get(target)) {
4802
- targetToKeysToCallbacks.set(target, new Map());
4803
- }
4804
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
4805
- if (!keyToCallbacks.get(key)) {
4806
- keyToCallbacks.set(key, new Set());
4807
- }
4808
- keyToCallbacks.get(key).add(callback);
4809
- if (!callbacksToTargets.has(callback)) {
4810
- callbacksToTargets.set(callback, new Set());
4811
- }
4812
- callbacksToTargets.get(callback).add(target);
4813
- }
4814
- /**
4815
- * Notify Reactives that are observing a given target that a key has changed on
4816
- * the target.
4817
- *
4818
- * @param target target whose Reactives should be notified that the target was
4819
- * changed.
4820
- * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
4821
- * or deleted)
4822
- */
4823
- function notifyReactives(target, key) {
4824
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
4825
- if (!keyToCallbacks) {
4826
- return;
4827
- }
4828
- const callbacks = keyToCallbacks.get(key);
4829
- if (!callbacks) {
4830
- return;
4831
- }
4832
- // Loop on copy because clearReactivesForCallback will modify the set in place
4833
- for (const callback of [...callbacks]) {
4834
- clearReactivesForCallback(callback);
4835
- callback();
4836
- }
4837
- }
4838
- const callbacksToTargets = new WeakMap();
4839
- /**
4840
- * Clears all subscriptions of the Reactives associated with a given callback.
4841
- *
4842
- * @param callback the callback for which the reactives need to be cleared
4843
- */
4844
- function clearReactivesForCallback(callback) {
4845
- const targetsToClear = callbacksToTargets.get(callback);
4846
- if (!targetsToClear) {
4847
- return;
4848
- }
4849
- for (const target of targetsToClear) {
4850
- const observedKeys = targetToKeysToCallbacks.get(target);
4851
- if (!observedKeys) {
4852
- continue;
4853
- }
4854
- for (const callbacks of observedKeys.values()) {
4855
- callbacks.delete(callback);
4856
- }
4857
- }
4858
- targetsToClear.clear();
4859
- }
4860
- const reactiveCache = new WeakMap();
4861
- /**
4862
- * Creates a reactive proxy for an object. Reading data on the reactive object
4863
- * subscribes to changes to the data. Writing data on the object will cause the
4864
- * notify callback to be called if there are suscriptions to that data. Nested
4865
- * objects and arrays are automatically made reactive as well.
4866
- *
4867
- * Whenever you are notified of a change, all subscriptions are cleared, and if
4868
- * you would like to be notified of any further changes, you should go read
4869
- * the underlying data again. We assume that if you don't go read it again after
4870
- * being notified, it means that you are no longer interested in that data.
4871
- *
4872
- * Subscriptions:
4873
- * + Reading a property on an object will subscribe you to changes in the value
4874
- * of that property.
4875
- * + Accessing an object keys (eg with Object.keys or with `for..in`) will
4876
- * subscribe you to the creation/deletion of keys. Checking the presence of a
4877
- * key on the object with 'in' has the same effect.
4878
- * - getOwnPropertyDescriptor does not currently subscribe you to the property.
4879
- * This is a choice that was made because changing a key's value will trigger
4880
- * this trap and we do not want to subscribe by writes. This also means that
4881
- * Object.hasOwnProperty doesn't subscribe as it goes through this trap.
4882
- *
4883
- * @param target the object for which to create a reactive proxy
4884
- * @param callback the function to call when an observed property of the
4885
- * reactive has changed
4886
- * @returns a proxy that tracks changes to it
4887
- */
4888
- function reactive(target, callback = () => { }) {
4889
- if (!canBeMadeReactive(target)) {
4890
- throw new Error(`Cannot make the given value reactive`);
4891
- }
4892
- if (SKIP in target) {
4893
- return target;
4894
- }
4895
- const originalTarget = target[TARGET];
4896
- if (originalTarget) {
4897
- return reactive(originalTarget, callback);
4898
- }
4899
- if (!reactiveCache.has(target)) {
4900
- reactiveCache.set(target, new Map());
4901
- }
4902
- const reactivesForTarget = reactiveCache.get(target);
4903
- if (!reactivesForTarget.has(callback)) {
4904
- const proxy = new Proxy(target, {
4905
- get(target, key, proxy) {
4906
- if (key === TARGET) {
4907
- return target;
4908
- }
4909
- observeTargetKey(target, key, callback);
4910
- const value = Reflect.get(target, key, proxy);
4911
- if (!canBeMadeReactive(value)) {
4912
- return value;
4913
- }
4914
- return reactive(value, callback);
4915
- },
4916
- set(target, key, value, proxy) {
4917
- const isNewKey = !Object.hasOwnProperty.call(target, key);
4918
- const originalValue = Reflect.get(target, key, proxy);
4919
- const ret = Reflect.set(target, key, value, proxy);
4920
- if (isNewKey) {
4921
- notifyReactives(target, KEYCHANGES);
4922
- }
4923
- // While Array length may trigger the set trap, it's not actually set by this
4924
- // method but is updated behind the scenes, and the trap is not called with the
4925
- // new value. We disable the "same-value-optimization" for it because of that.
4926
- if (originalValue !== value || (Array.isArray(target) && key === "length")) {
4927
- notifyReactives(target, key);
4928
- }
4929
- return ret;
4930
- },
4931
- deleteProperty(target, key) {
4932
- const ret = Reflect.deleteProperty(target, key);
4933
- notifyReactives(target, KEYCHANGES);
4934
- notifyReactives(target, key);
4935
- return ret;
4936
- },
4937
- ownKeys(target) {
4938
- observeTargetKey(target, KEYCHANGES, callback);
4939
- return Reflect.ownKeys(target);
4940
- },
4941
- has(target, key) {
4942
- // TODO: this observes all key changes instead of only the presence of the argument key
4943
- observeTargetKey(target, KEYCHANGES, callback);
4944
- return Reflect.has(target, key);
4945
- },
4946
- });
4947
- reactivesForTarget.set(callback, proxy);
4948
- }
4949
- return reactivesForTarget.get(callback);
4950
- }
4951
- const batchedRenderFunctions = new WeakMap();
4952
- /**
4953
- * Creates a reactive object that will be observed by the current component.
4954
- * Reading data from the returned object (eg during rendering) will cause the
4955
- * component to subscribe to that data and be rerendered when it changes.
4956
- *
4957
- * @param state the state to observe
4958
- * @returns a reactive object that will cause the component to re-render on
4959
- * relevant changes
4960
- * @see reactive
4961
- */
4962
- function useState(state) {
4963
- const node = getCurrent();
4964
- if (!batchedRenderFunctions.has(node)) {
4965
- batchedRenderFunctions.set(node, batched(() => node.render()));
4966
- onWillDestroy(() => clearReactivesForCallback(render));
4967
- }
4968
- const render = batchedRenderFunctions.get(node);
4969
- const reactiveState = reactive(state, render);
4970
- return reactiveState;
4971
- }
4972
-
4973
5468
  // -----------------------------------------------------------------------------
4974
5469
  // useRef
4975
5470
  // -----------------------------------------------------------------------------
@@ -5015,10 +5510,6 @@ function useChildSubEnv(envExtension) {
5015
5510
  const node = getCurrent();
5016
5511
  node.childEnv = extendEnv(node.childEnv, envExtension);
5017
5512
  }
5018
- // -----------------------------------------------------------------------------
5019
- // useEffect
5020
- // -----------------------------------------------------------------------------
5021
- const NO_OP = () => { };
5022
5513
  /**
5023
5514
  * This hook will run a callback when a component is mounted and patched, and
5024
5515
  * will run a cleanup function before patching and before unmounting the
@@ -5036,18 +5527,20 @@ function useEffect(effect, computeDependencies = () => [NaN]) {
5036
5527
  let dependencies;
5037
5528
  onMounted(() => {
5038
5529
  dependencies = computeDependencies();
5039
- cleanup = effect(...dependencies) || NO_OP;
5530
+ cleanup = effect(...dependencies);
5040
5531
  });
5041
5532
  onPatched(() => {
5042
5533
  const newDeps = computeDependencies();
5043
5534
  const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
5044
5535
  if (shouldReapply) {
5045
5536
  dependencies = newDeps;
5046
- cleanup();
5047
- cleanup = effect(...dependencies) || NO_OP;
5537
+ if (cleanup) {
5538
+ cleanup();
5539
+ }
5540
+ cleanup = effect(...dependencies);
5048
5541
  }
5049
5542
  });
5050
- onWillUnmount(() => cleanup());
5543
+ onWillUnmount(() => cleanup && cleanup());
5051
5544
  }
5052
5545
  // -----------------------------------------------------------------------------
5053
5546
  // useExternalListener
@@ -5074,7 +5567,6 @@ function useExternalListener(target, eventName, handler, eventParams) {
5074
5567
 
5075
5568
  config.shouldNormalizeDom = false;
5076
5569
  config.mainEventHandler = mainEventHandler;
5077
- UTILS.Portal = Portal;
5078
5570
  const blockDom = {
5079
5571
  config,
5080
5572
  // bdom entry points
@@ -5095,7 +5587,6 @@ const __info__ = {};
5095
5587
  exports.App = App;
5096
5588
  exports.Component = Component;
5097
5589
  exports.EventBus = EventBus;
5098
- exports.Memo = Memo;
5099
5590
  exports.__info__ = __info__;
5100
5591
  exports.blockDom = blockDom;
5101
5592
  exports.loadFile = loadFile;
@@ -5127,7 +5618,7 @@ exports.whenReady = whenReady;
5127
5618
  exports.xml = xml;
5128
5619
 
5129
5620
 
5130
- __info__.version = '2.0.0-alpha.1';
5131
- __info__.date = '2022-02-08T12:38:37.037Z';
5132
- __info__.hash = 'e8632df';
5621
+ __info__.version = '2.0.0-beta-4';
5622
+ __info__.date = '2022-03-29T13:50:04.545Z';
5623
+ __info__.hash = '55dbc01';
5133
5624
  __info__.url = 'https://github.com/odoo/owl';