@odoo/owl 2.0.0-alpha.3 → 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.es.js CHANGED
@@ -209,7 +209,8 @@ function updateClass(val, oldVal) {
209
209
  }
210
210
  function makePropSetter(name) {
211
211
  return function setProp(value) {
212
- this[name] = value || "";
212
+ // support 0, fallback to empty string for other falsy values
213
+ this[name] = value === 0 ? 0 : value || "";
213
214
  };
214
215
  }
215
216
  function isProp(tag, key) {
@@ -263,10 +264,14 @@ function createElementHandler(evName, capture = false) {
263
264
  this[eventKey] = data;
264
265
  this.addEventListener(evName, listener, { capture });
265
266
  }
267
+ function remove() {
268
+ delete this[eventKey];
269
+ this.removeEventListener(evName, listener, { capture });
270
+ }
266
271
  function update(data) {
267
272
  this[eventKey] = data;
268
273
  }
269
- return { setup, update };
274
+ return { setup, update, remove };
270
275
  }
271
276
  // Synthetic handler: a form of event delegation that allows placing only one
272
277
  // listener per event type.
@@ -283,7 +288,10 @@ function createSyntheticHandler(evName, capture = false) {
283
288
  _data[currentId] = data;
284
289
  this[eventKey] = _data;
285
290
  }
286
- return { setup, update: setup };
291
+ function remove() {
292
+ delete this[eventKey];
293
+ }
294
+ return { setup, update: setup, remove };
287
295
  }
288
296
  function nativeToSyntheticEvent(eventKey, event) {
289
297
  let dom = event.target;
@@ -508,7 +516,7 @@ const characterDataProto = CharacterData.prototype;
508
516
  const characterDataSetData = getDescriptor$1(characterDataProto, "data").set;
509
517
  const nodeGetFirstChild = getDescriptor$1(nodeProto$2, "firstChild").get;
510
518
  const nodeGetNextSibling = getDescriptor$1(nodeProto$2, "nextSibling").get;
511
- const NO_OP$1 = () => { };
519
+ const NO_OP = () => { };
512
520
  const cache$1 = {};
513
521
  /**
514
522
  * Compiling blocks is a multi-step process:
@@ -812,7 +820,7 @@ function updateCtx(ctx, tree) {
812
820
  idx: info.idx,
813
821
  refIdx: info.refIdx,
814
822
  setData: makeRefSetter(index, ctx.refList),
815
- updateData: NO_OP$1,
823
+ updateData: NO_OP,
816
824
  });
817
825
  }
818
826
  }
@@ -1278,6 +1286,75 @@ function html(str) {
1278
1286
  return new VHtml(str);
1279
1287
  }
1280
1288
 
1289
+ function createCatcher(eventsSpec) {
1290
+ let setupFns = [];
1291
+ let removeFns = [];
1292
+ for (let name in eventsSpec) {
1293
+ let index = eventsSpec[name];
1294
+ let { setup, remove } = createEventHandler(name);
1295
+ setupFns[index] = setup;
1296
+ removeFns[index] = remove;
1297
+ }
1298
+ let n = setupFns.length;
1299
+ class VCatcher {
1300
+ constructor(child, handlers) {
1301
+ this.afterNode = null;
1302
+ this.child = child;
1303
+ this.handlers = handlers;
1304
+ }
1305
+ mount(parent, afterNode) {
1306
+ this.parentEl = parent;
1307
+ this.afterNode = afterNode;
1308
+ this.child.mount(parent, afterNode);
1309
+ for (let i = 0; i < n; i++) {
1310
+ let origFn = this.handlers[i][0];
1311
+ const self = this;
1312
+ this.handlers[i][0] = function (ev) {
1313
+ const target = ev.target;
1314
+ let currentNode = self.child.firstNode();
1315
+ const afterNode = self.afterNode;
1316
+ while (currentNode !== afterNode) {
1317
+ if (currentNode.contains(target)) {
1318
+ return origFn.call(this, ev);
1319
+ }
1320
+ currentNode = currentNode.nextSibling;
1321
+ }
1322
+ };
1323
+ setupFns[i].call(parent, this.handlers[i]);
1324
+ }
1325
+ }
1326
+ moveBefore(other, afterNode) {
1327
+ this.afterNode = null;
1328
+ this.child.moveBefore(other ? other.child : null, afterNode);
1329
+ }
1330
+ patch(other, withBeforeRemove) {
1331
+ if (this === other) {
1332
+ return;
1333
+ }
1334
+ this.handlers = other.handlers;
1335
+ this.child.patch(other.child, withBeforeRemove);
1336
+ }
1337
+ beforeRemove() {
1338
+ this.child.beforeRemove();
1339
+ }
1340
+ remove() {
1341
+ for (let i = 0; i < n; i++) {
1342
+ removeFns[i].call(this.parentEl);
1343
+ }
1344
+ this.child.remove();
1345
+ }
1346
+ firstNode() {
1347
+ return this.child.firstNode();
1348
+ }
1349
+ toString() {
1350
+ return this.child.toString();
1351
+ }
1352
+ }
1353
+ return function (child, handlers) {
1354
+ return new VCatcher(child, handlers);
1355
+ };
1356
+ }
1357
+
1281
1358
  function mount$1(vnode, fixture, afterNode = null) {
1282
1359
  vnode.mount(fixture, afterNode);
1283
1360
  }
@@ -1291,143 +1368,464 @@ function remove(vnode, withBeforeRemove = false) {
1291
1368
  vnode.remove();
1292
1369
  }
1293
1370
 
1371
+ const mainEventHandler = (data, ev, currentTarget) => {
1372
+ const { data: _data, modifiers } = filterOutModifiersFromData(data);
1373
+ data = _data;
1374
+ let stopped = false;
1375
+ if (modifiers.length) {
1376
+ let selfMode = false;
1377
+ const isSelf = ev.target === currentTarget;
1378
+ for (const mod of modifiers) {
1379
+ switch (mod) {
1380
+ case "self":
1381
+ selfMode = true;
1382
+ if (isSelf) {
1383
+ continue;
1384
+ }
1385
+ else {
1386
+ return stopped;
1387
+ }
1388
+ case "prevent":
1389
+ if ((selfMode && isSelf) || !selfMode)
1390
+ ev.preventDefault();
1391
+ continue;
1392
+ case "stop":
1393
+ if ((selfMode && isSelf) || !selfMode)
1394
+ ev.stopPropagation();
1395
+ stopped = true;
1396
+ continue;
1397
+ }
1398
+ }
1399
+ }
1400
+ // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
1401
+ // We check this rather than data[0] being truthy (or typeof function) so that it crashes
1402
+ // as expected when there is a handler expression that evaluates to a falsy value
1403
+ if (Object.hasOwnProperty.call(data, 0)) {
1404
+ const handler = data[0];
1405
+ if (typeof handler !== "function") {
1406
+ throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
1407
+ }
1408
+ let node = data[1] ? data[1].__owl__ : null;
1409
+ if (node ? node.status === 1 /* MOUNTED */ : true) {
1410
+ handler.call(node ? node.component : null, ev);
1411
+ }
1412
+ }
1413
+ return stopped;
1414
+ };
1415
+
1416
+ // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
1417
+ const TARGET = Symbol("Target");
1418
+ // Escape hatch to prevent reactivity system to turn something into a reactive
1419
+ const SKIP = Symbol("Skip");
1420
+ // Special key to subscribe to, to be notified of key creation/deletion
1421
+ const KEYCHANGES = Symbol("Key changes");
1422
+ const objectToString = Object.prototype.toString;
1423
+ const objectHasOwnProperty = Object.prototype.hasOwnProperty;
1424
+ const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
1425
+ const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
1294
1426
  /**
1295
- * Apply default props (only top level).
1427
+ * extract "RawType" from strings like "[object RawType]" => this lets us ignore
1428
+ * many native objects such as Promise (whose toString is [object Promise])
1429
+ * or Date ([object Date]), while also supporting collections without using
1430
+ * instanceof in a loop
1296
1431
  *
1297
- * Note that this method does modify in place the props
1432
+ * @param obj the object to check
1433
+ * @returns the raw type of the object
1298
1434
  */
