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