1299
- function applyDefaultProps(props, ComponentClass) {
1300
- const defaultProps = ComponentClass.defaultProps;
1301
- if (defaultProps) {
1302
- for (let propName in defaultProps) {
1303
- if (props[propName] === undefined) {
1304
- props[propName] = defaultProps[propName];
1305
- }
1306
- }
1435
+ function rawType(obj) {
1436
+ return objectToString.call(obj).slice(8, -1);
1437
+ }
1438
+ /**
1439
+ * Checks whether a given value can be made into a reactive object.
1440
+ *
1441
+ * @param value the value to check
1442
+ * @returns whether the value can be made reactive
1443
+ */
1444
+ function canBeMadeReactive(value) {
1445
+ if (typeof value !== "object") {
1446
+ return false;
1307
1447
  }
1448
+ return SUPPORTED_RAW_TYPES.has(rawType(value));
1308
1449
  }
1309
- //------------------------------------------------------------------------------
1310
- // Prop validation helper
1311
- //------------------------------------------------------------------------------
1312
- function getPropDescription(staticProps) {
1313
- if (staticProps instanceof Array) {
1314
- return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
1450
+ /**
1451
+ * Creates a reactive from the given object/callback if possible and returns it,
1452
+ * returns the original object otherwise.
1453
+ *
1454
+ * @param value the value make reactive
1455
+ * @returns a reactive for the given object when possible, the original otherwise
1456
+ */
1457
+ function possiblyReactive(val, cb) {
1458
+ return canBeMadeReactive(val) ? reactive(val, cb) : val;
1459
+ }
1460
+ /**
1461
+ * Mark an object or array so that it is ignored by the reactivity system
1462
+ *
1463
+ * @param value the value to mark
1464
+ * @returns the object itself
1465
+ */
1466
+ function markRaw(value) {
1467
+ value[SKIP] = true;
1468
+ return value;
1469
+ }
1470
+ /**
1471
+ * Given a reactive objet, return the raw (non reactive) underlying object
1472
+ *
1473
+ * @param value a reactive value
1474
+ * @returns the underlying value
1475
+ */
1476
+ function toRaw(value) {
1477
+ return value[TARGET] || value;
1478
+ }
1479
+ const targetToKeysToCallbacks = new WeakMap();
1480
+ /**
1481
+ * Observes a given key on a target with an callback. The callback will be
1482
+ * called when the given key changes on the target.
1483
+ *
1484
+ * @param target the target whose key should be observed
1485
+ * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
1486
+ * or deletion)
1487
+ * @param callback the function to call when the key changes
1488
+ */
1489
+ function observeTargetKey(target, key, callback) {
1490
+ if (!targetToKeysToCallbacks.get(target)) {
1491
+ targetToKeysToCallbacks.set(target, new Map());
1315
1492
  }
1316
- return staticProps || { "*": true };
1493
+ const keyToCallbacks = targetToKeysToCallbacks.get(target);
1494
+ if (!keyToCallbacks.get(key)) {
1495
+ keyToCallbacks.set(key, new Set());
1496
+ }
1497
+ keyToCallbacks.get(key).add(callback);
1498
+ if (!callbacksToTargets.has(callback)) {
1499
+ callbacksToTargets.set(callback, new Set());
1500
+ }
1501
+ callbacksToTargets.get(callback).add(target);
1317
1502
  }
1318
1503
  /**
1319
- * Validate the component props (or next props) against the (static) props
1320
- * description. This is potentially an expensive operation: it may needs to
1321
- * visit recursively the props and all the children to check if they are valid.
1322
- * This is why it is only done in 'dev' mode.
1504
+ * Notify Reactives that are observing a given target that a key has changed on
1505
+ * the target.
1506
+ *
1507
+ * @param target target whose Reactives should be notified that the target was
1508
+ * changed.
1509
+ * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
1510
+ * or deleted)
1323
1511
  */
1324
- function validateProps(name, props, parent) {
1325
- const ComponentClass = typeof name !== "string"
1326
- ? name
1327
- : parent.constructor.components[name];
1328
- if (!ComponentClass) {
1329
- // this is an error, wrong component. We silently return here instead so the
1330
- // error is triggered by the usual path ('component' function)
1512
+ function notifyReactives(target, key) {
1513
+ const keyToCallbacks = targetToKeysToCallbacks.get(target);
1514
+ if (!keyToCallbacks) {
1331
1515
  return;
1332
1516
  }
1333
- applyDefaultProps(props, ComponentClass);
1334
- const defaultProps = ComponentClass.defaultProps || {};
1335
- let propsDef = getPropDescription(ComponentClass.props);
1336
- const allowAdditionalProps = "*" in propsDef;
1337
- for (let propName in propsDef) {
1338
- if (propName === "*") {
1339
- continue;
1340
- }
1341
- const propDef = propsDef[propName];
1342
- let isMandatory = !!propDef;
1343
- if (typeof propDef === "object" && "optional" in propDef) {
1344
- isMandatory = !propDef.optional;
1345
- }
1346
- if (isMandatory && propName in defaultProps) {
1347
- throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
1348
- }
1349
- if (props[propName] === undefined) {
1350
- if (isMandatory) {
1351
- throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
1352
- }
1353
- else {
1354
- continue;
1355
- }
1356
- }
1357
- let isValid;
1358
- try {
1359
- isValid = isValidProp(props[propName], propDef);
1360
- }
1361
- catch (e) {
1362
- e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
1363
- throw e;
1364
- }
1365
- if (!isValid) {
1366
- throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
1367
- }
1517
+ const callbacks = keyToCallbacks.get(key);
1518
+ if (!callbacks) {
1519
+ return;
1368
1520
  }
1369
- if (!allowAdditionalProps) {
1370
- for (let propName in props) {
1371
- if (!(propName in propsDef)) {
1372
- throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
1373
- }
1374
- }
1521
+ // Loop on copy because clearReactivesForCallback will modify the set in place
1522
+ for (const callback of [...callbacks]) {
1523
+ clearReactivesForCallback(callback);
1524
+ callback();
1375
1525
  }
1376
1526
  }
1527
+ const callbacksToTargets = new WeakMap();
1377
1528
  /**
1378
- * Check if an invidual prop value matches its (static) prop definition
1529
+ * Clears all subscriptions of the Reactives associated with a given callback.
1530
+ *
1531
+ * @param callback the callback for which the reactives need to be cleared
1379
1532
  */
1380
- function isValidProp(prop, propDef) {
1381
- if (propDef === true) {
1382
- return true;
1533
+ function clearReactivesForCallback(callback) {
1534
+ const targetsToClear = callbacksToTargets.get(callback);
1535
+ if (!targetsToClear) {
1536
+ return;
1383
1537
  }
1384
- if (typeof propDef === "function") {
1385
- // Check if a value is constructed by some Constructor. Note that there is a
1386
- // slight abuse of language: we want to consider primitive values as well.
1387
- //
1388
- // So, even though 1 is not an instance of Number, we want to consider that
1389
- // it is valid.
1390
- if (typeof prop === "object") {
1391
- return prop instanceof propDef;
1538
+ for (const target of targetsToClear) {
1539
+ const observedKeys = targetToKeysToCallbacks.get(target);
1540
+ if (!observedKeys) {
1541
+ continue;
1392
1542
  }
1393
- return typeof prop === propDef.name.toLowerCase();
1394
- }
1395
- else if (propDef instanceof Array) {
1396
- // If this code is executed, this means that we want to check if a prop
1397
- // matches at least one of its descriptor.
1398
- let result = false;
1399
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
1400
- result = result || isValidProp(prop, propDef[i]);
1543
+ for (const callbacks of observedKeys.values()) {
1544
+ callbacks.delete(callback);
1401
1545
  }
1402
- return result;
1403
1546
  }
1404
- // propsDef is an object
1405
- if (propDef.optional && prop === undefined) {
1406
- return true;
1547
+ targetsToClear.clear();
1548
+ }
1549
+ function getSubscriptions(callback) {
1550
+ const targets = callbacksToTargets.get(callback) || [];
1551
+ return [...targets].map((target) => {
1552
+ const keysToCallbacks = targetToKeysToCallbacks.get(target);
1553
+ return {
1554
+ target,
1555
+ keys: keysToCallbacks ? [...keysToCallbacks.keys()] : [],
1556
+ };
1557
+ });
1558
+ }
1559
+ const reactiveCache = new WeakMap();
1560
+ /**
1561
+ * Creates a reactive proxy for an object. Reading data on the reactive object
1562
+ * subscribes to changes to the data. Writing data on the object will cause the
1563
+ * notify callback to be called if there are suscriptions to that data. Nested
1564
+ * objects and arrays are automatically made reactive as well.
1565
+ *
1566
+ * Whenever you are notified of a change, all subscriptions are cleared, and if
1567
+ * you would like to be notified of any further changes, you should go read
1568
+ * the underlying data again. We assume that if you don't go read it again after
1569
+ * being notified, it means that you are no longer interested in that data.
1570
+ *
1571
+ * Subscriptions:
1572
+ * + Reading a property on an object will subscribe you to changes in the value
1573
+ * of that property.
1574
+ * + Accessing an object keys (eg with Object.keys or with `for..in`) will
1575
+ * subscribe you to the creation/deletion of keys. Checking the presence of a
1576
+ * key on the object with 'in' has the same effect.
1577
+ * - getOwnPropertyDescriptor does not currently subscribe you to the property.
1578
+ * This is a choice that was made because changing a key's value will trigger
1579
+ * this trap and we do not want to subscribe by writes. This also means that
1580
+ * Object.hasOwnProperty doesn't subscribe as it goes through this trap.
1581
+ *
1582
+ * @param target the object for which to create a reactive proxy
1583
+ * @param callback the function to call when an observed property of the
1584
+ * reactive has changed
1585
+ * @returns a proxy that tracks changes to it
1586
+ */
1587
+ function reactive(target, callback = () => { }) {
1588
+ if (!canBeMadeReactive(target)) {
1589
+ throw new Error(`Cannot make the given value reactive`);
1407
1590
  }
1408
- let result = propDef.type ? isValidProp(prop, propDef.type) : true;
1409
- if (propDef.validate) {
1410
- result = result && propDef.validate(prop);
1591
+ if (SKIP in target) {
1592
+ return target;
1411
1593
  }
1412
- if (propDef.type === Array && propDef.element) {
1413
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
1414
- result = result && isValidProp(prop[i], propDef.element);
1415
- }
1594
+ const originalTarget = target[TARGET];
1595
+ if (originalTarget) {
1596
+ return reactive(originalTarget, callback);
1416
1597
  }
1417
- if (propDef.type === Object && propDef.shape) {
1418
- const shape = propDef.shape;
1419
- for (let key in shape) {
1420
- result = result && isValidProp(prop[key], shape[key]);
1421
- }
1422
- if (result) {
1423
- for (let propName in prop) {
1424
- if (!(propName in shape)) {
1425
- throw new Error(`unknown prop '${propName}'`);
1426
- }
1598
+ if (!reactiveCache.has(target)) {
1599
+ reactiveCache.set(target, new Map());
1600
+ }
1601
+ const reactivesForTarget = reactiveCache.get(target);
1602
+ if (!reactivesForTarget.has(callback)) {
1603
+ const targetRawType = rawType(target);
1604
+ const handler = COLLECTION_RAWTYPES.has(targetRawType)
1605
+ ? collectionsProxyHandler(target, callback, targetRawType)
1606
+ : basicProxyHandler(callback);
1607
+ const proxy = new Proxy(target, handler);
1608
+ reactivesForTarget.set(callback, proxy);
1609
+ }
1610
+ return reactivesForTarget.get(callback);
1611
+ }
1612
+ /**
1613
+ * Creates a basic proxy handler for regular objects and arrays.
1614
+ *
1615
+ * @param callback @see reactive
1616
+ * @returns a proxy handler object
1617
+ */
1618
+ function basicProxyHandler(callback) {
1619
+ return {
1620
+ get(target, key, proxy) {
1621
+ if (key === TARGET) {
1622
+ return target;
1623
+ }
1624
+ // non-writable non-configurable properties cannot be made reactive
1625
+ const desc = Object.getOwnPropertyDescriptor(target, key);
1626
+ if (desc && !desc.writable && !desc.configurable) {
1627
+ return Reflect.get(target, key, proxy);
1628
+ }
1629
+ observeTargetKey(target, key, callback);
1630
+ return possiblyReactive(Reflect.get(target, key, proxy), callback);
1631
+ },
1632
+ set(target, key, value, proxy) {
1633
+ const isNewKey = !objectHasOwnProperty.call(target, key);
1634
+ const originalValue = Reflect.get(target, key, proxy);
1635
+ const ret = Reflect.set(target, key, value, proxy);
1636
+ if (isNewKey) {
1637
+ notifyReactives(target, KEYCHANGES);
1638
+ }
1639
+ // While Array length may trigger the set trap, it's not actually set by this
1640
+ // method but is updated behind the scenes, and the trap is not called with the
1641
+ // new value. We disable the "same-value-optimization" for it because of that.
1642
+ if (originalValue !== value || (Array.isArray(target) && key === "length")) {
1643
+ notifyReactives(target, key);
1427
1644
  }
1645
+ return ret;
1646
+ },
1647
+ deleteProperty(target, key) {
1648
+ const ret = Reflect.deleteProperty(target, key);
1649
+ // TODO: only notify when something was actually deleted
1650
+ notifyReactives(target, KEYCHANGES);
1651
+ notifyReactives(target, key);
1652
+ return ret;
1653
+ },
1654
+ ownKeys(target) {
1655
+ observeTargetKey(target, KEYCHANGES, callback);
1656
+ return Reflect.ownKeys(target);
1657
+ },
1658
+ has(target, key) {
1659
+ // TODO: this observes all key changes instead of only the presence of the argument key
1660
+ // observing the key itself would observe value changes instead of presence changes
1661
+ // so we may need a finer grained system to distinguish observing value vs presence.
1662
+ observeTargetKey(target, KEYCHANGES, callback);
1663
+ return Reflect.has(target, key);
1664
+ },
1665
+ };
1666
+ }
1667
+ /**
1668
+ * Creates a function that will observe the key that is passed to it when called
1669
+ * and delegates to the underlying method.
1670
+ *
1671
+ * @param methodName name of the method to delegate to
1672
+ * @param target @see reactive
1673
+ * @param callback @see reactive
1674
+ */
1675
+ function makeKeyObserver(methodName, target, callback) {
1676
+ return (key) => {
1677
+ key = toRaw(key);
1678
+ observeTargetKey(target, key, callback);
1679
+ return possiblyReactive(target[methodName](key), callback);
1680
+ };
1681
+ }
1682
+ /**
1683
+ * Creates an iterable that will delegate to the underlying iteration method and
1684
+ * observe keys as necessary.
1685
+ *
1686
+ * @param methodName name of the method to delegate to
1687
+ * @param target @see reactive
1688
+ * @param callback @see reactive
1689
+ */
1690
+ function makeIteratorObserver(methodName, target, callback) {
1691
+ return function* () {
1692
+ observeTargetKey(target, KEYCHANGES, callback);
1693
+ const keys = target.keys();
1694
+ for (const item of target[methodName]()) {
1695
+ const key = keys.next().value;
1696
+ observeTargetKey(target, key, callback);
1697
+ yield possiblyReactive(item, callback);
1428
1698
  }
1429
- }
1430
- return result;
1699
+ };
1700
+ }
1701
+ /**
1702
+ * Creates a forEach function that will delegate to forEach on the underlying
1703
+ * collection while observing key changes, and keys as they're iterated over,
1704
+ * and making the passed keys/values reactive.
1705
+ *
1706
+ * @param target @see reactive
1707
+ * @param callback @see reactive
1708
+ */
1709
+ function makeForEachObserver(target, callback) {
1710
+ return function forEach(forEachCb, thisArg) {
1711
+ observeTargetKey(target, KEYCHANGES, callback);
1712
+ target.forEach(function (val, key, targetObj) {
1713
+ observeTargetKey(target, key, callback);
1714
+ forEachCb.call(thisArg, possiblyReactive(val, callback), possiblyReactive(key, callback), possiblyReactive(targetObj, callback));
1715
+ }, thisArg);
1716
+ };
1717
+ }
1718
+ /**
1719
+ * Creates a function that will delegate to an underlying method, and check if
1720
+ * that method has modified the presence or value of a key, and notify the
1721
+ * reactives appropriately.
1722
+ *
1723
+ * @param setterName name of the method to delegate to
1724
+ * @param getterName name of the method which should be used to retrieve the
1725
+ * value before calling the delegate method for comparison purposes
1726
+ * @param target @see reactive
1727
+ */
1728
+ function delegateAndNotify(setterName, getterName, target) {
1729
+ return (key, value) => {
1730
+ key = toRaw(key);
1731
+ const hadKey = target.has(key);
1732
+ const originalValue = target[getterName](key);
1733
+ const ret = target[setterName](key, value);
1734
+ const hasKey = target.has(key);
1735
+ if (hadKey !== hasKey) {
1736
+ notifyReactives(target, KEYCHANGES);
1737
+ }
1738
+ if (originalValue !== value) {
1739
+ notifyReactives(target, key);
1740
+ }
1741
+ return ret;
1742
+ };
1743
+ }
1744
+ /**
1745
+ * Creates a function that will clear the underlying collection and notify that
1746
+ * the keys of the collection have changed.
1747
+ *
1748
+ * @param target @see reactive
1749
+ */
1750
+ function makeClearNotifier(target) {
1751
+ return () => {
1752
+ const allKeys = [...target.keys()];
1753
+ target.clear();
1754
+ notifyReactives(target, KEYCHANGES);
1755
+ for (const key of allKeys) {
1756
+ notifyReactives(target, key);
1757
+ }
1758
+ };
1759
+ }
1760
+ /**
1761
+ * Maps raw type of an object to an object containing functions that can be used
1762
+ * to build an appropritate proxy handler for that raw type. Eg: when making a
1763
+ * reactive set, calling the has method should mark the key that is being
1764
+ * retrieved as observed, and calling the add or delete method should notify the
1765
+ * reactives that the key which is being added or deleted has been modified.
1766
+ */
1767
+ const rawTypeToFuncHandlers = {
1768
+ Set: (target, callback) => ({
1769
+ has: makeKeyObserver("has", target, callback),
1770
+ add: delegateAndNotify("add", "has", target),
1771
+ delete: delegateAndNotify("delete", "has", target),
1772
+ keys: makeIteratorObserver("keys", target, callback),
1773
+ values: makeIteratorObserver("values", target, callback),
1774
+ entries: makeIteratorObserver("entries", target, callback),
1775
+ [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
1776
+ forEach: makeForEachObserver(target, callback),
1777
+ clear: makeClearNotifier(target),
1778
+ get size() {
1779
+ observeTargetKey(target, KEYCHANGES, callback);
1780
+ return target.size;
1781
+ },
1782
+ }),
1783
+ Map: (target, callback) => ({
1784
+ has: makeKeyObserver("has", target, callback),
1785
+ get: makeKeyObserver("get", target, callback),
1786
+ set: delegateAndNotify("set", "get", target),
1787
+ delete: delegateAndNotify("delete", "has", target),
1788
+ keys: makeIteratorObserver("keys", target, callback),
1789
+ values: makeIteratorObserver("values", target, callback),
1790
+ entries: makeIteratorObserver("entries", target, callback),
1791
+ [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
1792
+ forEach: makeForEachObserver(target, callback),
1793
+ clear: makeClearNotifier(target),
1794
+ get size() {
1795
+ observeTargetKey(target, KEYCHANGES, callback);
1796
+ return target.size;
1797
+ },
1798
+ }),
1799
+ WeakMap: (target, callback) => ({
1800
+ has: makeKeyObserver("has", target, callback),
1801
+ get: makeKeyObserver("get", target, callback),
1802
+ set: delegateAndNotify("set", "get", target),
1803
+ delete: delegateAndNotify("delete", "has", target),
1804
+ }),
1805
+ };
1806
+ /**
1807
+ * Creates a proxy handler for collections (Set/Map/WeakMap)
1808
+ *
1809
+ * @param callback @see reactive
1810
+ * @param target @see reactive
1811
+ * @returns a proxy handler object
1812
+ */
1813
+ function collectionsProxyHandler(target, callback, targetRawType) {
1814
+ // TODO: if performance is an issue we can create the special handlers lazily when each
1815
+ // property is read.
1816
+ const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
1817
+ return Object.assign(basicProxyHandler(callback), {
1818
+ get(target, key) {
1819
+ if (key === TARGET) {
1820
+ return target;
1821
+ }
1822
+ if (objectHasOwnProperty.call(specialHandlers, key)) {
1823
+ return specialHandlers[key];
1824
+ }
1825
+ observeTargetKey(target, key, callback);
1826
+ return possiblyReactive(target[key], callback);
1827
+ },
1828
+ });
1431
1829
  }
1432
1830
 
1433
1831
  /**
@@ -1445,11 +1843,13 @@ function batched(callback) {
1445
1843
  await Promise.resolve();
1446
1844
  if (!called) {
1447
1845
  called = true;
1448
- callback();
1449
1846
  // wait for all calls in this microtick to fall through before resetting "called"
1450
- // so that only the first call to the batched function calls the original callback
1451
- await Promise.resolve();
1452
- called = false;
1847
+ // so that only the first call to the batched function calls the original callback.
1848
+ // Schedule this before calling the callback so that calls to the batched function
1849
+ // within the callback will proceed only after resetting called to false, and have
1850
+ // a chance to execute the callback again
1851
+ Promise.resolve().then(() => (called = false));
1852
+ callback();
1453
1853
  }
1454
1854
  };
1455
1855
  }
@@ -1496,769 +1896,580 @@ class Markup extends String {
1496
1896
  */
1497
1897
  function markup(value) {
1498
1898
  return new Markup(value);
1499
- }
1899
+ }
1900
+ // -----------------------------------------------------------------------------
1901
+ // xml tag helper
1902
+ // -----------------------------------------------------------------------------
1903
+ const globalTemplates = {};
1904
+ function xml(...args) {
1905
+ const name = `__template__${xml.nextId++}`;
1906
+ const value = String.raw(...args);
1907
+ globalTemplates[name] = value;
1908
+ return name;
1909
+ }
1910
+ xml.nextId = 1;
1500
1911
 
1501
- // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
1502
- const TARGET = Symbol("Target");
1503
- // Escape hatch to prevent reactivity system to turn something into a reactive
1504
- const SKIP = Symbol("Skip");
1505
- // Special key to subscribe to, to be notified of key creation/deletion
1506
- const KEYCHANGES = Symbol("Key changes");
1507
- const objectToString = Object.prototype.toString;
1508
- /**
1509
- * Checks whether a given value can be made into a reactive object.
1510
- *
1511
- * @param value the value to check
1512
- * @returns whether the value can be made reactive
1513
- */
1514
- function canBeMadeReactive(value) {
1515
- if (typeof value !== "object") {
1912
+ // Maps fibers to thrown errors
1913
+ const fibersInError = new WeakMap();
1914
+ const nodeErrorHandlers = new WeakMap();
1915
+ function _handleError(node, error, isFirstRound = false) {
1916
+ if (!node) {
1516
1917
  return false;
1517
1918
  }
1518
- // extract "RawType" from strings like "[object RawType]" => this lets us
1519
- // ignore many native objects such as Promise (whose toString is [object Promise])
1520
- // or Date ([object Date]).
1521
- const rawType = objectToString.call(value).slice(8, -1);
1522
- return rawType === "Object" || rawType === "Array";
1523
- }
1524
- /**
1525
- * Mark an object or array so that it is ignored by the reactivity system
1526
- *
1527
- * @param value the value to mark
1528
- * @returns the object itself
1529
- */
1530
- function markRaw(value) {
1531
- value[SKIP] = true;
1532
- return value;
1533
- }
1534
- /**
1535
- * Given a reactive objet, return the raw (non reactive) underlying object
1536
- *
1537
- * @param value a reactive value
1538
- * @returns the underlying value
1539
- */
1540
- function toRaw(value) {
1541
- return value[TARGET] || value;
1542
- }
1543
- const targetToKeysToCallbacks = new WeakMap();
1544
- /**
1545
- * Observes a given key on a target with an callback. The callback will be
1546
- * called when the given key changes on the target.
1547
- *
1548
- * @param target the target whose key should be observed
1549
- * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
1550
- * or deletion)
1551
- * @param callback the function to call when the key changes
1552
- */
1553
- function observeTargetKey(target, key, callback) {
1554
- if (!targetToKeysToCallbacks.get(target)) {
1555
- targetToKeysToCallbacks.set(target, new Map());
1919
+ const fiber = node.fiber;
1920
+ if (fiber) {
1921
+ fibersInError.set(fiber, error);
1556
1922
  }
1557
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
1558
- if (!keyToCallbacks.get(key)) {
1559
- keyToCallbacks.set(key, new Set());
1923
+ const errorHandlers = nodeErrorHandlers.get(node);
1924
+ if (errorHandlers) {
1925
+ let stopped = false;
1926
+ // execute in the opposite order
1927
+ for (let i = errorHandlers.length - 1; i >= 0; i--) {
1928
+ try {
1929
+ errorHandlers[i](error);
1930
+ stopped = true;
1931
+ break;
1932
+ }
1933
+ catch (e) {
1934
+ error = e;
1935
+ }
1936
+ }
1937
+ if (stopped) {
1938
+ if (isFirstRound && fiber && fiber.node.fiber) {
1939
+ const root = fiber.root;
1940
+ root.setCounter(root.counter - 1);
1941
+ }
1942
+ return true;
1943
+ }
1560
1944
  }
1561
- keyToCallbacks.get(key).add(callback);
1562
- if (!callbacksToTargets.has(callback)) {
1563
- callbacksToTargets.set(callback, new Set());
1945
+ return _handleError(node.parent, error);
1946
+ }
1947
+ function handleError(params) {
1948
+ const error = params.error;
1949
+ const node = "node" in params ? params.node : params.fiber.node;
1950
+ const fiber = "fiber" in params ? params.fiber : node.fiber;
1951
+ // resets the fibers on components if possible. This is important so that
1952
+ // new renderings can be properly included in the initial one, if any.
1953
+ let current = fiber;
1954
+ do {
1955
+ current.node.fiber = current;
1956
+ current = current.parent;
1957
+ } while (current);
1958
+ fibersInError.set(fiber.root, error);
1959
+ const handled = _handleError(node, error, true);
1960
+ if (!handled) {
1961
+ console.warn(`[Owl] Unhandled error. Destroying the root component`);
1962
+ try {
1963
+ node.app.destroy();
1964
+ }
1965
+ catch (e) {
1966
+ console.error(e);
1967
+ }
1564
1968
  }
1565
- callbacksToTargets.get(callback).add(target);
1969
+ }
1970
+
1971
+ function makeChildFiber(node, parent) {
1972
+ let current = node.fiber;
1973
+ if (current) {
1974
+ cancelFibers(current.children);
1975
+ current.root = null;
1976
+ if (current instanceof RootFiber && current.delayedRenders.length) {
1977
+ let root = parent.root;
1978
+ root.delayedRenders = root.delayedRenders.concat(current.delayedRenders);
1979
+ }
1980
+ }
1981
+ return new Fiber(node, parent);
1566
1982
  }
1567
- /**
1568
- * Notify Reactives that are observing a given target that a key has changed on
1569
- * the target.
1570
- *
1571
- * @param target target whose Reactives should be notified that the target was
1572
- * changed.
1573
- * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
1574
- * or deleted)
1575
- */
1576
- function notifyReactives(target, key) {
1577
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
1578
- if (!keyToCallbacks) {
1579
- return;
1983
+ function makeRootFiber(node) {
1984
+ let current = node.fiber;
1985
+ if (current) {
1986
+ let root = current.root;
1987
+ root.setCounter(root.counter + 1 - cancelFibers(current.children));
1988
+ current.children = [];
1989
+ current.bdom = null;
1990
+ if (current === root) {
1991
+ root.reachedChildren = new WeakSet();
1992
+ }
1993
+ if (fibersInError.has(current)) {
1994
+ fibersInError.delete(current);
1995
+ fibersInError.delete(root);
1996
+ current.appliedToDom = false;
1997
+ }
1998
+ return current;
1580
1999
  }
1581
- const callbacks = keyToCallbacks.get(key);
1582
- if (!callbacks) {
1583
- return;
2000
+ const fiber = new RootFiber(node, null);
2001
+ if (node.willPatch.length) {
2002
+ fiber.willPatch.push(fiber);
1584
2003
  }
1585
- // Loop on copy because clearReactivesForCallback will modify the set in place
1586
- for (const callback of [...callbacks]) {
1587
- clearReactivesForCallback(callback);
1588
- callback();
2004
+ if (node.patched.length) {
2005
+ fiber.patched.push(fiber);
1589
2006
  }
2007
+ return fiber;
1590
2008
  }
1591
- const callbacksToTargets = new WeakMap();
1592
2009
  /**
1593
- * Clears all subscriptions of the Reactives associated with a given callback.
1594
- *
1595
- * @param callback the callback for which the reactives need to be cleared
2010
+ * @returns number of not-yet rendered fibers cancelled
1596
2011
  */
1597
- function clearReactivesForCallback(callback) {
1598
- const targetsToClear = callbacksToTargets.get(callback);
1599
- if (!targetsToClear) {
1600
- return;
1601
- }
1602
- for (const target of targetsToClear) {
1603
- const observedKeys = targetToKeysToCallbacks.get(target);
1604
- if (!observedKeys) {
1605
- continue;
2012
+ function cancelFibers(fibers) {
2013
+ let result = 0;
2014
+ for (let fiber of fibers) {
2015
+ fiber.node.fiber = null;
2016
+ if (fiber.bdom) {
2017
+ // if fiber has been rendered, this means that the component props have
2018
+ // been updated. however, this fiber will not be patched to the dom, so
2019
+ // it could happen that the next render compare the current props with
2020
+ // the same props, and skip the render completely. With the next line,
2021
+ // we kindly request the component code to force a render, so it works as
2022
+ // expected.
2023
+ fiber.node.forceNextRender = true;
1606
2024
  }
1607
- for (const callbacks of observedKeys.values()) {
1608
- callbacks.delete(callback);
2025
+ else {
2026
+ result++;
1609
2027
  }
2028
+ result += cancelFibers(fiber.children);
1610
2029
  }
1611
- targetsToClear.clear();
2030
+ return result;
1612
2031
  }
1613
- const reactiveCache = new WeakMap();
1614
- /**
1615
- * Creates a reactive proxy for an object. Reading data on the reactive object
1616
- * subscribes to changes to the data. Writing data on the object will cause the
1617
- * notify callback to be called if there are suscriptions to that data. Nested
1618
- * objects and arrays are automatically made reactive as well.
1619
- *
1620
- * Whenever you are notified of a change, all subscriptions are cleared, and if
1621
- * you would like to be notified of any further changes, you should go read
1622
- * the underlying data again. We assume that if you don't go read it again after
1623
- * being notified, it means that you are no longer interested in that data.
1624
- *
1625
- * Subscriptions:
1626
- * + Reading a property on an object will subscribe you to changes in the value
1627
- * of that property.
1628
- * + Accessing an object keys (eg with Object.keys or with `for..in`) will
1629
- * subscribe you to the creation/deletion of keys. Checking the presence of a
1630
- * key on the object with 'in' has the same effect.
1631
- * - getOwnPropertyDescriptor does not currently subscribe you to the property.
1632
- * This is a choice that was made because changing a key's value will trigger
1633
- * this trap and we do not want to subscribe by writes. This also means that
1634
- * Object.hasOwnProperty doesn't subscribe as it goes through this trap.
1635
- *
1636
- * @param target the object for which to create a reactive proxy
1637
- * @param callback the function to call when an observed property of the
1638
- * reactive has changed
1639
- * @returns a proxy that tracks changes to it
1640
- */
1641
- function reactive(target, callback = () => { }) {
1642
- if (!canBeMadeReactive(target)) {
1643
- throw new Error(`Cannot make the given value reactive`);
2032
+ class Fiber {
2033
+ constructor(node, parent) {
2034
+ this.bdom = null;
2035
+ this.children = [];
2036
+ this.appliedToDom = false;
2037
+ this.deep = false;
2038
+ this.node = node;
2039
+ this.parent = parent;
2040
+ if (parent) {
2041
+ this.deep = parent.deep;
2042
+ const root = parent.root;
2043
+ root.setCounter(root.counter + 1);
2044
+ this.root = root;
2045
+ parent.children.push(this);
2046
+ }
2047
+ else {
2048
+ this.root = this;
2049
+ }
1644
2050
  }
1645
- if (SKIP in target) {
1646
- return target;
2051
+ render() {
2052
+ // if some parent has a fiber => register in followup
2053
+ let prev = this.root.node;
2054
+ let current = prev.parent;
2055
+ while (current) {
2056
+ if (current.fiber) {
2057
+ let root = current.fiber.root;
2058
+ if (root.counter) {
2059
+ root.delayedRenders.push(this);
2060
+ return;
2061
+ }
2062
+ else {
2063
+ if (!root.reachedChildren.has(prev)) {
2064
+ // is dead
2065
+ this.node.app.scheduler.shouldClear = true;
2066
+ return;
2067
+ }
2068
+ current = root.node;
2069
+ }
2070
+ }
2071
+ prev = current;
2072
+ current = current.parent;
2073
+ }
2074
+ // there are no current rendering from above => we can render
2075
+ this._render();
1647
2076
  }
1648
- const originalTarget = target[TARGET];
1649
- if (originalTarget) {
1650
- return reactive(originalTarget, callback);
2077
+ _render() {
2078
+ const node = this.node;
2079
+ const root = this.root;
2080
+ if (root) {
2081
+ try {
2082
+ this.bdom = node.renderFn();
2083
+ root.setCounter(root.counter - 1);
2084
+ }
2085
+ catch (e) {
2086
+ handleError({ node, error: e });
2087
+ }
2088
+ }
1651
2089
  }
1652
- if (!reactiveCache.has(target)) {
1653
- reactiveCache.set(target, new Map());
2090
+ }
2091
+ class RootFiber extends Fiber {
2092
+ constructor() {
2093
+ super(...arguments);
2094
+ this.counter = 1;
2095
+ // only add stuff in this if they have registered some hooks
2096
+ this.willPatch = [];
2097
+ this.patched = [];
2098
+ this.mounted = [];
2099
+ // A fiber is typically locked when it is completing and the patch has not, or is being applied.
2100
+ // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
2101
+ this.locked = false;
2102
+ this.delayedRenders = [];
2103
+ this.reachedChildren = new WeakSet();
1654
2104
  }
1655
- const reactivesForTarget = reactiveCache.get(target);
1656
- if (!reactivesForTarget.has(callback)) {
1657
- const proxy = new Proxy(target, {
1658
- get(target, key, proxy) {
1659
- if (key === TARGET) {
1660
- return target;
1661
- }
1662
- observeTargetKey(target, key, callback);
1663
- const value = Reflect.get(target, key, proxy);
1664
- if (!canBeMadeReactive(value)) {
1665
- return value;
2105
+ complete() {
2106
+ const node = this.node;
2107
+ this.locked = true;
2108
+ let current = undefined;
2109
+ try {
2110
+ // Step 1: calling all willPatch lifecycle hooks
2111
+ for (current of this.willPatch) {
2112
+ // because of the asynchronous nature of the rendering, some parts of the
2113
+ // UI may have been rendered, then deleted in a followup rendering, and we
2114
+ // do not want to call onWillPatch in that case.
2115
+ let node = current.node;
2116
+ if (node.fiber === current) {
2117
+ const component = node.component;
2118
+ for (let cb of node.willPatch) {
2119
+ cb.call(component);
2120
+ }
1666
2121
  }
1667
- return reactive(value, callback);
1668
- },
1669
- set(target, key, value, proxy) {
1670
- const isNewKey = !Object.hasOwnProperty.call(target, key);
1671
- const originalValue = Reflect.get(target, key, proxy);
1672
- const ret = Reflect.set(target, key, value, proxy);
1673
- if (isNewKey) {
1674
- notifyReactives(target, KEYCHANGES);
2122
+ }
2123
+ current = undefined;
2124
+ // Step 2: patching the dom
2125
+ node._patch();
2126
+ this.locked = false;
2127
+ // Step 4: calling all mounted lifecycle hooks
2128
+ let mountedFibers = this.mounted;
2129
+ while ((current = mountedFibers.pop())) {
2130
+ current = current;
2131
+ if (current.appliedToDom) {
2132
+ for (let cb of current.node.mounted) {
2133
+ cb();
2134
+ }
1675
2135
  }
1676
- // While Array length may trigger the set trap, it's not actually set by this
1677
- // method but is updated behind the scenes, and the trap is not called with the
1678
- // new value. We disable the "same-value-optimization" for it because of that.
1679
- if (originalValue !== value || (Array.isArray(target) && key === "length")) {
1680
- notifyReactives(target, key);
2136
+ }
2137
+ // Step 5: calling all patched hooks
2138
+ let patchedFibers = this.patched;
2139
+ while ((current = patchedFibers.pop())) {
2140
+ current = current;
2141
+ if (current.appliedToDom) {
2142
+ for (let cb of current.node.patched) {
2143
+ cb();
2144
+ }
1681
2145
  }
1682
- return ret;
1683
- },
1684
- deleteProperty(target, key) {
1685
- const ret = Reflect.deleteProperty(target, key);
1686
- notifyReactives(target, KEYCHANGES);
1687
- notifyReactives(target, key);
1688
- return ret;
1689
- },
1690
- ownKeys(target) {
1691
- observeTargetKey(target, KEYCHANGES, callback);
1692
- return Reflect.ownKeys(target);
1693
- },
1694
- has(target, key) {
1695
- // TODO: this observes all key changes instead of only the presence of the argument key
1696
- observeTargetKey(target, KEYCHANGES, callback);
1697
- return Reflect.has(target, key);
1698
- },
1699
- });
1700
- reactivesForTarget.set(callback, proxy);
1701
- }
1702
- return reactivesForTarget.get(callback);
1703
- }
1704
-
1705
- /**
1706
- * This file contains utility functions that will be injected in each template,
1707
- * to perform various useful tasks in the compiled code.
1708
- */
1709
- function withDefault(value, defaultValue) {
1710
- return value === undefined || value === null || value === false ? defaultValue : value;
1711
- }
1712
- function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
1713
- key = key + "__slot_" + name;
1714
- const slots = ctx.props[TARGET].slots || {};
1715
- const { __render, __ctx, __scope } = slots[name] || {};
1716
- const slotScope = Object.create(__ctx || {});
1717
- if (__scope) {
1718
- slotScope[__scope] = extra || {};
1719
- }
1720
- const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
1721
- if (defaultContent) {
1722
- let child1 = undefined;
1723
- let child2 = undefined;
1724
- if (slotBDom) {
1725
- child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
2146
+ }
1726
2147
  }
1727
- else {
1728
- child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
2148
+ catch (e) {
2149
+ this.locked = false;
2150
+ handleError({ fiber: current || this, error: e });
1729
2151
  }
1730
- return multi([child1, child2]);
1731
2152
  }
1732
- return slotBDom || text("");
1733
- }
1734
- function capture(ctx) {
1735
- const component = ctx.__owl__.component;
1736
- const result = Object.create(component);
1737
- for (let k in ctx) {
1738
- result[k] = ctx[k];
2153
+ setCounter(newValue) {
2154
+ this.counter = newValue;
2155
+ if (newValue === 0) {
2156
+ if (this.delayedRenders.length) {
2157
+ for (let f of this.delayedRenders) {
2158
+ if (f.root) {
2159
+ f.render();
2160
+ }
2161
+ }
2162
+ this.delayedRenders = [];
2163
+ }
2164
+ this.node.app.scheduler.flush();
2165
+ }
1739
2166
  }
1740
- return result;
1741
- }
1742
- function withKey(elem, k) {
1743
- elem.key = k;
1744
- return elem;
1745
2167
  }
1746
- function prepareList(collection) {
1747
- let keys;
1748
- let values;
1749
- if (Array.isArray(collection)) {
1750
- keys = collection;
1751
- values = collection;
1752
- }
1753
- else if (collection) {
1754
- values = Object.keys(collection);
1755
- keys = Object.values(collection);
2168
+ class MountFiber extends RootFiber {
2169
+ constructor(node, target, options = {}) {
2170
+ super(node, null);
2171
+ this.target = target;
2172
+ this.position = options.position || "last-child";
1756
2173
  }
1757
- else {
1758
- throw new Error("Invalid loop expression");
2174
+ complete() {
2175
+ let current = this;
2176
+ try {
2177
+ const node = this.node;
2178
+ node.app.constructor.validateTarget(this.target);
2179
+ if (node.bdom) {
2180
+ // this is a complicated situation: if we mount a fiber with an existing
2181
+ // bdom, this means that this same fiber was already completed, mounted,
2182
+ // but a crash occurred in some mounted hook. Then, it was handled and
2183
+ // the new rendering is being applied.
2184
+ node.updateDom();
2185
+ }
2186
+ else {
2187
+ node.bdom = this.bdom;
2188
+ if (this.position === "last-child" || this.target.childNodes.length === 0) {
2189
+ mount$1(node.bdom, this.target);
2190
+ }
2191
+ else {
2192
+ const firstChild = this.target.childNodes[0];
2193
+ mount$1(node.bdom, this.target, firstChild);
2194
+ }
2195
+ }
2196
+ // unregistering the fiber before mounted since it can do another render
2197
+ // and that the current rendering is obviously completed
2198
+ node.fiber = null;
2199
+ node.status = 1 /* MOUNTED */;
2200
+ this.appliedToDom = true;
2201
+ let mountedFibers = this.mounted;
2202
+ while ((current = mountedFibers.pop())) {
2203
+ if (current.appliedToDom) {
2204
+ for (let cb of current.node.mounted) {
2205
+ cb();
2206
+ }
2207
+ }
2208
+ }
2209
+ }
2210
+ catch (e) {
2211
+ handleError({ fiber: current, error: e });
2212
+ }
1759
2213
  }
1760
- const n = values.length;
1761
- return [keys, values, n, new Array(n)];
1762
- }
1763
- const isBoundary = Symbol("isBoundary");
1764
- function setContextValue(ctx, key, value) {
1765
- const ctx0 = ctx;
1766
- while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
1767
- const newCtx = ctx.__proto__;
1768
- if (!newCtx) {
1769
- ctx = ctx0;
1770
- break;
2214
+ }
2215
+
2216
+ /**
2217
+ * Apply default props (only top level).
2218
+ *
2219
+ * Note that this method does modify in place the props
2220
+ */
2221
+ function applyDefaultProps(props, ComponentClass) {
2222
+ const defaultProps = ComponentClass.defaultProps;
2223
+ if (defaultProps) {
2224
+ for (let propName in defaultProps) {
2225
+ if (props[propName] === undefined) {
2226
+ props[propName] = defaultProps[propName];
2227
+ }
1771
2228
  }
1772
- ctx = newCtx;
1773
2229
  }
1774
- ctx[key] = value;
1775
- }
1776
- function toNumber(val) {
1777
- const n = parseFloat(val);
1778
- return isNaN(n) ? val : n;
1779
2230
  }
1780
- function shallowEqual$1(l1, l2) {
1781
- for (let i = 0, l = l1.length; i < l; i++) {
1782
- if (l1[i] !== l2[i]) {
1783
- return false;
1784
- }
2231
+ //------------------------------------------------------------------------------
2232
+ // Prop validation helper
2233
+ //------------------------------------------------------------------------------
2234
+ function getPropDescription(staticProps) {
2235
+ if (staticProps instanceof Array) {
2236
+ return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
1785
2237
  }
1786
- return true;
2238
+ return staticProps || { "*": true };
1787
2239
  }
1788
- class LazyValue {
1789
- constructor(fn, ctx, node) {
1790
- this.fn = fn;
1791
- this.ctx = capture(ctx);
1792
- this.node = node;
2240
+ /**
2241
+ * Validate the component props (or next props) against the (static) props
2242
+ * description. This is potentially an expensive operation: it may needs to
2243
+ * visit recursively the props and all the children to check if they are valid.
2244
+ * This is why it is only done in 'dev' mode.
2245
+ */
2246
+ function validateProps(name, props, parent) {
2247
+ const ComponentClass = typeof name !== "string"
2248
+ ? name
2249
+ : parent.constructor.components[name];
2250
+ if (!ComponentClass) {
2251
+ // this is an error, wrong component. We silently return here instead so the
2252
+ // error is triggered by the usual path ('component' function)
2253
+ return;
1793
2254
  }
1794
- evaluate() {
1795
- return this.fn(this.ctx, this.node);
2255
+ applyDefaultProps(props, ComponentClass);
2256
+ const defaultProps = ComponentClass.defaultProps || {};
2257
+ let propsDef = getPropDescription(ComponentClass.props);
2258
+ const allowAdditionalProps = "*" in propsDef;
2259
+ for (let propName in propsDef) {
2260
+ if (propName === "*") {
2261
+ continue;
2262
+ }
2263
+ const propDef = propsDef[propName];
2264
+ let isMandatory = !!propDef;
2265
+ if (typeof propDef === "object" && "optional" in propDef) {
2266
+ isMandatory = !propDef.optional;
2267
+ }
2268
+ if (isMandatory && propName in defaultProps) {
2269
+ throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
2270
+ }
2271
+ if (props[propName] === undefined) {
2272
+ if (isMandatory) {
2273
+ throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
2274
+ }
2275
+ else {
2276
+ continue;
2277
+ }
2278
+ }
2279
+ let isValid;
2280
+ try {
2281
+ isValid = isValidProp(props[propName], propDef);
2282
+ }
2283
+ catch (e) {
2284
+ e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
2285
+ throw e;
2286
+ }
2287
+ if (!isValid) {
2288
+ throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
2289
+ }
1796
2290
  }
1797
- toString() {
1798
- return this.evaluate().toString();
2291
+ if (!allowAdditionalProps) {
2292
+ for (let propName in props) {
2293
+ if (!(propName in propsDef)) {
2294
+ throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
2295
+ }
2296
+ }
1799
2297
  }
1800
2298
  }
1801
- /*
1802
- * Safely outputs `value` as a block depending on the nature of `value`
2299
+ /**
2300
+ * Check if an invidual prop value matches its (static) prop definition
1803
2301
  */
1804
- function safeOutput(value) {
1805
- if (!value) {
1806
- return value;
1807
- }
1808
- let safeKey;
1809
- let block;
1810
- if (value instanceof Markup) {
1811
- safeKey = `string_safe`;
1812
- block = html(value);
2302
+ function isValidProp(prop, propDef) {
2303
+ if (propDef === true) {
2304
+ return true;
1813
2305
  }
1814
- else if (value instanceof LazyValue) {
1815
- safeKey = `lazy_value`;
1816
- block = value.evaluate();
2306
+ if (typeof propDef === "function") {
2307
+ // Check if a value is constructed by some Constructor. Note that there is a
2308
+ // slight abuse of language: we want to consider primitive values as well.
2309
+ //
2310
+ // So, even though 1 is not an instance of Number, we want to consider that
2311
+ // it is valid.
2312
+ if (typeof prop === "object") {
2313
+ return prop instanceof propDef;
2314
+ }
2315
+ return typeof prop === propDef.name.toLowerCase();
1817
2316
  }
1818
- else if (value instanceof String || typeof value === "string") {
1819
- safeKey = "string_unsafe";
1820
- block = text(value);
2317
+ else if (propDef instanceof Array) {
2318
+ // If this code is executed, this means that we want to check if a prop
2319
+ // matches at least one of its descriptor.
2320
+ let result = false;
2321
+ for (let i = 0, iLen = propDef.length; i < iLen; i++) {
2322
+ result = result || isValidProp(prop, propDef[i]);
2323
+ }
2324
+ return result;
1821
2325
  }
1822
- else {
1823
- // Assuming it is a block
1824
- safeKey = "block_safe";
1825
- block = value;
2326
+ // propsDef is an object
2327
+ if (propDef.optional && prop === undefined) {
2328
+ return true;
1826
2329
  }
1827
- return toggler(safeKey, block);
1828
- }
1829
- let boundFunctions = new WeakMap();
1830
- function bind(ctx, fn) {
1831
- let component = ctx.__owl__.component;
1832
- let boundFnMap = boundFunctions.get(component);
1833
- if (!boundFnMap) {
1834
- boundFnMap = new WeakMap();
1835
- boundFunctions.set(component, boundFnMap);
2330
+ let result = propDef.type ? isValidProp(prop, propDef.type) : true;
2331
+ if (propDef.validate) {
2332
+ result = result && propDef.validate(prop);
1836
2333
  }
1837
- let boundFn = boundFnMap.get(fn);
1838
- if (!boundFn) {
1839
- boundFn = fn.bind(component);
1840
- boundFnMap.set(fn, boundFn);
2334
+ if (propDef.type === Array && propDef.element) {
2335
+ for (let i = 0, iLen = prop.length; i < iLen; i++) {
2336
+ result = result && isValidProp(prop[i], propDef.element);
2337
+ }
1841
2338
  }
1842
- return boundFn;
1843
- }
1844
- function multiRefSetter(refs, name) {
1845
- let count = 0;
1846
- return (el) => {
1847
- if (el) {
1848
- count++;
1849
- if (count > 1) {
1850
- throw new Error("Cannot have 2 elements with same ref name at the same time");
1851
- }
2339
+ if (propDef.type === Object && propDef.shape) {
2340
+ const shape = propDef.shape;
2341
+ for (let key in shape) {
2342
+ result = result && isValidProp(prop[key], shape[key]);
1852
2343
  }
1853
- if (count === 0 || el) {
1854
- refs[name] = el;
2344
+ if (result) {
2345
+ for (let propName in prop) {
2346
+ if (!(propName in shape)) {
2347
+ throw new Error(`unknown prop '${propName}'`);
2348
+ }
2349
+ }
1855
2350
  }
1856
- };
2351
+ }
2352
+ return result;
2353
+ }
2354
+
2355
+ let currentNode = null;
2356
+ function getCurrent() {
2357
+ if (!currentNode) {
2358
+ throw new Error("No active component (a hook function should only be called in 'setup')");
2359
+ }
2360
+ return currentNode;
1857
2361
  }
1858
- const UTILS = {
1859
- withDefault,
1860
- zero: Symbol("zero"),
1861
- isBoundary,
1862
- callSlot,
1863
- capture,
1864
- withKey,
1865
- prepareList,
1866
- setContextValue,
1867
- multiRefSetter,
1868
- shallowEqual: shallowEqual$1,
1869
- toNumber,
1870
- validateProps,
1871
- LazyValue,
1872
- safeOutput,
1873
- bind,
1874
- };
1875
-
1876
- const mainEventHandler = (data, ev, currentTarget) => {
1877
- const { data: _data, modifiers } = filterOutModifiersFromData(data);
1878
- data = _data;
1879
- let stopped = false;
1880
- if (modifiers.length) {
1881
- let selfMode = false;
1882
- const isSelf = ev.target === currentTarget;
1883
- for (const mod of modifiers) {
1884
- switch (mod) {
1885
- case "self":
1886
- selfMode = true;
1887
- if (isSelf) {
1888
- continue;
1889
- }
1890
- else {
1891
- return stopped;
1892
- }
1893
- case "prevent":
1894
- if ((selfMode && isSelf) || !selfMode)
1895
- ev.preventDefault();
1896
- continue;
1897
- case "stop":
1898
- if ((selfMode && isSelf) || !selfMode)
1899
- ev.stopPropagation();
1900
- stopped = true;
1901
- continue;
1902
- }
1903
- }
1904
- }
1905
- // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
1906
- // We check this rather than data[0] being truthy (or typeof function) so that it crashes
1907
- // as expected when there is a handler expression that evaluates to a falsy value
1908
- if (Object.hasOwnProperty.call(data, 0)) {
1909
- const handler = data[0];
1910
- if (typeof handler !== "function") {
1911
- throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
1912
- }
1913
- let node = data[1] ? data[1].__owl__ : null;
1914
- if (node ? node.status === 1 /* MOUNTED */ : true) {
1915
- handler.call(node ? node.component : null, ev);
1916
- }
1917
- }
1918
- return stopped;
1919
- };
1920
-
1921
- // Maps fibers to thrown errors
1922
- const fibersInError = new WeakMap();
1923
- const nodeErrorHandlers = new WeakMap();
1924
- function _handleError(node, error, isFirstRound = false) {
1925
- if (!node) {
1926
- return false;
1927
- }
1928
- const fiber = node.fiber;
1929
- if (fiber) {
1930
- fibersInError.set(fiber, error);
2362
+ function useComponent() {
2363
+ return currentNode.component;
2364
+ }
2365
+ // -----------------------------------------------------------------------------
2366
+ // Integration with reactivity system (useState)
2367
+ // -----------------------------------------------------------------------------
2368
+ const batchedRenderFunctions = new WeakMap();
2369
+ /**
2370
+ * Creates a reactive object that will be observed by the current component.
2371
+ * Reading data from the returned object (eg during rendering) will cause the
2372
+ * component to subscribe to that data and be rerendered when it changes.
2373
+ *
2374
+ * @param state the state to observe
2375
+ * @returns a reactive object that will cause the component to re-render on
2376
+ * relevant changes
2377
+ * @see reactive
2378
+ */
2379
+ function useState(state) {
2380
+ const node = getCurrent();
2381
+ let render = batchedRenderFunctions.get(node);
2382
+ if (!render) {
2383
+ render = batched(node.render.bind(node));
2384
+ batchedRenderFunctions.set(node, render);
2385
+ // manual implementation of onWillDestroy to break cyclic dependency
2386
+ node.willDestroy.push(clearReactivesForCallback.bind(null, render));
1931
2387
  }
1932
- const errorHandlers = nodeErrorHandlers.get(node);
1933
- if (errorHandlers) {
1934
- let stopped = false;
1935
- // execute in the opposite order
1936
- for (let i = errorHandlers.length - 1; i >= 0; i--) {
1937
- try {
1938
- errorHandlers[i](error);
1939
- stopped = true;
1940
- break;
1941
- }
1942
- catch (e) {
1943
- error = e;
1944
- }
1945
- }
1946
- if (stopped) {
1947
- if (isFirstRound && fiber && fiber.node.fiber) {
1948
- fiber.root.counter--;
1949
- }
2388
+ return reactive(state, render);
2389
+ }
2390
+ function arePropsDifferent(props1, props2) {
2391
+ for (let k in props1) {
2392
+ if (props1[k] !== props2[k]) {
1950
2393
  return true;
1951
2394
  }
1952
2395
  }
1953
- return _handleError(node.parent, error);
2396
+ return Object.keys(props1).length !== Object.keys(props2).length;
1954
2397
  }
1955
- function handleError(params) {
1956
- const error = params.error;
1957
- const node = "node" in params ? params.node : params.fiber.node;
1958
- const fiber = "fiber" in params ? params.fiber : node.fiber;
1959
- // resets the fibers on components if possible. This is important so that
1960
- // new renderings can be properly included in the initial one, if any.
1961
- let current = fiber;
1962
- do {
1963
- current.node.fiber = current;
1964
- current = current.parent;
1965
- } while (current);
1966
- fibersInError.set(fiber.root, error);
1967
- const handled = _handleError(node, error, true);
1968
- if (!handled) {
1969
- console.warn(`[Owl] Unhandled error. Destroying the root component`);
1970
- try {
1971
- node.app.destroy();
2398
+ function component(name, props, key, ctx, parent) {
2399
+ let node = ctx.children[key];
2400
+ let isDynamic = typeof name !== "string";
2401
+ if (node) {
2402
+ if (node.status < 1 /* MOUNTED */) {
2403
+ node.destroy();
2404
+ node = undefined;
1972
2405
  }
1973
- catch (e) {
1974
- console.error(e);
2406
+ else if (node.status === 2 /* DESTROYED */) {
2407
+ node = undefined;
1975
2408
  }
1976
2409
  }
1977
- }
1978
-
1979
- function makeChildFiber(node, parent) {
1980
- let current = node.fiber;
1981
- if (current) {
1982
- cancelFibers(current.children);
1983
- current.root = null;
2410
+ if (isDynamic && node && node.component.constructor !== name) {
2411
+ node = undefined;
1984
2412
  }
1985
- return new Fiber(node, parent);
1986
- }
1987
- function makeRootFiber(node) {
1988
- let current = node.fiber;
1989
- if (current) {
1990
- let root = current.root;
1991
- root.counter = root.counter + 1 - cancelFibers(current.children);
1992
- current.children = [];
1993
- current.bdom = null;
1994
- if (fibersInError.has(current)) {
1995
- fibersInError.delete(current);
1996
- fibersInError.delete(root);
1997
- current.appliedToDom = false;
2413
+ const parentFiber = ctx.fiber;
2414
+ if (node) {
2415
+ let shouldRender = node.forceNextRender;
2416
+ if (shouldRender) {
2417
+ node.forceNextRender = false;
1998
2418
  }
1999
- return current;
2000
- }
2001
- const fiber = new RootFiber(node, null);
2002
- if (node.willPatch.length) {
2003
- fiber.willPatch.push(fiber);
2004
- }
2005
- if (node.patched.length) {
2006
- fiber.patched.push(fiber);
2007
- }
2008
- return fiber;
2009
- }
2010
- /**
2011
- * @returns number of not-yet rendered fibers cancelled
2012
- */
2013
- function cancelFibers(fibers) {
2014
- let result = 0;
2015
- for (let fiber of fibers) {
2016
- fiber.node.fiber = null;
2017
- if (!fiber.bdom) {
2018
- result++;
2419
+ else {
2420
+ const currentProps = node.component.props[TARGET];
2421
+ shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
2422
+ }
2423
+ if (shouldRender) {
2424
+ node.updateAndRender(props, parentFiber);
2019
2425
  }
2020
- result += cancelFibers(fiber.children);
2021
2426
  }
2022
- return result;
2023
- }
2024
- class Fiber {
2025
- constructor(node, parent) {
2026
- this.bdom = null;
2027
- this.children = [];
2028
- this.appliedToDom = false;
2029
- this.deep = false;
2030
- this.node = node;
2031
- this.parent = parent;
2032
- if (parent) {
2033
- this.deep = parent.deep;
2034
- const root = parent.root;
2035
- root.counter++;
2036
- this.root = root;
2037
- parent.children.push(this);
2427
+ else {
2428
+ // new component
2429
+ let C;
2430
+ if (isDynamic) {
2431
+ C = name;
2038
2432
  }
2039
2433
  else {
2040
- this.root = this;
2434
+ C = parent.constructor.components[name];
2435
+ if (!C) {
2436
+ throw new Error(`Cannot find the definition of component "${name}"`);
2437
+ }
2041
2438
  }
2439
+ node = new ComponentNode(C, props, ctx.app, ctx);
2440
+ ctx.children[key] = node;
2441
+ node.initiateRender(new Fiber(node, parentFiber));
2042
2442
  }
2443
+ parentFiber.root.reachedChildren.add(node);
2444
+ return node;
2043
2445
  }
2044
- class RootFiber extends Fiber {
2045
- constructor() {
2046
- super(...arguments);
2047
- this.counter = 1;
2048
- // only add stuff in this if they have registered some hooks
2446
+ class ComponentNode {
2447
+ constructor(C, props, app, parent) {
2448
+ this.fiber = null;
2449
+ this.bdom = null;
2450
+ this.status = 0 /* NEW */;
2451
+ this.forceNextRender = false;
2452
+ this.children = Object.create(null);
2453
+ this.refs = {};
2454
+ this.willStart = [];
2455
+ this.willUpdateProps = [];
2456
+ this.willUnmount = [];
2457
+ this.mounted = [];
2049
2458
  this.willPatch = [];
2050
2459
  this.patched = [];
2051
- this.mounted = [];
2052
- // A fiber is typically locked when it is completing and the patch has not, or is being applied.
2053
- // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
2054
- this.locked = false;
2055
- }
2056
- complete() {
2057
- const node = this.node;
2058
- this.locked = true;
2059
- let current = undefined;
2060
- try {
2061
- // Step 1: calling all willPatch lifecycle hooks
2062
- for (current of this.willPatch) {
2063
- // because of the asynchronous nature of the rendering, some parts of the
2064
- // UI may have been rendered, then deleted in a followup rendering, and we
2065
- // do not want to call onWillPatch in that case.
2066
- let node = current.node;
2067
- if (node.fiber === current) {
2068
- const component = node.component;
2069
- for (let cb of node.willPatch) {
2070
- cb.call(component);
2071
- }
2072
- }
2073
- }
2074
- current = undefined;
2075
- // Step 2: patching the dom
2076
- node._patch();
2077
- this.locked = false;
2078
- // Step 4: calling all mounted lifecycle hooks
2079
- let mountedFibers = this.mounted;
2080
- while ((current = mountedFibers.pop())) {
2081
- current = current;
2082
- if (current.appliedToDom) {
2083
- for (let cb of current.node.mounted) {
2084
- cb();
2085
- }
2086
- }
2087
- }
2088
- // Step 5: calling all patched hooks
2089
- let patchedFibers = this.patched;
2090
- while ((current = patchedFibers.pop())) {
2091
- current = current;
2092
- if (current.appliedToDom) {
2093
- for (let cb of current.node.patched) {
2094
- cb();
2095
- }
2096
- }
2097
- }
2098
- }
2099
- catch (e) {
2100
- this.locked = false;
2101
- handleError({ fiber: current || this, error: e });
2102
- }
2103
- }
2104
- }
2105
- class MountFiber extends RootFiber {
2106
- constructor(node, target, options = {}) {
2107
- super(node, null);
2108
- this.target = target;
2109
- this.position = options.position || "last-child";
2110
- }
2111
- complete() {
2112
- let current = this;
2113
- try {
2114
- const node = this.node;
2115
- node.app.constructor.validateTarget(this.target);
2116
- if (node.bdom) {
2117
- // this is a complicated situation: if we mount a fiber with an existing
2118
- // bdom, this means that this same fiber was already completed, mounted,
2119
- // but a crash occurred in some mounted hook. Then, it was handled and
2120
- // the new rendering is being applied.
2121
- node.updateDom();
2122
- }
2123
- else {
2124
- node.bdom = this.bdom;
2125
- if (this.position === "last-child" || this.target.childNodes.length === 0) {
2126
- mount$1(node.bdom, this.target);
2127
- }
2128
- else {
2129
- const firstChild = this.target.childNodes[0];
2130
- mount$1(node.bdom, this.target, firstChild);
2131
- }
2132
- }
2133
- // unregistering the fiber before mounted since it can do another render
2134
- // and that the current rendering is obviously completed
2135
- node.fiber = null;
2136
- node.status = 1 /* MOUNTED */;
2137
- this.appliedToDom = true;
2138
- let mountedFibers = this.mounted;
2139
- while ((current = mountedFibers.pop())) {
2140
- if (current.appliedToDom) {
2141
- for (let cb of current.node.mounted) {
2142
- cb();
2143
- }
2144
- }
2145
- }
2146
- }
2147
- catch (e) {
2148
- handleError({ fiber: current, error: e });
2149
- }
2150
- }
2151
- }
2152
-
2153
- let currentNode = null;
2154
- function getCurrent() {
2155
- if (!currentNode) {
2156
- throw new Error("No active component (a hook function should only be called in 'setup')");
2157
- }
2158
- return currentNode;
2159
- }
2160
- function useComponent() {
2161
- return currentNode.component;
2162
- }
2163
- // -----------------------------------------------------------------------------
2164
- // Integration with reactivity system (useState)
2165
- // -----------------------------------------------------------------------------
2166
- const batchedRenderFunctions = new WeakMap();
2167
- /**
2168
- * Creates a reactive object that will be observed by the current component.
2169
- * Reading data from the returned object (eg during rendering) will cause the
2170
- * component to subscribe to that data and be rerendered when it changes.
2171
- *
2172
- * @param state the state to observe
2173
- * @returns a reactive object that will cause the component to re-render on
2174
- * relevant changes
2175
- * @see reactive
2176
- */
2177
- function useState(state) {
2178
- const node = getCurrent();
2179
- let render = batchedRenderFunctions.get(node);
2180
- if (!render) {
2181
- render = batched(node.render.bind(node));
2182
- batchedRenderFunctions.set(node, render);
2183
- // manual implementation of onWillDestroy to break cyclic dependency
2184
- node.willDestroy.push(clearReactivesForCallback.bind(null, render));
2185
- }
2186
- return reactive(state, render);
2187
- }
2188
- function arePropsDifferent(props1, props2) {
2189
- for (let k in props1) {
2190
- if (props1[k] !== props2[k]) {
2191
- return true;
2192
- }
2193
- }
2194
- return Object.keys(props1).length !== Object.keys(props2).length;
2195
- }
2196
- function component(name, props, key, ctx, parent) {
2197
- let node = ctx.children[key];
2198
- let isDynamic = typeof name !== "string";
2199
- if (node) {
2200
- if (node.status < 1 /* MOUNTED */) {
2201
- node.destroy();
2202
- node = undefined;
2203
- }
2204
- else if (node.status === 2 /* DESTROYED */) {
2205
- node = undefined;
2206
- }
2207
- }
2208
- if (isDynamic && node && node.component.constructor !== name) {
2209
- node = undefined;
2210
- }
2211
- const parentFiber = ctx.fiber;
2212
- if (node) {
2213
- const currentProps = node.component.props[TARGET];
2214
- if (parentFiber.deep || arePropsDifferent(currentProps, props)) {
2215
- node.updateAndRender(props, parentFiber);
2216
- }
2217
- }
2218
- else {
2219
- // new component
2220
- let C;
2221
- if (isDynamic) {
2222
- C = name;
2223
- }
2224
- else {
2225
- C = parent.constructor.components[name];
2226
- if (!C) {
2227
- throw new Error(`Cannot find the definition of component "${name}"`);
2228
- }
2229
- }
2230
- node = new ComponentNode(C, props, ctx.app, ctx);
2231
- ctx.children[key] = node;
2232
- node.initiateRender(new Fiber(node, parentFiber));
2233
- }
2234
- return node;
2235
- }
2236
- class ComponentNode {
2237
- constructor(C, props, app, parent) {
2238
- this.fiber = null;
2239
- this.bdom = null;
2240
- this.status = 0 /* NEW */;
2241
- this.children = Object.create(null);
2242
- this.refs = {};
2243
- this.willStart = [];
2244
- this.willUpdateProps = [];
2245
- this.willUnmount = [];
2246
- this.mounted = [];
2247
- this.willPatch = [];
2248
- this.patched = [];
2249
- this.willDestroy = [];
2250
- currentNode = this;
2251
- this.app = app;
2252
- this.parent = parent || null;
2253
- this.level = parent ? parent.level + 1 : 0;
2254
- applyDefaultProps(props, C);
2255
- const env = (parent && parent.childEnv) || app.env;
2256
- this.childEnv = env;
2257
- props = useState(props);
2258
- this.component = new C(props, env, this);
2259
- this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
2260
- this.component.setup();
2261
- currentNode = null;
2460
+ this.willDestroy = [];
2461
+ currentNode = this;
2462
+ this.app = app;
2463
+ this.parent = parent || null;
2464
+ this.level = parent ? parent.level + 1 : 0;
2465
+ applyDefaultProps(props, C);
2466
+ const env = (parent && parent.childEnv) || app.env;
2467
+ this.childEnv = env;
2468
+ props = useState(props);
2469
+ this.component = new C(props, env, this);
2470
+ this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
2471
+ this.component.setup();
2472
+ currentNode = null;
2262
2473
  }
2263
2474
  mountComponent(target, options) {
2264
2475
  const fiber = new MountFiber(this, target, options);
@@ -2279,7 +2490,7 @@ class ComponentNode {
2279
2490
  return;
2280
2491
  }
2281
2492
  if (this.status === 0 /* NEW */ && this.fiber === fiber) {
2282
- this._render(fiber);
2493
+ fiber.render();
2283
2494
  }
2284
2495
  }
2285
2496
  async render(deep = false) {
@@ -2323,16 +2534,7 @@ class ComponentNode {
2323
2534
  // embedded in a rendering coming from above, so the fiber will be rendered
2324
2535
  // in the next microtick anyway, so we should not render it again.
2325
2536
  if (this.fiber === fiber && (current || !fiber.parent)) {
2326
- this._render(fiber);
2327
- }
2328
- }
2329
- _render(fiber) {
2330
- try {
2331
- fiber.bdom = this.renderFn();
2332
- fiber.root.counter--;
2333
- }
2334
- catch (e) {
2335
- handleError({ node: this, error: e });
2537
+ fiber.render();
2336
2538
  }
2337
2539
  }
2338
2540
  destroy() {
@@ -2372,7 +2574,7 @@ class ComponentNode {
2372
2574
  return;
2373
2575
  }
2374
2576
  component.props = props;
2375
- this._render(fiber);
2577
+ fiber.render();
2376
2578
  const parentRoot = parentFiber.root;
2377
2579
  if (this.willPatch.length) {
2378
2580
  parentRoot.willPatch.push(fiber);
@@ -2460,98 +2662,76 @@ class ComponentNode {
2460
2662
  }
2461
2663
  }
2462
2664
  }
2665
+ // ---------------------------------------------------------------------------
2666
+ // Some debug helpers
2667
+ // ---------------------------------------------------------------------------
2668
+ get name() {
2669
+ return this.component.constructor.name;
2670
+ }
2671
+ get subscriptions() {
2672
+ const render = batchedRenderFunctions.get(this);
2673
+ return render ? getSubscriptions(render) : [];
2674
+ }
2463
2675
  }
2464
2676
 
2465
- function wrapError(fn, hookName) {
2466
- const error = new Error(`The following error occurred in ${hookName}: `);
2467
- return (...args) => {
2468
- try {
2469
- const result = fn(...args);
2470
- if (result instanceof Promise) {
2471
- return result.catch((cause) => {
2472
- error.cause = cause;
2473
- if (cause instanceof Error) {
2474
- error.message += `"${cause.message}"`;
2677
+ // -----------------------------------------------------------------------------
2678
+ // Scheduler
2679
+ // -----------------------------------------------------------------------------
2680
+ class Scheduler {
2681
+ constructor() {
2682
+ this.tasks = new Set();
2683
+ this.frame = 0;
2684
+ this.shouldClear = false;
2685
+ this.requestAnimationFrame = Scheduler.requestAnimationFrame;
2686
+ }
2687
+ addFiber(fiber) {
2688
+ this.tasks.add(fiber.root);
2689
+ }
2690
+ /**
2691
+ * Process all current tasks. This only applies to the fibers that are ready.
2692
+ * Other tasks are left unchanged.
2693
+ */
2694
+ flush() {
2695
+ if (this.frame === 0) {
2696
+ this.frame = this.requestAnimationFrame(() => {
2697
+ this.frame = 0;
2698
+ this.tasks.forEach((fiber) => this.processFiber(fiber));
2699
+ if (this.shouldClear) {
2700
+ this.shouldClear = false;
2701
+ for (let task of this.tasks) {
2702
+ if (task.node.status === 2 /* DESTROYED */) {
2703
+ this.tasks.delete(task);
2704
+ }
2475
2705
  }
2476
- throw error;
2477
- });
2478
- }
2479
- return result;
2706
+ }
2707
+ });
2480
2708
  }
2481
- catch (cause) {
2482
- if (cause instanceof Error) {
2483
- error.message += `"${cause.message}"`;
2709
+ }
2710
+ processFiber(fiber) {
2711
+ if (fiber.root !== fiber) {
2712
+ this.tasks.delete(fiber);
2713
+ return;
2714
+ }
2715
+ const hasError = fibersInError.has(fiber);
2716
+ if (hasError && fiber.counter !== 0) {
2717
+ this.tasks.delete(fiber);
2718
+ return;
2719
+ }
2720
+ if (fiber.node.status === 2 /* DESTROYED */) {
2721
+ this.tasks.delete(fiber);
2722
+ return;
2723
+ }
2724
+ if (fiber.counter === 0) {
2725
+ if (!hasError) {
2726
+ fiber.complete();
2484
2727
  }
2485
- throw error;
2728
+ this.tasks.delete(fiber);
2486
2729
  }
2487
- };
2730
+ }
2488
2731
  }
2489
- // -----------------------------------------------------------------------------
2490
- // hooks
2491
- // -----------------------------------------------------------------------------
2492
- function onWillStart(fn) {
2493
- const node = getCurrent();
2494
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2495
- node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
2496
- }
2497
- function onWillUpdateProps(fn) {
2498
- const node = getCurrent();
2499
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2500
- node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
2501
- }
2502
- function onMounted(fn) {
2503
- const node = getCurrent();
2504
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2505
- node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
2506
- }
2507
- function onWillPatch(fn) {
2508
- const node = getCurrent();
2509
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2510
- node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
2511
- }
2512
- function onPatched(fn) {
2513
- const node = getCurrent();
2514
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2515
- node.patched.push(decorate(fn.bind(node.component), "onPatched"));
2516
- }
2517
- function onWillUnmount(fn) {
2518
- const node = getCurrent();
2519
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2520
- node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
2521
- }
2522
- function onWillDestroy(fn) {
2523
- const node = getCurrent();
2524
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2525
- node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
2526
- }
2527
- function onWillRender(fn) {
2528
- const node = getCurrent();
2529
- const renderFn = node.renderFn;
2530
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2531
- node.renderFn = decorate(() => {
2532
- fn.call(node.component);
2533
- return renderFn();
2534
- }, "onWillRender");
2535
- }
2536
- function onRendered(fn) {
2537
- const node = getCurrent();
2538
- const renderFn = node.renderFn;
2539
- const decorate = node.app.dev ? wrapError : (fn) => fn;
2540
- node.renderFn = decorate(() => {
2541
- const result = renderFn();
2542
- fn.call(node.component);
2543
- return result;
2544
- }, "onRendered");
2545
- }
2546
- function onError(callback) {
2547
- const node = getCurrent();
2548
- let handlers = nodeErrorHandlers.get(node);
2549
- if (!handlers) {
2550
- handlers = [];
2551
- nodeErrorHandlers.set(node, handlers);
2552
- }
2553
- handlers.push(callback.bind(node.component));
2554
- }
2732
+ // capture the value of requestAnimationFrame as soon as possible, to avoid
2733
+ // interactions with other code, such as test frameworks that override them
2734
+ Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
2555
2735
 
2556
2736
  /**
2557
2737
  * Owl QWeb Expression Parser
@@ -2705,24 +2885,31 @@ const TOKENIZERS = [
2705
2885
  function tokenize(expr) {
2706
2886
  const result = [];
2707
2887
  let token = true;
2708
- while (token) {
2709
- expr = expr.trim();
2710
- if (expr) {
2711
- for (let tokenizer of TOKENIZERS) {
2712
- token = tokenizer(expr);
2713
- if (token) {
2714
- result.push(token);
2715
- expr = expr.slice(token.size || token.value.length);
2716
- break;
2888
+ let error;
2889
+ let current = expr;
2890
+ try {
2891
+ while (token) {
2892
+ current = current.trim();
2893
+ if (current) {
2894
+ for (let tokenizer of TOKENIZERS) {
2895
+ token = tokenizer(current);
2896
+ if (token) {
2897
+ result.push(token);
2898
+ current = current.slice(token.size || token.value.length);
2899
+ break;
2900
+ }
2717
2901
  }
2718
2902
  }
2719
- }
2720
- else {
2721
- token = false;
2903
+ else {
2904
+ token = false;
2905
+ }
2722
2906
  }
2723
2907
  }
2724
- if (expr.length) {
2725
- throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
2908
+ catch (e) {
2909
+ error = e; // Silence all errors and throw a generic error below
2910
+ }
2911
+ if (current.length || error) {
2912
+ throw new Error(`Tokenizer error: could not tokenize \`${expr}\``);
2726
2913
  }
2727
2914
  return result;
2728
2915
  }
@@ -2927,7 +3114,7 @@ function createContext(parentCtx, params) {
2927
3114
  }, params);
2928
3115
  }
2929
3116
  class CodeTarget {
2930
- constructor(name) {
3117
+ constructor(name, on) {
2931
3118
  this.indentLevel = 0;
2932
3119
  this.loopLevel = 0;
2933
3120
  this.code = [];
@@ -2938,6 +3125,7 @@ class CodeTarget {
2938
3125
  this.refInfo = {};
2939
3126
  this.shouldProtectScope = false;
2940
3127
  this.name = name;
3128
+ this.on = on || null;
2941
3129
  }
2942
3130
  addLine(line, idx) {
2943
3131
  const prefix = new Array(this.indentLevel + 2).join(" ");
@@ -2987,7 +3175,7 @@ class CodeGenerator {
2987
3175
  this.targets = [];
2988
3176
  this.target = new CodeTarget("template");
2989
3177
  this.translatableAttributes = TRANSLATABLE_ATTRS;
2990
- this.staticCalls = [];
3178
+ this.staticDefs = [];
2991
3179
  this.helpers = new Set();
2992
3180
  this.translateFn = options.translateFn || ((s) => s);
2993
3181
  if (options.translatableAttributes) {
@@ -3030,8 +3218,8 @@ class CodeGenerator {
3030
3218
  if (this.templateName) {
3031
3219
  mainCode.push(`// Template name: "${this.templateName}"`);
3032
3220
  }
3033
- for (let { id, template } of this.staticCalls) {
3034
- mainCode.push(`const ${id} = getTemplate(${template});`);
3221
+ for (let { id, expr } of this.staticDefs) {
3222
+ mainCode.push(`const ${id} = ${expr};`);
3035
3223
  }
3036
3224
  // define all blocks
3037
3225
  if (this.blocks.length) {
@@ -3067,19 +3255,21 @@ class CodeGenerator {
3067
3255
  }
3068
3256
  return code;
3069
3257
  }
3070
- compileInNewTarget(prefix, ast, ctx) {
3258
+ compileInNewTarget(prefix, ast, ctx, on) {
3071
3259
  const name = this.generateId(prefix);
3072
3260
  const initialTarget = this.target;
3073
- const target = new CodeTarget(name);
3261
+ const target = new CodeTarget(name, on);
3074
3262
  this.targets.push(target);
3075
3263
  this.target = target;
3076
- const subCtx = createContext(ctx);
3077
- this.compileAST(ast, subCtx);
3264
+ this.compileAST(ast, createContext(ctx));
3078
3265
  this.target = initialTarget;
3079
3266
  return name;
3080
3267
  }
3081
- addLine(line) {
3082
- this.target.addLine(line);
3268
+ addLine(line, idx) {
3269
+ this.target.addLine(line, idx);
3270
+ }
3271
+ define(varName, expr) {
3272
+ this.addLine(`const ${varName} = ${expr};`);
3083
3273
  }
3084
3274
  generateId(prefix = "") {
3085
3275
  this.ids[prefix] = (this.ids[prefix] || 0) + 1;
@@ -3121,10 +3311,13 @@ class CodeGenerator {
3121
3311
  blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
3122
3312
  }
3123
3313
  if (block.isRoot && !ctx.preventRoot) {
3314
+ if (this.target.on) {
3315
+ blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
3316
+ }
3124
3317
  this.addLine(`return ${blockExpr};`);
3125
3318
  }
3126
3319
  else {
3127
- this.addLine(`let ${block.varName} = ${blockExpr};`);
3320
+ this.define(block.varName, blockExpr);
3128
3321
  }
3129
3322
  }
3130
3323
  /**
@@ -3152,7 +3345,7 @@ class CodeGenerator {
3152
3345
  if (!mapping.has(tok.varName)) {
3153
3346
  const varId = this.generateId("v");
3154
3347
  mapping.set(tok.varName, varId);
3155
- this.addLine(`const ${varId} = ${tok.value};`);
3348
+ this.define(varId, tok.value);
3156
3349
  }
3157
3350
  tok.value = mapping.get(tok.varName);
3158
3351
  }
@@ -3291,7 +3484,7 @@ class CodeGenerator {
3291
3484
  this.blocks.push(block);
3292
3485
  if (ast.dynamicTag) {
3293
3486
  const tagExpr = this.generateId("tag");
3294
- this.addLine(`let ${tagExpr} = ${compileExpr(ast.dynamicTag)};`);
3487
+ this.define(tagExpr, compileExpr(ast.dynamicTag));
3295
3488
  block.dynamicTagName = tagExpr;
3296
3489
  }
3297
3490
  }
@@ -3373,10 +3566,10 @@ class CodeGenerator {
3373
3566
  const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
3374
3567
  const baseExpression = compileExpr(baseExpr);
3375
3568
  const bExprId = this.generateId("bExpr");
3376
- this.addLine(`const ${bExprId} = ${baseExpression};`);
3569
+ this.define(bExprId, baseExpression);
3377
3570
  const expression = compileExpr(expr);
3378
3571
  const exprId = this.generateId("expr");
3379
- this.addLine(`const ${exprId} = ${expression};`);
3572
+ this.define(exprId, expression);
3380
3573
  const fullExpression = `${bExprId}[${exprId}]`;
3381
3574
  let idx;
3382
3575
  if (specialInitTargetAttr) {
@@ -3386,7 +3579,7 @@ class CodeGenerator {
3386
3579
  else if (hasDynamicChildren) {
3387
3580
  const bValueId = this.generateId("bValue");
3388
3581
  tModelSelectedExpr = `${bValueId}`;
3389
- this.addLine(`let ${tModelSelectedExpr} = ${fullExpression}`);
3582
+ this.define(tModelSelectedExpr, fullExpression);
3390
3583
  }
3391
3584
  else {
3392
3585
  idx = block.insertData(`${fullExpression}`, "attr");
@@ -3434,14 +3627,14 @@ class CodeGenerator {
3434
3627
  const children = block.children.slice();
3435
3628
  let current = children.shift();
3436
3629
  for (let i = codeIdx; i < code.length; i++) {
3437
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3438
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3630
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3631
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3439
3632
  current = children.shift();
3440
3633
  if (!current)
3441
3634
  break;
3442
3635
  }
3443
3636
  }
3444
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3637
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3445
3638
  }
3446
3639
  }
3447
3640
  }
@@ -3529,14 +3722,14 @@ class CodeGenerator {
3529
3722
  const children = block.children.slice();
3530
3723
  let current = children.shift();
3531
3724
  for (let i = codeIdx; i < code.length; i++) {
3532
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3533
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3725
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3726
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3534
3727
  current = children.shift();
3535
3728
  if (!current)
3536
3729
  break;
3537
3730
  }
3538
3731
  }
3539
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3732
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3540
3733
  }
3541
3734
  // note: this part is duplicated from end of compilemulti:
3542
3735
  const args = block.children.map((c) => c.varName).join(", ");
@@ -3557,10 +3750,10 @@ class CodeGenerator {
3557
3750
  const l = `l_block${block.id}`;
3558
3751
  const c = `c_block${block.id}`;
3559
3752
  this.helpers.add("prepareList");
3560
- this.addLine(`const [${keys}, ${vals}, ${l}, ${c}] = prepareList(${compileExpr(ast.collection)});`);
3753
+ this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);
3561
3754
  // Throw errors on duplicate keys in dev mode
3562
3755
  if (this.dev) {
3563
- this.addLine(`const keys${block.id} = new Set();`);
3756
+ this.define(`keys${block.id}`, `new Set()`);
3564
3757
  }
3565
3758
  this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
3566
3759
  this.target.indentLevel++;
@@ -3577,7 +3770,7 @@ class CodeGenerator {
3577
3770
  if (!ast.hasNoValue) {
3578
3771
  this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`);
3579
3772
  }
3580
- this.addLine(`let key${this.target.loopLevel} = ${ast.key ? compileExpr(ast.key) : loopVar};`);
3773
+ this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
3581
3774
  if (this.dev) {
3582
3775
  // Throw error on duplicate keys in dev mode
3583
3776
  this.addLine(`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new Error(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`);
@@ -3587,8 +3780,8 @@ class CodeGenerator {
3587
3780
  if (ast.memo) {
3588
3781
  this.target.hasCache = true;
3589
3782
  id = this.generateId();
3590
- this.addLine(`let memo${id} = ${compileExpr(ast.memo)}`);
3591
- this.addLine(`let vnode${id} = cache[key${this.target.loopLevel}];`);
3783
+ this.define(`memo${id}`, compileExpr(ast.memo));
3784
+ this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
3592
3785
  this.addLine(`if (vnode${id}) {`);
3593
3786
  this.target.indentLevel++;
3594
3787
  this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);
@@ -3616,7 +3809,7 @@ class CodeGenerator {
3616
3809
  }
3617
3810
  compileTKey(ast, ctx) {
3618
3811
  const tKeyExpr = this.generateId("tKey_");
3619
- this.addLine(`const ${tKeyExpr} = ${compileExpr(ast.expr)};`);
3812
+ this.define(tKeyExpr, compileExpr(ast.expr));
3620
3813
  ctx = createContext(ctx, {
3621
3814
  tKeyExpr,
3622
3815
  block: ctx.block,
@@ -3661,14 +3854,14 @@ class CodeGenerator {
3661
3854
  const children = block.children.slice();
3662
3855
  let current = children.shift();
3663
3856
  for (let i = codeIdx; i < code.length; i++) {
3664
- if (code[i].trimStart().startsWith(`let ${current.varName} `)) {
3665
- code[i] = code[i].replace(`let ${current.varName}`, current.varName);
3857
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
3858
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
3666
3859
  current = children.shift();
3667
3860
  if (!current)
3668
3861
  break;
3669
3862
  }
3670
3863
  }
3671
- this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3864
+ this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3672
3865
  }
3673
3866
  }
3674
3867
  const args = block.children.map((c) => c.varName).join(", ");
@@ -3699,7 +3892,7 @@ class CodeGenerator {
3699
3892
  const key = `key + \`${this.generateComponentKey()}\``;
3700
3893
  if (isDynamic) {
3701
3894
  const templateVar = this.generateId("template");
3702
- this.addLine(`const ${templateVar} = ${subTemplate};`);
3895
+ this.define(templateVar, subTemplate);
3703
3896
  block = this.createBlock(block, "multi", ctx);
3704
3897
  this.helpers.add("call");
3705
3898
  this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block, {
@@ -3710,7 +3903,7 @@ class CodeGenerator {
3710
3903
  else {
3711
3904
  const id = this.generateId(`callTemplate_`);
3712
3905
  this.helpers.add("getTemplate");
3713
- this.staticCalls.push({ id, template: subTemplate });
3906
+ this.staticDefs.push({ id, expr: `getTemplate(${subTemplate})` });
3714
3907
  block = this.createBlock(block, "multi", ctx);
3715
3908
  this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block, {
3716
3909
  ...ctx,
@@ -3804,26 +3997,25 @@ class CodeGenerator {
3804
3997
  compileComponent(ast, ctx) {
3805
3998
  let { block } = ctx;
3806
3999
  // props
3807
- const hasSlotsProp = "slots" in ast.props;
4000
+ const hasSlotsProp = "slots" in (ast.props || {});
3808
4001
  const props = [];
3809
- const propExpr = this.formatPropObject(ast.props);
4002
+ const propExpr = this.formatPropObject(ast.props || {});
3810
4003
  if (propExpr) {
3811
4004
  props.push(propExpr);
3812
4005
  }
3813
4006
  // slots
3814
- const hasSlot = !!Object.keys(ast.slots).length;
3815
4007
  let slotDef = "";
3816
- if (hasSlot) {
4008
+ if (ast.slots) {
3817
4009
  let ctxStr = "ctx";
3818
4010
  if (this.target.loopLevel || !this.hasSafeContext) {
3819
4011
  ctxStr = this.generateId("ctx");
3820
4012
  this.helpers.add("capture");
3821
- this.addLine(`const ${ctxStr} = capture(ctx);`);
4013
+ this.define(ctxStr, `capture(ctx)`);
3822
4014
  }
3823
4015
  let slotStr = [];
3824
4016
  for (let slotName in ast.slots) {
3825
- const slotAst = ast.slots[slotName].content;
3826
- const name = this.compileInNewTarget("slot", slotAst, ctx);
4017
+ const slotAst = ast.slots[slotName];
4018
+ const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
3827
4019
  const params = [`__render: ${name}, __ctx: ${ctxStr}`];
3828
4020
  const scope = ast.slots[slotName].scope;
3829
4021
  if (scope) {
@@ -3849,7 +4041,7 @@ class CodeGenerator {
3849
4041
  let propVar;
3850
4042
  if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
3851
4043
  propVar = this.generateId("props");
3852
- this.addLine(`const ${propVar} = ${propString};`);
4044
+ this.define(propVar, propString);
3853
4045
  propString = propVar;
3854
4046
  }
3855
4047
  if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
@@ -3861,7 +4053,7 @@ class CodeGenerator {
3861
4053
  let expr;
3862
4054
  if (ast.isDynamic) {
3863
4055
  expr = this.generateId("Comp");
3864
- this.addLine(`let ${expr} = ${compileExpr(ast.name)};`);
4056
+ this.define(expr, compileExpr(ast.name));
3865
4057
  }
3866
4058
  else {
3867
4059
  expr = `\`${ast.name}\``;
@@ -3882,9 +4074,28 @@ class CodeGenerator {
3882
4074
  if (ast.isDynamic) {
3883
4075
  blockExpr = `toggler(${expr}, ${blockExpr})`;
3884
4076
  }
4077
+ // event handling
4078
+ if (ast.on) {
4079
+ blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);
4080
+ }
3885
4081
  block = this.createBlock(block, "multi", ctx);
3886
4082
  this.insertBlock(blockExpr, block, ctx);
3887
4083
  }
4084
+ wrapWithEventCatcher(expr, on) {
4085
+ this.helpers.add("createCatcher");
4086
+ let name = this.generateId("catcher");
4087
+ let spec = {};
4088
+ let handlers = [];
4089
+ for (let ev in on) {
4090
+ let handlerId = this.generateId("hdlr");
4091
+ let idx = handlers.push(handlerId) - 1;
4092
+ spec[ev] = idx;
4093
+ const handler = this.generateHandlerCode(ev, on[ev]);
4094
+ this.define(handlerId, handler);
4095
+ }
4096
+ this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });
4097
+ return `${name}(${expr}, [${handlers.join(",")}])`;
4098
+ }
3888
4099
  compileTSlot(ast, ctx) {
3889
4100
  this.helpers.add("callSlot");
3890
4101
  let { block } = ctx;
@@ -3906,13 +4117,17 @@ class CodeGenerator {
3906
4117
  else {
3907
4118
  if (dynamic) {
3908
4119
  let name = this.generateId("slot");
3909
- this.addLine(`const ${name} = ${slotName};`);
4120
+ this.define(name, slotName);
3910
4121
  blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}), ${dynamic}, ${scope})`;
3911
4122
  }
3912
4123
  else {
3913
4124
  blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
3914
4125
  }
3915
4126
  }
4127
+ // event handling
4128
+ if (ast.on) {
4129
+ blockString = this.wrapWithEventCatcher(blockString, ast.on);
4130
+ }
3916
4131
  if (block) {
3917
4132
  this.insertAnchor(block);
3918
4133
  }
@@ -3933,7 +4148,7 @@ class CodeGenerator {
3933
4148
  if (this.target.loopLevel || !this.hasSafeContext) {
3934
4149
  ctxStr = this.generateId("ctx");
3935
4150
  this.helpers.add("capture");
3936
- this.addLine(`const ${ctxStr} = capture(ctx);`);
4151
+ this.define(ctxStr, `capture(ctx);`);
3937
4152
  }
3938
4153
  const blockString = `component(Portal, {target: ${ast.target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
3939
4154
  if (block) {
@@ -4067,8 +4282,8 @@ function parseDOMNode(node, ctx) {
4067
4282
  const ref = node.getAttribute("t-ref");
4068
4283
  node.removeAttribute("t-ref");
4069
4284
  const nodeAttrsNames = node.getAttributeNames();
4070
- const attrs = {};
4071
- const on = {};
4285
+ let attrs = null;
4286
+ let on = null;
4072
4287
  let model = null;
4073
4288
  for (let attr of nodeAttrsNames) {
4074
4289
  const value = node.getAttribute(attr);
@@ -4076,6 +4291,7 @@ function parseDOMNode(node, ctx) {
4076
4291
  if (attr === "t-on") {
4077
4292
  throw new Error("Missing event name with t-on directive");
4078
4293
  }
4294
+ on = on || {};
4079
4295
  on[attr.slice(5)] = value;
4080
4296
  }
4081
4297
  else if (attr.startsWith("t-model")) {
@@ -4113,6 +4329,7 @@ function parseDOMNode(node, ctx) {
4113
4329
  targetAttr: isCheckboxInput ? "checked" : "value",
4114
4330
  specialInitTargetAttr: isRadioInput ? "checked" : null,
4115
4331
  eventType,
4332
+ hasDynamicChildren: false,
4116
4333
  shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
4117
4334
  shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
4118
4335
  };
@@ -4133,6 +4350,7 @@ function parseDOMNode(node, ctx) {
4133
4350
  if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
4134
4351
  tModel.hasDynamicChildren = true;
4135
4352
  }
4353
+ attrs = attrs || {};
4136
4354
  attrs[attr] = value;
4137
4355
  }
4138
4356
  }
@@ -4283,7 +4501,7 @@ function parseTCall(node, ctx) {
4283
4501
  if (ast && ast.type === 11 /* TComponent */) {
4284
4502
  return {
4285
4503
  ...ast,
4286
- slots: { default: { content: tcall } },
4504
+ slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
4287
4505
  };
4288
4506
  }
4289
4507
  }
@@ -4367,7 +4585,6 @@ function parseTSetNode(node, ctx) {
4367
4585
  // -----------------------------------------------------------------------------
4368
4586
  // Error messages when trying to use an unsupported directive on a component
4369
4587
  const directiveErrorMap = new Map([
4370
- ["t-on", "t-on is no longer supported on components. Consider passing a callback in props."],
4371
4588
  [
4372
4589
  "t-ref",
4373
4590
  "t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
@@ -4396,18 +4613,26 @@ function parseComponent(node, ctx) {
4396
4613
  node.removeAttribute("t-props");
4397
4614
  const defaultSlotScope = node.getAttribute("t-slot-scope");
4398
4615
  node.removeAttribute("t-slot-scope");
4399
- const props = {};
4616
+ let on = null;
4617
+ let props = null;
4400
4618
  for (let name of node.getAttributeNames()) {
4401
4619
  const value = node.getAttribute(name);
4402
4620
  if (name.startsWith("t-")) {
4403
- const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
4404
- throw new Error(message || `unsupported directive on Component: ${name}`);
4621
+ if (name.startsWith("t-on-")) {
4622
+ on = on || {};
4623
+ on[name.slice(5)] = value;
4624
+ }
4625
+ else {
4626
+ const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
4627
+ throw new Error(message || `unsupported directive on Component: ${name}`);
4628
+ }
4405
4629
  }
4406
4630
  else {
4631
+ props = props || {};
4407
4632
  props[name] = value;
4408
4633
  }
4409
4634
  }
4410
- const slots = {};
4635
+ let slots = null;
4411
4636
  if (node.hasChildNodes()) {
4412
4637
  const clone = node.cloneNode(true);
4413
4638
  // named slots
@@ -4435,246 +4660,614 @@ function parseComponent(node, ctx) {
4435
4660
  slotNode.remove();
4436
4661
  const slotAst = parseNode(slotNode, ctx);
4437
4662
  if (slotAst) {
4438
- const slotInfo = { content: slotAst };
4439
- const attrs = {};
4663
+ let on = null;
4664
+ let attrs = null;
4665
+ let scope = null;
4440
4666
  for (let attributeName of slotNode.getAttributeNames()) {
4441
4667
  const value = slotNode.getAttribute(attributeName);
4442
4668
  if (attributeName === "t-slot-scope") {
4443
- slotInfo.scope = value;
4669
+ scope = value;
4444
4670
  continue;
4445
4671
  }
4446
- attrs[attributeName] = value;
4672
+ else if (attributeName.startsWith("t-on-")) {
4673
+ on = on || {};
4674
+ on[attributeName.slice(5)] = value;
4675
+ }
4676
+ else {
4677
+ attrs = attrs || {};
4678
+ attrs[attributeName] = value;
4679
+ }
4680
+ }
4681
+ slots = slots || {};
4682
+ slots[name] = { content: slotAst, on, attrs, scope };
4683
+ }
4684
+ }
4685
+ // default slot
4686
+ const defaultContent = parseChildNodes(clone, ctx);
4687
+ if (defaultContent) {
4688
+ slots = slots || {};
4689
+ slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
4690
+ }
4691
+ }
4692
+ return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
4693
+ }
4694
+ // -----------------------------------------------------------------------------
4695
+ // Slots
4696
+ // -----------------------------------------------------------------------------
4697
+ function parseTSlot(node, ctx) {
4698
+ if (!node.hasAttribute("t-slot")) {
4699
+ return null;
4700
+ }
4701
+ const name = node.getAttribute("t-slot");
4702
+ node.removeAttribute("t-slot");
4703
+ let attrs = null;
4704
+ let on = null;
4705
+ for (let attributeName of node.getAttributeNames()) {
4706
+ const value = node.getAttribute(attributeName);
4707
+ if (attributeName.startsWith("t-on-")) {
4708
+ on = on || {};
4709
+ on[attributeName.slice(5)] = value;
4710
+ }
4711
+ else {
4712
+ attrs = attrs || {};
4713
+ attrs[attributeName] = value;
4714
+ }
4715
+ }
4716
+ return {
4717
+ type: 14 /* TSlot */,
4718
+ name,
4719
+ attrs,
4720
+ on,
4721
+ defaultContent: parseChildNodes(node, ctx),
4722
+ };
4723
+ }
4724
+ function parseTTranslation(node, ctx) {
4725
+ if (node.getAttribute("t-translation") !== "off") {
4726
+ return null;
4727
+ }
4728
+ node.removeAttribute("t-translation");
4729
+ return {
4730
+ type: 16 /* TTranslation */,
4731
+ content: parseNode(node, ctx),
4732
+ };
4733
+ }
4734
+ // -----------------------------------------------------------------------------
4735
+ // Portal
4736
+ // -----------------------------------------------------------------------------
4737
+ function parseTPortal(node, ctx) {
4738
+ if (!node.hasAttribute("t-portal")) {
4739
+ return null;
4740
+ }
4741
+ const target = node.getAttribute("t-portal");
4742
+ node.removeAttribute("t-portal");
4743
+ const content = parseNode(node, ctx);
4744
+ if (!content) {
4745
+ return {
4746
+ type: 0 /* Text */,
4747
+ value: "",
4748
+ };
4749
+ }
4750
+ return {
4751
+ type: 17 /* TPortal */,
4752
+ target,
4753
+ content,
4754
+ };
4755
+ }
4756
+ // -----------------------------------------------------------------------------
4757
+ // helpers
4758
+ // -----------------------------------------------------------------------------
4759
+ /**
4760
+ * Parse all the child nodes of a given node and return a list of ast elements
4761
+ */
4762
+ function parseChildren(node, ctx) {
4763
+ const children = [];
4764
+ for (let child of node.childNodes) {
4765
+ const childAst = parseNode(child, ctx);
4766
+ if (childAst) {
4767
+ if (childAst.type === 3 /* Multi */) {
4768
+ children.push(...childAst.content);
4769
+ }
4770
+ else {
4771
+ children.push(childAst);
4772
+ }
4773
+ }
4774
+ }
4775
+ return children;
4776
+ }
4777
+ /**
4778
+ * Parse all the child nodes of a given node and return an ast if possible.
4779
+ * In the case there are multiple children, they are wrapped in a astmulti.
4780
+ */
4781
+ function parseChildNodes(node, ctx) {
4782
+ const children = parseChildren(node, ctx);
4783
+ switch (children.length) {
4784
+ case 0:
4785
+ return null;
4786
+ case 1:
4787
+ return children[0];
4788
+ default:
4789
+ return { type: 3 /* Multi */, content: children };
4790
+ }
4791
+ }
4792
+ /**
4793
+ * Normalizes the content of an Element so that t-if/t-elif/t-else directives
4794
+ * immediately follow one another (by removing empty text nodes or comments).
4795
+ * Throws an error when a conditional branching statement is malformed. This
4796
+ * function modifies the Element in place.
4797
+ *
4798
+ * @param el the element containing the tree that should be normalized
4799
+ */
4800
+ function normalizeTIf(el) {
4801
+ let tbranch = el.querySelectorAll("[t-elif], [t-else]");
4802
+ for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
4803
+ let node = tbranch[i];
4804
+ let prevElem = node.previousElementSibling;
4805
+ let pattr = (name) => prevElem.getAttribute(name);
4806
+ let nattr = (name) => +!!node.getAttribute(name);
4807
+ if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
4808
+ if (pattr("t-foreach")) {
4809
+ throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
4810
+ }
4811
+ if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
4812
+ return a + b;
4813
+ }) > 1) {
4814
+ throw new Error("Only one conditional branching directive is allowed per node");
4815
+ }
4816
+ // All text (with only spaces) and comment nodes (nodeType 8) between
4817
+ // branch nodes are removed
4818
+ let textNode;
4819
+ while ((textNode = node.previousSibling) !== prevElem) {
4820
+ if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
4821
+ throw new Error("text is not allowed between branching directives");
4822
+ }
4823
+ textNode.remove();
4824
+ }
4825
+ }
4826
+ else {
4827
+ throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
4828
+ }
4829
+ }
4830
+ }
4831
+ /**
4832
+ * Normalizes the content of an Element so that t-esc directives on components
4833
+ * are removed and instead places a <t t-esc=""> as the default slot of the
4834
+ * component. Also throws if the component already has content. This function
4835
+ * modifies the Element in place.
4836
+ *
4837
+ * @param el the element containing the tree that should be normalized
4838
+ */
4839
+ function normalizeTEsc(el) {
4840
+ const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
4841
+ for (const el of elements) {
4842
+ if (el.childNodes.length) {
4843
+ throw new Error("Cannot have t-esc on a component that already has content");
4844
+ }
4845
+ const value = el.getAttribute("t-esc");
4846
+ el.removeAttribute("t-esc");
4847
+ const t = el.ownerDocument.createElement("t");
4848
+ if (value != null) {
4849
+ t.setAttribute("t-esc", value);
4850
+ }
4851
+ el.appendChild(t);
4852
+ }
4853
+ }
4854
+ /**
4855
+ * Normalizes the tree inside a given element and do some preliminary validation
4856
+ * on it. This function modifies the Element in place.
4857
+ *
4858
+ * @param el the element containing the tree that should be normalized
4859
+ */
4860
+ function normalizeXML(el) {
4861
+ normalizeTIf(el);
4862
+ normalizeTEsc(el);
4863
+ }
4864
+ /**
4865
+ * Parses an XML string into an XML document, throwing errors on parser errors
4866
+ * instead of returning an XML document containing the parseerror.
4867
+ *
4868
+ * @param xml the string to parse
4869
+ * @returns an XML document corresponding to the content of the string
4870
+ */
4871
+ function parseXML$1(xml) {
4872
+ const parser = new DOMParser();
4873
+ const doc = parser.parseFromString(xml, "text/xml");
4874
+ if (doc.getElementsByTagName("parsererror").length) {
4875
+ let msg = "Invalid XML in template.";
4876
+ const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
4877
+ if (parsererrorText) {
4878
+ msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
4879
+ const re = /\d+/g;
4880
+ const firstMatch = re.exec(parsererrorText);
4881
+ if (firstMatch) {
4882
+ const lineNumber = Number(firstMatch[0]);
4883
+ const line = xml.split("\n")[lineNumber - 1];
4884
+ const secondMatch = re.exec(parsererrorText);
4885
+ if (line && secondMatch) {
4886
+ const columnIndex = Number(secondMatch[0]) - 1;
4887
+ if (line[columnIndex]) {
4888
+ msg +=
4889
+ `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
4890
+ `${line}\n${"-".repeat(columnIndex - 1)}^`;
4891
+ }
4447
4892
  }
4448
- if (Object.keys(attrs).length) {
4449
- slotInfo.attrs = attrs;
4893
+ }
4894
+ }
4895
+ throw new Error(msg);
4896
+ }
4897
+ return doc;
4898
+ }
4899
+
4900
+ function compile(template, options = {}) {
4901
+ // parsing
4902
+ const ast = parse(template);
4903
+ // some work
4904
+ const hasSafeContext = template instanceof Node
4905
+ ? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
4906
+ : !template.includes("t-set") && !template.includes("t-call");
4907
+ // code generation
4908
+ const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
4909
+ const code = codeGenerator.generateCode();
4910
+ // template function
4911
+ return new Function("bdom, helpers", code);
4912
+ }
4913
+
4914
+ const TIMEOUT = Symbol("timeout");
4915
+ function wrapError(fn, hookName) {
4916
+ const error = new Error(`The following error occurred in ${hookName}: `);
4917
+ const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
4918
+ const node = getCurrent();
4919
+ return (...args) => {
4920
+ try {
4921
+ const result = fn(...args);
4922
+ if (result instanceof Promise) {
4923
+ if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
4924
+ const fiber = node.fiber;
4925
+ Promise.race([
4926
+ result,
4927
+ new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
4928
+ ]).then((res) => {
4929
+ if (res === TIMEOUT && node.fiber === fiber) {
4930
+ console.warn(timeoutError);
4931
+ }
4932
+ });
4450
4933
  }
4451
- slots[name] = slotInfo;
4934
+ return result.catch((cause) => {
4935
+ error.cause = cause;
4936
+ if (cause instanceof Error) {
4937
+ error.message += `"${cause.message}"`;
4938
+ }
4939
+ throw error;
4940
+ });
4452
4941
  }
4942
+ return result;
4943
+ }
4944
+ catch (cause) {
4945
+ if (cause instanceof Error) {
4946
+ error.message += `"${cause.message}"`;
4947
+ }
4948
+ throw error;
4949
+ }
4950
+ };
4951
+ }
4952
+ // -----------------------------------------------------------------------------
4953
+ // hooks
4954
+ // -----------------------------------------------------------------------------
4955
+ function onWillStart(fn) {
4956
+ const node = getCurrent();
4957
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4958
+ node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
4959
+ }
4960
+ function onWillUpdateProps(fn) {
4961
+ const node = getCurrent();
4962
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4963
+ node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
4964
+ }
4965
+ function onMounted(fn) {
4966
+ const node = getCurrent();
4967
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4968
+ node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
4969
+ }
4970
+ function onWillPatch(fn) {
4971
+ const node = getCurrent();
4972
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4973
+ node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
4974
+ }
4975
+ function onPatched(fn) {
4976
+ const node = getCurrent();
4977
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4978
+ node.patched.push(decorate(fn.bind(node.component), "onPatched"));
4979
+ }
4980
+ function onWillUnmount(fn) {
4981
+ const node = getCurrent();
4982
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4983
+ node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
4984
+ }
4985
+ function onWillDestroy(fn) {
4986
+ const node = getCurrent();
4987
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4988
+ node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
4989
+ }
4990
+ function onWillRender(fn) {
4991
+ const node = getCurrent();
4992
+ const renderFn = node.renderFn;
4993
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
4994
+ fn = decorate(fn.bind(node.component), "onWillRender");
4995
+ node.renderFn = () => {
4996
+ fn();
4997
+ return renderFn();
4998
+ };
4999
+ }
5000
+ function onRendered(fn) {
5001
+ const node = getCurrent();
5002
+ const renderFn = node.renderFn;
5003
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
5004
+ fn = decorate(fn.bind(node.component), "onRendered");
5005
+ node.renderFn = () => {
5006
+ const result = renderFn();
5007
+ fn();
5008
+ return result;
5009
+ };
5010
+ }
5011
+ function onError(callback) {
5012
+ const node = getCurrent();
5013
+ let handlers = nodeErrorHandlers.get(node);
5014
+ if (!handlers) {
5015
+ handlers = [];
5016
+ nodeErrorHandlers.set(node, handlers);
5017
+ }
5018
+ handlers.push(callback.bind(node.component));
5019
+ }
5020
+
5021
+ class Component {
5022
+ constructor(props, env, node) {
5023
+ this.props = props;
5024
+ this.env = env;
5025
+ this.__owl__ = node;
5026
+ }
5027
+ setup() { }
5028
+ render(deep = false) {
5029
+ this.__owl__.render(deep);
5030
+ }
5031
+ }
5032
+ Component.template = "";
5033
+
5034
+ const VText = text("").constructor;
5035
+ class VPortal extends VText {
5036
+ constructor(selector, realBDom) {
5037
+ super("");
5038
+ this.target = null;
5039
+ this.selector = selector;
5040
+ this.realBDom = realBDom;
5041
+ }
5042
+ mount(parent, anchor) {
5043
+ super.mount(parent, anchor);
5044
+ this.target = document.querySelector(this.selector);
5045
+ if (!this.target) {
5046
+ let el = this.el;
5047
+ while (el && el.parentElement instanceof HTMLElement) {
5048
+ el = el.parentElement;
5049
+ }
5050
+ this.target = el && el.querySelector(this.selector);
5051
+ if (!this.target) {
5052
+ throw new Error("invalid portal target");
5053
+ }
5054
+ }
5055
+ this.realBDom.mount(this.target, null);
5056
+ }
5057
+ beforeRemove() {
5058
+ this.realBDom.beforeRemove();
5059
+ }
5060
+ remove() {
5061
+ if (this.realBDom) {
5062
+ super.remove();
5063
+ this.realBDom.remove();
5064
+ this.realBDom = null;
5065
+ }
5066
+ }
5067
+ patch(other) {
5068
+ super.patch(other);
5069
+ if (this.realBDom) {
5070
+ this.realBDom.patch(other.realBDom, true);
4453
5071
  }
4454
- // default slot
4455
- const defaultContent = parseChildNodes(clone, ctx);
4456
- if (defaultContent) {
4457
- slots.default = { content: defaultContent };
4458
- if (defaultSlotScope) {
4459
- slots.default.scope = defaultSlotScope;
4460
- }
5072
+ else {
5073
+ this.realBDom = other.realBDom;
5074
+ this.realBDom.mount(this.target, null);
4461
5075
  }
4462
5076
  }
4463
- return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots };
4464
5077
  }
4465
- // -----------------------------------------------------------------------------
4466
- // Slots
4467
- // -----------------------------------------------------------------------------
4468
- function parseTSlot(node, ctx) {
4469
- if (!node.hasAttribute("t-slot")) {
4470
- return null;
5078
+ class Portal extends Component {
5079
+ setup() {
5080
+ const node = this.__owl__;
5081
+ const renderFn = node.renderFn;
5082
+ node.renderFn = () => new VPortal(this.props.target, renderFn());
5083
+ onWillUnmount(() => {
5084
+ if (node.bdom) {
5085
+ node.bdom.remove();
5086
+ }
5087
+ });
4471
5088
  }
4472
- const name = node.getAttribute("t-slot");
4473
- node.removeAttribute("t-slot");
4474
- const attrs = {};
4475
- for (let attributeName of node.getAttributeNames()) {
4476
- const value = node.getAttribute(attributeName);
4477
- attrs[attributeName] = value;
5089
+ }
5090
+ Portal.template = xml `<t t-slot="default"/>`;
5091
+ Portal.props = {
5092
+ target: {
5093
+ type: String,
5094
+ },
5095
+ slots: true,
5096
+ };
5097
+
5098
+ /**
5099
+ * This file contains utility functions that will be injected in each template,
5100
+ * to perform various useful tasks in the compiled code.
5101
+ */
5102
+ function withDefault(value, defaultValue) {
5103
+ return value === undefined || value === null || value === false ? defaultValue : value;
5104
+ }
5105
+ function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
5106
+ key = key + "__slot_" + name;
5107
+ const slots = ctx.props[TARGET].slots || {};
5108
+ const { __render, __ctx, __scope } = slots[name] || {};
5109
+ const slotScope = Object.create(__ctx || {});
5110
+ if (__scope) {
5111
+ slotScope[__scope] = extra;
4478
5112
  }
4479
- return {
4480
- type: 14 /* TSlot */,
4481
- name,
4482
- attrs,
4483
- defaultContent: parseChildNodes(node, ctx),
4484
- };
5113
+ const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
5114
+ if (defaultContent) {
5115
+ let child1 = undefined;
5116
+ let child2 = undefined;
5117
+ if (slotBDom) {
5118
+ child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
5119
+ }
5120
+ else {
5121
+ child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
5122
+ }
5123
+ return multi([child1, child2]);
5124
+ }
5125
+ return slotBDom || text("");
4485
5126
  }
4486
- function parseTTranslation(node, ctx) {
4487
- if (node.getAttribute("t-translation") !== "off") {
4488
- return null;
5127
+ function capture(ctx) {
5128
+ const component = ctx.__owl__.component;
5129
+ const result = Object.create(component);
5130
+ for (let k in ctx) {
5131
+ result[k] = ctx[k];
4489
5132
  }
4490
- node.removeAttribute("t-translation");
4491
- return {
4492
- type: 16 /* TTranslation */,
4493
- content: parseNode(node, ctx),
4494
- };
5133
+ return result;
4495
5134
  }
4496
- // -----------------------------------------------------------------------------
4497
- // Portal
4498
- // -----------------------------------------------------------------------------
4499
- function parseTPortal(node, ctx) {
4500
- if (!node.hasAttribute("t-portal")) {
4501
- return null;
5135
+ function withKey(elem, k) {
5136
+ elem.key = k;
5137
+ return elem;
5138
+ }
5139
+ function prepareList(collection) {
5140
+ let keys;
5141
+ let values;
5142
+ if (Array.isArray(collection)) {
5143
+ keys = collection;
5144
+ values = collection;
4502
5145
  }
4503
- const target = node.getAttribute("t-portal");
4504
- node.removeAttribute("t-portal");
4505
- const content = parseNode(node, ctx);
4506
- if (!content) {
4507
- return {
4508
- type: 0 /* Text */,
4509
- value: "",
4510
- };
5146
+ else if (collection) {
5147
+ values = Object.keys(collection);
5148
+ keys = Object.values(collection);
4511
5149
  }
4512
- return {
4513
- type: 17 /* TPortal */,
4514
- target,
4515
- content,
4516
- };
5150
+ else {
5151
+ throw new Error("Invalid loop expression");
5152
+ }
5153
+ const n = values.length;
5154
+ return [keys, values, n, new Array(n)];
4517
5155
  }
4518
- // -----------------------------------------------------------------------------
4519
- // helpers
4520
- // -----------------------------------------------------------------------------
4521
- /**
4522
- * Parse all the child nodes of a given node and return a list of ast elements
4523
- */
4524
- function parseChildren(node, ctx) {
4525
- const children = [];
4526
- for (let child of node.childNodes) {
4527
- const childAst = parseNode(child, ctx);
4528
- if (childAst) {
4529
- if (childAst.type === 3 /* Multi */) {
4530
- children.push(...childAst.content);
4531
- }
4532
- else {
4533
- children.push(childAst);
4534
- }
5156
+ const isBoundary = Symbol("isBoundary");
5157
+ function setContextValue(ctx, key, value) {
5158
+ const ctx0 = ctx;
5159
+ while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
5160
+ const newCtx = ctx.__proto__;
5161
+ if (!newCtx) {
5162
+ ctx = ctx0;
5163
+ break;
4535
5164
  }
5165
+ ctx = newCtx;
4536
5166
  }
4537
- return children;
5167
+ ctx[key] = value;
4538
5168
  }
4539
- /**
4540
- * Parse all the child nodes of a given node and return an ast if possible.
4541
- * In the case there are multiple children, they are wrapped in a astmulti.
4542
- */
4543
- function parseChildNodes(node, ctx) {
4544
- const children = parseChildren(node, ctx);
4545
- switch (children.length) {
4546
- case 0:
4547
- return null;
4548
- case 1:
4549
- return children[0];
4550
- default:
4551
- return { type: 3 /* Multi */, content: children };
4552
- }
5169
+ function toNumber(val) {
5170
+ const n = parseFloat(val);
5171
+ return isNaN(n) ? val : n;
4553
5172
  }
4554
- /**
4555
- * Normalizes the content of an Element so that t-if/t-elif/t-else directives
4556
- * immediately follow one another (by removing empty text nodes or comments).
4557
- * Throws an error when a conditional branching statement is malformed. This
4558
- * function modifies the Element in place.
4559
- *
4560
- * @param el the element containing the tree that should be normalized
4561
- */
4562
- function normalizeTIf(el) {
4563
- let tbranch = el.querySelectorAll("[t-elif], [t-else]");
4564
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
4565
- let node = tbranch[i];
4566
- let prevElem = node.previousElementSibling;
4567
- let pattr = (name) => prevElem.getAttribute(name);
4568
- let nattr = (name) => +!!node.getAttribute(name);
4569
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
4570
- if (pattr("t-foreach")) {
4571
- throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
4572
- }
4573
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
4574
- return a + b;
4575
- }) > 1) {
4576
- throw new Error("Only one conditional branching directive is allowed per node");
4577
- }
4578
- // All text (with only spaces) and comment nodes (nodeType 8) between
4579
- // branch nodes are removed
4580
- let textNode;
4581
- while ((textNode = node.previousSibling) !== prevElem) {
4582
- if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
4583
- throw new Error("text is not allowed between branching directives");
4584
- }
4585
- textNode.remove();
4586
- }
4587
- }
4588
- else {
4589
- throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
5173
+ function shallowEqual(l1, l2) {
5174
+ for (let i = 0, l = l1.length; i < l; i++) {
5175
+ if (l1[i] !== l2[i]) {
5176
+ return false;
4590
5177
  }
4591
5178
  }
5179
+ return true;
4592
5180
  }
4593
- /**
4594
- * Normalizes the content of an Element so that t-esc directives on components
4595
- * are removed and instead places a <t t-esc=""> as the default slot of the
4596
- * component. Also throws if the component already has content. This function
4597
- * modifies the Element in place.
4598
- *
4599
- * @param el the element containing the tree that should be normalized
5181
+ class LazyValue {
5182
+ constructor(fn, ctx, node) {
5183
+ this.fn = fn;
5184
+ this.ctx = capture(ctx);
5185
+ this.node = node;
5186
+ }
5187
+ evaluate() {
5188
+ return this.fn(this.ctx, this.node);
5189
+ }
5190
+ toString() {
5191
+ return this.evaluate().toString();
5192
+ }
5193
+ }
5194
+ /*
5195
+ * Safely outputs `value` as a block depending on the nature of `value`
4600
5196
  */
4601
- function normalizeTEsc(el) {
4602
- const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
4603
- for (const el of elements) {
4604
- if (el.childNodes.length) {
4605
- throw new Error("Cannot have t-esc on a component that already has content");
4606
- }
4607
- const value = el.getAttribute("t-esc");
4608
- el.removeAttribute("t-esc");
4609
- const t = el.ownerDocument.createElement("t");
4610
- if (value != null) {
4611
- t.setAttribute("t-esc", value);
4612
- }
4613
- el.appendChild(t);
5197
+ function safeOutput(value) {
5198
+ if (!value) {
5199
+ return value;
5200
+ }
5201
+ let safeKey;
5202
+ let block;
5203
+ if (value instanceof Markup) {
5204
+ safeKey = `string_safe`;
5205
+ block = html(value);
5206
+ }
5207
+ else if (value instanceof LazyValue) {
5208
+ safeKey = `lazy_value`;
5209
+ block = value.evaluate();
5210
+ }
5211
+ else if (value instanceof String || typeof value === "string") {
5212
+ safeKey = "string_unsafe";
5213
+ block = text(value);
4614
5214
  }
5215
+ else {
5216
+ // Assuming it is a block
5217
+ safeKey = "block_safe";
5218
+ block = value;
5219
+ }
5220
+ return toggler(safeKey, block);
4615
5221
  }
4616
- /**
4617
- * Normalizes the tree inside a given element and do some preliminary validation
4618
- * on it. This function modifies the Element in place.
4619
- *
4620
- * @param el the element containing the tree that should be normalized
4621
- */
4622
- function normalizeXML(el) {
4623
- normalizeTIf(el);
4624
- normalizeTEsc(el);
5222
+ let boundFunctions = new WeakMap();
5223
+ function bind(ctx, fn) {
5224
+ let component = ctx.__owl__.component;
5225
+ let boundFnMap = boundFunctions.get(component);
5226
+ if (!boundFnMap) {
5227
+ boundFnMap = new WeakMap();
5228
+ boundFunctions.set(component, boundFnMap);
5229
+ }
5230
+ let boundFn = boundFnMap.get(fn);
5231
+ if (!boundFn) {
5232
+ boundFn = fn.bind(component);
5233
+ boundFnMap.set(fn, boundFn);
5234
+ }
5235
+ return boundFn;
4625
5236
  }
4626
- /**
4627
- * Parses an XML string into an XML document, throwing errors on parser errors
4628
- * instead of returning an XML document containing the parseerror.
4629
- *
4630
- * @param xml the string to parse
4631
- * @returns an XML document corresponding to the content of the string
4632
- */
4633
- function parseXML$1(xml) {
4634
- const parser = new DOMParser();
4635
- const doc = parser.parseFromString(xml, "text/xml");
4636
- if (doc.getElementsByTagName("parsererror").length) {
4637
- let msg = "Invalid XML in template.";
4638
- const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
4639
- if (parsererrorText) {
4640
- msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
4641
- const re = /\d+/g;
4642
- const firstMatch = re.exec(parsererrorText);
4643
- if (firstMatch) {
4644
- const lineNumber = Number(firstMatch[0]);
4645
- const line = xml.split("\n")[lineNumber - 1];
4646
- const secondMatch = re.exec(parsererrorText);
4647
- if (line && secondMatch) {
4648
- const columnIndex = Number(secondMatch[0]) - 1;
4649
- if (line[columnIndex]) {
4650
- msg +=
4651
- `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
4652
- `${line}\n${"-".repeat(columnIndex - 1)}^`;
4653
- }
4654
- }
5237
+ function multiRefSetter(refs, name) {
5238
+ let count = 0;
5239
+ return (el) => {
5240
+ if (el) {
5241
+ count++;
5242
+ if (count > 1) {
5243
+ throw new Error("Cannot have 2 elements with same ref name at the same time");
4655
5244
  }
4656
5245
  }
4657
- throw new Error(msg);
4658
- }
4659
- return doc;
4660
- }
4661
-
4662
- function compile(template, options = {}) {
4663
- // parsing
4664
- const ast = parse(template);
4665
- // some work
4666
- const hasSafeContext = template instanceof Node
4667
- ? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
4668
- : !template.includes("t-set") && !template.includes("t-call");
4669
- // code generation
4670
- const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
4671
- const code = codeGenerator.generateCode();
4672
- // template function
4673
- return new Function("bdom, helpers", code);
4674
- }
5246
+ if (count === 0 || el) {
5247
+ refs[name] = el;
5248
+ }
5249
+ };
5250
+ }
5251
+ const helpers = {
5252
+ withDefault,
5253
+ zero: Symbol("zero"),
5254
+ isBoundary,
5255
+ callSlot,
5256
+ capture,
5257
+ withKey,
5258
+ prepareList,
5259
+ setContextValue,
5260
+ multiRefSetter,
5261
+ shallowEqual,
5262
+ toNumber,
5263
+ validateProps,
5264
+ LazyValue,
5265
+ safeOutput,
5266
+ bind,
5267
+ createCatcher,
5268
+ };
4675
5269
 
4676
5270
  const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
4677
- const globalTemplates = {};
4678
5271
  function parseXML(xml) {
4679
5272
  const parser = new DOMParser();
4680
5273
  const doc = parser.parseFromString(xml, "text/xml");
@@ -4703,23 +5296,32 @@ function parseXML(xml) {
4703
5296
  }
4704
5297
  return doc;
4705
5298
  }
5299
+ /**
5300
+ * Returns the helpers object that will be injected in each template closure
5301
+ * function
5302
+ */
5303
+ function makeHelpers(getTemplate) {
5304
+ return Object.assign({}, helpers, {
5305
+ Portal,
5306
+ markRaw,
5307
+ getTemplate,
5308
+ call: (owner, subTemplate, ctx, parent, key) => {
5309
+ const template = getTemplate(subTemplate);
5310
+ return toggler(subTemplate, template.call(owner, ctx, parent, key));
5311
+ },
5312
+ });
5313
+ }
4706
5314
  class TemplateSet {
4707
5315
  constructor(config = {}) {
4708
5316
  this.rawTemplates = Object.create(globalTemplates);
4709
5317
  this.templates = {};
4710
- this.utils = Object.assign({}, UTILS, {
4711
- call: (owner, subTemplate, ctx, parent, key) => {
4712
- const template = this.getTemplate(subTemplate);
4713
- return toggler(subTemplate, template.call(owner, ctx, parent, key));
4714
- },
4715
- getTemplate: (name) => this.getTemplate(name),
4716
- });
4717
5318
  this.dev = config.dev || false;
4718
5319
  this.translateFn = config.translateFn;
4719
5320
  this.translatableAttributes = config.translatableAttributes;
4720
5321
  if (config.templates) {
4721
5322
  this.addTemplates(config.templates);
4722
5323
  }
5324
+ this.helpers = makeHelpers(this.getTemplate.bind(this));
4723
5325
  }
4724
5326
  addTemplate(name, template, options = {}) {
4725
5327
  if (name in this.rawTemplates && !options.allowDuplicate) {
@@ -4757,7 +5359,7 @@ class TemplateSet {
4757
5359
  this.templates[name] = function (context, parent) {
4758
5360
  return templates[name].call(this, context, parent);
4759
5361
  };
4760
- const template = templateFn(bdom, this.utils);
5362
+ const template = templateFn(bdom, this.helpers);
4761
5363
  this.templates[name] = template;
4762
5364
  }
4763
5365
  return this.templates[name];
@@ -4770,160 +5372,9 @@ class TemplateSet {
4770
5372
  translatableAttributes: this.translatableAttributes,
4771
5373
  });
4772
5374
  }
4773
- }
4774
- // -----------------------------------------------------------------------------
4775
- // xml tag helper
4776
- // -----------------------------------------------------------------------------
4777
- function xml(...args) {
4778
- const name = `__template__${xml.nextId++}`;
4779
- const value = String.raw(...args);
4780
- globalTemplates[name] = value;
4781
- return name;
4782
- }
4783
- xml.nextId = 1;
4784
-
4785
- class Component {
4786
- constructor(props, env, node) {
4787
- this.props = props;
4788
- this.env = env;
4789
- this.__owl__ = node;
4790
- }
4791
- setup() { }
4792
- render(deep = false) {
4793
- this.__owl__.render(deep);
4794
- }
4795
- }
4796
- Component.template = "";
4797
-
4798
- const VText = text("").constructor;
4799
- class VPortal extends VText {
4800
- constructor(selector, realBDom) {
4801
- super("");
4802
- this.target = null;
4803
- this.selector = selector;
4804
- this.realBDom = realBDom;
4805
- }
4806
- mount(parent, anchor) {
4807
- super.mount(parent, anchor);
4808
- this.target = document.querySelector(this.selector);
4809
- if (!this.target) {
4810
- let el = this.el;
4811
- while (el && el.parentElement instanceof HTMLElement) {
4812
- el = el.parentElement;
4813
- }
4814
- this.target = el && el.querySelector(this.selector);
4815
- if (!this.target) {
4816
- throw new Error("invalid portal target");
4817
- }
4818
- }
4819
- this.realBDom.mount(this.target, null);
4820
- }
4821
- beforeRemove() {
4822
- this.realBDom.beforeRemove();
4823
- }
4824
- remove() {
4825
- if (this.realBDom) {
4826
- super.remove();
4827
- this.realBDom.remove();
4828
- this.realBDom = null;
4829
- }
4830
- }
4831
- patch(other) {
4832
- super.patch(other);
4833
- if (this.realBDom) {
4834
- this.realBDom.patch(other.realBDom, true);
4835
- }
4836
- else {
4837
- this.realBDom = other.realBDom;
4838
- this.realBDom.mount(this.target, null);
4839
- }
4840
- }
4841
- }
4842
- class Portal extends Component {
4843
- setup() {
4844
- const node = this.__owl__;
4845
- const renderFn = node.renderFn;
4846
- node.renderFn = () => new VPortal(this.props.target, renderFn());
4847
- onWillUnmount(() => {
4848
- if (node.bdom) {
4849
- node.bdom.remove();
4850
- }
4851
- });
4852
- }
4853
- }
4854
- Portal.template = xml `<t t-slot="default"/>`;
4855
- Portal.props = {
4856
- target: {
4857
- type: String,
4858
- },
4859
- slots: true,
4860
- };
4861
-
4862
- // -----------------------------------------------------------------------------
4863
- // Scheduler
4864
- // -----------------------------------------------------------------------------
4865
- class Scheduler {
4866
- constructor() {
4867
- this.tasks = new Set();
4868
- this.isRunning = false;
4869
- this.requestAnimationFrame = Scheduler.requestAnimationFrame;
4870
- }
4871
- start() {
4872
- this.isRunning = true;
4873
- this.scheduleTasks();
4874
- }
4875
- stop() {
4876
- this.isRunning = false;
4877
- }
4878
- addFiber(fiber) {
4879
- this.tasks.add(fiber.root);
4880
- if (!this.isRunning) {
4881
- this.start();
4882
- }
4883
- }
4884
- /**
4885
- * Process all current tasks. This only applies to the fibers that are ready.
4886
- * Other tasks are left unchanged.
4887
- */
4888
- flush() {
4889
- this.tasks.forEach((fiber) => {
4890
- if (fiber.root !== fiber) {
4891
- this.tasks.delete(fiber);
4892
- return;
4893
- }
4894
- const hasError = fibersInError.has(fiber);
4895
- if (hasError && fiber.counter !== 0) {
4896
- this.tasks.delete(fiber);
4897
- return;
4898
- }
4899
- if (fiber.node.status === 2 /* DESTROYED */) {
4900
- this.tasks.delete(fiber);
4901
- return;
4902
- }
4903
- if (fiber.counter === 0) {
4904
- if (!hasError) {
4905
- fiber.complete();
4906
- }
4907
- this.tasks.delete(fiber);
4908
- }
4909
- });
4910
- if (this.tasks.size === 0) {
4911
- this.stop();
4912
- }
4913
- }
4914
- scheduleTasks() {
4915
- this.requestAnimationFrame(() => {
4916
- this.flush();
4917
- if (this.isRunning) {
4918
- this.scheduleTasks();
4919
- }
4920
- });
4921
- }
4922
- }
4923
- // capture the value of requestAnimationFrame as soon as possible, to avoid
4924
- // interactions with other code, such as test frameworks that override them
4925
- Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
5375
+ }
4926
5376
 
5377
+ let hasBeenLogged = false;
4927
5378
  const DEV_MSG = () => {
4928
5379
  const hash = window.owl ? window.owl.__info__.hash : "master";
4929
5380
  return `Owl is running in 'dev' mode.
@@ -4940,11 +5391,13 @@ class App extends TemplateSet {
4940
5391
  if (config.test) {
4941
5392
  this.dev = true;
4942
5393
  }
4943
- if (this.dev && !config.test) {
5394
+ if (this.dev && !config.test && !hasBeenLogged) {
4944
5395
  console.info(DEV_MSG());
5396
+ hasBeenLogged = true;
4945
5397
  }
4946
- const descrs = Object.getOwnPropertyDescriptors(config.env || {});
4947
- this.env = Object.freeze(Object.defineProperties({}, descrs));
5398
+ const env = config.env || {};
5399
+ const descrs = Object.getOwnPropertyDescriptors(env);
5400
+ this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
4948
5401
  this.props = config.props || {};
4949
5402
  }
4950
5403
  mount(target, options) {
@@ -4987,6 +5440,7 @@ class App extends TemplateSet {
4987
5440
  }
4988
5441
  destroy() {
4989
5442
  if (this.root) {
5443
+ this.scheduler.flush();
4990
5444
  this.root.destroy();
4991
5445
  }
4992
5446
  }
@@ -5007,45 +5461,6 @@ function status(component) {
5007
5461
  }
5008
5462
  }
5009
5463
 
5010
- class Memo extends Component {
5011
- constructor(props, env, node) {
5012
- super(props, env, node);
5013
- // prevent patching process conditionally
5014
- let applyPatch = false;
5015
- const patchFn = node.patch;
5016
- node.patch = () => {
5017
- if (applyPatch) {
5018
- patchFn.call(node);
5019
- applyPatch = false;
5020
- }
5021
- };
5022
- // check props change, and render/apply patch if it changed
5023
- let prevProps = props;
5024
- const updateAndRender = node.updateAndRender;
5025
- node.updateAndRender = function (props, parentFiber) {
5026
- const shouldUpdate = !shallowEqual(prevProps, props);
5027
- if (shouldUpdate) {
5028
- prevProps = props;
5029
- updateAndRender.call(node, props, parentFiber);
5030
- applyPatch = true;
5031
- }
5032
- return Promise.resolve();
5033
- };
5034
- }
5035
- }
5036
- Memo.template = xml `<t t-slot="default"/>`;
5037
- /**
5038
- * we assume that each object have the same set of keys
5039
- */
5040
- function shallowEqual(p1, p2) {
5041
- for (let k in p1) {
5042
- if (k !== "slots" && p1[k] !== p2[k]) {
5043
- return false;
5044
- }
5045
- }
5046
- return true;
5047
- }
5048
-
5049
5464
  // -----------------------------------------------------------------------------
5050
5465
  // useRef
5051
5466
  // -----------------------------------------------------------------------------
@@ -5091,10 +5506,6 @@ function useChildSubEnv(envExtension) {
5091
5506
  const node = getCurrent();
5092
5507
  node.childEnv = extendEnv(node.childEnv, envExtension);
5093
5508
  }
5094
- // -----------------------------------------------------------------------------
5095
- // useEffect
5096
- // -----------------------------------------------------------------------------
5097
- const NO_OP = () => { };
5098
5509
  /**
5099
5510
  * This hook will run a callback when a component is mounted and patched, and
5100
5511
  * will run a cleanup function before patching and before unmounting the
@@ -5112,18 +5523,20 @@ function useEffect(effect, computeDependencies = () => [NaN]) {
5112
5523
  let dependencies;
5113
5524
  onMounted(() => {
5114
5525
  dependencies = computeDependencies();
5115
- cleanup = effect(...dependencies) || NO_OP;
5526
+ cleanup = effect(...dependencies);
5116
5527
  });
5117
5528
  onPatched(() => {
5118
5529
  const newDeps = computeDependencies();
5119
5530
  const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
5120
5531
  if (shouldReapply) {
5121
5532
  dependencies = newDeps;
5122
- cleanup();
5123
- cleanup = effect(...dependencies) || NO_OP;
5533
+ if (cleanup) {
5534
+ cleanup();
5535
+ }
5536
+ cleanup = effect(...dependencies);
5124
5537
  }
5125
5538
  });
5126
- onWillUnmount(() => cleanup());
5539
+ onWillUnmount(() => cleanup && cleanup());
5127
5540
  }
5128
5541
  // -----------------------------------------------------------------------------
5129
5542
  // useExternalListener
@@ -5150,8 +5563,6 @@ function useExternalListener(target, eventName, handler, eventParams) {
5150
5563
 
5151
5564
  config.shouldNormalizeDom = false;
5152
5565
  config.mainEventHandler = mainEventHandler;
5153
- UTILS.Portal = Portal;
5154
- UTILS.markRaw = markRaw;
5155
5566
  const blockDom = {
5156
5567
  config,
5157
5568
  // bdom entry points
@@ -5169,10 +5580,10 @@ const blockDom = {
5169
5580
  };
5170
5581
  const __info__ = {};
5171
5582
 
5172
- export { App, Component, EventBus, Memo, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, whenReady, xml };
5583
+ export { App, Component, EventBus, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, whenReady, xml };
5173
5584
 
5174
5585
 
5175
- __info__.version = '2.0.0-alpha.3';
5176
- __info__.date = '2022-02-25T09:57:37.893Z';
5177
- __info__.hash = '076b0d7';
5586
+ __info__.version = '2.0.0-beta-4';
5587
+ __info__.date = '2022-03-29T13:50:04.545Z';
5588
+ __info__.hash = '55dbc01';
5178
5589
  __info__.url = 'https://github.com/odoo/owl';