@odoo/owl 2.0.0-beta.3 → 2.0.0

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.
Files changed (46) hide show
  1. package/README.md +3 -1
  2. package/dist/owl.cjs.js +2077 -1794
  3. package/dist/owl.es.js +2076 -1795
  4. package/dist/owl.iife.js +2077 -1794
  5. package/dist/owl.iife.min.js +1 -1
  6. package/dist/types/compiler/code_generator.d.ts +30 -30
  7. package/dist/types/compiler/index.d.ts +3 -2
  8. package/dist/types/compiler/inline_expressions.d.ts +1 -22
  9. package/dist/types/compiler/parser.d.ts +2 -1
  10. package/dist/types/index.d.ts +1 -27
  11. package/dist/types/owl.d.ts +532 -0
  12. package/dist/types/{app → runtime}/app.d.ts +10 -5
  13. package/dist/types/{blockdom → runtime/blockdom}/attributes.d.ts +0 -0
  14. package/dist/types/{blockdom → runtime/blockdom}/block_compiler.d.ts +0 -0
  15. package/dist/types/{blockdom → runtime/blockdom}/config.d.ts +0 -0
  16. package/dist/types/{blockdom → runtime/blockdom}/event_catcher.d.ts +0 -0
  17. package/dist/types/{blockdom → runtime/blockdom}/events.d.ts +0 -0
  18. package/dist/types/{blockdom → runtime/blockdom}/html.d.ts +0 -0
  19. package/dist/types/{blockdom → runtime/blockdom}/index.d.ts +0 -0
  20. package/dist/types/{blockdom → runtime/blockdom}/list.d.ts +0 -0
  21. package/dist/types/{blockdom → runtime/blockdom}/multi.d.ts +0 -0
  22. package/dist/types/{blockdom → runtime/blockdom}/text.d.ts +0 -0
  23. package/dist/types/{blockdom → runtime/blockdom}/toggler.d.ts +0 -0
  24. package/dist/types/{component → runtime}/component.d.ts +6 -2
  25. package/dist/types/{component → runtime}/component_node.d.ts +13 -13
  26. package/dist/types/{component → runtime}/error_handling.d.ts +3 -0
  27. package/dist/types/{component/handler.d.ts → runtime/event_handling.d.ts} +0 -0
  28. package/dist/types/{component → runtime}/fibers.d.ts +5 -1
  29. package/dist/types/{hooks.d.ts → runtime/hooks.d.ts} +1 -1
  30. package/dist/types/runtime/index.d.ts +31 -0
  31. package/dist/types/{component → runtime}/lifecycle_hooks.d.ts +0 -0
  32. package/dist/types/runtime/portal.d.ts +15 -0
  33. package/dist/types/{reactivity.d.ts → runtime/reactivity.d.ts} +0 -0
  34. package/dist/types/{component → runtime}/scheduler.d.ts +3 -4
  35. package/dist/types/{component → runtime}/status.d.ts +0 -0
  36. package/dist/types/{app → runtime}/template_helpers.d.ts +17 -4
  37. package/dist/types/runtime/template_set.d.ts +32 -0
  38. package/dist/types/{utils.d.ts → runtime/utils.d.ts} +0 -7
  39. package/dist/types/runtime/validation.d.ts +32 -0
  40. package/package.json +7 -4
  41. package/CHANGELOG.md +0 -766
  42. package/dist/owl.cjs.min.js +0 -1
  43. package/dist/owl.es.min.js +0 -1
  44. package/dist/types/app/template_set.d.ts +0 -27
  45. package/dist/types/component/props_validation.d.ts +0 -14
  46. package/dist/types/portal.d.ts +0 -11
package/dist/owl.es.js CHANGED
@@ -77,6 +77,69 @@ function toggler(key, child) {
77
77
  return new VToggler(key, child);
78
78
  }
79
79
 
80
+ // Custom error class that wraps error that happen in the owl lifecycle
81
+ class OwlError extends Error {
82
+ }
83
+ // Maps fibers to thrown errors
84
+ const fibersInError = new WeakMap();
85
+ const nodeErrorHandlers = new WeakMap();
86
+ function _handleError(node, error) {
87
+ if (!node) {
88
+ return false;
89
+ }
90
+ const fiber = node.fiber;
91
+ if (fiber) {
92
+ fibersInError.set(fiber, error);
93
+ }
94
+ const errorHandlers = nodeErrorHandlers.get(node);
95
+ if (errorHandlers) {
96
+ let handled = false;
97
+ // execute in the opposite order
98
+ for (let i = errorHandlers.length - 1; i >= 0; i--) {
99
+ try {
100
+ errorHandlers[i](error);
101
+ handled = true;
102
+ break;
103
+ }
104
+ catch (e) {
105
+ error = e;
106
+ }
107
+ }
108
+ if (handled) {
109
+ return true;
110
+ }
111
+ }
112
+ return _handleError(node.parent, error);
113
+ }
114
+ function handleError(params) {
115
+ let { error } = params;
116
+ // Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)
117
+ if (!(error instanceof OwlError)) {
118
+ error = Object.assign(new OwlError(`An error occured in the owl lifecycle (see this Error's "cause" property)`), { cause: error });
119
+ }
120
+ const node = "node" in params ? params.node : params.fiber.node;
121
+ const fiber = "fiber" in params ? params.fiber : node.fiber;
122
+ // resets the fibers on components if possible. This is important so that
123
+ // new renderings can be properly included in the initial one, if any.
124
+ let current = fiber;
125
+ do {
126
+ current.node.fiber = current;
127
+ current = current.parent;
128
+ } while (current);
129
+ fibersInError.set(fiber.root, error);
130
+ const handled = _handleError(node, error);
131
+ if (!handled) {
132
+ console.warn(`[Owl] Unhandled error. Destroying the root component`);
133
+ try {
134
+ node.app.destroy();
135
+ }
136
+ catch (e) {
137
+ console.error(e);
138
+ }
139
+ throw error;
140
+ }
141
+ }
142
+
80
143
  const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype;
81
144
  const tokenList = DOMTokenList.prototype;
82
145
  const tokenListAdd = tokenList.add;
@@ -167,6 +230,10 @@ function toClassObj(expr) {
167
230
  for (let key in expr) {
168
231
  const value = expr[key];
169
232
  if (value) {
233
+ key = trim.call(key);
234
+ if (!key) {
235
+ continue;
236
+ }
170
237
  const words = split.call(key, wordRegexp);
171
238
  for (let word of words) {
172
239
  result[word] = value;
@@ -209,7 +276,8 @@ function updateClass(val, oldVal) {
209
276
  }
210
277
  function makePropSetter(name) {
211
278
  return function setProp(value) {
212
- this[name] = value || "";
279
+ // support 0, fallback to empty string for other falsy values
280
+ this[name] = value === 0 ? 0 : value ? value.valueOf() : "";
213
281
  };
214
282
  }
215
283
  function isProp(tag, key) {
@@ -252,7 +320,7 @@ function createElementHandler(evName, capture = false) {
252
320
  }
253
321
  function listener(ev) {
254
322
  const currentTarget = ev.currentTarget;
255
- if (!currentTarget || !document.contains(currentTarget))
323
+ if (!currentTarget || !currentTarget.ownerDocument.contains(currentTarget))
256
324
  return;
257
325
  const data = currentTarget[eventKey];
258
326
  if (!data)
@@ -515,7 +583,7 @@ const characterDataProto = CharacterData.prototype;
515
583
  const characterDataSetData = getDescriptor$1(characterDataProto, "data").set;
516
584
  const nodeGetFirstChild = getDescriptor$1(nodeProto$2, "firstChild").get;
517
585
  const nodeGetNextSibling = getDescriptor$1(nodeProto$2, "nextSibling").get;
518
- const NO_OP$1 = () => { };
586
+ const NO_OP = () => { };
519
587
  const cache$1 = {};
520
588
  /**
521
589
  * Compiling blocks is a multi-step process:
@@ -604,6 +672,15 @@ function buildTree(node, parent = null, domParentTree = null) {
604
672
  : document.createElement(tagName);
605
673
  }
606
674
  if (el instanceof Element) {
675
+ if (!domParentTree) {
676
+ // some html elements may have side effects when setting their attributes.
677
+ // For example, setting the src attribute of an <img/> will trigger a
678
+ // request to get the corresponding image. This is something that we
679
+ // don't want at compile time. We avoid that by putting the content of
680
+ // the block in a <template/> element
681
+ const fragment = document.createElement("template").content;
682
+ fragment.appendChild(el);
683
+ }
607
684
  for (let i = 0; i < attrs.length; i++) {
608
685
  const attrName = attrs[i].name;
609
686
  const attrValue = attrs[i].value;
@@ -693,7 +770,7 @@ function buildTree(node, parent = null, domParentTree = null) {
693
770
  };
694
771
  }
695
772
  }
696
- throw new Error("boom");
773
+ throw new OwlError("boom");
697
774
  }
698
775
  function addRef(tree) {
699
776
  tree.isRef = true;
@@ -819,7 +896,7 @@ function updateCtx(ctx, tree) {
819
896
  idx: info.idx,
820
897
  refIdx: info.refIdx,
821
898
  setData: makeRefSetter(index, ctx.refList),
822
- updateData: NO_OP$1,
899
+ updateData: NO_OP,
823
900
  });
824
901
  }
825
902
  }
@@ -1286,29 +1363,35 @@ function html(str) {
1286
1363
  }
1287
1364
 
1288
1365
  function createCatcher(eventsSpec) {
1289
- let setupFns = [];
1290
- let removeFns = [];
1291
- for (let name in eventsSpec) {
1292
- let index = eventsSpec[name];
1293
- let { setup, remove } = createEventHandler(name);
1294
- setupFns[index] = setup;
1295
- removeFns[index] = remove;
1296
- }
1297
- let n = setupFns.length;
1366
+ const n = Object.keys(eventsSpec).length;
1298
1367
  class VCatcher {
1299
1368
  constructor(child, handlers) {
1369
+ this.handlerFns = [];
1300
1370
  this.afterNode = null;
1301
1371
  this.child = child;
1302
- this.handlers = handlers;
1372
+ this.handlerData = handlers;
1303
1373
  }
1304
1374
  mount(parent, afterNode) {
1305
1375
  this.parentEl = parent;
1306
- this.afterNode = afterNode;
1307
1376
  this.child.mount(parent, afterNode);
1377
+ this.afterNode = document.createTextNode("");
1378
+ parent.insertBefore(this.afterNode, afterNode);
1379
+ this.wrapHandlerData();
1380
+ for (let name in eventsSpec) {
1381
+ const index = eventsSpec[name];
1382
+ const handler = createEventHandler(name);
1383
+ this.handlerFns[index] = handler;
1384
+ handler.setup.call(parent, this.handlerData[index]);
1385
+ }
1386
+ }
1387
+ wrapHandlerData() {
1308
1388
  for (let i = 0; i < n; i++) {
1309
- let origFn = this.handlers[i][0];
1389
+ let handler = this.handlerData[i];
1390
+ // handler = [...mods, fn, comp], so we need to replace second to last elem
1391
+ let idx = handler.length - 2;
1392
+ let origFn = handler[idx];
1310
1393
  const self = this;
1311
- this.handlers[i][0] = function (ev) {
1394
+ handler[idx] = function (ev) {
1312
1395
  const target = ev.target;
1313
1396
  let currentNode = self.child.firstNode();
1314
1397
  const afterNode = self.afterNode;
@@ -1319,18 +1402,21 @@ function createCatcher(eventsSpec) {
1319
1402
  currentNode = currentNode.nextSibling;
1320
1403
  }
1321
1404
  };
1322
- setupFns[i].call(parent, this.handlers[i]);
1323
1405
  }
1324
1406
  }
1325
1407
  moveBefore(other, afterNode) {
1326
- this.afterNode = null;
1327
1408
  this.child.moveBefore(other ? other.child : null, afterNode);
1409
+ this.parentEl.insertBefore(this.afterNode, afterNode);
1328
1410
  }
1329
1411
  patch(other, withBeforeRemove) {
1330
1412
  if (this === other) {
1331
1413
  return;
1332
1414
  }
1333
- this.handlers = other.handlers;
1415
+ this.handlerData = other.handlerData;
1416
+ this.wrapHandlerData();
1417
+ for (let i = 0; i < n; i++) {
1418
+ this.handlerFns[i].update.call(this.parentEl, this.handlerData[i]);
1419
+ }
1334
1420
  this.child.patch(other.child, withBeforeRemove);
1335
1421
  }
1336
1422
  beforeRemove() {
@@ -1338,9 +1424,10 @@ function createCatcher(eventsSpec) {
1338
1424
  }
1339
1425
  remove() {
1340
1426
  for (let i = 0; i < n; i++) {
1341
- removeFns[i].call(this.parentEl);
1427
+ this.handlerFns[i].remove.call(this.parentEl);
1342
1428
  }
1343
1429
  this.child.remove();
1430
+ this.afterNode.remove();
1344
1431
  }
1345
1432
  firstNode() {
1346
1433
  return this.child.firstNode();
@@ -1367,142 +1454,339 @@ function remove(vnode, withBeforeRemove = false) {
1367
1454
  vnode.remove();
1368
1455
  }
1369
1456
 
1370
- const mainEventHandler = (data, ev, currentTarget) => {
1371
- const { data: _data, modifiers } = filterOutModifiersFromData(data);
1372
- data = _data;
1373
- let stopped = false;
1374
- if (modifiers.length) {
1375
- let selfMode = false;
1376
- const isSelf = ev.target === currentTarget;
1377
- for (const mod of modifiers) {
1378
- switch (mod) {
1379
- case "self":
1380
- selfMode = true;
1381
- if (isSelf) {
1382
- continue;
1383
- }
1384
- else {
1385
- return stopped;
1386
- }
1387
- case "prevent":
1388
- if ((selfMode && isSelf) || !selfMode)
1389
- ev.preventDefault();
1390
- continue;
1391
- case "stop":
1392
- if ((selfMode && isSelf) || !selfMode)
1393
- ev.stopPropagation();
1394
- stopped = true;
1395
- continue;
1396
- }
1397
- }
1457
+ function makeChildFiber(node, parent) {
1458
+ let current = node.fiber;
1459
+ if (current) {
1460
+ cancelFibers(current.children);
1461
+ current.root = null;
1398
1462
  }
1399
- // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
1400
- // We check this rather than data[0] being truthy (or typeof function) so that it crashes
1401
- // as expected when there is a handler expression that evaluates to a falsy value
1402
- if (Object.hasOwnProperty.call(data, 0)) {
1403
- const handler = data[0];
1404
- if (typeof handler !== "function") {
1405
- throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
1406
- }
1407
- let node = data[1] ? data[1].__owl__ : null;
1408
- if (node ? node.status === 1 /* MOUNTED */ : true) {
1409
- handler.call(node ? node.component : null, ev);
1463
+ return new Fiber(node, parent);
1464
+ }
1465
+ function makeRootFiber(node) {
1466
+ let current = node.fiber;
1467
+ if (current) {
1468
+ let root = current.root;
1469
+ // lock root fiber because canceling children fibers may destroy components,
1470
+ // which means any arbitrary code can be run in onWillDestroy, which may
1471
+ // trigger new renderings
1472
+ root.locked = true;
1473
+ root.setCounter(root.counter + 1 - cancelFibers(current.children));
1474
+ root.locked = false;
1475
+ current.children = [];
1476
+ current.childrenMap = {};
1477
+ current.bdom = null;
1478
+ if (fibersInError.has(current)) {
1479
+ fibersInError.delete(current);
1480
+ fibersInError.delete(root);
1481
+ current.appliedToDom = false;
1410
1482
  }
1483
+ return current;
1411
1484
  }
1412
- return stopped;
1413
- };
1414
-
1415
- // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
1416
- const TARGET = Symbol("Target");
1417
- // Escape hatch to prevent reactivity system to turn something into a reactive
1418
- const SKIP = Symbol("Skip");
1419
- // Special key to subscribe to, to be notified of key creation/deletion
1420
- const KEYCHANGES = Symbol("Key changes");
1421
- const objectToString = Object.prototype.toString;
1422
- const objectHasOwnProperty = Object.prototype.hasOwnProperty;
1423
- const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
1424
- const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
1425
- /**
1426
- * extract "RawType" from strings like "[object RawType]" => this lets us ignore
1427
- * many native objects such as Promise (whose toString is [object Promise])
1428
- * or Date ([object Date]), while also supporting collections without using
1429
- * instanceof in a loop
1430
- *
1431
- * @param obj the object to check
1432
- * @returns the raw type of the object
1433
- */
1434
- function rawType(obj) {
1435
- return objectToString.call(obj).slice(8, -1);
1436
- }
1437
- /**
1438
- * Checks whether a given value can be made into a reactive object.
1439
- *
1440
- * @param value the value to check
1441
- * @returns whether the value can be made reactive
1442
- */
1443
- function canBeMadeReactive(value) {
1444
- if (typeof value !== "object") {
1445
- return false;
1485
+ const fiber = new RootFiber(node, null);
1486
+ if (node.willPatch.length) {
1487
+ fiber.willPatch.push(fiber);
1446
1488
  }
1447
- return SUPPORTED_RAW_TYPES.has(rawType(value));
1448
- }
1449
- /**
1450
- * Creates a reactive from the given object/callback if possible and returns it,
1451
- * returns the original object otherwise.
1452
- *
1453
- * @param value the value make reactive
1454
- * @returns a reactive for the given object when possible, the original otherwise
1455
- */
1456
- function possiblyReactive(val, cb) {
1457
- return canBeMadeReactive(val) ? reactive(val, cb) : val;
1489
+ if (node.patched.length) {
1490
+ fiber.patched.push(fiber);
1491
+ }
1492
+ return fiber;
1458
1493
  }
1459
- /**
1460
- * Mark an object or array so that it is ignored by the reactivity system
1461
- *
1462
- * @param value the value to mark
1463
- * @returns the object itself
1464
- */
1465
- function markRaw(value) {
1466
- value[SKIP] = true;
1467
- return value;
1494
+ function throwOnRender() {
1495
+ throw new OwlError("Attempted to render cancelled fiber");
1468
1496
  }
1469
1497
  /**
1470
- * Given a reactive objet, return the raw (non reactive) underlying object
1471
- *
1472
- * @param value a reactive value
1473
- * @returns the underlying value
1498
+ * @returns number of not-yet rendered fibers cancelled
1474
1499
  */
1475
- function toRaw(value) {
1476
- return value[TARGET] || value;
1500
+ function cancelFibers(fibers) {
1501
+ let result = 0;
1502
+ for (let fiber of fibers) {
1503
+ let node = fiber.node;
1504
+ fiber.render = throwOnRender;
1505
+ if (node.status === 0 /* NEW */) {
1506
+ node.destroy();
1507
+ delete node.parent.children[node.parentKey];
1508
+ }
1509
+ node.fiber = null;
1510
+ if (fiber.bdom) {
1511
+ // if fiber has been rendered, this means that the component props have
1512
+ // been updated. however, this fiber will not be patched to the dom, so
1513
+ // it could happen that the next render compare the current props with
1514
+ // the same props, and skip the render completely. With the next line,
1515
+ // we kindly request the component code to force a render, so it works as
1516
+ // expected.
1517
+ node.forceNextRender = true;
1518
+ }
1519
+ else {
1520
+ result++;
1521
+ }
1522
+ result += cancelFibers(fiber.children);
1523
+ }
1524
+ return result;
1477
1525
  }
1478
- const targetToKeysToCallbacks = new WeakMap();
1479
- /**
1480
- * Observes a given key on a target with an callback. The callback will be
1481
- * called when the given key changes on the target.
1482
- *
1483
- * @param target the target whose key should be observed
1484
- * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
1485
- * or deletion)
1486
- * @param callback the function to call when the key changes
1487
- */
1488
- function observeTargetKey(target, key, callback) {
1489
- if (!targetToKeysToCallbacks.get(target)) {
1490
- targetToKeysToCallbacks.set(target, new Map());
1526
+ class Fiber {
1527
+ constructor(node, parent) {
1528
+ this.bdom = null;
1529
+ this.children = [];
1530
+ this.appliedToDom = false;
1531
+ this.deep = false;
1532
+ this.childrenMap = {};
1533
+ this.node = node;
1534
+ this.parent = parent;
1535
+ if (parent) {
1536
+ this.deep = parent.deep;
1537
+ const root = parent.root;
1538
+ root.setCounter(root.counter + 1);
1539
+ this.root = root;
1540
+ parent.children.push(this);
1541
+ }
1542
+ else {
1543
+ this.root = this;
1544
+ }
1491
1545
  }
1492
- const keyToCallbacks = targetToKeysToCallbacks.get(target);
1493
- if (!keyToCallbacks.get(key)) {
1494
- keyToCallbacks.set(key, new Set());
1546
+ render() {
1547
+ // if some parent has a fiber => register in followup
1548
+ let prev = this.root.node;
1549
+ let scheduler = prev.app.scheduler;
1550
+ let current = prev.parent;
1551
+ while (current) {
1552
+ if (current.fiber) {
1553
+ let root = current.fiber.root;
1554
+ if (root.counter === 0 && prev.parentKey in current.fiber.childrenMap) {
1555
+ current = root.node;
1556
+ }
1557
+ else {
1558
+ scheduler.delayedRenders.push(this);
1559
+ return;
1560
+ }
1561
+ }
1562
+ prev = current;
1563
+ current = current.parent;
1564
+ }
1565
+ // there are no current rendering from above => we can render
1566
+ this._render();
1495
1567
  }
1496
- keyToCallbacks.get(key).add(callback);
1497
- if (!callbacksToTargets.has(callback)) {
1498
- callbacksToTargets.set(callback, new Set());
1568
+ _render() {
1569
+ const node = this.node;
1570
+ const root = this.root;
1571
+ if (root) {
1572
+ try {
1573
+ this.bdom = true;
1574
+ this.bdom = node.renderFn();
1575
+ }
1576
+ catch (e) {
1577
+ node.app.handleError({ node, error: e });
1578
+ }
1579
+ root.setCounter(root.counter - 1);
1580
+ }
1499
1581
  }
1500
- callbacksToTargets.get(callback).add(target);
1501
1582
  }
1502
- /**
1503
- * Notify Reactives that are observing a given target that a key has changed on
1504
- * the target.
1505
- *
1583
+ class RootFiber extends Fiber {
1584
+ constructor() {
1585
+ super(...arguments);
1586
+ this.counter = 1;
1587
+ // only add stuff in this if they have registered some hooks
1588
+ this.willPatch = [];
1589
+ this.patched = [];
1590
+ this.mounted = [];
1591
+ // A fiber is typically locked when it is completing and the patch has not, or is being applied.
1592
+ // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
1593
+ this.locked = false;
1594
+ }
1595
+ complete() {
1596
+ const node = this.node;
1597
+ this.locked = true;
1598
+ let current = undefined;
1599
+ try {
1600
+ // Step 1: calling all willPatch lifecycle hooks
1601
+ for (current of this.willPatch) {
1602
+ // because of the asynchronous nature of the rendering, some parts of the
1603
+ // UI may have been rendered, then deleted in a followup rendering, and we
1604
+ // do not want to call onWillPatch in that case.
1605
+ let node = current.node;
1606
+ if (node.fiber === current) {
1607
+ const component = node.component;
1608
+ for (let cb of node.willPatch) {
1609
+ cb.call(component);
1610
+ }
1611
+ }
1612
+ }
1613
+ current = undefined;
1614
+ // Step 2: patching the dom
1615
+ node._patch();
1616
+ this.locked = false;
1617
+ // Step 4: calling all mounted lifecycle hooks
1618
+ let mountedFibers = this.mounted;
1619
+ while ((current = mountedFibers.pop())) {
1620
+ current = current;
1621
+ if (current.appliedToDom) {
1622
+ for (let cb of current.node.mounted) {
1623
+ cb();
1624
+ }
1625
+ }
1626
+ }
1627
+ // Step 5: calling all patched hooks
1628
+ let patchedFibers = this.patched;
1629
+ while ((current = patchedFibers.pop())) {
1630
+ current = current;
1631
+ if (current.appliedToDom) {
1632
+ for (let cb of current.node.patched) {
1633
+ cb();
1634
+ }
1635
+ }
1636
+ }
1637
+ }
1638
+ catch (e) {
1639
+ this.locked = false;
1640
+ node.app.handleError({ fiber: current || this, error: e });
1641
+ }
1642
+ }
1643
+ setCounter(newValue) {
1644
+ this.counter = newValue;
1645
+ if (newValue === 0) {
1646
+ this.node.app.scheduler.flush();
1647
+ }
1648
+ }
1649
+ }
1650
+ class MountFiber extends RootFiber {
1651
+ constructor(node, target, options = {}) {
1652
+ super(node, null);
1653
+ this.target = target;
1654
+ this.position = options.position || "last-child";
1655
+ }
1656
+ complete() {
1657
+ let current = this;
1658
+ try {
1659
+ const node = this.node;
1660
+ node.children = this.childrenMap;
1661
+ node.app.constructor.validateTarget(this.target);
1662
+ if (node.bdom) {
1663
+ // this is a complicated situation: if we mount a fiber with an existing
1664
+ // bdom, this means that this same fiber was already completed, mounted,
1665
+ // but a crash occurred in some mounted hook. Then, it was handled and
1666
+ // the new rendering is being applied.
1667
+ node.updateDom();
1668
+ }
1669
+ else {
1670
+ node.bdom = this.bdom;
1671
+ if (this.position === "last-child" || this.target.childNodes.length === 0) {
1672
+ mount$1(node.bdom, this.target);
1673
+ }
1674
+ else {
1675
+ const firstChild = this.target.childNodes[0];
1676
+ mount$1(node.bdom, this.target, firstChild);
1677
+ }
1678
+ }
1679
+ // unregistering the fiber before mounted since it can do another render
1680
+ // and that the current rendering is obviously completed
1681
+ node.fiber = null;
1682
+ node.status = 1 /* MOUNTED */;
1683
+ this.appliedToDom = true;
1684
+ let mountedFibers = this.mounted;
1685
+ while ((current = mountedFibers.pop())) {
1686
+ if (current.appliedToDom) {
1687
+ for (let cb of current.node.mounted) {
1688
+ cb();
1689
+ }
1690
+ }
1691
+ }
1692
+ }
1693
+ catch (e) {
1694
+ this.node.app.handleError({ fiber: current, error: e });
1695
+ }
1696
+ }
1697
+ }
1698
+
1699
+ // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
1700
+ const TARGET = Symbol("Target");
1701
+ // Escape hatch to prevent reactivity system to turn something into a reactive
1702
+ const SKIP = Symbol("Skip");
1703
+ // Special key to subscribe to, to be notified of key creation/deletion
1704
+ const KEYCHANGES = Symbol("Key changes");
1705
+ const objectToString = Object.prototype.toString;
1706
+ const objectHasOwnProperty = Object.prototype.hasOwnProperty;
1707
+ const SUPPORTED_RAW_TYPES = new Set(["Object", "Array", "Set", "Map", "WeakMap"]);
1708
+ const COLLECTION_RAWTYPES = new Set(["Set", "Map", "WeakMap"]);
1709
+ /**
1710
+ * extract "RawType" from strings like "[object RawType]" => this lets us ignore
1711
+ * many native objects such as Promise (whose toString is [object Promise])
1712
+ * or Date ([object Date]), while also supporting collections without using
1713
+ * instanceof in a loop
1714
+ *
1715
+ * @param obj the object to check
1716
+ * @returns the raw type of the object
1717
+ */
1718
+ function rawType(obj) {
1719
+ return objectToString.call(obj).slice(8, -1);
1720
+ }
1721
+ /**
1722
+ * Checks whether a given value can be made into a reactive object.
1723
+ *
1724
+ * @param value the value to check
1725
+ * @returns whether the value can be made reactive
1726
+ */
1727
+ function canBeMadeReactive(value) {
1728
+ if (typeof value !== "object") {
1729
+ return false;
1730
+ }
1731
+ return SUPPORTED_RAW_TYPES.has(rawType(value));
1732
+ }
1733
+ /**
1734
+ * Creates a reactive from the given object/callback if possible and returns it,
1735
+ * returns the original object otherwise.
1736
+ *
1737
+ * @param value the value make reactive
1738
+ * @returns a reactive for the given object when possible, the original otherwise
1739
+ */
1740
+ function possiblyReactive(val, cb) {
1741
+ return canBeMadeReactive(val) ? reactive(val, cb) : val;
1742
+ }
1743
+ /**
1744
+ * Mark an object or array so that it is ignored by the reactivity system
1745
+ *
1746
+ * @param value the value to mark
1747
+ * @returns the object itself
1748
+ */
1749
+ function markRaw(value) {
1750
+ value[SKIP] = true;
1751
+ return value;
1752
+ }
1753
+ /**
1754
+ * Given a reactive objet, return the raw (non reactive) underlying object
1755
+ *
1756
+ * @param value a reactive value
1757
+ * @returns the underlying value
1758
+ */
1759
+ function toRaw(value) {
1760
+ return value[TARGET] || value;
1761
+ }
1762
+ const targetToKeysToCallbacks = new WeakMap();
1763
+ /**
1764
+ * Observes a given key on a target with an callback. The callback will be
1765
+ * called when the given key changes on the target.
1766
+ *
1767
+ * @param target the target whose key should be observed
1768
+ * @param key the key to observe (or Symbol(KEYCHANGES) for key creation
1769
+ * or deletion)
1770
+ * @param callback the function to call when the key changes
1771
+ */
1772
+ function observeTargetKey(target, key, callback) {
1773
+ if (!targetToKeysToCallbacks.get(target)) {
1774
+ targetToKeysToCallbacks.set(target, new Map());
1775
+ }
1776
+ const keyToCallbacks = targetToKeysToCallbacks.get(target);
1777
+ if (!keyToCallbacks.get(key)) {
1778
+ keyToCallbacks.set(key, new Set());
1779
+ }
1780
+ keyToCallbacks.get(key).add(callback);
1781
+ if (!callbacksToTargets.has(callback)) {
1782
+ callbacksToTargets.set(callback, new Set());
1783
+ }
1784
+ callbacksToTargets.get(callback).add(target);
1785
+ }
1786
+ /**
1787
+ * Notify Reactives that are observing a given target that a key has changed on
1788
+ * the target.
1789
+ *
1506
1790
  * @param target target whose Reactives should be notified that the target was
1507
1791
  * changed.
1508
1792
  * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
@@ -1585,7 +1869,7 @@ const reactiveCache = new WeakMap();
1585
1869
  */
1586
1870
  function reactive(target, callback = () => { }) {
1587
1871
  if (!canBeMadeReactive(target)) {
1588
- throw new Error(`Cannot make the given value reactive`);
1872
+ throw new OwlError(`Cannot make the given value reactive`);
1589
1873
  }
1590
1874
  if (SKIP in target) {
1591
1875
  return target;
@@ -1595,7 +1879,7 @@ function reactive(target, callback = () => { }) {
1595
1879
  return reactive(originalTarget, callback);
1596
1880
  }
1597
1881
  if (!reactiveCache.has(target)) {
1598
- reactiveCache.set(target, new Map());
1882
+ reactiveCache.set(target, new WeakMap());
1599
1883
  }
1600
1884
  const reactivesForTarget = reactiveCache.get(target);
1601
1885
  if (!reactivesForTarget.has(callback)) {
@@ -1620,6 +1904,11 @@ function basicProxyHandler(callback) {
1620
1904
  if (key === TARGET) {
1621
1905
  return target;
1622
1906
  }
1907
+ // non-writable non-configurable properties cannot be made reactive
1908
+ const desc = Object.getOwnPropertyDescriptor(target, key);
1909
+ if (desc && !desc.writable && !desc.configurable) {
1910
+ return Reflect.get(target, key, proxy);
1911
+ }
1623
1912
  observeTargetKey(target, key, callback);
1624
1913
  return possiblyReactive(Reflect.get(target, key, proxy), callback);
1625
1914
  },
@@ -1692,6 +1981,23 @@ function makeIteratorObserver(methodName, target, callback) {
1692
1981
  }
1693
1982
  };
1694
1983
  }
1984
+ /**
1985
+ * Creates a forEach function that will delegate to forEach on the underlying
1986
+ * collection while observing key changes, and keys as they're iterated over,
1987
+ * and making the passed keys/values reactive.
1988
+ *
1989
+ * @param target @see reactive
1990
+ * @param callback @see reactive
1991
+ */
1992
+ function makeForEachObserver(target, callback) {
1993
+ return function forEach(forEachCb, thisArg) {
1994
+ observeTargetKey(target, KEYCHANGES, callback);
1995
+ target.forEach(function (val, key, targetObj) {
1996
+ observeTargetKey(target, key, callback);
1997
+ forEachCb.call(thisArg, possiblyReactive(val, callback), possiblyReactive(key, callback), possiblyReactive(targetObj, callback));
1998
+ }, thisArg);
1999
+ };
2000
+ }
1695
2001
  /**
1696
2002
  * Creates a function that will delegate to an underlying method, and check if
1697
2003
  * that method has modified the presence or value of a key, and notify the
@@ -1750,6 +2056,7 @@ const rawTypeToFuncHandlers = {
1750
2056
  values: makeIteratorObserver("values", target, callback),
1751
2057
  entries: makeIteratorObserver("entries", target, callback),
1752
2058
  [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
2059
+ forEach: makeForEachObserver(target, callback),
1753
2060
  clear: makeClearNotifier(target),
1754
2061
  get size() {
1755
2062
  observeTargetKey(target, KEYCHANGES, callback);
@@ -1765,6 +2072,7 @@ const rawTypeToFuncHandlers = {
1765
2072
  values: makeIteratorObserver("values", target, callback),
1766
2073
  entries: makeIteratorObserver("entries", target, callback),
1767
2074
  [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
2075
+ forEach: makeForEachObserver(target, callback),
1768
2076
  clear: makeClearNotifier(target),
1769
2077
  get size() {
1770
2078
  observeTargetKey(target, KEYCHANGES, callback);
@@ -1818,21 +2126,29 @@ function batched(callback) {
1818
2126
  await Promise.resolve();
1819
2127
  if (!called) {
1820
2128
  called = true;
1821
- callback();
1822
2129
  // wait for all calls in this microtick to fall through before resetting "called"
1823
- // so that only the first call to the batched function calls the original callback
1824
- await Promise.resolve();
1825
- called = false;
2130
+ // so that only the first call to the batched function calls the original callback.
2131
+ // Schedule this before calling the callback so that calls to the batched function
2132
+ // within the callback will proceed only after resetting called to false, and have
2133
+ // a chance to execute the callback again
2134
+ Promise.resolve().then(() => (called = false));
2135
+ callback();
1826
2136
  }
1827
2137
  };
1828
2138
  }
1829
2139
  function validateTarget(target) {
1830
- if (!(target instanceof HTMLElement)) {
1831
- throw new Error("Cannot mount component: the target is not a valid DOM element");
1832
- }
1833
- if (!document.body.contains(target)) {
1834
- throw new Error("Cannot mount a component on a detached dom node");
2140
+ // Get the document and HTMLElement corresponding to the target to allow mounting in iframes
2141
+ const document = target && target.ownerDocument;
2142
+ if (document) {
2143
+ const HTMLElement = document.defaultView.HTMLElement;
2144
+ if (target instanceof HTMLElement) {
2145
+ if (!document.body.contains(target)) {
2146
+ throw new OwlError("Cannot mount a component on a detached dom node");
2147
+ }
2148
+ return;
2149
+ }
1835
2150
  }
2151
+ throw new OwlError("Cannot mount component: the target is not a valid DOM element");
1836
2152
  }
1837
2153
  class EventBus extends EventTarget {
1838
2154
  trigger(name, payload) {
@@ -1852,7 +2168,7 @@ function whenReady(fn) {
1852
2168
  async function loadFile(url) {
1853
2169
  const result = await fetch(url);
1854
2170
  if (!result.ok) {
1855
- throw new Error("Error while fetching xml templates");
2171
+ throw new OwlError("Error while fetching xml templates");
1856
2172
  }
1857
2173
  return await result.text();
1858
2174
  }
@@ -1869,861 +2185,1054 @@ class Markup extends String {
1869
2185
  */
1870
2186
  function markup(value) {
1871
2187
  return new Markup(value);
1872
- }
1873
- // -----------------------------------------------------------------------------
1874
- // xml tag helper
1875
- // -----------------------------------------------------------------------------
1876
- const globalTemplates = {};
1877
- function xml(...args) {
1878
- const name = `__template__${xml.nextId++}`;
1879
- const value = String.raw(...args);
1880
- globalTemplates[name] = value;
1881
- return name;
1882
- }
1883
- xml.nextId = 1;
2188
+ }
1884
2189
 
1885
- // Maps fibers to thrown errors
1886
- const fibersInError = new WeakMap();
1887
- const nodeErrorHandlers = new WeakMap();
1888
- function _handleError(node, error, isFirstRound = false) {
1889
- if (!node) {
1890
- return false;
2190
+ let currentNode = null;
2191
+ function getCurrent() {
2192
+ if (!currentNode) {
2193
+ throw new OwlError("No active component (a hook function should only be called in 'setup')");
1891
2194
  }
1892
- const fiber = node.fiber;
1893
- if (fiber) {
1894
- fibersInError.set(fiber, error);
2195
+ return currentNode;
2196
+ }
2197
+ function useComponent() {
2198
+ return currentNode.component;
2199
+ }
2200
+ /**
2201
+ * Apply default props (only top level).
2202
+ */
2203
+ function applyDefaultProps(props, defaultProps) {
2204
+ for (let propName in defaultProps) {
2205
+ if (props[propName] === undefined) {
2206
+ props[propName] = defaultProps[propName];
2207
+ }
1895
2208
  }
1896
- const errorHandlers = nodeErrorHandlers.get(node);
1897
- if (errorHandlers) {
1898
- let stopped = false;
1899
- // execute in the opposite order
1900
- for (let i = errorHandlers.length - 1; i >= 0; i--) {
1901
- try {
1902
- errorHandlers[i](error);
1903
- stopped = true;
1904
- break;
1905
- }
1906
- catch (e) {
1907
- error = e;
1908
- }
2209
+ }
2210
+ // -----------------------------------------------------------------------------
2211
+ // Integration with reactivity system (useState)
2212
+ // -----------------------------------------------------------------------------
2213
+ const batchedRenderFunctions = new WeakMap();
2214
+ /**
2215
+ * Creates a reactive object that will be observed by the current component.
2216
+ * Reading data from the returned object (eg during rendering) will cause the
2217
+ * component to subscribe to that data and be rerendered when it changes.
2218
+ *
2219
+ * @param state the state to observe
2220
+ * @returns a reactive object that will cause the component to re-render on
2221
+ * relevant changes
2222
+ * @see reactive
2223
+ */
2224
+ function useState(state) {
2225
+ const node = getCurrent();
2226
+ let render = batchedRenderFunctions.get(node);
2227
+ if (!render) {
2228
+ render = batched(node.render.bind(node, false));
2229
+ batchedRenderFunctions.set(node, render);
2230
+ // manual implementation of onWillDestroy to break cyclic dependency
2231
+ node.willDestroy.push(clearReactivesForCallback.bind(null, render));
2232
+ }
2233
+ return reactive(state, render);
2234
+ }
2235
+ class ComponentNode {
2236
+ constructor(C, props, app, parent, parentKey) {
2237
+ this.fiber = null;
2238
+ this.bdom = null;
2239
+ this.status = 0 /* NEW */;
2240
+ this.forceNextRender = false;
2241
+ this.children = Object.create(null);
2242
+ this.refs = {};
2243
+ this.willStart = [];
2244
+ this.willUpdateProps = [];
2245
+ this.willUnmount = [];
2246
+ this.mounted = [];
2247
+ this.willPatch = [];
2248
+ this.patched = [];
2249
+ this.willDestroy = [];
2250
+ currentNode = this;
2251
+ this.app = app;
2252
+ this.parent = parent;
2253
+ this.props = props;
2254
+ this.parentKey = parentKey;
2255
+ const defaultProps = C.defaultProps;
2256
+ props = Object.assign({}, props);
2257
+ if (defaultProps) {
2258
+ applyDefaultProps(props, defaultProps);
1909
2259
  }
1910
- if (stopped) {
1911
- if (isFirstRound && fiber && fiber.node.fiber) {
1912
- fiber.root.counter--;
2260
+ const env = (parent && parent.childEnv) || app.env;
2261
+ this.childEnv = env;
2262
+ for (const key in props) {
2263
+ const prop = props[key];
2264
+ if (prop && typeof prop === "object" && prop[TARGET]) {
2265
+ props[key] = useState(prop);
1913
2266
  }
1914
- return true;
1915
2267
  }
2268
+ this.component = new C(props, env, this);
2269
+ this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
2270
+ this.component.setup();
2271
+ currentNode = null;
1916
2272
  }
1917
- return _handleError(node.parent, error);
1918
- }
1919
- function handleError(params) {
1920
- const error = params.error;
1921
- const node = "node" in params ? params.node : params.fiber.node;
1922
- const fiber = "fiber" in params ? params.fiber : node.fiber;
1923
- // resets the fibers on components if possible. This is important so that
1924
- // new renderings can be properly included in the initial one, if any.
1925
- let current = fiber;
1926
- do {
1927
- current.node.fiber = current;
1928
- current = current.parent;
1929
- } while (current);
1930
- fibersInError.set(fiber.root, error);
1931
- const handled = _handleError(node, error, true);
1932
- if (!handled) {
1933
- console.warn(`[Owl] Unhandled error. Destroying the root component`);
2273
+ mountComponent(target, options) {
2274
+ const fiber = new MountFiber(this, target, options);
2275
+ this.app.scheduler.addFiber(fiber);
2276
+ this.initiateRender(fiber);
2277
+ }
2278
+ async initiateRender(fiber) {
2279
+ this.fiber = fiber;
2280
+ if (this.mounted.length) {
2281
+ fiber.root.mounted.push(fiber);
2282
+ }
2283
+ const component = this.component;
1934
2284
  try {
1935
- node.app.destroy();
2285
+ await Promise.all(this.willStart.map((f) => f.call(component)));
1936
2286
  }
1937
2287
  catch (e) {
1938
- console.error(e);
2288
+ this.app.handleError({ node: this, error: e });
2289
+ return;
1939
2290
  }
1940
- }
1941
- }
1942
-
1943
- function makeChildFiber(node, parent) {
1944
- let current = node.fiber;
1945
- if (current) {
1946
- cancelFibers(current.children);
1947
- current.root = null;
1948
- }
1949
- return new Fiber(node, parent);
1950
- }
1951
- function makeRootFiber(node) {
1952
- let current = node.fiber;
1953
- if (current) {
1954
- let root = current.root;
1955
- root.counter = root.counter + 1 - cancelFibers(current.children);
1956
- current.children = [];
1957
- current.bdom = null;
1958
- if (fibersInError.has(current)) {
1959
- fibersInError.delete(current);
1960
- fibersInError.delete(root);
1961
- current.appliedToDom = false;
2291
+ if (this.status === 0 /* NEW */ && this.fiber === fiber) {
2292
+ fiber.render();
1962
2293
  }
1963
- return current;
1964
- }
1965
- const fiber = new RootFiber(node, null);
1966
- if (node.willPatch.length) {
1967
- fiber.willPatch.push(fiber);
1968
2294
  }
1969
- if (node.patched.length) {
1970
- fiber.patched.push(fiber);
1971
- }
1972
- return fiber;
1973
- }
1974
- /**
1975
- * @returns number of not-yet rendered fibers cancelled
1976
- */
1977
- function cancelFibers(fibers) {
1978
- let result = 0;
1979
- for (let fiber of fibers) {
1980
- fiber.node.fiber = null;
1981
- if (fiber.bdom) {
1982
- // if fiber has been rendered, this means that the component props have
1983
- // been updated. however, this fiber will not be patched to the dom, so
1984
- // it could happen that the next render compare the current props with
1985
- // the same props, and skip the render completely. With the next line,
1986
- // we kindly request the component code to force a render, so it works as
1987
- // expected.
1988
- fiber.node.forceNextRender = true;
2295
+ async render(deep) {
2296
+ let current = this.fiber;
2297
+ if (current && (current.root.locked || current.bdom === true)) {
2298
+ await Promise.resolve();
2299
+ // situation may have changed after the microtask tick
2300
+ current = this.fiber;
1989
2301
  }
1990
- else {
1991
- result++;
2302
+ if (current) {
2303
+ if (!current.bdom && !fibersInError.has(current)) {
2304
+ if (deep) {
2305
+ // we want the render from this point on to be with deep=true
2306
+ current.deep = deep;
2307
+ }
2308
+ return;
2309
+ }
2310
+ // if current rendering was with deep=true, we want this one to be the same
2311
+ deep = deep || current.deep;
1992
2312
  }
1993
- result += cancelFibers(fiber.children);
1994
- }
1995
- return result;
1996
- }
1997
- class Fiber {
1998
- constructor(node, parent) {
1999
- this.bdom = null;
2000
- this.children = [];
2001
- this.appliedToDom = false;
2002
- this.deep = false;
2003
- this.node = node;
2004
- this.parent = parent;
2005
- if (parent) {
2006
- this.deep = parent.deep;
2007
- const root = parent.root;
2008
- root.counter++;
2009
- this.root = root;
2010
- parent.children.push(this);
2313
+ else if (!this.bdom) {
2314
+ return;
2011
2315
  }
2012
- else {
2013
- this.root = this;
2316
+ const fiber = makeRootFiber(this);
2317
+ fiber.deep = deep;
2318
+ this.fiber = fiber;
2319
+ this.app.scheduler.addFiber(fiber);
2320
+ await Promise.resolve();
2321
+ if (this.status === 2 /* DESTROYED */) {
2322
+ return;
2323
+ }
2324
+ // We only want to actually render the component if the following two
2325
+ // conditions are true:
2326
+ // * this.fiber: it could be null, in which case the render has been cancelled
2327
+ // * (current || !fiber.parent): if current is not null, this means that the
2328
+ // render function was called when a render was already occurring. In this
2329
+ // case, the pending rendering was cancelled, and the fiber needs to be
2330
+ // rendered to complete the work. If current is null, we check that the
2331
+ // fiber has no parent. If that is the case, the fiber was downgraded from
2332
+ // a root fiber to a child fiber in the previous microtick, because it was
2333
+ // embedded in a rendering coming from above, so the fiber will be rendered
2334
+ // in the next microtick anyway, so we should not render it again.
2335
+ if (this.fiber === fiber && (current || !fiber.parent)) {
2336
+ fiber.render();
2014
2337
  }
2015
2338
  }
2016
- }
2017
- class RootFiber extends Fiber {
2018
- constructor() {
2019
- super(...arguments);
2020
- this.counter = 1;
2021
- // only add stuff in this if they have registered some hooks
2022
- this.willPatch = [];
2023
- this.patched = [];
2024
- this.mounted = [];
2025
- // A fiber is typically locked when it is completing and the patch has not, or is being applied.
2026
- // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
2027
- this.locked = false;
2339
+ destroy() {
2340
+ let shouldRemove = this.status === 1 /* MOUNTED */;
2341
+ this._destroy();
2342
+ if (shouldRemove) {
2343
+ this.bdom.remove();
2344
+ }
2028
2345
  }
2029
- complete() {
2030
- const node = this.node;
2031
- this.locked = true;
2032
- let current = undefined;
2033
- try {
2034
- // Step 1: calling all willPatch lifecycle hooks
2035
- for (current of this.willPatch) {
2036
- // because of the asynchronous nature of the rendering, some parts of the
2037
- // UI may have been rendered, then deleted in a followup rendering, and we
2038
- // do not want to call onWillPatch in that case.
2039
- let node = current.node;
2040
- if (node.fiber === current) {
2041
- const component = node.component;
2042
- for (let cb of node.willPatch) {
2043
- cb.call(component);
2044
- }
2045
- }
2346
+ _destroy() {
2347
+ const component = this.component;
2348
+ if (this.status === 1 /* MOUNTED */) {
2349
+ for (let cb of this.willUnmount) {
2350
+ cb.call(component);
2046
2351
  }
2047
- current = undefined;
2048
- // Step 2: patching the dom
2049
- node._patch();
2050
- this.locked = false;
2051
- // Step 4: calling all mounted lifecycle hooks
2052
- let mountedFibers = this.mounted;
2053
- while ((current = mountedFibers.pop())) {
2054
- current = current;
2055
- if (current.appliedToDom) {
2056
- for (let cb of current.node.mounted) {
2057
- cb();
2058
- }
2352
+ }
2353
+ for (let child of Object.values(this.children)) {
2354
+ child._destroy();
2355
+ }
2356
+ if (this.willDestroy.length) {
2357
+ try {
2358
+ for (let cb of this.willDestroy) {
2359
+ cb.call(component);
2059
2360
  }
2060
2361
  }
2061
- // Step 5: calling all patched hooks
2062
- let patchedFibers = this.patched;
2063
- while ((current = patchedFibers.pop())) {
2064
- current = current;
2065
- if (current.appliedToDom) {
2066
- for (let cb of current.node.patched) {
2067
- cb();
2068
- }
2069
- }
2362
+ catch (e) {
2363
+ this.app.handleError({ error: e, node: this });
2070
2364
  }
2071
2365
  }
2072
- catch (e) {
2073
- this.locked = false;
2074
- handleError({ fiber: current || this, error: e });
2075
- }
2076
- }
2077
- }
2078
- class MountFiber extends RootFiber {
2079
- constructor(node, target, options = {}) {
2080
- super(node, null);
2081
- this.target = target;
2082
- this.position = options.position || "last-child";
2366
+ this.status = 2 /* DESTROYED */;
2083
2367
  }
2084
- complete() {
2085
- let current = this;
2086
- try {
2087
- const node = this.node;
2088
- node.app.constructor.validateTarget(this.target);
2089
- if (node.bdom) {
2090
- // this is a complicated situation: if we mount a fiber with an existing
2091
- // bdom, this means that this same fiber was already completed, mounted,
2092
- // but a crash occurred in some mounted hook. Then, it was handled and
2093
- // the new rendering is being applied.
2094
- node.updateDom();
2095
- }
2096
- else {
2097
- node.bdom = this.bdom;
2098
- if (this.position === "last-child" || this.target.childNodes.length === 0) {
2099
- mount$1(node.bdom, this.target);
2100
- }
2101
- else {
2102
- const firstChild = this.target.childNodes[0];
2103
- mount$1(node.bdom, this.target, firstChild);
2104
- }
2105
- }
2106
- // unregistering the fiber before mounted since it can do another render
2107
- // and that the current rendering is obviously completed
2108
- node.fiber = null;
2109
- node.status = 1 /* MOUNTED */;
2110
- this.appliedToDom = true;
2111
- let mountedFibers = this.mounted;
2112
- while ((current = mountedFibers.pop())) {
2113
- if (current.appliedToDom) {
2114
- for (let cb of current.node.mounted) {
2115
- cb();
2116
- }
2117
- }
2118
- }
2119
- }
2120
- catch (e) {
2121
- handleError({ fiber: current, error: e });
2368
+ async updateAndRender(props, parentFiber) {
2369
+ const rawProps = props;
2370
+ props = Object.assign({}, props);
2371
+ // update
2372
+ const fiber = makeChildFiber(this, parentFiber);
2373
+ this.fiber = fiber;
2374
+ const component = this.component;
2375
+ const defaultProps = component.constructor.defaultProps;
2376
+ if (defaultProps) {
2377
+ applyDefaultProps(props, defaultProps);
2122
2378
  }
2123
- }
2124
- }
2125
-
2126
- /**
2127
- * Apply default props (only top level).
2128
- *
2129
- * Note that this method does modify in place the props
2130
- */
2131
- function applyDefaultProps(props, ComponentClass) {
2132
- const defaultProps = ComponentClass.defaultProps;
2133
- if (defaultProps) {
2134
- for (let propName in defaultProps) {
2135
- if (props[propName] === undefined) {
2136
- props[propName] = defaultProps[propName];
2379
+ currentNode = this;
2380
+ for (const key in props) {
2381
+ const prop = props[key];
2382
+ if (prop && typeof prop === "object" && prop[TARGET]) {
2383
+ props[key] = useState(prop);
2137
2384
  }
2138
2385
  }
2139
- }
2140
- }
2141
- //------------------------------------------------------------------------------
2142
- // Prop validation helper
2143
- //------------------------------------------------------------------------------
2144
- function getPropDescription(staticProps) {
2145
- if (staticProps instanceof Array) {
2146
- return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
2147
- }
2148
- return staticProps || { "*": true };
2149
- }
2150
- /**
2151
- * Validate the component props (or next props) against the (static) props
2152
- * description. This is potentially an expensive operation: it may needs to
2153
- * visit recursively the props and all the children to check if they are valid.
2154
- * This is why it is only done in 'dev' mode.
2155
- */
2156
- function validateProps(name, props, parent) {
2157
- const ComponentClass = typeof name !== "string"
2158
- ? name
2159
- : parent.constructor.components[name];
2160
- if (!ComponentClass) {
2161
- // this is an error, wrong component. We silently return here instead so the
2162
- // error is triggered by the usual path ('component' function)
2163
- return;
2164
- }
2165
- applyDefaultProps(props, ComponentClass);
2166
- const defaultProps = ComponentClass.defaultProps || {};
2167
- let propsDef = getPropDescription(ComponentClass.props);
2168
- const allowAdditionalProps = "*" in propsDef;
2169
- for (let propName in propsDef) {
2170
- if (propName === "*") {
2171
- continue;
2172
- }
2173
- const propDef = propsDef[propName];
2174
- let isMandatory = !!propDef;
2175
- if (typeof propDef === "object" && "optional" in propDef) {
2176
- isMandatory = !propDef.optional;
2386
+ currentNode = null;
2387
+ const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
2388
+ await prom;
2389
+ if (fiber !== this.fiber) {
2390
+ return;
2177
2391
  }
2178
- if (isMandatory && propName in defaultProps) {
2179
- throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
2392
+ component.props = props;
2393
+ this.props = rawProps;
2394
+ fiber.render();
2395
+ const parentRoot = parentFiber.root;
2396
+ if (this.willPatch.length) {
2397
+ parentRoot.willPatch.push(fiber);
2180
2398
  }
2181
- if (props[propName] === undefined) {
2182
- if (isMandatory) {
2183
- throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
2184
- }
2185
- else {
2186
- continue;
2187
- }
2399
+ if (this.patched.length) {
2400
+ parentRoot.patched.push(fiber);
2188
2401
  }
2189
- let isValid;
2190
- try {
2191
- isValid = isValidProp(props[propName], propDef);
2402
+ }
2403
+ /**
2404
+ * Finds a child that has dom that is not yet updated, and update it. This
2405
+ * method is meant to be used only in the context of repatching the dom after
2406
+ * a mounted hook failed and was handled.
2407
+ */
2408
+ updateDom() {
2409
+ if (!this.fiber) {
2410
+ return;
2192
2411
  }
2193
- catch (e) {
2194
- e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
2195
- throw e;
2412
+ if (this.bdom === this.fiber.bdom) {
2413
+ // If the error was handled by some child component, we need to find it to
2414
+ // apply its change
2415
+ for (let k in this.children) {
2416
+ const child = this.children[k];
2417
+ child.updateDom();
2418
+ }
2196
2419
  }
2197
- if (!isValid) {
2198
- throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
2420
+ else {
2421
+ // if we get here, this is the component that handled the error and rerendered
2422
+ // itself, so we can simply patch the dom
2423
+ this.bdom.patch(this.fiber.bdom, false);
2424
+ this.fiber.appliedToDom = true;
2425
+ this.fiber = null;
2199
2426
  }
2200
2427
  }
2201
- if (!allowAdditionalProps) {
2202
- for (let propName in props) {
2203
- if (!(propName in propsDef)) {
2204
- throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
2205
- }
2428
+ // ---------------------------------------------------------------------------
2429
+ // Block DOM methods
2430
+ // ---------------------------------------------------------------------------
2431
+ firstNode() {
2432
+ const bdom = this.bdom;
2433
+ return bdom ? bdom.firstNode() : undefined;
2434
+ }
2435
+ mount(parent, anchor) {
2436
+ const bdom = this.fiber.bdom;
2437
+ this.bdom = bdom;
2438
+ bdom.mount(parent, anchor);
2439
+ this.status = 1 /* MOUNTED */;
2440
+ this.fiber.appliedToDom = true;
2441
+ this.children = this.fiber.childrenMap;
2442
+ this.fiber = null;
2443
+ }
2444
+ moveBefore(other, afterNode) {
2445
+ this.bdom.moveBefore(other ? other.bdom : null, afterNode);
2446
+ }
2447
+ patch() {
2448
+ if (this.fiber && this.fiber.parent) {
2449
+ // we only patch here renderings coming from above. renderings initiated
2450
+ // by the component will be patched independently in the appropriate
2451
+ // fiber.complete
2452
+ this._patch();
2206
2453
  }
2207
2454
  }
2208
- }
2209
- /**
2210
- * Check if an invidual prop value matches its (static) prop definition
2211
- */
2212
- function isValidProp(prop, propDef) {
2213
- if (propDef === true) {
2214
- return true;
2215
- }
2216
- if (typeof propDef === "function") {
2217
- // Check if a value is constructed by some Constructor. Note that there is a
2218
- // slight abuse of language: we want to consider primitive values as well.
2219
- //
2220
- // So, even though 1 is not an instance of Number, we want to consider that
2221
- // it is valid.
2222
- if (typeof prop === "object") {
2223
- return prop instanceof propDef;
2224
- }
2225
- return typeof prop === propDef.name.toLowerCase();
2226
- }
2227
- else if (propDef instanceof Array) {
2228
- // If this code is executed, this means that we want to check if a prop
2229
- // matches at least one of its descriptor.
2230
- let result = false;
2231
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
2232
- result = result || isValidProp(prop, propDef[i]);
2455
+ _patch() {
2456
+ let hasChildren = false;
2457
+ for (let _k in this.children) {
2458
+ hasChildren = true;
2459
+ break;
2233
2460
  }
2234
- return result;
2461
+ const fiber = this.fiber;
2462
+ this.children = fiber.childrenMap;
2463
+ this.bdom.patch(fiber.bdom, hasChildren);
2464
+ fiber.appliedToDom = true;
2465
+ this.fiber = null;
2235
2466
  }
2236
- // propsDef is an object
2237
- if (propDef.optional && prop === undefined) {
2238
- return true;
2467
+ beforeRemove() {
2468
+ this._destroy();
2239
2469
  }
2240
- let result = propDef.type ? isValidProp(prop, propDef.type) : true;
2241
- if (propDef.validate) {
2242
- result = result && propDef.validate(prop);
2470
+ remove() {
2471
+ this.bdom.remove();
2243
2472
  }
2244
- if (propDef.type === Array && propDef.element) {
2245
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
2246
- result = result && isValidProp(prop[i], propDef.element);
2247
- }
2473
+ // ---------------------------------------------------------------------------
2474
+ // Some debug helpers
2475
+ // ---------------------------------------------------------------------------
2476
+ get name() {
2477
+ return this.component.constructor.name;
2248
2478
  }
2249
- if (propDef.type === Object && propDef.shape) {
2250
- const shape = propDef.shape;
2251
- for (let key in shape) {
2252
- result = result && isValidProp(prop[key], shape[key]);
2253
- }
2254
- if (result) {
2255
- for (let propName in prop) {
2256
- if (!(propName in shape)) {
2257
- throw new Error(`unknown prop '${propName}'`);
2258
- }
2259
- }
2260
- }
2479
+ get subscriptions() {
2480
+ const render = batchedRenderFunctions.get(this);
2481
+ return render ? getSubscriptions(render) : [];
2261
2482
  }
2262
- return result;
2263
2483
  }
2264
2484
 
2265
- let currentNode = null;
2266
- function getCurrent() {
2267
- if (!currentNode) {
2268
- throw new Error("No active component (a hook function should only be called in 'setup')");
2269
- }
2270
- return currentNode;
2271
- }
2272
- function useComponent() {
2273
- return currentNode.component;
2485
+ const TIMEOUT = Symbol("timeout");
2486
+ function wrapError(fn, hookName) {
2487
+ const error = new OwlError(`The following error occurred in ${hookName}: `);
2488
+ const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
2489
+ const node = getCurrent();
2490
+ return (...args) => {
2491
+ const onError = (cause) => {
2492
+ error.cause = cause;
2493
+ if (cause instanceof Error) {
2494
+ error.message += `"${cause.message}"`;
2495
+ }
2496
+ else {
2497
+ error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
2498
+ }
2499
+ throw error;
2500
+ };
2501
+ try {
2502
+ const result = fn(...args);
2503
+ if (result instanceof Promise) {
2504
+ if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
2505
+ const fiber = node.fiber;
2506
+ Promise.race([
2507
+ result.catch(() => { }),
2508
+ new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
2509
+ ]).then((res) => {
2510
+ if (res === TIMEOUT && node.fiber === fiber) {
2511
+ console.warn(timeoutError);
2512
+ }
2513
+ });
2514
+ }
2515
+ return result.catch(onError);
2516
+ }
2517
+ return result;
2518
+ }
2519
+ catch (cause) {
2520
+ onError(cause);
2521
+ }
2522
+ };
2274
2523
  }
2275
2524
  // -----------------------------------------------------------------------------
2276
- // Integration with reactivity system (useState)
2525
+ // hooks
2277
2526
  // -----------------------------------------------------------------------------
2278
- const batchedRenderFunctions = new WeakMap();
2279
- /**
2280
- * Creates a reactive object that will be observed by the current component.
2281
- * Reading data from the returned object (eg during rendering) will cause the
2282
- * component to subscribe to that data and be rerendered when it changes.
2283
- *
2284
- * @param state the state to observe
2285
- * @returns a reactive object that will cause the component to re-render on
2286
- * relevant changes
2287
- * @see reactive
2288
- */
2289
- function useState(state) {
2527
+ function onWillStart(fn) {
2290
2528
  const node = getCurrent();
2291
- let render = batchedRenderFunctions.get(node);
2292
- if (!render) {
2293
- render = batched(node.render.bind(node));
2294
- batchedRenderFunctions.set(node, render);
2295
- // manual implementation of onWillDestroy to break cyclic dependency
2296
- node.willDestroy.push(clearReactivesForCallback.bind(null, render));
2297
- if (node.app.dev) {
2298
- Object.defineProperty(node, "subscriptions", {
2299
- get() {
2300
- return getSubscriptions(render);
2301
- },
2302
- });
2303
- }
2304
- }
2305
- return reactive(state, render);
2529
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2530
+ node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
2306
2531
  }
2307
- function arePropsDifferent(props1, props2) {
2308
- for (let k in props1) {
2309
- if (props1[k] !== props2[k]) {
2310
- return true;
2311
- }
2312
- }
2313
- return Object.keys(props1).length !== Object.keys(props2).length;
2532
+ function onWillUpdateProps(fn) {
2533
+ const node = getCurrent();
2534
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2535
+ node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
2314
2536
  }
2315
- function component(name, props, key, ctx, parent) {
2316
- let node = ctx.children[key];
2317
- let isDynamic = typeof name !== "string";
2318
- if (node) {
2319
- if (node.status < 1 /* MOUNTED */) {
2320
- node.destroy();
2321
- node = undefined;
2322
- }
2323
- else if (node.status === 2 /* DESTROYED */) {
2324
- node = undefined;
2325
- }
2326
- }
2327
- if (isDynamic && node && node.component.constructor !== name) {
2328
- node = undefined;
2329
- }
2330
- const parentFiber = ctx.fiber;
2331
- if (node) {
2332
- let shouldRender = node.forceNextRender;
2333
- if (shouldRender) {
2334
- node.forceNextRender = false;
2335
- }
2336
- else {
2337
- const currentProps = node.component.props[TARGET];
2338
- shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
2339
- }
2340
- if (shouldRender) {
2341
- node.updateAndRender(props, parentFiber);
2342
- }
2343
- }
2344
- else {
2345
- // new component
2346
- let C;
2347
- if (isDynamic) {
2348
- C = name;
2349
- }
2350
- else {
2351
- C = parent.constructor.components[name];
2352
- if (!C) {
2353
- throw new Error(`Cannot find the definition of component "${name}"`);
2354
- }
2355
- }
2356
- node = new ComponentNode(C, props, ctx.app, ctx);
2357
- ctx.children[key] = node;
2358
- node.initiateRender(new Fiber(node, parentFiber));
2359
- }
2360
- return node;
2537
+ function onMounted(fn) {
2538
+ const node = getCurrent();
2539
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2540
+ node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
2361
2541
  }
2362
- class ComponentNode {
2363
- constructor(C, props, app, parent) {
2364
- this.fiber = null;
2365
- this.bdom = null;
2366
- this.status = 0 /* NEW */;
2367
- this.forceNextRender = false;
2368
- this.children = Object.create(null);
2369
- this.refs = {};
2370
- this.willStart = [];
2371
- this.willUpdateProps = [];
2372
- this.willUnmount = [];
2373
- this.mounted = [];
2374
- this.willPatch = [];
2375
- this.patched = [];
2376
- this.willDestroy = [];
2377
- currentNode = this;
2378
- this.app = app;
2379
- this.parent = parent || null;
2380
- this.level = parent ? parent.level + 1 : 0;
2381
- applyDefaultProps(props, C);
2382
- const env = (parent && parent.childEnv) || app.env;
2383
- this.childEnv = env;
2384
- props = useState(props);
2385
- this.component = new C(props, env, this);
2386
- this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
2387
- this.component.setup();
2388
- currentNode = null;
2542
+ function onWillPatch(fn) {
2543
+ const node = getCurrent();
2544
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2545
+ node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
2546
+ }
2547
+ function onPatched(fn) {
2548
+ const node = getCurrent();
2549
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2550
+ node.patched.push(decorate(fn.bind(node.component), "onPatched"));
2551
+ }
2552
+ function onWillUnmount(fn) {
2553
+ const node = getCurrent();
2554
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2555
+ node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
2556
+ }
2557
+ function onWillDestroy(fn) {
2558
+ const node = getCurrent();
2559
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2560
+ node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
2561
+ }
2562
+ function onWillRender(fn) {
2563
+ const node = getCurrent();
2564
+ const renderFn = node.renderFn;
2565
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2566
+ fn = decorate(fn.bind(node.component), "onWillRender");
2567
+ node.renderFn = () => {
2568
+ fn();
2569
+ return renderFn();
2570
+ };
2571
+ }
2572
+ function onRendered(fn) {
2573
+ const node = getCurrent();
2574
+ const renderFn = node.renderFn;
2575
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2576
+ fn = decorate(fn.bind(node.component), "onRendered");
2577
+ node.renderFn = () => {
2578
+ const result = renderFn();
2579
+ fn();
2580
+ return result;
2581
+ };
2582
+ }
2583
+ function onError(callback) {
2584
+ const node = getCurrent();
2585
+ let handlers = nodeErrorHandlers.get(node);
2586
+ if (!handlers) {
2587
+ handlers = [];
2588
+ nodeErrorHandlers.set(node, handlers);
2389
2589
  }
2390
- mountComponent(target, options) {
2391
- const fiber = new MountFiber(this, target, options);
2392
- this.app.scheduler.addFiber(fiber);
2393
- this.initiateRender(fiber);
2590
+ handlers.push(callback.bind(node.component));
2591
+ }
2592
+
2593
+ class Component {
2594
+ constructor(props, env, node) {
2595
+ this.props = props;
2596
+ this.env = env;
2597
+ this.__owl__ = node;
2394
2598
  }
2395
- async initiateRender(fiber) {
2396
- this.fiber = fiber;
2397
- if (this.mounted.length) {
2398
- fiber.root.mounted.push(fiber);
2399
- }
2400
- const component = this.component;
2401
- try {
2402
- await Promise.all(this.willStart.map((f) => f.call(component)));
2599
+ setup() { }
2600
+ render(deep = false) {
2601
+ this.__owl__.render(deep === true);
2602
+ }
2603
+ }
2604
+ Component.template = "";
2605
+
2606
+ const VText = text("").constructor;
2607
+ class VPortal extends VText {
2608
+ constructor(selector, content) {
2609
+ super("");
2610
+ this.target = null;
2611
+ this.selector = selector;
2612
+ this.content = content;
2613
+ }
2614
+ mount(parent, anchor) {
2615
+ super.mount(parent, anchor);
2616
+ this.target = document.querySelector(this.selector);
2617
+ if (this.target) {
2618
+ this.content.mount(this.target, null);
2403
2619
  }
2404
- catch (e) {
2405
- handleError({ node: this, error: e });
2406
- return;
2620
+ else {
2621
+ this.content.mount(parent, anchor);
2407
2622
  }
2408
- if (this.status === 0 /* NEW */ && this.fiber === fiber) {
2409
- this._render(fiber);
2623
+ }
2624
+ beforeRemove() {
2625
+ this.content.beforeRemove();
2626
+ }
2627
+ remove() {
2628
+ if (this.content) {
2629
+ super.remove();
2630
+ this.content.remove();
2631
+ this.content = null;
2410
2632
  }
2411
2633
  }
2412
- async render(deep = false) {
2413
- let current = this.fiber;
2414
- if (current && current.root.locked) {
2415
- await Promise.resolve();
2416
- // situation may have changed after the microtask tick
2417
- current = this.fiber;
2634
+ patch(other) {
2635
+ super.patch(other);
2636
+ if (this.content) {
2637
+ this.content.patch(other.content, true);
2418
2638
  }
2419
- if (current) {
2420
- if (!current.bdom && !fibersInError.has(current)) {
2421
- if (deep) {
2422
- // we want the render from this point on to be with deep=true
2423
- current.deep = deep;
2639
+ else {
2640
+ this.content = other.content;
2641
+ this.content.mount(this.target, null);
2642
+ }
2643
+ }
2644
+ }
2645
+ /**
2646
+ * kind of similar to <t t-slot="default"/>, but it wraps it around a VPortal
2647
+ */
2648
+ function portalTemplate(app, bdom, helpers) {
2649
+ let { callSlot } = helpers;
2650
+ return function template(ctx, node, key = "") {
2651
+ return new VPortal(ctx.props.target, callSlot(ctx, node, key, "default", false, null));
2652
+ };
2653
+ }
2654
+ class Portal extends Component {
2655
+ setup() {
2656
+ const node = this.__owl__;
2657
+ onMounted(() => {
2658
+ const portal = node.bdom;
2659
+ if (!portal.target) {
2660
+ const target = document.querySelector(this.props.target);
2661
+ if (target) {
2662
+ portal.content.moveBefore(target, null);
2663
+ }
2664
+ else {
2665
+ throw new OwlError("invalid portal target");
2424
2666
  }
2425
- return;
2426
2667
  }
2427
- // if current rendering was with deep=true, we want this one to be the same
2428
- deep = deep || current.deep;
2429
- }
2430
- else if (!this.bdom) {
2431
- return;
2432
- }
2433
- const fiber = makeRootFiber(this);
2434
- fiber.deep = deep;
2435
- this.fiber = fiber;
2436
- this.app.scheduler.addFiber(fiber);
2437
- await Promise.resolve();
2438
- if (this.status === 2 /* DESTROYED */) {
2439
- return;
2440
- }
2441
- // We only want to actually render the component if the following two
2442
- // conditions are true:
2443
- // * this.fiber: it could be null, in which case the render has been cancelled
2444
- // * (current || !fiber.parent): if current is not null, this means that the
2445
- // render function was called when a render was already occurring. In this
2446
- // case, the pending rendering was cancelled, and the fiber needs to be
2447
- // rendered to complete the work. If current is null, we check that the
2448
- // fiber has no parent. If that is the case, the fiber was downgraded from
2449
- // a root fiber to a child fiber in the previous microtick, because it was
2450
- // embedded in a rendering coming from above, so the fiber will be rendered
2451
- // in the next microtick anyway, so we should not render it again.
2452
- if (this.fiber === fiber && (current || !fiber.parent)) {
2453
- this._render(fiber);
2454
- }
2668
+ });
2669
+ onWillUnmount(() => {
2670
+ const portal = node.bdom;
2671
+ portal.remove();
2672
+ });
2455
2673
  }
2456
- _render(fiber) {
2457
- try {
2458
- fiber.bdom = this.renderFn();
2459
- fiber.root.counter--;
2460
- }
2461
- catch (e) {
2462
- handleError({ node: this, error: e });
2463
- }
2674
+ }
2675
+ Portal.template = "__portal__";
2676
+ Portal.props = {
2677
+ target: {
2678
+ type: String,
2679
+ },
2680
+ slots: true,
2681
+ };
2682
+
2683
+ // -----------------------------------------------------------------------------
2684
+ // helpers
2685
+ // -----------------------------------------------------------------------------
2686
+ const isUnionType = (t) => Array.isArray(t);
2687
+ const isBaseType = (t) => typeof t !== "object";
2688
+ const isValueType = (t) => typeof t === "object" && t && "value" in t;
2689
+ function isOptional(t) {
2690
+ return typeof t === "object" && "optional" in t ? t.optional || false : false;
2691
+ }
2692
+ function describeType(type) {
2693
+ return type === "*" || type === true ? "value" : type.name.toLowerCase();
2694
+ }
2695
+ function describe(info) {
2696
+ if (isBaseType(info)) {
2697
+ return describeType(info);
2464
2698
  }
2465
- destroy() {
2466
- let shouldRemove = this.status === 1 /* MOUNTED */;
2467
- this._destroy();
2468
- if (shouldRemove) {
2469
- this.bdom.remove();
2470
- }
2699
+ else if (isUnionType(info)) {
2700
+ return info.map(describe).join(" or ");
2471
2701
  }
2472
- _destroy() {
2473
- const component = this.component;
2474
- if (this.status === 1 /* MOUNTED */) {
2475
- for (let cb of this.willUnmount) {
2476
- cb.call(component);
2702
+ else if (isValueType(info)) {
2703
+ return String(info.value);
2704
+ }
2705
+ if ("element" in info) {
2706
+ return `list of ${describe({ type: info.element, optional: false })}s`;
2707
+ }
2708
+ if ("shape" in info) {
2709
+ return `object`;
2710
+ }
2711
+ return describe(info.type || "*");
2712
+ }
2713
+ function toSchema(spec) {
2714
+ return Object.fromEntries(spec.map((e) => e.endsWith("?") ? [e.slice(0, -1), { optional: true }] : [e, { type: "*", optional: false }]));
2715
+ }
2716
+ /**
2717
+ * Main validate function
2718
+ */
2719
+ function validate(obj, spec) {
2720
+ let errors = validateSchema(obj, spec);
2721
+ if (errors.length) {
2722
+ throw new OwlError("Invalid object: " + errors.join(", "));
2723
+ }
2724
+ }
2725
+ /**
2726
+ * Helper validate function, to get the list of errors. useful if one want to
2727
+ * manipulate the errors without parsing an error object
2728
+ */
2729
+ function validateSchema(obj, schema) {
2730
+ if (Array.isArray(schema)) {
2731
+ schema = toSchema(schema);
2732
+ }
2733
+ let errors = [];
2734
+ // check if each value in obj has correct shape
2735
+ for (let key in obj) {
2736
+ if (key in schema) {
2737
+ let result = validateType(key, obj[key], schema[key]);
2738
+ if (result) {
2739
+ errors.push(result);
2477
2740
  }
2478
2741
  }
2479
- for (let child of Object.values(this.children)) {
2480
- child._destroy();
2742
+ else if (!("*" in schema)) {
2743
+ errors.push(`unknown key '${key}'`);
2481
2744
  }
2482
- for (let cb of this.willDestroy) {
2483
- cb.call(component);
2745
+ }
2746
+ // check that all specified keys are defined in obj
2747
+ for (let key in schema) {
2748
+ const spec = schema[key];
2749
+ if (key !== "*" && !isOptional(spec) && !(key in obj)) {
2750
+ const isObj = typeof spec === "object" && !Array.isArray(spec);
2751
+ const isAny = spec === "*" || (isObj && "type" in spec ? spec.type === "*" : isObj);
2752
+ let detail = isAny ? "" : ` (should be a ${describe(spec)})`;
2753
+ errors.push(`'${key}' is missing${detail}`);
2484
2754
  }
2485
- this.status = 2 /* DESTROYED */;
2486
2755
  }
2487
- async updateAndRender(props, parentFiber) {
2488
- // update
2489
- const fiber = makeChildFiber(this, parentFiber);
2490
- this.fiber = fiber;
2491
- const component = this.component;
2492
- applyDefaultProps(props, component.constructor);
2493
- currentNode = this;
2494
- props = useState(props);
2495
- currentNode = null;
2496
- const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
2497
- await prom;
2498
- if (fiber !== this.fiber) {
2499
- return;
2756
+ return errors;
2757
+ }
2758
+ function validateBaseType(key, value, type) {
2759
+ if (typeof type === "function") {
2760
+ if (typeof value === "object") {
2761
+ if (!(value instanceof type)) {
2762
+ return `'${key}' is not a ${describeType(type)}`;
2763
+ }
2500
2764
  }
2501
- component.props = props;
2502
- this._render(fiber);
2503
- const parentRoot = parentFiber.root;
2504
- if (this.willPatch.length) {
2505
- parentRoot.willPatch.push(fiber);
2765
+ else if (typeof value !== type.name.toLowerCase()) {
2766
+ return `'${key}' is not a ${describeType(type)}`;
2506
2767
  }
2507
- if (this.patched.length) {
2508
- parentRoot.patched.push(fiber);
2768
+ }
2769
+ return null;
2770
+ }
2771
+ function validateArrayType(key, value, descr) {
2772
+ if (!Array.isArray(value)) {
2773
+ return `'${key}' is not a list of ${describe(descr)}s`;
2774
+ }
2775
+ for (let i = 0; i < value.length; i++) {
2776
+ const error = validateType(`${key}[${i}]`, value[i], descr);
2777
+ if (error) {
2778
+ return error;
2509
2779
  }
2510
2780
  }
2511
- /**
2512
- * Finds a child that has dom that is not yet updated, and update it. This
2513
- * method is meant to be used only in the context of repatching the dom after
2514
- * a mounted hook failed and was handled.
2515
- */
2516
- updateDom() {
2517
- if (!this.fiber) {
2518
- return;
2781
+ return null;
2782
+ }
2783
+ function validateType(key, value, descr) {
2784
+ if (value === undefined) {
2785
+ return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
2786
+ }
2787
+ else if (isBaseType(descr)) {
2788
+ return validateBaseType(key, value, descr);
2789
+ }
2790
+ else if (isValueType(descr)) {
2791
+ return value === descr.value ? null : `'${key}' is not equal to '${descr.value}'`;
2792
+ }
2793
+ else if (isUnionType(descr)) {
2794
+ let validDescr = descr.find((p) => !validateType(key, value, p));
2795
+ return validDescr ? null : `'${key}' is not a ${describe(descr)}`;
2796
+ }
2797
+ let result = null;
2798
+ if ("element" in descr) {
2799
+ result = validateArrayType(key, value, descr.element);
2800
+ }
2801
+ else if ("shape" in descr && !result) {
2802
+ if (typeof value !== "object" || Array.isArray(value)) {
2803
+ result = `'${key}' is not an object`;
2519
2804
  }
2520
- if (this.bdom === this.fiber.bdom) {
2521
- // If the error was handled by some child component, we need to find it to
2522
- // apply its change
2523
- for (let k in this.children) {
2524
- const child = this.children[k];
2525
- child.updateDom();
2805
+ else {
2806
+ const errors = validateSchema(value, descr.shape);
2807
+ if (errors.length) {
2808
+ result = `'${key}' has not the correct shape (${errors.join(", ")})`;
2526
2809
  }
2527
2810
  }
2528
- else {
2529
- // if we get here, this is the component that handled the error and rerendered
2530
- // itself, so we can simply patch the dom
2531
- this.bdom.patch(this.fiber.bdom, false);
2532
- this.fiber.appliedToDom = true;
2533
- this.fiber = null;
2534
- }
2535
2811
  }
2536
- // ---------------------------------------------------------------------------
2537
- // Block DOM methods
2538
- // ---------------------------------------------------------------------------
2539
- firstNode() {
2540
- const bdom = this.bdom;
2541
- return bdom ? bdom.firstNode() : undefined;
2812
+ if ("type" in descr && !result) {
2813
+ result = validateType(key, value, descr.type);
2542
2814
  }
2543
- mount(parent, anchor) {
2544
- const bdom = this.fiber.bdom;
2545
- this.bdom = bdom;
2546
- bdom.mount(parent, anchor);
2547
- this.status = 1 /* MOUNTED */;
2548
- this.fiber.appliedToDom = true;
2549
- this.fiber = null;
2815
+ if ("validate" in descr && !result) {
2816
+ result = !descr.validate(value) ? `'${key}' is not valid` : null;
2550
2817
  }
2551
- moveBefore(other, afterNode) {
2552
- this.bdom.moveBefore(other ? other.bdom : null, afterNode);
2818
+ return result;
2819
+ }
2820
+
2821
+ const ObjectCreate = Object.create;
2822
+ /**
2823
+ * This file contains utility functions that will be injected in each template,
2824
+ * to perform various useful tasks in the compiled code.
2825
+ */
2826
+ function withDefault(value, defaultValue) {
2827
+ return value === undefined || value === null || value === false ? defaultValue : value;
2828
+ }
2829
+ function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
2830
+ key = key + "__slot_" + name;
2831
+ const slots = ctx.props.slots || {};
2832
+ const { __render, __ctx, __scope } = slots[name] || {};
2833
+ const slotScope = ObjectCreate(__ctx || {});
2834
+ if (__scope) {
2835
+ slotScope[__scope] = extra;
2553
2836
  }
2554
- patch() {
2555
- if (this.fiber && this.fiber.parent) {
2556
- // we only patch here renderings coming from above. renderings initiated
2557
- // by the component will be patched independently in the appropriate
2558
- // fiber.complete
2559
- this._patch();
2837
+ const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
2838
+ if (defaultContent) {
2839
+ let child1 = undefined;
2840
+ let child2 = undefined;
2841
+ if (slotBDom) {
2842
+ child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
2560
2843
  }
2561
- }
2562
- _patch() {
2563
- const hasChildren = Object.keys(this.children).length > 0;
2564
- this.bdom.patch(this.fiber.bdom, hasChildren);
2565
- if (hasChildren) {
2566
- this.cleanOutdatedChildren();
2844
+ else {
2845
+ child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
2567
2846
  }
2568
- this.fiber.appliedToDom = true;
2569
- this.fiber = null;
2847
+ return multi([child1, child2]);
2570
2848
  }
2571
- beforeRemove() {
2572
- this._destroy();
2849
+ return slotBDom || text("");
2850
+ }
2851
+ function capture(ctx) {
2852
+ const component = ctx.__owl__.component;
2853
+ const result = ObjectCreate(component);
2854
+ for (let k in ctx) {
2855
+ result[k] = ctx[k];
2573
2856
  }
2574
- remove() {
2575
- this.bdom.remove();
2857
+ return result;
2858
+ }
2859
+ function withKey(elem, k) {
2860
+ elem.key = k;
2861
+ return elem;
2862
+ }
2863
+ function prepareList(collection) {
2864
+ let keys;
2865
+ let values;
2866
+ if (Array.isArray(collection)) {
2867
+ keys = collection;
2868
+ values = collection;
2576
2869
  }
2577
- cleanOutdatedChildren() {
2578
- const children = this.children;
2579
- for (const key in children) {
2580
- const node = children[key];
2581
- const status = node.status;
2582
- if (status !== 1 /* MOUNTED */) {
2583
- delete children[key];
2584
- if (status !== 2 /* DESTROYED */) {
2585
- node.destroy();
2586
- }
2587
- }
2870
+ else if (collection) {
2871
+ values = Object.keys(collection);
2872
+ keys = Object.values(collection);
2873
+ }
2874
+ else {
2875
+ throw new OwlError("Invalid loop expression");
2876
+ }
2877
+ const n = values.length;
2878
+ return [keys, values, n, new Array(n)];
2879
+ }
2880
+ const isBoundary = Symbol("isBoundary");
2881
+ function setContextValue(ctx, key, value) {
2882
+ const ctx0 = ctx;
2883
+ while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
2884
+ const newCtx = ctx.__proto__;
2885
+ if (!newCtx) {
2886
+ ctx = ctx0;
2887
+ break;
2588
2888
  }
2889
+ ctx = newCtx;
2589
2890
  }
2590
- }
2591
-
2592
- // -----------------------------------------------------------------------------
2593
- // Scheduler
2594
- // -----------------------------------------------------------------------------
2595
- class Scheduler {
2596
- constructor() {
2597
- this.tasks = new Set();
2598
- this.isRunning = false;
2599
- this.requestAnimationFrame = Scheduler.requestAnimationFrame;
2891
+ ctx[key] = value;
2892
+ }
2893
+ function toNumber(val) {
2894
+ const n = parseFloat(val);
2895
+ return isNaN(n) ? val : n;
2896
+ }
2897
+ function shallowEqual(l1, l2) {
2898
+ for (let i = 0, l = l1.length; i < l; i++) {
2899
+ if (l1[i] !== l2[i]) {
2900
+ return false;
2901
+ }
2600
2902
  }
2601
- start() {
2602
- this.isRunning = true;
2603
- this.scheduleTasks();
2903
+ return true;
2904
+ }
2905
+ class LazyValue {
2906
+ constructor(fn, ctx, component, node, key) {
2907
+ this.fn = fn;
2908
+ this.ctx = capture(ctx);
2909
+ this.component = component;
2910
+ this.node = node;
2911
+ this.key = key;
2604
2912
  }
2605
- stop() {
2606
- this.isRunning = false;
2913
+ evaluate() {
2914
+ return this.fn.call(this.component, this.ctx, this.node, this.key);
2607
2915
  }
2608
- addFiber(fiber) {
2609
- this.tasks.add(fiber.root);
2610
- if (!this.isRunning) {
2611
- this.start();
2612
- }
2916
+ toString() {
2917
+ return this.evaluate().toString();
2613
2918
  }
2614
- /**
2615
- * Process all current tasks. This only applies to the fibers that are ready.
2616
- * Other tasks are left unchanged.
2617
- */
2618
- flush() {
2619
- this.tasks.forEach((fiber) => {
2620
- if (fiber.root !== fiber) {
2621
- this.tasks.delete(fiber);
2622
- return;
2919
+ }
2920
+ /*
2921
+ * Safely outputs `value` as a block depending on the nature of `value`
2922
+ */
2923
+ function safeOutput(value, defaultValue) {
2924
+ if (value === undefined) {
2925
+ return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
2926
+ }
2927
+ let safeKey;
2928
+ let block;
2929
+ switch (typeof value) {
2930
+ case "object":
2931
+ if (value instanceof Markup) {
2932
+ safeKey = `string_safe`;
2933
+ block = html(value);
2623
2934
  }
2624
- const hasError = fibersInError.has(fiber);
2625
- if (hasError && fiber.counter !== 0) {
2626
- this.tasks.delete(fiber);
2627
- return;
2935
+ else if (value instanceof LazyValue) {
2936
+ safeKey = `lazy_value`;
2937
+ block = value.evaluate();
2628
2938
  }
2629
- if (fiber.node.status === 2 /* DESTROYED */) {
2630
- this.tasks.delete(fiber);
2631
- return;
2939
+ else if (value instanceof String) {
2940
+ safeKey = "string_unsafe";
2941
+ block = text(value);
2632
2942
  }
2633
- if (fiber.counter === 0) {
2634
- if (!hasError) {
2635
- fiber.complete();
2636
- }
2637
- this.tasks.delete(fiber);
2943
+ else {
2944
+ // Assuming it is a block
2945
+ safeKey = "block_safe";
2946
+ block = value;
2638
2947
  }
2639
- });
2640
- if (this.tasks.size === 0) {
2641
- this.stop();
2642
- }
2948
+ break;
2949
+ case "string":
2950
+ safeKey = "string_unsafe";
2951
+ block = text(value);
2952
+ break;
2953
+ default:
2954
+ safeKey = "string_unsafe";
2955
+ block = text(String(value));
2643
2956
  }
2644
- scheduleTasks() {
2645
- this.requestAnimationFrame(() => {
2646
- this.flush();
2647
- if (this.isRunning) {
2648
- this.scheduleTasks();
2649
- }
2650
- });
2957
+ return toggler(safeKey, block);
2958
+ }
2959
+ let boundFunctions = new WeakMap();
2960
+ const WeakMapGet = WeakMap.prototype.get;
2961
+ const WeakMapSet = WeakMap.prototype.set;
2962
+ function bind(ctx, fn) {
2963
+ let component = ctx.__owl__.component;
2964
+ let boundFnMap = WeakMapGet.call(boundFunctions, component);
2965
+ if (!boundFnMap) {
2966
+ boundFnMap = new WeakMap();
2967
+ WeakMapSet.call(boundFunctions, component, boundFnMap);
2651
2968
  }
2969
+ let boundFn = WeakMapGet.call(boundFnMap, fn);
2970
+ if (!boundFn) {
2971
+ boundFn = fn.bind(component);
2972
+ WeakMapSet.call(boundFnMap, fn, boundFn);
2973
+ }
2974
+ return boundFn;
2975
+ }
2976
+ function multiRefSetter(refs, name) {
2977
+ let count = 0;
2978
+ return (el) => {
2979
+ if (el) {
2980
+ count++;
2981
+ if (count > 1) {
2982
+ throw new OwlError("Cannot have 2 elements with same ref name at the same time");
2983
+ }
2984
+ }
2985
+ if (count === 0 || el) {
2986
+ refs[name] = el;
2987
+ }
2988
+ };
2652
2989
  }
2653
- // capture the value of requestAnimationFrame as soon as possible, to avoid
2654
- // interactions with other code, such as test frameworks that override them
2655
- Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
2656
-
2657
2990
  /**
2658
- * Owl QWeb Expression Parser
2659
- *
2660
- * Owl needs in various contexts to be able to understand the structure of a
2661
- * string representing a javascript expression. The usual goal is to be able
2662
- * to rewrite some variables. For example, if a template has
2663
- *
2664
- * ```xml
2665
- * <t t-if="computeSomething({val: state.val})">...</t>
2666
- * ```
2667
- *
2668
- * this needs to be translated in something like this:
2669
- *
2670
- * ```js
2671
- * if (context["computeSomething"]({val: context["state"].val})) { ... }
2672
- * ```
2673
- *
2674
- * This file contains the implementation of an extremely naive tokenizer/parser
2675
- * and evaluator for javascript expressions. The supported grammar is basically
2676
- * only expressive enough to understand the shape of objects, of arrays, and
2677
- * various operators.
2991
+ * Validate the component props (or next props) against the (static) props
2992
+ * description. This is potentially an expensive operation: it may needs to
2993
+ * visit recursively the props and all the children to check if they are valid.
2994
+ * This is why it is only done in 'dev' mode.
2678
2995
  */
2679
- //------------------------------------------------------------------------------
2680
- // Misc types, constants and helpers
2681
- //------------------------------------------------------------------------------
2682
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
2683
- const WORD_REPLACEMENT = Object.assign(Object.create(null), {
2684
- and: "&&",
2685
- or: "||",
2686
- gt: ">",
2687
- gte: ">=",
2688
- lt: "<",
2689
- lte: "<=",
2690
- });
2691
- const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {
2692
- "{": "LEFT_BRACE",
2693
- "}": "RIGHT_BRACE",
2694
- "[": "LEFT_BRACKET",
2695
- "]": "RIGHT_BRACKET",
2696
- ":": "COLON",
2697
- ",": "COMMA",
2698
- "(": "LEFT_PAREN",
2699
- ")": "RIGHT_PAREN",
2700
- });
2701
- // note that the space after typeof is relevant. It makes sure that the formatted
2702
- // expression has a space after typeof
2703
- const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
2704
- let tokenizeString = function (expr) {
2705
- let s = expr[0];
2706
- let start = s;
2707
- if (s !== "'" && s !== '"' && s !== "`") {
2708
- return false;
2996
+ function validateProps(name, props, comp) {
2997
+ const ComponentClass = typeof name !== "string"
2998
+ ? name
2999
+ : comp.constructor.components[name];
3000
+ if (!ComponentClass) {
3001
+ // this is an error, wrong component. We silently return here instead so the
3002
+ // error is triggered by the usual path ('component' function)
3003
+ return;
2709
3004
  }
2710
- let i = 1;
2711
- let cur;
2712
- while (expr[i] && expr[i] !== start) {
2713
- cur = expr[i];
2714
- s += cur;
2715
- if (cur === "\\") {
2716
- i++;
2717
- cur = expr[i];
2718
- if (!cur) {
2719
- throw new Error("Invalid expression");
3005
+ const schema = ComponentClass.props;
3006
+ if (!schema) {
3007
+ if (comp.__owl__.app.warnIfNoStaticProps) {
3008
+ console.warn(`Component '${ComponentClass.name}' does not have a static props description`);
3009
+ }
3010
+ return;
3011
+ }
3012
+ const defaultProps = ComponentClass.defaultProps;
3013
+ if (defaultProps) {
3014
+ let isMandatory = (name) => Array.isArray(schema)
3015
+ ? schema.includes(name)
3016
+ : name in schema && !("*" in schema) && !isOptional(schema[name]);
3017
+ for (let p in defaultProps) {
3018
+ if (isMandatory(p)) {
3019
+ throw new OwlError(`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`);
2720
3020
  }
2721
- s += cur;
2722
3021
  }
2723
- i++;
2724
3022
  }
2725
- if (expr[i] !== start) {
2726
- throw new Error("Invalid expression");
3023
+ const errors = validateSchema(props, schema);
3024
+ if (errors.length) {
3025
+ throw new OwlError(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", "));
3026
+ }
3027
+ }
3028
+ const helpers = {
3029
+ withDefault,
3030
+ zero: Symbol("zero"),
3031
+ isBoundary,
3032
+ callSlot,
3033
+ capture,
3034
+ withKey,
3035
+ prepareList,
3036
+ setContextValue,
3037
+ multiRefSetter,
3038
+ shallowEqual,
3039
+ toNumber,
3040
+ validateProps,
3041
+ LazyValue,
3042
+ safeOutput,
3043
+ bind,
3044
+ createCatcher,
3045
+ markRaw,
3046
+ OwlError,
3047
+ };
3048
+
3049
+ const bdom = { text, createBlock, list, multi, html, toggler, comment };
3050
+ function parseXML$1(xml) {
3051
+ const parser = new DOMParser();
3052
+ const doc = parser.parseFromString(xml, "text/xml");
3053
+ if (doc.getElementsByTagName("parsererror").length) {
3054
+ let msg = "Invalid XML in template.";
3055
+ const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
3056
+ if (parsererrorText) {
3057
+ msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
3058
+ const re = /\d+/g;
3059
+ const firstMatch = re.exec(parsererrorText);
3060
+ if (firstMatch) {
3061
+ const lineNumber = Number(firstMatch[0]);
3062
+ const line = xml.split("\n")[lineNumber - 1];
3063
+ const secondMatch = re.exec(parsererrorText);
3064
+ if (line && secondMatch) {
3065
+ const columnIndex = Number(secondMatch[0]) - 1;
3066
+ if (line[columnIndex]) {
3067
+ msg +=
3068
+ `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
3069
+ `${line}\n${"-".repeat(columnIndex - 1)}^`;
3070
+ }
3071
+ }
3072
+ }
3073
+ }
3074
+ throw new OwlError(msg);
3075
+ }
3076
+ return doc;
3077
+ }
3078
+ class TemplateSet {
3079
+ constructor(config = {}) {
3080
+ this.rawTemplates = Object.create(globalTemplates);
3081
+ this.templates = {};
3082
+ this.Portal = Portal;
3083
+ this.dev = config.dev || false;
3084
+ this.translateFn = config.translateFn;
3085
+ this.translatableAttributes = config.translatableAttributes;
3086
+ if (config.templates) {
3087
+ this.addTemplates(config.templates);
3088
+ }
3089
+ }
3090
+ static registerTemplate(name, fn) {
3091
+ globalTemplates[name] = fn;
3092
+ }
3093
+ addTemplate(name, template) {
3094
+ if (name in this.rawTemplates) {
3095
+ const rawTemplate = this.rawTemplates[name];
3096
+ const currentAsString = typeof rawTemplate === "string"
3097
+ ? rawTemplate
3098
+ : rawTemplate instanceof Element
3099
+ ? rawTemplate.outerHTML
3100
+ : rawTemplate.toString();
3101
+ const newAsString = typeof template === "string" ? template : template.outerHTML;
3102
+ if (currentAsString === newAsString) {
3103
+ return;
3104
+ }
3105
+ throw new OwlError(`Template ${name} already defined with different content`);
3106
+ }
3107
+ this.rawTemplates[name] = template;
3108
+ }
3109
+ addTemplates(xml) {
3110
+ if (!xml) {
3111
+ // empty string
3112
+ return;
3113
+ }
3114
+ xml = xml instanceof Document ? xml : parseXML$1(xml);
3115
+ for (const template of xml.querySelectorAll("[t-name]")) {
3116
+ const name = template.getAttribute("t-name");
3117
+ this.addTemplate(name, template);
3118
+ }
3119
+ }
3120
+ getTemplate(name) {
3121
+ if (!(name in this.templates)) {
3122
+ const rawTemplate = this.rawTemplates[name];
3123
+ if (rawTemplate === undefined) {
3124
+ let extraInfo = "";
3125
+ try {
3126
+ const componentName = getCurrent().component.constructor.name;
3127
+ extraInfo = ` (for component "${componentName}")`;
3128
+ }
3129
+ catch { }
3130
+ throw new OwlError(`Missing template: "${name}"${extraInfo}`);
3131
+ }
3132
+ const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
3133
+ const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
3134
+ // first add a function to lazily get the template, in case there is a
3135
+ // recursive call to the template name
3136
+ const templates = this.templates;
3137
+ this.templates[name] = function (context, parent) {
3138
+ return templates[name].call(this, context, parent);
3139
+ };
3140
+ const template = templateFn(this, bdom, helpers);
3141
+ this.templates[name] = template;
3142
+ }
3143
+ return this.templates[name];
3144
+ }
3145
+ _compileTemplate(name, template) {
3146
+ throw new OwlError(`Unable to compile a template. Please use owl full build instead`);
3147
+ }
3148
+ callTemplate(owner, subTemplate, ctx, parent, key) {
3149
+ const template = this.getTemplate(subTemplate);
3150
+ return toggler(subTemplate, template.call(owner, ctx, parent, key));
3151
+ }
3152
+ }
3153
+ // -----------------------------------------------------------------------------
3154
+ // xml tag helper
3155
+ // -----------------------------------------------------------------------------
3156
+ const globalTemplates = {};
3157
+ function xml(...args) {
3158
+ const name = `__template__${xml.nextId++}`;
3159
+ const value = String.raw(...args);
3160
+ globalTemplates[name] = value;
3161
+ return name;
3162
+ }
3163
+ xml.nextId = 1;
3164
+ TemplateSet.registerTemplate("__portal__", portalTemplate);
3165
+
3166
+ /**
3167
+ * Owl QWeb Expression Parser
3168
+ *
3169
+ * Owl needs in various contexts to be able to understand the structure of a
3170
+ * string representing a javascript expression. The usual goal is to be able
3171
+ * to rewrite some variables. For example, if a template has
3172
+ *
3173
+ * ```xml
3174
+ * <t t-if="computeSomething({val: state.val})">...</t>
3175
+ * ```
3176
+ *
3177
+ * this needs to be translated in something like this:
3178
+ *
3179
+ * ```js
3180
+ * if (context["computeSomething"]({val: context["state"].val})) { ... }
3181
+ * ```
3182
+ *
3183
+ * This file contains the implementation of an extremely naive tokenizer/parser
3184
+ * and evaluator for javascript expressions. The supported grammar is basically
3185
+ * only expressive enough to understand the shape of objects, of arrays, and
3186
+ * various operators.
3187
+ */
3188
+ //------------------------------------------------------------------------------
3189
+ // Misc types, constants and helpers
3190
+ //------------------------------------------------------------------------------
3191
+ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
3192
+ const WORD_REPLACEMENT = Object.assign(Object.create(null), {
3193
+ and: "&&",
3194
+ or: "||",
3195
+ gt: ">",
3196
+ gte: ">=",
3197
+ lt: "<",
3198
+ lte: "<=",
3199
+ });
3200
+ const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {
3201
+ "{": "LEFT_BRACE",
3202
+ "}": "RIGHT_BRACE",
3203
+ "[": "LEFT_BRACKET",
3204
+ "]": "RIGHT_BRACKET",
3205
+ ":": "COLON",
3206
+ ",": "COMMA",
3207
+ "(": "LEFT_PAREN",
3208
+ ")": "RIGHT_PAREN",
3209
+ });
3210
+ // note that the space after typeof is relevant. It makes sure that the formatted
3211
+ // expression has a space after typeof. Currently we don't support delete and void
3212
+ const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~".split(",");
3213
+ let tokenizeString = function (expr) {
3214
+ let s = expr[0];
3215
+ let start = s;
3216
+ if (s !== "'" && s !== '"' && s !== "`") {
3217
+ return false;
3218
+ }
3219
+ let i = 1;
3220
+ let cur;
3221
+ while (expr[i] && expr[i] !== start) {
3222
+ cur = expr[i];
3223
+ s += cur;
3224
+ if (cur === "\\") {
3225
+ i++;
3226
+ cur = expr[i];
3227
+ if (!cur) {
3228
+ throw new OwlError("Invalid expression");
3229
+ }
3230
+ s += cur;
3231
+ }
3232
+ i++;
3233
+ }
3234
+ if (expr[i] !== start) {
3235
+ throw new OwlError("Invalid expression");
2727
3236
  }
2728
3237
  s += start;
2729
3238
  if (start === "`") {
@@ -2830,7 +3339,7 @@ function tokenize(expr) {
2830
3339
  error = e; // Silence all errors and throw a generic error below
2831
3340
  }
2832
3341
  if (current.length || error) {
2833
- throw new Error(`Tokenizer error: could not tokenize \`${expr}\``);
3342
+ throw new OwlError(`Tokenizer error: could not tokenize \`${expr}\``);
2834
3343
  }
2835
3344
  return result;
2836
3345
  }
@@ -2941,26 +3450,35 @@ function compileExprToArray(expr) {
2941
3450
  }
2942
3451
  return tokens;
2943
3452
  }
3453
+ // Leading spaces are trimmed during tokenization, so they need to be added back for some values
3454
+ const paddedValues = new Map([["in ", " in "]]);
2944
3455
  function compileExpr(expr) {
2945
3456
  return compileExprToArray(expr)
2946
- .map((t) => t.value)
3457
+ .map((t) => paddedValues.get(t.value) || t.value)
2947
3458
  .join("");
2948
3459
  }
2949
- const INTERP_REGEXP = /\{\{.*?\}\}/g;
2950
- const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
2951
- function interpolate(s) {
3460
+ const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
3461
+ function replaceDynamicParts(s, replacer) {
2952
3462
  let matches = s.match(INTERP_REGEXP);
2953
3463
  if (matches && matches[0].length === s.length) {
2954
- return `(${compileExpr(s.slice(2, -2))})`;
3464
+ return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
2955
3465
  }
2956
- let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
3466
+ let r = s.replace(INTERP_REGEXP, (s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}");
2957
3467
  return "`" + r + "`";
3468
+ }
3469
+ function interpolate(s) {
3470
+ return replaceDynamicParts(s, compileExpr);
2958
3471
  }
2959
3472
 
2960
3473
  // using a non-html document so that <inner/outer>HTML serializes as XML instead
2961
3474
  // of HTML (as we will parse it as xml later)
2962
3475
  const xmlDoc = document.implementation.createDocument(null, null, null);
2963
3476
  const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic"]);
3477
+ let nextDataIds = {};
3478
+ function generateId(prefix = "") {
3479
+ nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;
3480
+ return prefix + nextDataIds[prefix];
3481
+ }
2964
3482
  // -----------------------------------------------------------------------------
2965
3483
  // BlockDescription
2966
3484
  // -----------------------------------------------------------------------------
@@ -2979,12 +3497,8 @@ class BlockDescription {
2979
3497
  this.target = target;
2980
3498
  this.type = type;
2981
3499
  }
2982
- static generateId(prefix) {
2983
- this.nextDataIds[prefix] = (this.nextDataIds[prefix] || 0) + 1;
2984
- return prefix + this.nextDataIds[prefix];
2985
- }
2986
3500
  insertData(str, prefix = "d") {
2987
- const id = BlockDescription.generateId(prefix);
3501
+ const id = generateId(prefix);
2988
3502
  this.target.addLine(`let ${id} = ${str};`);
2989
3503
  return this.data.push(id) - 1;
2990
3504
  }
@@ -3022,7 +3536,6 @@ class BlockDescription {
3022
3536
  }
3023
3537
  }
3024
3538
  BlockDescription.nextBlockId = 1;
3025
- BlockDescription.nextDataIds = {};
3026
3539
  function createContext(parentCtx, params) {
3027
3540
  return Object.assign({
3028
3541
  block: null,
@@ -3084,19 +3597,26 @@ class CodeTarget {
3084
3597
  result.push(`}`);
3085
3598
  return result.join("\n ");
3086
3599
  }
3600
+ currentKey(ctx) {
3601
+ let key = this.loopLevel ? `key${this.loopLevel}` : "key";
3602
+ if (ctx.tKeyExpr) {
3603
+ key = `${ctx.tKeyExpr} + ${key}`;
3604
+ }
3605
+ return key;
3606
+ }
3087
3607
  }
3088
3608
  const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
3089
3609
  const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
3090
3610
  class CodeGenerator {
3091
3611
  constructor(ast, options) {
3092
3612
  this.blocks = [];
3093
- this.ids = {};
3094
3613
  this.nextBlockId = 1;
3095
3614
  this.isDebug = false;
3096
3615
  this.targets = [];
3097
3616
  this.target = new CodeTarget("template");
3098
3617
  this.translatableAttributes = TRANSLATABLE_ATTRS;
3099
3618
  this.staticDefs = [];
3619
+ this.slotNames = new Set();
3100
3620
  this.helpers = new Set();
3101
3621
  this.translateFn = options.translateFn || ((s) => s);
3102
3622
  if (options.translatableAttributes) {
@@ -3120,7 +3640,7 @@ class CodeGenerator {
3120
3640
  const ast = this.ast;
3121
3641
  this.isDebug = ast.type === 12 /* TDebug */;
3122
3642
  BlockDescription.nextBlockId = 1;
3123
- BlockDescription.nextDataIds = {};
3643
+ nextDataIds = {};
3124
3644
  this.compileAST(ast, {
3125
3645
  block: null,
3126
3646
  index: 0,
@@ -3130,9 +3650,7 @@ class CodeGenerator {
3130
3650
  tKeyExpr: null,
3131
3651
  });
3132
3652
  // define blocks and utility functions
3133
- let mainCode = [
3134
- ` let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;`,
3135
- ];
3653
+ let mainCode = [` let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];
3136
3654
  if (this.helpers.size) {
3137
3655
  mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
3138
3656
  }
@@ -3148,6 +3666,7 @@ class CodeGenerator {
3148
3666
  for (let block of this.blocks) {
3149
3667
  if (block.dom) {
3150
3668
  let xmlString = block.asXmlString();
3669
+ xmlString = xmlString.replace(/`/g, "\\`");
3151
3670
  if (block.dynamicTagName) {
3152
3671
  xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
3153
3672
  xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
@@ -3177,7 +3696,7 @@ class CodeGenerator {
3177
3696
  return code;
3178
3697
  }
3179
3698
  compileInNewTarget(prefix, ast, ctx, on) {
3180
- const name = this.generateId(prefix);
3699
+ const name = generateId(prefix);
3181
3700
  const initialTarget = this.target;
3182
3701
  const target = new CodeTarget(name, on);
3183
3702
  this.targets.push(target);
@@ -3192,12 +3711,8 @@ class CodeGenerator {
3192
3711
  define(varName, expr) {
3193
3712
  this.addLine(`const ${varName} = ${expr};`);
3194
3713
  }
3195
- generateId(prefix = "") {
3196
- this.ids[prefix] = (this.ids[prefix] || 0) + 1;
3197
- return prefix + this.ids[prefix];
3198
- }
3199
- insertAnchor(block) {
3200
- const tag = `block-child-${block.children.length}`;
3714
+ insertAnchor(block, index = block.children.length) {
3715
+ const tag = `block-child-${index}`;
3201
3716
  const anchor = xmlDoc.createElement(tag);
3202
3717
  block.insert(anchor);
3203
3718
  }
@@ -3218,18 +3733,14 @@ class CodeGenerator {
3218
3733
  }
3219
3734
  insertBlock(expression, block, ctx) {
3220
3735
  let blockExpr = block.generateExpr(expression);
3221
- const tKeyExpr = ctx.tKeyExpr;
3222
3736
  if (block.parentVar) {
3223
- let keyArg = `key${this.target.loopLevel}`;
3224
- if (tKeyExpr) {
3225
- keyArg = `${tKeyExpr} + ${keyArg}`;
3226
- }
3737
+ let key = this.target.currentKey(ctx);
3227
3738
  this.helpers.add("withKey");
3228
- this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${keyArg});`);
3739
+ this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${key});`);
3229
3740
  return;
3230
3741
  }
3231
- if (tKeyExpr) {
3232
- blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
3742
+ if (ctx.tKeyExpr) {
3743
+ blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
3233
3744
  }
3234
3745
  if (block.isRoot && !ctx.preventRoot) {
3235
3746
  if (this.target.on) {
@@ -3264,7 +3775,7 @@ class CodeGenerator {
3264
3775
  .map((tok) => {
3265
3776
  if (tok.varName && !tok.isLocal) {
3266
3777
  if (!mapping.has(tok.varName)) {
3267
- const varId = this.generateId("v");
3778
+ const varId = generateId("v");
3268
3779
  mapping.set(tok.varName, varId);
3269
3780
  this.define(varId, tok.value);
3270
3781
  }
@@ -3274,74 +3785,62 @@ class CodeGenerator {
3274
3785
  })
3275
3786
  .join("");
3276
3787
  }
3788
+ /**
3789
+ * @returns the newly created block name, if any
3790
+ */
3277
3791
  compileAST(ast, ctx) {
3278
3792
  switch (ast.type) {
3279
3793
  case 1 /* Comment */:
3280
- this.compileComment(ast, ctx);
3281
- break;
3794
+ return this.compileComment(ast, ctx);
3282
3795
  case 0 /* Text */:
3283
- this.compileText(ast, ctx);
3284
- break;
3796
+ return this.compileText(ast, ctx);
3285
3797
  case 2 /* DomNode */:
3286
- this.compileTDomNode(ast, ctx);
3287
- break;
3798
+ return this.compileTDomNode(ast, ctx);
3288
3799
  case 4 /* TEsc */:
3289
- this.compileTEsc(ast, ctx);
3290
- break;
3800
+ return this.compileTEsc(ast, ctx);
3291
3801
  case 8 /* TOut */:
3292
- this.compileTOut(ast, ctx);
3293
- break;
3802
+ return this.compileTOut(ast, ctx);
3294
3803
  case 5 /* TIf */:
3295
- this.compileTIf(ast, ctx);
3296
- break;
3804
+ return this.compileTIf(ast, ctx);
3297
3805
  case 9 /* TForEach */:
3298
- this.compileTForeach(ast, ctx);
3299
- break;
3806
+ return this.compileTForeach(ast, ctx);
3300
3807
  case 10 /* TKey */:
3301
- this.compileTKey(ast, ctx);
3302
- break;
3808
+ return this.compileTKey(ast, ctx);
3303
3809
  case 3 /* Multi */:
3304
- this.compileMulti(ast, ctx);
3305
- break;
3810
+ return this.compileMulti(ast, ctx);
3306
3811
  case 7 /* TCall */:
3307
- this.compileTCall(ast, ctx);
3308
- break;
3812
+ return this.compileTCall(ast, ctx);
3309
3813
  case 15 /* TCallBlock */:
3310
- this.compileTCallBlock(ast, ctx);
3311
- break;
3814
+ return this.compileTCallBlock(ast, ctx);
3312
3815
  case 6 /* TSet */:
3313
- this.compileTSet(ast, ctx);
3314
- break;
3816
+ return this.compileTSet(ast, ctx);
3315
3817
  case 11 /* TComponent */:
3316
- this.compileComponent(ast, ctx);
3317
- break;
3818
+ return this.compileComponent(ast, ctx);
3318
3819
  case 12 /* TDebug */:
3319
- this.compileDebug(ast, ctx);
3320
- break;
3820
+ return this.compileDebug(ast, ctx);
3321
3821
  case 13 /* TLog */:
3322
- this.compileLog(ast, ctx);
3323
- break;
3822
+ return this.compileLog(ast, ctx);
3324
3823
  case 14 /* TSlot */:
3325
- this.compileTSlot(ast, ctx);
3326
- break;
3824
+ return this.compileTSlot(ast, ctx);
3327
3825
  case 16 /* TTranslation */:
3328
- this.compileTTranslation(ast, ctx);
3329
- break;
3826
+ return this.compileTTranslation(ast, ctx);
3330
3827
  case 17 /* TPortal */:
3331
- this.compileTPortal(ast, ctx);
3828
+ return this.compileTPortal(ast, ctx);
3332
3829
  }
3333
3830
  }
3334
3831
  compileDebug(ast, ctx) {
3335
3832
  this.addLine(`debugger;`);
3336
3833
  if (ast.content) {
3337
- this.compileAST(ast.content, ctx);
3834
+ return this.compileAST(ast.content, ctx);
3338
3835
  }
3836
+ return null;
3339
3837
  }
3340
3838
  compileLog(ast, ctx) {
3341
3839
  this.addLine(`console.log(${compileExpr(ast.expr)});`);
3342
3840
  if (ast.content) {
3343
- this.compileAST(ast.content, ctx);
3841
+ return this.compileAST(ast.content, ctx);
3344
3842
  }
3843
+ return null;
3345
3844
  }
3346
3845
  compileComment(ast, ctx) {
3347
3846
  let { block, forceNewBlock } = ctx;
@@ -3357,6 +3856,7 @@ class CodeGenerator {
3357
3856
  const text = xmlDoc.createComment(ast.value);
3358
3857
  block.insert(text);
3359
3858
  }
3859
+ return block.varName;
3360
3860
  }
3361
3861
  compileText(ast, ctx) {
3362
3862
  let { block, forceNewBlock } = ctx;
@@ -3376,6 +3876,7 @@ class CodeGenerator {
3376
3876
  const createFn = ast.type === 0 /* Text */ ? xmlDoc.createTextNode : xmlDoc.createComment;
3377
3877
  block.insert(createFn.call(xmlDoc, value));
3378
3878
  }
3879
+ return block.varName;
3379
3880
  }
3380
3881
  generateHandlerCode(rawEvent, handler) {
3381
3882
  const modifiers = rawEvent
@@ -3383,7 +3884,7 @@ class CodeGenerator {
3383
3884
  .slice(1)
3384
3885
  .map((m) => {
3385
3886
  if (!MODS.has(m)) {
3386
- throw new Error(`Unknown event modifier: '${m}'`);
3887
+ throw new OwlError(`Unknown event modifier: '${m}'`);
3387
3888
  }
3388
3889
  return `"${m}"`;
3389
3890
  });
@@ -3404,7 +3905,7 @@ class CodeGenerator {
3404
3905
  block = this.createBlock(block, "block", ctx);
3405
3906
  this.blocks.push(block);
3406
3907
  if (ast.dynamicTag) {
3407
- const tagExpr = this.generateId("tag");
3908
+ const tagExpr = generateId("tag");
3408
3909
  this.define(tagExpr, compileExpr(ast.dynamicTag));
3409
3910
  block.dynamicTagName = tagExpr;
3410
3911
  }
@@ -3425,13 +3926,23 @@ class CodeGenerator {
3425
3926
  attrs["block-attribute-" + idx] = attrName;
3426
3927
  }
3427
3928
  else if (key.startsWith("t-att")) {
3929
+ attrName = key === "t-att" ? null : key.slice(6);
3428
3930
  expr = compileExpr(ast.attrs[key]);
3931
+ if (attrName && isProp(ast.tag, attrName)) {
3932
+ // we force a new string or new boolean to bypass the equality check in blockdom when patching same value
3933
+ if (attrName === "value") {
3934
+ // When the expression is falsy, fall back to an empty string
3935
+ expr = `new String((${expr}) || "")`;
3936
+ }
3937
+ else {
3938
+ expr = `new Boolean(${expr})`;
3939
+ }
3940
+ }
3429
3941
  const idx = block.insertData(expr, "attr");
3430
3942
  if (key === "t-att") {
3431
3943
  attrs[`block-attributes`] = String(idx);
3432
3944
  }
3433
3945
  else {
3434
- attrName = key.slice(6);
3435
3946
  attrs[`block-attribute-${idx}`] = attrName;
3436
3947
  }
3437
3948
  }
@@ -3459,8 +3970,8 @@ class CodeGenerator {
3459
3970
  this.target.hasRef = true;
3460
3971
  const isDynamic = INTERP_REGEXP.test(ast.ref);
3461
3972
  if (isDynamic) {
3462
- const str = ast.ref.replace(INTERP_REGEXP, (expr) => "${" + this.captureExpression(expr.slice(2, -2), true) + "}");
3463
- const idx = block.insertData(`(el) => refs[\`${str}\`] = el`, "ref");
3973
+ const str = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
3974
+ const idx = block.insertData(`(el) => refs[${str}] = el`, "ref");
3464
3975
  attrs["block-ref"] = String(idx);
3465
3976
  }
3466
3977
  else {
@@ -3474,7 +3985,7 @@ class CodeGenerator {
3474
3985
  info[1] = `multiRefSetter(refs, \`${name}\`)`;
3475
3986
  }
3476
3987
  else {
3477
- let id = this.generateId("ref");
3988
+ let id = generateId("ref");
3478
3989
  this.target.refInfo[name] = [id, `(el) => refs[\`${name}\`] = el`];
3479
3990
  const index = block.data.push(id) - 1;
3480
3991
  attrs["block-ref"] = String(index);
@@ -3486,10 +3997,10 @@ class CodeGenerator {
3486
3997
  if (ast.model) {
3487
3998
  const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
3488
3999
  const baseExpression = compileExpr(baseExpr);
3489
- const bExprId = this.generateId("bExpr");
4000
+ const bExprId = generateId("bExpr");
3490
4001
  this.define(bExprId, baseExpression);
3491
4002
  const expression = compileExpr(expr);
3492
- const exprId = this.generateId("expr");
4003
+ const exprId = generateId("expr");
3493
4004
  this.define(exprId, expression);
3494
4005
  const fullExpression = `${bExprId}[${exprId}]`;
3495
4006
  let idx;
@@ -3498,7 +4009,7 @@ class CodeGenerator {
3498
4009
  attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
3499
4010
  }
3500
4011
  else if (hasDynamicChildren) {
3501
- const bValueId = this.generateId("bValue");
4012
+ const bValueId = generateId("bValue");
3502
4013
  tModelSelectedExpr = `${bValueId}`;
3503
4014
  this.define(tModelSelectedExpr, fullExpression);
3504
4015
  }
@@ -3558,6 +4069,7 @@ class CodeGenerator {
3558
4069
  this.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx);
3559
4070
  }
3560
4071
  }
4072
+ return block.varName;
3561
4073
  }
3562
4074
  compileTEsc(ast, ctx) {
3563
4075
  let { block, forceNewBlock } = ctx;
@@ -3582,6 +4094,7 @@ class CodeGenerator {
3582
4094
  const text = xmlDoc.createElement(`block-text-${idx}`);
3583
4095
  block.insert(text);
3584
4096
  }
4097
+ return block.varName;
3585
4098
  }
3586
4099
  compileTOut(ast, ctx) {
3587
4100
  let { block } = ctx;
@@ -3589,20 +4102,38 @@ class CodeGenerator {
3589
4102
  this.insertAnchor(block);
3590
4103
  }
3591
4104
  block = this.createBlock(block, "html", ctx);
3592
- this.helpers.add(ast.expr === "0" ? "zero" : "safeOutput");
3593
- let expr = ast.expr === "0" ? "ctx[zero]" : `safeOutput(${compileExpr(ast.expr)})`;
3594
- if (ast.body) {
3595
- const nextId = BlockDescription.nextBlockId;
4105
+ let blockStr;
4106
+ if (ast.expr === "0") {
4107
+ this.helpers.add("zero");
4108
+ blockStr = `ctx[zero]`;
4109
+ }
4110
+ else if (ast.body) {
4111
+ let bodyValue = null;
4112
+ bodyValue = BlockDescription.nextBlockId;
3596
4113
  const subCtx = createContext(ctx);
3597
4114
  this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);
3598
- this.helpers.add("withDefault");
3599
- expr = `withDefault(${expr}, b${nextId})`;
4115
+ this.helpers.add("safeOutput");
4116
+ blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;
3600
4117
  }
3601
- this.insertBlock(`${expr}`, block, ctx);
4118
+ else {
4119
+ this.helpers.add("safeOutput");
4120
+ blockStr = `safeOutput(${compileExpr(ast.expr)})`;
4121
+ }
4122
+ this.insertBlock(blockStr, block, ctx);
4123
+ return block.varName;
4124
+ }
4125
+ compileTIfBranch(content, block, ctx) {
4126
+ this.target.indentLevel++;
4127
+ let childN = block.children.length;
4128
+ this.compileAST(content, createContext(ctx, { block, index: ctx.index }));
4129
+ if (block.children.length > childN) {
4130
+ // we have some content => need to insert an anchor at correct index
4131
+ this.insertAnchor(block, childN);
4132
+ }
4133
+ this.target.indentLevel--;
3602
4134
  }
3603
4135
  compileTIf(ast, ctx, nextNode) {
3604
- let { block, forceNewBlock, index } = ctx;
3605
- let currentIndex = index;
4136
+ let { block, forceNewBlock } = ctx;
3606
4137
  const codeIdx = this.target.code.length;
3607
4138
  const isNewBlock = !block || (block.type !== "multi" && forceNewBlock);
3608
4139
  if (block) {
@@ -3612,28 +4143,16 @@ class CodeGenerator {
3612
4143
  block = this.createBlock(block, "multi", ctx);
3613
4144
  }
3614
4145
  this.addLine(`if (${compileExpr(ast.condition)}) {`);
3615
- this.target.indentLevel++;
3616
- this.insertAnchor(block);
3617
- const subCtx = createContext(ctx, { block, index: currentIndex });
3618
- this.compileAST(ast.content, subCtx);
3619
- this.target.indentLevel--;
4146
+ this.compileTIfBranch(ast.content, block, ctx);
3620
4147
  if (ast.tElif) {
3621
4148
  for (let clause of ast.tElif) {
3622
4149
  this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
3623
- this.target.indentLevel++;
3624
- this.insertAnchor(block);
3625
- const subCtx = createContext(ctx, { block, index: currentIndex });
3626
- this.compileAST(clause.content, subCtx);
3627
- this.target.indentLevel--;
4150
+ this.compileTIfBranch(clause.content, block, ctx);
3628
4151
  }
3629
4152
  }
3630
4153
  if (ast.tElse) {
3631
4154
  this.addLine(`} else {`);
3632
- this.target.indentLevel++;
3633
- this.insertAnchor(block);
3634
- const subCtx = createContext(ctx, { block, index: currentIndex });
3635
- this.compileAST(ast.tElse, subCtx);
3636
- this.target.indentLevel--;
4155
+ this.compileTIfBranch(ast.tElse, block, ctx);
3637
4156
  }
3638
4157
  this.addLine("}");
3639
4158
  if (isNewBlock) {
@@ -3656,6 +4175,7 @@ class CodeGenerator {
3656
4175
  const args = block.children.map((c) => c.varName).join(", ");
3657
4176
  this.insertBlock(`multi([${args}])`, block, ctx);
3658
4177
  }
4178
+ return block.varName;
3659
4179
  }
3660
4180
  compileTForeach(ast, ctx) {
3661
4181
  let { block } = ctx;
@@ -3694,13 +4214,14 @@ class CodeGenerator {
3694
4214
  this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
3695
4215
  if (this.dev) {
3696
4216
  // Throw error on duplicate keys in dev mode
3697
- this.addLine(`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new Error(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`);
4217
+ this.helpers.add("OwlError");
4218
+ this.addLine(`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new OwlError(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`);
3698
4219
  this.addLine(`keys${block.id}.add(key${this.target.loopLevel});`);
3699
4220
  }
3700
4221
  let id;
3701
4222
  if (ast.memo) {
3702
4223
  this.target.hasCache = true;
3703
- id = this.generateId();
4224
+ id = generateId();
3704
4225
  this.define(`memo${id}`, compileExpr(ast.memo));
3705
4226
  this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
3706
4227
  this.addLine(`if (vnode${id}) {`);
@@ -3727,16 +4248,17 @@ class CodeGenerator {
3727
4248
  this.addLine(`ctx = ctx.__proto__;`);
3728
4249
  }
3729
4250
  this.insertBlock("l", block, ctx);
4251
+ return block.varName;
3730
4252
  }
3731
4253
  compileTKey(ast, ctx) {
3732
- const tKeyExpr = this.generateId("tKey_");
4254
+ const tKeyExpr = generateId("tKey_");
3733
4255
  this.define(tKeyExpr, compileExpr(ast.expr));
3734
4256
  ctx = createContext(ctx, {
3735
4257
  tKeyExpr,
3736
4258
  block: ctx.block,
3737
4259
  index: ctx.index,
3738
4260
  });
3739
- this.compileAST(ast.content, ctx);
4261
+ return this.compileAST(ast.content, ctx);
3740
4262
  }
3741
4263
  compileMulti(ast, ctx) {
3742
4264
  let { block, forceNewBlock } = ctx;
@@ -3744,11 +4266,13 @@ class CodeGenerator {
3744
4266
  let codeIdx = this.target.code.length;
3745
4267
  if (isNewBlock) {
3746
4268
  const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length;
4269
+ let result = null;
3747
4270
  if (n <= 1) {
3748
4271
  for (let child of ast.content) {
3749
- this.compileAST(child, ctx);
4272
+ const blockName = this.compileAST(child, ctx);
4273
+ result = result || blockName;
3750
4274
  }
3751
- return;
4275
+ return result;
3752
4276
  }
3753
4277
  block = this.createBlock(block, "multi", ctx);
3754
4278
  }
@@ -3788,19 +4312,24 @@ class CodeGenerator {
3788
4312
  const args = block.children.map((c) => c.varName).join(", ");
3789
4313
  this.insertBlock(`multi([${args}])`, block, ctx);
3790
4314
  }
4315
+ return block.varName;
3791
4316
  }
3792
4317
  compileTCall(ast, ctx) {
3793
4318
  let { block, forceNewBlock } = ctx;
4319
+ let ctxVar = ctx.ctxVar || "ctx";
4320
+ if (ast.context) {
4321
+ ctxVar = generateId("ctx");
4322
+ this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
4323
+ }
3794
4324
  if (ast.body) {
3795
- this.addLine(`ctx = Object.create(ctx);`);
3796
- this.addLine(`ctx[isBoundary] = 1;`);
4325
+ this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
4326
+ this.addLine(`${ctxVar}[isBoundary] = 1;`);
3797
4327
  this.helpers.add("isBoundary");
3798
- const nextId = BlockDescription.nextBlockId;
3799
- const subCtx = createContext(ctx, { preventRoot: true });
3800
- this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);
3801
- if (nextId !== BlockDescription.nextBlockId) {
4328
+ const subCtx = createContext(ctx, { preventRoot: true, ctxVar });
4329
+ const bl = this.compileMulti({ type: 3 /* Multi */, content: ast.body }, subCtx);
4330
+ if (bl) {
3802
4331
  this.helpers.add("zero");
3803
- this.addLine(`ctx[zero] = b${nextId};`);
4332
+ this.addLine(`${ctxVar}[zero] = ${bl};`);
3804
4333
  }
3805
4334
  }
3806
4335
  const isDynamic = INTERP_REGEXP.test(ast.name);
@@ -3812,28 +4341,30 @@ class CodeGenerator {
3812
4341
  }
3813
4342
  const key = `key + \`${this.generateComponentKey()}\``;
3814
4343
  if (isDynamic) {
3815
- const templateVar = this.generateId("template");
4344
+ const templateVar = generateId("template");
4345
+ if (!this.staticDefs.find((d) => d.id === "call")) {
4346
+ this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
4347
+ }
3816
4348
  this.define(templateVar, subTemplate);
3817
4349
  block = this.createBlock(block, "multi", ctx);
3818
- this.helpers.add("call");
3819
- this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block, {
4350
+ this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block, {
3820
4351
  ...ctx,
3821
4352
  forceNewBlock: !block,
3822
4353
  });
3823
4354
  }
3824
4355
  else {
3825
- const id = this.generateId(`callTemplate_`);
3826
- this.helpers.add("getTemplate");
3827
- this.staticDefs.push({ id, expr: `getTemplate(${subTemplate})` });
4356
+ const id = generateId(`callTemplate_`);
4357
+ this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
3828
4358
  block = this.createBlock(block, "multi", ctx);
3829
- this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block, {
4359
+ this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block, {
3830
4360
  ...ctx,
3831
4361
  forceNewBlock: !block,
3832
4362
  });
3833
4363
  }
3834
4364
  if (ast.body && !ctx.isLast) {
3835
- this.addLine(`ctx = ctx.__proto__;`);
4365
+ this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`);
3836
4366
  }
4367
+ return block.varName;
3837
4368
  }
3838
4369
  compileTCallBlock(ast, ctx) {
3839
4370
  let { block, forceNewBlock } = ctx;
@@ -3844,6 +4375,7 @@ class CodeGenerator {
3844
4375
  }
3845
4376
  block = this.createBlock(block, "multi", ctx);
3846
4377
  this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block });
4378
+ return block.varName;
3847
4379
  }
3848
4380
  compileTSet(ast, ctx) {
3849
4381
  this.target.shouldProtectScope = true;
@@ -3853,7 +4385,8 @@ class CodeGenerator {
3853
4385
  this.helpers.add("LazyValue");
3854
4386
  const bodyAst = { type: 3 /* Multi */, content: ast.body };
3855
4387
  const name = this.compileInNewTarget("value", bodyAst, ctx);
3856
- let value = `new LazyValue(${name}, ctx, node)`;
4388
+ let key = this.target.currentKey(ctx);
4389
+ let value = `new LazyValue(${name}, ctx, this, node, ${key})`;
3857
4390
  value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
3858
4391
  this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
3859
4392
  }
@@ -3871,11 +4404,12 @@ class CodeGenerator {
3871
4404
  value = expr;
3872
4405
  }
3873
4406
  this.helpers.add("setContextValue");
3874
- this.addLine(`setContextValue(ctx, "${ast.name}", ${value});`);
4407
+ this.addLine(`setContextValue(${ctx.ctxVar || "ctx"}, "${ast.name}", ${value});`);
3875
4408
  }
4409
+ return null;
3876
4410
  }
3877
4411
  generateComponentKey() {
3878
- const parts = [this.generateId("__")];
4412
+ const parts = [generateId("__")];
3879
4413
  for (let i = 0; i < this.target.loopLevel; i++) {
3880
4414
  parts.push(`\${key${i + 1}}`);
3881
4415
  }
@@ -3902,48 +4436,50 @@ class CodeGenerator {
3902
4436
  value = `bind(ctx, ${value || undefined})`;
3903
4437
  }
3904
4438
  else {
3905
- throw new Error("Invalid prop suffix");
4439
+ throw new OwlError("Invalid prop suffix");
3906
4440
  }
3907
4441
  }
3908
4442
  name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
3909
4443
  return `${name}: ${value || undefined}`;
3910
4444
  }
3911
4445
  formatPropObject(obj) {
3912
- const params = [];
3913
- for (const [n, v] of Object.entries(obj)) {
3914
- params.push(this.formatProp(n, v));
4446
+ return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
4447
+ }
4448
+ getPropString(props, dynProps) {
4449
+ let propString = `{${props.join(",")}}`;
4450
+ if (dynProps) {
4451
+ propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? ", " + propString : ""})`;
3915
4452
  }
3916
- return params.join(", ");
4453
+ return propString;
3917
4454
  }
3918
4455
  compileComponent(ast, ctx) {
3919
4456
  let { block } = ctx;
3920
4457
  // props
3921
4458
  const hasSlotsProp = "slots" in (ast.props || {});
3922
- const props = [];
3923
- const propExpr = this.formatPropObject(ast.props || {});
3924
- if (propExpr) {
3925
- props.push(propExpr);
3926
- }
4459
+ const props = ast.props ? this.formatPropObject(ast.props) : [];
3927
4460
  // slots
3928
4461
  let slotDef = "";
3929
4462
  if (ast.slots) {
3930
4463
  let ctxStr = "ctx";
3931
4464
  if (this.target.loopLevel || !this.hasSafeContext) {
3932
- ctxStr = this.generateId("ctx");
4465
+ ctxStr = generateId("ctx");
3933
4466
  this.helpers.add("capture");
3934
4467
  this.define(ctxStr, `capture(ctx)`);
3935
4468
  }
3936
4469
  let slotStr = [];
3937
4470
  for (let slotName in ast.slots) {
3938
4471
  const slotAst = ast.slots[slotName];
3939
- const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
3940
- const params = [`__render: ${name}, __ctx: ${ctxStr}`];
4472
+ const params = [];
4473
+ if (slotAst.content) {
4474
+ const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
4475
+ params.push(`__render: ${name}, __ctx: ${ctxStr}`);
4476
+ }
3941
4477
  const scope = ast.slots[slotName].scope;
3942
4478
  if (scope) {
3943
4479
  params.push(`__scope: "${scope}"`);
3944
4480
  }
3945
4481
  if (ast.slots[slotName].attrs) {
3946
- params.push(this.formatPropObject(ast.slots[slotName].attrs));
4482
+ params.push(...this.formatPropObject(ast.slots[slotName].attrs));
3947
4483
  }
3948
4484
  const slotInfo = `{${params.join(", ")}}`;
3949
4485
  slotStr.push(`'${slotName}': ${slotInfo}`);
@@ -3954,14 +4490,10 @@ class CodeGenerator {
3954
4490
  this.helpers.add("markRaw");
3955
4491
  props.push(`slots: markRaw(${slotDef})`);
3956
4492
  }
3957
- const propStr = `{${props.join(",")}}`;
3958
- let propString = propStr;
3959
- if (ast.dynamicProps) {
3960
- propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}${props.length ? ", " + propStr : ""})`;
3961
- }
4493
+ let propString = this.getPropString(props, ast.dynamicProps);
3962
4494
  let propVar;
3963
4495
  if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
3964
- propVar = this.generateId("props");
4496
+ propVar = generateId("props");
3965
4497
  this.define(propVar, propString);
3966
4498
  propString = propVar;
3967
4499
  }
@@ -3973,14 +4505,14 @@ class CodeGenerator {
3973
4505
  const key = this.generateComponentKey();
3974
4506
  let expr;
3975
4507
  if (ast.isDynamic) {
3976
- expr = this.generateId("Comp");
4508
+ expr = generateId("Comp");
3977
4509
  this.define(expr, compileExpr(ast.name));
3978
4510
  }
3979
4511
  else {
3980
4512
  expr = `\`${ast.name}\``;
3981
4513
  }
3982
4514
  if (this.dev) {
3983
- this.addLine(`helpers.validateProps(${expr}, ${propVar}, ctx);`);
4515
+ this.addLine(`helpers.validateProps(${expr}, ${propVar}, this);`);
3984
4516
  }
3985
4517
  if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {
3986
4518
  // todo: check the forcenewblock condition
@@ -3990,8 +4522,12 @@ class CodeGenerator {
3990
4522
  if (ctx.tKeyExpr) {
3991
4523
  keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
3992
4524
  }
3993
- const blockArgs = `${expr}, ${propString}, ${keyArg}, node, ctx`;
3994
- let blockExpr = `component(${blockArgs})`;
4525
+ let id = generateId("comp");
4526
+ this.staticDefs.push({
4527
+ id,
4528
+ expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, ${!ast.props && !ast.dynamicProps})`,
4529
+ });
4530
+ let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;
3995
4531
  if (ast.isDynamic) {
3996
4532
  blockExpr = `toggler(${expr}, ${blockExpr})`;
3997
4533
  }
@@ -4001,14 +4537,15 @@ class CodeGenerator {
4001
4537
  }
4002
4538
  block = this.createBlock(block, "multi", ctx);
4003
4539
  this.insertBlock(blockExpr, block, ctx);
4540
+ return block.varName;
4004
4541
  }
4005
4542
  wrapWithEventCatcher(expr, on) {
4006
4543
  this.helpers.add("createCatcher");
4007
- let name = this.generateId("catcher");
4544
+ let name = generateId("catcher");
4008
4545
  let spec = {};
4009
4546
  let handlers = [];
4010
4547
  for (let ev in on) {
4011
- let handlerId = this.generateId("hdlr");
4548
+ let handlerId = generateId("hdlr");
4012
4549
  let idx = handlers.push(handlerId) - 1;
4013
4550
  spec[ev] = idx;
4014
4551
  const handler = this.generateHandlerCode(ev, on[ev]);
@@ -4023,26 +4560,39 @@ class CodeGenerator {
4023
4560
  let blockString;
4024
4561
  let slotName;
4025
4562
  let dynamic = false;
4563
+ let isMultiple = false;
4026
4564
  if (ast.name.match(INTERP_REGEXP)) {
4027
4565
  dynamic = true;
4566
+ isMultiple = true;
4028
4567
  slotName = interpolate(ast.name);
4029
4568
  }
4030
4569
  else {
4031
4570
  slotName = "'" + ast.name + "'";
4571
+ isMultiple = isMultiple || this.slotNames.has(ast.name);
4572
+ this.slotNames.add(ast.name);
4032
4573
  }
4033
- const scope = ast.attrs ? `{${this.formatPropObject(ast.attrs)}}` : null;
4574
+ const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
4575
+ if (ast.attrs) {
4576
+ delete ast.attrs["t-props"];
4577
+ }
4578
+ let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
4579
+ if (isMultiple) {
4580
+ key = `${key} + \`${this.generateComponentKey()}\``;
4581
+ }
4582
+ const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
4583
+ const scope = this.getPropString(props, dynProps);
4034
4584
  if (ast.defaultContent) {
4035
4585
  const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
4036
- blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
4586
+ blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name})`;
4037
4587
  }
4038
4588
  else {
4039
4589
  if (dynamic) {
4040
- let name = this.generateId("slot");
4590
+ let name = generateId("slot");
4041
4591
  this.define(name, slotName);
4042
- blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}), ${dynamic}, ${scope})`;
4592
+ blockString = `toggler(${name}, callSlot(ctx, node, ${key}, ${name}, ${dynamic}, ${scope}))`;
4043
4593
  }
4044
4594
  else {
4045
- blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
4595
+ blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope})`;
4046
4596
  }
4047
4597
  }
4048
4598
  // event handling
@@ -4054,42 +4604,50 @@ class CodeGenerator {
4054
4604
  }
4055
4605
  block = this.createBlock(block, "multi", ctx);
4056
4606
  this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
4607
+ return block.varName;
4057
4608
  }
4058
4609
  compileTTranslation(ast, ctx) {
4059
4610
  if (ast.content) {
4060
- this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
4611
+ return this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
4061
4612
  }
4613
+ return null;
4062
4614
  }
4063
4615
  compileTPortal(ast, ctx) {
4064
- this.helpers.add("Portal");
4616
+ if (!this.staticDefs.find((d) => d.id === "Portal")) {
4617
+ this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
4618
+ }
4065
4619
  let { block } = ctx;
4066
4620
  const name = this.compileInNewTarget("slot", ast.content, ctx);
4067
4621
  const key = this.generateComponentKey();
4068
4622
  let ctxStr = "ctx";
4069
4623
  if (this.target.loopLevel || !this.hasSafeContext) {
4070
- ctxStr = this.generateId("ctx");
4624
+ ctxStr = generateId("ctx");
4071
4625
  this.helpers.add("capture");
4072
- this.define(ctxStr, `capture(ctx);`);
4626
+ this.define(ctxStr, `capture(ctx)`);
4073
4627
  }
4074
- const blockString = `component(Portal, {target: ${ast.target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
4628
+ let id = generateId("comp");
4629
+ this.staticDefs.push({
4630
+ id,
4631
+ expr: `app.createComponent(null, false, true, false, false)`,
4632
+ });
4633
+ const target = compileExpr(ast.target);
4634
+ const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
4075
4635
  if (block) {
4076
4636
  this.insertAnchor(block);
4077
4637
  }
4078
4638
  block = this.createBlock(block, "multi", ctx);
4079
4639
  this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
4640
+ return block.varName;
4080
4641
  }
4081
4642
  }
4082
4643
 
4083
- // -----------------------------------------------------------------------------
4084
- // AST Type definition
4085
- // -----------------------------------------------------------------------------
4086
4644
  // -----------------------------------------------------------------------------
4087
4645
  // Parser
4088
4646
  // -----------------------------------------------------------------------------
4089
4647
  const cache = new WeakMap();
4090
4648
  function parse(xml) {
4091
4649
  if (typeof xml === "string") {
4092
- const elem = parseXML$1(`<t>${xml}</t>`).firstChild;
4650
+ const elem = parseXML(`<t>${xml}</t>`).firstChild;
4093
4651
  return _parse(elem);
4094
4652
  }
4095
4653
  let ast = cache.get(xml);
@@ -4191,7 +4749,7 @@ function parseDOMNode(node, ctx) {
4191
4749
  return null;
4192
4750
  }
4193
4751
  if (tagName.startsWith("block-")) {
4194
- throw new Error(`Invalid tag name: '${tagName}'`);
4752
+ throw new OwlError(`Invalid tag name: '${tagName}'`);
4195
4753
  }
4196
4754
  ctx = Object.assign({}, ctx);
4197
4755
  if (tagName === "pre") {
@@ -4210,14 +4768,14 @@ function parseDOMNode(node, ctx) {
4210
4768
  const value = node.getAttribute(attr);
4211
4769
  if (attr.startsWith("t-on")) {
4212
4770
  if (attr === "t-on") {
4213
- throw new Error("Missing event name with t-on directive");
4771
+ throw new OwlError("Missing event name with t-on directive");
4214
4772
  }
4215
4773
  on = on || {};
4216
4774
  on[attr.slice(5)] = value;
4217
4775
  }
4218
4776
  else if (attr.startsWith("t-model")) {
4219
4777
  if (!["input", "select", "textarea"].includes(tagName)) {
4220
- throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
4778
+ throw new OwlError("The t-model directive only works with <input>, <textarea> and <select>");
4221
4779
  }
4222
4780
  let baseExpr, expr;
4223
4781
  if (hasDotAtTheEnd.test(value)) {
@@ -4231,7 +4789,7 @@ function parseDOMNode(node, ctx) {
4231
4789
  expr = value.slice(index + 1, -1);
4232
4790
  }
4233
4791
  else {
4234
- throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
4792
+ throw new OwlError(`Invalid t-model expression: "${value}" (it should be assignable)`);
4235
4793
  }
4236
4794
  const typeAttr = node.getAttribute("type");
4237
4795
  const isInput = tagName === "input";
@@ -4261,11 +4819,11 @@ function parseDOMNode(node, ctx) {
4261
4819
  }
4262
4820
  }
4263
4821
  else if (attr.startsWith("block-")) {
4264
- throw new Error(`Invalid attribute: '${attr}'`);
4822
+ throw new OwlError(`Invalid attribute: '${attr}'`);
4265
4823
  }
4266
4824
  else if (attr !== "t-name") {
4267
4825
  if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
4268
- throw new Error(`Unknown QWeb directive: '${attr}'`);
4826
+ throw new OwlError(`Unknown QWeb directive: '${attr}'`);
4269
4827
  }
4270
4828
  const tModel = ctx.tModelInfo;
4271
4829
  if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
@@ -4316,7 +4874,7 @@ function parseTEscNode(node, ctx) {
4316
4874
  };
4317
4875
  }
4318
4876
  if (ast.type === 11 /* TComponent */) {
4319
- throw new Error("t-esc is not supported on Component nodes");
4877
+ throw new OwlError("t-esc is not supported on Component nodes");
4320
4878
  }
4321
4879
  return tesc;
4322
4880
  }
@@ -4364,7 +4922,7 @@ function parseTForEach(node, ctx) {
4364
4922
  node.removeAttribute("t-as");
4365
4923
  const key = node.getAttribute("t-key");
4366
4924
  if (!key) {
4367
- throw new Error(`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`);
4925
+ throw new OwlError(`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`);
4368
4926
  }
4369
4927
  node.removeAttribute("t-key");
4370
4928
  const memo = node.getAttribute("t-memo") || "";
@@ -4411,10 +4969,12 @@ function parseTCall(node, ctx) {
4411
4969
  return null;
4412
4970
  }
4413
4971
  const subTemplate = node.getAttribute("t-call");
4972
+ const context = node.getAttribute("t-call-context");
4414
4973
  node.removeAttribute("t-call");
4974
+ node.removeAttribute("t-call-context");
4415
4975
  if (node.tagName !== "t") {
4416
4976
  const ast = parseNode(node, ctx);
4417
- const tcall = { type: 7 /* TCall */, name: subTemplate, body: null };
4977
+ const tcall = { type: 7 /* TCall */, name: subTemplate, body: null, context };
4418
4978
  if (ast && ast.type === 2 /* DomNode */) {
4419
4979
  ast.content = [tcall];
4420
4980
  return ast;
@@ -4431,6 +4991,7 @@ function parseTCall(node, ctx) {
4431
4991
  type: 7 /* TCall */,
4432
4992
  name: subTemplate,
4433
4993
  body: body.length ? body : null,
4994
+ context,
4434
4995
  };
4435
4996
  }
4436
4997
  // -----------------------------------------------------------------------------
@@ -4519,662 +5080,274 @@ const directiveErrorMap = new Map([
4519
5080
  function parseComponent(node, ctx) {
4520
5081
  let name = node.tagName;
4521
5082
  const firstLetter = name[0];
4522
- let isDynamic = node.hasAttribute("t-component");
4523
- if (isDynamic && name !== "t") {
4524
- throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
4525
- }
4526
- if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
4527
- return null;
4528
- }
4529
- if (isDynamic) {
4530
- name = node.getAttribute("t-component");
4531
- node.removeAttribute("t-component");
4532
- }
4533
- const dynamicProps = node.getAttribute("t-props");
4534
- node.removeAttribute("t-props");
4535
- const defaultSlotScope = node.getAttribute("t-slot-scope");
4536
- node.removeAttribute("t-slot-scope");
4537
- let on = null;
4538
- let props = null;
4539
- for (let name of node.getAttributeNames()) {
4540
- const value = node.getAttribute(name);
4541
- if (name.startsWith("t-")) {
4542
- if (name.startsWith("t-on-")) {
4543
- on = on || {};
4544
- on[name.slice(5)] = value;
4545
- }
4546
- else {
4547
- const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
4548
- throw new Error(message || `unsupported directive on Component: ${name}`);
4549
- }
4550
- }
4551
- else {
4552
- props = props || {};
4553
- props[name] = value;
4554
- }
4555
- }
4556
- let slots = null;
4557
- if (node.hasChildNodes()) {
4558
- const clone = node.cloneNode(true);
4559
- // named slots
4560
- const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
4561
- for (let slotNode of slotNodes) {
4562
- if (slotNode.tagName !== "t") {
4563
- throw new Error(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);
4564
- }
4565
- const name = slotNode.getAttribute("t-set-slot");
4566
- // check if this is defined in a sub component (in which case it should
4567
- // be ignored)
4568
- let el = slotNode.parentElement;
4569
- let isInSubComponent = false;
4570
- while (el !== clone) {
4571
- if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
4572
- isInSubComponent = true;
4573
- break;
4574
- }
4575
- el = el.parentElement;
4576
- }
4577
- if (isInSubComponent) {
4578
- continue;
4579
- }
4580
- slotNode.removeAttribute("t-set-slot");
4581
- slotNode.remove();
4582
- const slotAst = parseNode(slotNode, ctx);
4583
- if (slotAst) {
4584
- let on = null;
4585
- let attrs = null;
4586
- let scope = null;
4587
- for (let attributeName of slotNode.getAttributeNames()) {
4588
- const value = slotNode.getAttribute(attributeName);
4589
- if (attributeName === "t-slot-scope") {
4590
- scope = value;
4591
- continue;
4592
- }
4593
- else if (attributeName.startsWith("t-on-")) {
4594
- on = on || {};
4595
- on[attributeName.slice(5)] = value;
4596
- }
4597
- else {
4598
- attrs = attrs || {};
4599
- attrs[attributeName] = value;
4600
- }
4601
- }
4602
- slots = slots || {};
4603
- slots[name] = { content: slotAst, on, attrs, scope };
4604
- }
4605
- }
4606
- // default slot
4607
- const defaultContent = parseChildNodes(clone, ctx);
4608
- if (defaultContent) {
4609
- slots = slots || {};
4610
- slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
4611
- }
4612
- }
4613
- return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
4614
- }
4615
- // -----------------------------------------------------------------------------
4616
- // Slots
4617
- // -----------------------------------------------------------------------------
4618
- function parseTSlot(node, ctx) {
4619
- if (!node.hasAttribute("t-slot")) {
4620
- return null;
4621
- }
4622
- const name = node.getAttribute("t-slot");
4623
- node.removeAttribute("t-slot");
4624
- let attrs = null;
4625
- let on = null;
4626
- for (let attributeName of node.getAttributeNames()) {
4627
- const value = node.getAttribute(attributeName);
4628
- if (attributeName.startsWith("t-on-")) {
4629
- on = on || {};
4630
- on[attributeName.slice(5)] = value;
4631
- }
4632
- else {
4633
- attrs = attrs || {};
4634
- attrs[attributeName] = value;
4635
- }
4636
- }
4637
- return {
4638
- type: 14 /* TSlot */,
4639
- name,
4640
- attrs,
4641
- on,
4642
- defaultContent: parseChildNodes(node, ctx),
4643
- };
4644
- }
4645
- function parseTTranslation(node, ctx) {
4646
- if (node.getAttribute("t-translation") !== "off") {
4647
- return null;
4648
- }
4649
- node.removeAttribute("t-translation");
4650
- return {
4651
- type: 16 /* TTranslation */,
4652
- content: parseNode(node, ctx),
4653
- };
4654
- }
4655
- // -----------------------------------------------------------------------------
4656
- // Portal
4657
- // -----------------------------------------------------------------------------
4658
- function parseTPortal(node, ctx) {
4659
- if (!node.hasAttribute("t-portal")) {
4660
- return null;
4661
- }
4662
- const target = node.getAttribute("t-portal");
4663
- node.removeAttribute("t-portal");
4664
- const content = parseNode(node, ctx);
4665
- if (!content) {
4666
- return {
4667
- type: 0 /* Text */,
4668
- value: "",
4669
- };
4670
- }
4671
- return {
4672
- type: 17 /* TPortal */,
4673
- target,
4674
- content,
4675
- };
4676
- }
4677
- // -----------------------------------------------------------------------------
4678
- // helpers
4679
- // -----------------------------------------------------------------------------
4680
- /**
4681
- * Parse all the child nodes of a given node and return a list of ast elements
4682
- */
4683
- function parseChildren(node, ctx) {
4684
- const children = [];
4685
- for (let child of node.childNodes) {
4686
- const childAst = parseNode(child, ctx);
4687
- if (childAst) {
4688
- if (childAst.type === 3 /* Multi */) {
4689
- children.push(...childAst.content);
4690
- }
4691
- else {
4692
- children.push(childAst);
4693
- }
4694
- }
4695
- }
4696
- return children;
4697
- }
4698
- /**
4699
- * Parse all the child nodes of a given node and return an ast if possible.
4700
- * In the case there are multiple children, they are wrapped in a astmulti.
4701
- */
4702
- function parseChildNodes(node, ctx) {
4703
- const children = parseChildren(node, ctx);
4704
- switch (children.length) {
4705
- case 0:
4706
- return null;
4707
- case 1:
4708
- return children[0];
4709
- default:
4710
- return { type: 3 /* Multi */, content: children };
4711
- }
4712
- }
4713
- /**
4714
- * Normalizes the content of an Element so that t-if/t-elif/t-else directives
4715
- * immediately follow one another (by removing empty text nodes or comments).
4716
- * Throws an error when a conditional branching statement is malformed. This
4717
- * function modifies the Element in place.
4718
- *
4719
- * @param el the element containing the tree that should be normalized
4720
- */
4721
- function normalizeTIf(el) {
4722
- let tbranch = el.querySelectorAll("[t-elif], [t-else]");
4723
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
4724
- let node = tbranch[i];
4725
- let prevElem = node.previousElementSibling;
4726
- let pattr = (name) => prevElem.getAttribute(name);
4727
- let nattr = (name) => +!!node.getAttribute(name);
4728
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
4729
- if (pattr("t-foreach")) {
4730
- throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
4731
- }
4732
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
4733
- return a + b;
4734
- }) > 1) {
4735
- throw new Error("Only one conditional branching directive is allowed per node");
4736
- }
4737
- // All text (with only spaces) and comment nodes (nodeType 8) between
4738
- // branch nodes are removed
4739
- let textNode;
4740
- while ((textNode = node.previousSibling) !== prevElem) {
4741
- if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
4742
- throw new Error("text is not allowed between branching directives");
4743
- }
4744
- textNode.remove();
4745
- }
4746
- }
4747
- else {
4748
- throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
4749
- }
4750
- }
4751
- }
4752
- /**
4753
- * Normalizes the content of an Element so that t-esc directives on components
4754
- * are removed and instead places a <t t-esc=""> as the default slot of the
4755
- * component. Also throws if the component already has content. This function
4756
- * modifies the Element in place.
4757
- *
4758
- * @param el the element containing the tree that should be normalized
4759
- */
4760
- function normalizeTEsc(el) {
4761
- const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
4762
- for (const el of elements) {
4763
- if (el.childNodes.length) {
4764
- throw new Error("Cannot have t-esc on a component that already has content");
4765
- }
4766
- const value = el.getAttribute("t-esc");
4767
- el.removeAttribute("t-esc");
4768
- const t = el.ownerDocument.createElement("t");
4769
- if (value != null) {
4770
- t.setAttribute("t-esc", value);
4771
- }
4772
- el.appendChild(t);
4773
- }
4774
- }
4775
- /**
4776
- * Normalizes the tree inside a given element and do some preliminary validation
4777
- * on it. This function modifies the Element in place.
4778
- *
4779
- * @param el the element containing the tree that should be normalized
4780
- */
4781
- function normalizeXML(el) {
4782
- normalizeTIf(el);
4783
- normalizeTEsc(el);
4784
- }
4785
- /**
4786
- * Parses an XML string into an XML document, throwing errors on parser errors
4787
- * instead of returning an XML document containing the parseerror.
4788
- *
4789
- * @param xml the string to parse
4790
- * @returns an XML document corresponding to the content of the string
4791
- */
4792
- function parseXML$1(xml) {
4793
- const parser = new DOMParser();
4794
- const doc = parser.parseFromString(xml, "text/xml");
4795
- if (doc.getElementsByTagName("parsererror").length) {
4796
- let msg = "Invalid XML in template.";
4797
- const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
4798
- if (parsererrorText) {
4799
- msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
4800
- const re = /\d+/g;
4801
- const firstMatch = re.exec(parsererrorText);
4802
- if (firstMatch) {
4803
- const lineNumber = Number(firstMatch[0]);
4804
- const line = xml.split("\n")[lineNumber - 1];
4805
- const secondMatch = re.exec(parsererrorText);
4806
- if (line && secondMatch) {
4807
- const columnIndex = Number(secondMatch[0]) - 1;
4808
- if (line[columnIndex]) {
4809
- msg +=
4810
- `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
4811
- `${line}\n${"-".repeat(columnIndex - 1)}^`;
4812
- }
4813
- }
4814
- }
4815
- }
4816
- throw new Error(msg);
4817
- }
4818
- return doc;
4819
- }
4820
-
4821
- function compile(template, options = {}) {
4822
- // parsing
4823
- const ast = parse(template);
4824
- // some work
4825
- const hasSafeContext = template instanceof Node
4826
- ? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
4827
- : !template.includes("t-set") && !template.includes("t-call");
4828
- // code generation
4829
- const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
4830
- const code = codeGenerator.generateCode();
4831
- // template function
4832
- return new Function("bdom, helpers", code);
4833
- }
4834
-
4835
- function wrapError(fn, hookName) {
4836
- const error = new Error(`The following error occurred in ${hookName}: `);
4837
- return (...args) => {
4838
- try {
4839
- const result = fn(...args);
4840
- if (result instanceof Promise) {
4841
- return result.catch((cause) => {
4842
- error.cause = cause;
4843
- if (cause instanceof Error) {
4844
- error.message += `"${cause.message}"`;
4845
- }
4846
- throw error;
4847
- });
4848
- }
4849
- return result;
4850
- }
4851
- catch (cause) {
4852
- if (cause instanceof Error) {
4853
- error.message += `"${cause.message}"`;
4854
- }
4855
- throw error;
4856
- }
4857
- };
4858
- }
4859
- // -----------------------------------------------------------------------------
4860
- // hooks
4861
- // -----------------------------------------------------------------------------
4862
- function onWillStart(fn) {
4863
- const node = getCurrent();
4864
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4865
- node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
4866
- }
4867
- function onWillUpdateProps(fn) {
4868
- const node = getCurrent();
4869
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4870
- node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
4871
- }
4872
- function onMounted(fn) {
4873
- const node = getCurrent();
4874
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4875
- node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
4876
- }
4877
- function onWillPatch(fn) {
4878
- const node = getCurrent();
4879
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4880
- node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
4881
- }
4882
- function onPatched(fn) {
4883
- const node = getCurrent();
4884
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4885
- node.patched.push(decorate(fn.bind(node.component), "onPatched"));
4886
- }
4887
- function onWillUnmount(fn) {
4888
- const node = getCurrent();
4889
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4890
- node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
4891
- }
4892
- function onWillDestroy(fn) {
4893
- const node = getCurrent();
4894
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4895
- node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
4896
- }
4897
- function onWillRender(fn) {
4898
- const node = getCurrent();
4899
- const renderFn = node.renderFn;
4900
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4901
- fn = decorate(fn.bind(node.component), "onWillRender");
4902
- node.renderFn = () => {
4903
- fn();
4904
- return renderFn();
4905
- };
4906
- }
4907
- function onRendered(fn) {
4908
- const node = getCurrent();
4909
- const renderFn = node.renderFn;
4910
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4911
- fn = decorate(fn.bind(node.component), "onRendered");
4912
- node.renderFn = () => {
4913
- const result = renderFn();
4914
- fn();
4915
- return result;
4916
- };
4917
- }
4918
- function onError(callback) {
4919
- const node = getCurrent();
4920
- let handlers = nodeErrorHandlers.get(node);
4921
- if (!handlers) {
4922
- handlers = [];
4923
- nodeErrorHandlers.set(node, handlers);
4924
- }
4925
- handlers.push(callback.bind(node.component));
4926
- }
4927
-
4928
- class Component {
4929
- constructor(props, env, node) {
4930
- this.props = props;
4931
- this.env = env;
4932
- this.__owl__ = node;
4933
- }
4934
- setup() { }
4935
- render(deep = false) {
4936
- this.__owl__.render(deep);
4937
- }
4938
- }
4939
- Component.template = "";
4940
-
4941
- const VText = text("").constructor;
4942
- class VPortal extends VText {
4943
- constructor(selector, realBDom) {
4944
- super("");
4945
- this.target = null;
4946
- this.selector = selector;
4947
- this.realBDom = realBDom;
4948
- }
4949
- mount(parent, anchor) {
4950
- super.mount(parent, anchor);
4951
- this.target = document.querySelector(this.selector);
4952
- if (!this.target) {
4953
- let el = this.el;
4954
- while (el && el.parentElement instanceof HTMLElement) {
4955
- el = el.parentElement;
4956
- }
4957
- this.target = el && el.querySelector(this.selector);
4958
- if (!this.target) {
4959
- throw new Error("invalid portal target");
4960
- }
4961
- }
4962
- this.realBDom.mount(this.target, null);
5083
+ let isDynamic = node.hasAttribute("t-component");
5084
+ if (isDynamic && name !== "t") {
5085
+ throw new OwlError(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
4963
5086
  }
4964
- beforeRemove() {
4965
- this.realBDom.beforeRemove();
5087
+ if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
5088
+ return null;
4966
5089
  }
4967
- remove() {
4968
- if (this.realBDom) {
4969
- super.remove();
4970
- this.realBDom.remove();
4971
- this.realBDom = null;
4972
- }
5090
+ if (isDynamic) {
5091
+ name = node.getAttribute("t-component");
5092
+ node.removeAttribute("t-component");
4973
5093
  }
4974
- patch(other) {
4975
- super.patch(other);
4976
- if (this.realBDom) {
4977
- this.realBDom.patch(other.realBDom, true);
5094
+ const dynamicProps = node.getAttribute("t-props");
5095
+ node.removeAttribute("t-props");
5096
+ const defaultSlotScope = node.getAttribute("t-slot-scope");
5097
+ node.removeAttribute("t-slot-scope");
5098
+ let on = null;
5099
+ let props = null;
5100
+ for (let name of node.getAttributeNames()) {
5101
+ const value = node.getAttribute(name);
5102
+ if (name.startsWith("t-")) {
5103
+ if (name.startsWith("t-on-")) {
5104
+ on = on || {};
5105
+ on[name.slice(5)] = value;
5106
+ }
5107
+ else {
5108
+ const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
5109
+ throw new OwlError(message || `unsupported directive on Component: ${name}`);
5110
+ }
4978
5111
  }
4979
5112
  else {
4980
- this.realBDom = other.realBDom;
4981
- this.realBDom.mount(this.target, null);
5113
+ props = props || {};
5114
+ props[name] = value;
4982
5115
  }
4983
5116
  }
4984
- }
4985
- class Portal extends Component {
4986
- setup() {
4987
- const node = this.__owl__;
4988
- const renderFn = node.renderFn;
4989
- node.renderFn = () => new VPortal(this.props.target, renderFn());
4990
- onWillUnmount(() => {
4991
- if (node.bdom) {
4992
- node.bdom.remove();
5117
+ let slots = null;
5118
+ if (node.hasChildNodes()) {
5119
+ const clone = node.cloneNode(true);
5120
+ // named slots
5121
+ const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
5122
+ for (let slotNode of slotNodes) {
5123
+ if (slotNode.tagName !== "t") {
5124
+ throw new OwlError(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);
4993
5125
  }
4994
- });
5126
+ const name = slotNode.getAttribute("t-set-slot");
5127
+ // check if this is defined in a sub component (in which case it should
5128
+ // be ignored)
5129
+ let el = slotNode.parentElement;
5130
+ let isInSubComponent = false;
5131
+ while (el !== clone) {
5132
+ if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
5133
+ isInSubComponent = true;
5134
+ break;
5135
+ }
5136
+ el = el.parentElement;
5137
+ }
5138
+ if (isInSubComponent) {
5139
+ continue;
5140
+ }
5141
+ slotNode.removeAttribute("t-set-slot");
5142
+ slotNode.remove();
5143
+ const slotAst = parseNode(slotNode, ctx);
5144
+ let on = null;
5145
+ let attrs = null;
5146
+ let scope = null;
5147
+ for (let attributeName of slotNode.getAttributeNames()) {
5148
+ const value = slotNode.getAttribute(attributeName);
5149
+ if (attributeName === "t-slot-scope") {
5150
+ scope = value;
5151
+ continue;
5152
+ }
5153
+ else if (attributeName.startsWith("t-on-")) {
5154
+ on = on || {};
5155
+ on[attributeName.slice(5)] = value;
5156
+ }
5157
+ else {
5158
+ attrs = attrs || {};
5159
+ attrs[attributeName] = value;
5160
+ }
5161
+ }
5162
+ slots = slots || {};
5163
+ slots[name] = { content: slotAst, on, attrs, scope };
5164
+ }
5165
+ // default slot
5166
+ const defaultContent = parseChildNodes(clone, ctx);
5167
+ if (defaultContent) {
5168
+ slots = slots || {};
5169
+ slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
5170
+ }
4995
5171
  }
5172
+ return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
4996
5173
  }
4997
- Portal.template = xml `<t t-slot="default"/>`;
4998
- Portal.props = {
4999
- target: {
5000
- type: String,
5001
- },
5002
- slots: true,
5003
- };
5004
-
5005
- /**
5006
- * This file contains utility functions that will be injected in each template,
5007
- * to perform various useful tasks in the compiled code.
5008
- */
5009
- function withDefault(value, defaultValue) {
5010
- return value === undefined || value === null || value === false ? defaultValue : value;
5011
- }
5012
- function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
5013
- key = key + "__slot_" + name;
5014
- const slots = ctx.props[TARGET].slots || {};
5015
- const { __render, __ctx, __scope } = slots[name] || {};
5016
- const slotScope = Object.create(__ctx || {});
5017
- if (__scope) {
5018
- slotScope[__scope] = extra;
5174
+ // -----------------------------------------------------------------------------
5175
+ // Slots
5176
+ // -----------------------------------------------------------------------------
5177
+ function parseTSlot(node, ctx) {
5178
+ if (!node.hasAttribute("t-slot")) {
5179
+ return null;
5019
5180
  }
5020
- const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
5021
- if (defaultContent) {
5022
- let child1 = undefined;
5023
- let child2 = undefined;
5024
- if (slotBDom) {
5025
- child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
5181
+ const name = node.getAttribute("t-slot");
5182
+ node.removeAttribute("t-slot");
5183
+ let attrs = null;
5184
+ let on = null;
5185
+ for (let attributeName of node.getAttributeNames()) {
5186
+ const value = node.getAttribute(attributeName);
5187
+ if (attributeName.startsWith("t-on-")) {
5188
+ on = on || {};
5189
+ on[attributeName.slice(5)] = value;
5026
5190
  }
5027
5191
  else {
5028
- child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
5192
+ attrs = attrs || {};
5193
+ attrs[attributeName] = value;
5029
5194
  }
5030
- return multi([child1, child2]);
5031
5195
  }
5032
- return slotBDom || text("");
5196
+ return {
5197
+ type: 14 /* TSlot */,
5198
+ name,
5199
+ attrs,
5200
+ on,
5201
+ defaultContent: parseChildNodes(node, ctx),
5202
+ };
5033
5203
  }
5034
- function capture(ctx) {
5035
- const component = ctx.__owl__.component;
5036
- const result = Object.create(component);
5037
- for (let k in ctx) {
5038
- result[k] = ctx[k];
5204
+ function parseTTranslation(node, ctx) {
5205
+ if (node.getAttribute("t-translation") !== "off") {
5206
+ return null;
5039
5207
  }
5040
- return result;
5041
- }
5042
- function withKey(elem, k) {
5043
- elem.key = k;
5044
- return elem;
5208
+ node.removeAttribute("t-translation");
5209
+ return {
5210
+ type: 16 /* TTranslation */,
5211
+ content: parseNode(node, ctx),
5212
+ };
5045
5213
  }
5046
- function prepareList(collection) {
5047
- let keys;
5048
- let values;
5049
- if (Array.isArray(collection)) {
5050
- keys = collection;
5051
- values = collection;
5052
- }
5053
- else if (collection) {
5054
- values = Object.keys(collection);
5055
- keys = Object.values(collection);
5214
+ // -----------------------------------------------------------------------------
5215
+ // Portal
5216
+ // -----------------------------------------------------------------------------
5217
+ function parseTPortal(node, ctx) {
5218
+ if (!node.hasAttribute("t-portal")) {
5219
+ return null;
5056
5220
  }
5057
- else {
5058
- throw new Error("Invalid loop expression");
5221
+ const target = node.getAttribute("t-portal");
5222
+ node.removeAttribute("t-portal");
5223
+ const content = parseNode(node, ctx);
5224
+ if (!content) {
5225
+ return {
5226
+ type: 0 /* Text */,
5227
+ value: "",
5228
+ };
5059
5229
  }
5060
- const n = values.length;
5061
- return [keys, values, n, new Array(n)];
5230
+ return {
5231
+ type: 17 /* TPortal */,
5232
+ target,
5233
+ content,
5234
+ };
5062
5235
  }
5063
- const isBoundary = Symbol("isBoundary");
5064
- function setContextValue(ctx, key, value) {
5065
- const ctx0 = ctx;
5066
- while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
5067
- const newCtx = ctx.__proto__;
5068
- if (!newCtx) {
5069
- ctx = ctx0;
5070
- break;
5236
+ // -----------------------------------------------------------------------------
5237
+ // helpers
5238
+ // -----------------------------------------------------------------------------
5239
+ /**
5240
+ * Parse all the child nodes of a given node and return a list of ast elements
5241
+ */
5242
+ function parseChildren(node, ctx) {
5243
+ const children = [];
5244
+ for (let child of node.childNodes) {
5245
+ const childAst = parseNode(child, ctx);
5246
+ if (childAst) {
5247
+ if (childAst.type === 3 /* Multi */) {
5248
+ children.push(...childAst.content);
5249
+ }
5250
+ else {
5251
+ children.push(childAst);
5252
+ }
5071
5253
  }
5072
- ctx = newCtx;
5073
5254
  }
5074
- ctx[key] = value;
5255
+ return children;
5075
5256
  }
5076
- function toNumber(val) {
5077
- const n = parseFloat(val);
5078
- return isNaN(n) ? val : n;
5257
+ /**
5258
+ * Parse all the child nodes of a given node and return an ast if possible.
5259
+ * In the case there are multiple children, they are wrapped in a astmulti.
5260
+ */
5261
+ function parseChildNodes(node, ctx) {
5262
+ const children = parseChildren(node, ctx);
5263
+ switch (children.length) {
5264
+ case 0:
5265
+ return null;
5266
+ case 1:
5267
+ return children[0];
5268
+ default:
5269
+ return { type: 3 /* Multi */, content: children };
5270
+ }
5079
5271
  }
5080
- function shallowEqual(l1, l2) {
5081
- for (let i = 0, l = l1.length; i < l; i++) {
5082
- if (l1[i] !== l2[i]) {
5083
- return false;
5272
+ /**
5273
+ * Normalizes the content of an Element so that t-if/t-elif/t-else directives
5274
+ * immediately follow one another (by removing empty text nodes or comments).
5275
+ * Throws an error when a conditional branching statement is malformed. This
5276
+ * function modifies the Element in place.
5277
+ *
5278
+ * @param el the element containing the tree that should be normalized
5279
+ */
5280
+ function normalizeTIf(el) {
5281
+ let tbranch = el.querySelectorAll("[t-elif], [t-else]");
5282
+ for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
5283
+ let node = tbranch[i];
5284
+ let prevElem = node.previousElementSibling;
5285
+ let pattr = (name) => prevElem.getAttribute(name);
5286
+ let nattr = (name) => +!!node.getAttribute(name);
5287
+ if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
5288
+ if (pattr("t-foreach")) {
5289
+ throw new OwlError("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
5290
+ }
5291
+ if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
5292
+ return a + b;
5293
+ }) > 1) {
5294
+ throw new OwlError("Only one conditional branching directive is allowed per node");
5295
+ }
5296
+ // All text (with only spaces) and comment nodes (nodeType 8) between
5297
+ // branch nodes are removed
5298
+ let textNode;
5299
+ while ((textNode = node.previousSibling) !== prevElem) {
5300
+ if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
5301
+ throw new OwlError("text is not allowed between branching directives");
5302
+ }
5303
+ textNode.remove();
5304
+ }
5305
+ }
5306
+ else {
5307
+ throw new OwlError("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
5084
5308
  }
5085
- }
5086
- return true;
5087
- }
5088
- class LazyValue {
5089
- constructor(fn, ctx, node) {
5090
- this.fn = fn;
5091
- this.ctx = capture(ctx);
5092
- this.node = node;
5093
- }
5094
- evaluate() {
5095
- return this.fn(this.ctx, this.node);
5096
- }
5097
- toString() {
5098
- return this.evaluate().toString();
5099
5309
  }
5100
5310
  }
5101
- /*
5102
- * Safely outputs `value` as a block depending on the nature of `value`
5311
+ /**
5312
+ * Normalizes the content of an Element so that t-esc directives on components
5313
+ * are removed and instead places a <t t-esc=""> as the default slot of the
5314
+ * component. Also throws if the component already has content. This function
5315
+ * modifies the Element in place.
5316
+ *
5317
+ * @param el the element containing the tree that should be normalized
5103
5318
  */
5104
- function safeOutput(value) {
5105
- if (!value) {
5106
- return value;
5107
- }
5108
- let safeKey;
5109
- let block;
5110
- if (value instanceof Markup) {
5111
- safeKey = `string_safe`;
5112
- block = html(value);
5113
- }
5114
- else if (value instanceof LazyValue) {
5115
- safeKey = `lazy_value`;
5116
- block = value.evaluate();
5117
- }
5118
- else if (value instanceof String || typeof value === "string") {
5119
- safeKey = "string_unsafe";
5120
- block = text(value);
5121
- }
5122
- else {
5123
- // Assuming it is a block
5124
- safeKey = "block_safe";
5125
- block = value;
5126
- }
5127
- return toggler(safeKey, block);
5128
- }
5129
- let boundFunctions = new WeakMap();
5130
- function bind(ctx, fn) {
5131
- let component = ctx.__owl__.component;
5132
- let boundFnMap = boundFunctions.get(component);
5133
- if (!boundFnMap) {
5134
- boundFnMap = new WeakMap();
5135
- boundFunctions.set(component, boundFnMap);
5136
- }
5137
- let boundFn = boundFnMap.get(fn);
5138
- if (!boundFn) {
5139
- boundFn = fn.bind(component);
5140
- boundFnMap.set(fn, boundFn);
5141
- }
5142
- return boundFn;
5143
- }
5144
- function multiRefSetter(refs, name) {
5145
- let count = 0;
5146
- return (el) => {
5147
- if (el) {
5148
- count++;
5149
- if (count > 1) {
5150
- throw new Error("Cannot have 2 elements with same ref name at the same time");
5151
- }
5319
+ function normalizeTEsc(el) {
5320
+ const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
5321
+ for (const el of elements) {
5322
+ if (el.childNodes.length) {
5323
+ throw new OwlError("Cannot have t-esc on a component that already has content");
5152
5324
  }
5153
- if (count === 0 || el) {
5154
- refs[name] = el;
5325
+ const value = el.getAttribute("t-esc");
5326
+ el.removeAttribute("t-esc");
5327
+ const t = el.ownerDocument.createElement("t");
5328
+ if (value != null) {
5329
+ t.setAttribute("t-esc", value);
5155
5330
  }
5156
- };
5331
+ el.appendChild(t);
5332
+ }
5157
5333
  }
5158
- const helpers = {
5159
- withDefault,
5160
- zero: Symbol("zero"),
5161
- isBoundary,
5162
- callSlot,
5163
- capture,
5164
- withKey,
5165
- prepareList,
5166
- setContextValue,
5167
- multiRefSetter,
5168
- shallowEqual,
5169
- toNumber,
5170
- validateProps,
5171
- LazyValue,
5172
- safeOutput,
5173
- bind,
5174
- createCatcher,
5175
- };
5176
-
5177
- const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
5334
+ /**
5335
+ * Normalizes the tree inside a given element and do some preliminary validation
5336
+ * on it. This function modifies the Element in place.
5337
+ *
5338
+ * @param el the element containing the tree that should be normalized
5339
+ */
5340
+ function normalizeXML(el) {
5341
+ normalizeTIf(el);
5342
+ normalizeTEsc(el);
5343
+ }
5344
+ /**
5345
+ * Parses an XML string into an XML document, throwing errors on parser errors
5346
+ * instead of returning an XML document containing the parseerror.
5347
+ *
5348
+ * @param xml the string to parse
5349
+ * @returns an XML document corresponding to the content of the string
5350
+ */
5178
5351
  function parseXML(xml) {
5179
5352
  const parser = new DOMParser();
5180
5353
  const doc = parser.parseFromString(xml, "text/xml");
@@ -5199,87 +5372,134 @@ function parseXML(xml) {
5199
5372
  }
5200
5373
  }
5201
5374
  }
5202
- throw new Error(msg);
5375
+ throw new OwlError(msg);
5203
5376
  }
5204
5377
  return doc;
5205
- }
5206
- /**
5207
- * Returns the helpers object that will be injected in each template closure
5208
- * function
5209
- */
5210
- function makeHelpers(getTemplate) {
5211
- return Object.assign({}, helpers, {
5212
- Portal,
5213
- markRaw,
5214
- getTemplate,
5215
- call: (owner, subTemplate, ctx, parent, key) => {
5216
- const template = getTemplate(subTemplate);
5217
- return toggler(subTemplate, template.call(owner, ctx, parent, key));
5218
- },
5219
- });
5220
- }
5221
- class TemplateSet {
5222
- constructor(config = {}) {
5223
- this.rawTemplates = Object.create(globalTemplates);
5224
- this.templates = {};
5225
- this.dev = config.dev || false;
5226
- this.translateFn = config.translateFn;
5227
- this.translatableAttributes = config.translatableAttributes;
5228
- if (config.templates) {
5229
- this.addTemplates(config.templates);
5230
- }
5231
- this.helpers = makeHelpers(this.getTemplate.bind(this));
5232
- }
5233
- addTemplate(name, template, options = {}) {
5234
- if (name in this.rawTemplates && !options.allowDuplicate) {
5235
- throw new Error(`Template ${name} already defined`);
5378
+ }
5379
+
5380
+ function compile(template, options = {}) {
5381
+ // parsing
5382
+ const ast = parse(template);
5383
+ // some work
5384
+ const hasSafeContext = template instanceof Node
5385
+ ? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
5386
+ : !template.includes("t-set") && !template.includes("t-call");
5387
+ // code generation
5388
+ const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
5389
+ const code = codeGenerator.generateCode();
5390
+ // template function
5391
+ return new Function("app, bdom, helpers", code);
5392
+ }
5393
+
5394
+ const mainEventHandler = (data, ev, currentTarget) => {
5395
+ const { data: _data, modifiers } = filterOutModifiersFromData(data);
5396
+ data = _data;
5397
+ let stopped = false;
5398
+ if (modifiers.length) {
5399
+ let selfMode = false;
5400
+ const isSelf = ev.target === currentTarget;
5401
+ for (const mod of modifiers) {
5402
+ switch (mod) {
5403
+ case "self":
5404
+ selfMode = true;
5405
+ if (isSelf) {
5406
+ continue;
5407
+ }
5408
+ else {
5409
+ return stopped;
5410
+ }
5411
+ case "prevent":
5412
+ if ((selfMode && isSelf) || !selfMode)
5413
+ ev.preventDefault();
5414
+ continue;
5415
+ case "stop":
5416
+ if ((selfMode && isSelf) || !selfMode)
5417
+ ev.stopPropagation();
5418
+ stopped = true;
5419
+ continue;
5420
+ }
5236
5421
  }
5237
- this.rawTemplates[name] = template;
5238
5422
  }
5239
- addTemplates(xml, options = {}) {
5240
- if (!xml) {
5241
- // empty string
5242
- return;
5423
+ // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
5424
+ // We check this rather than data[0] being truthy (or typeof function) so that it crashes
5425
+ // as expected when there is a handler expression that evaluates to a falsy value
5426
+ if (Object.hasOwnProperty.call(data, 0)) {
5427
+ const handler = data[0];
5428
+ if (typeof handler !== "function") {
5429
+ throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);
5243
5430
  }
5244
- xml = xml instanceof Document ? xml : parseXML(xml);
5245
- for (const template of xml.querySelectorAll("[t-name]")) {
5246
- const name = template.getAttribute("t-name");
5247
- this.addTemplate(name, template, options);
5431
+ let node = data[1] ? data[1].__owl__ : null;
5432
+ if (node ? node.status === 1 /* MOUNTED */ : true) {
5433
+ handler.call(node ? node.component : null, ev);
5248
5434
  }
5249
5435
  }
5250
- getTemplate(name) {
5251
- if (!(name in this.templates)) {
5252
- const rawTemplate = this.rawTemplates[name];
5253
- if (rawTemplate === undefined) {
5254
- let extraInfo = "";
5255
- try {
5256
- const componentName = getCurrent().component.constructor.name;
5257
- extraInfo = ` (for component "${componentName}")`;
5436
+ return stopped;
5437
+ };
5438
+
5439
+ // -----------------------------------------------------------------------------
5440
+ // Scheduler
5441
+ // -----------------------------------------------------------------------------
5442
+ class Scheduler {
5443
+ constructor() {
5444
+ this.tasks = new Set();
5445
+ this.frame = 0;
5446
+ this.delayedRenders = [];
5447
+ this.requestAnimationFrame = Scheduler.requestAnimationFrame;
5448
+ }
5449
+ addFiber(fiber) {
5450
+ this.tasks.add(fiber.root);
5451
+ }
5452
+ /**
5453
+ * Process all current tasks. This only applies to the fibers that are ready.
5454
+ * Other tasks are left unchanged.
5455
+ */
5456
+ flush() {
5457
+ if (this.delayedRenders.length) {
5458
+ let renders = this.delayedRenders;
5459
+ this.delayedRenders = [];
5460
+ for (let f of renders) {
5461
+ if (f.root && f.node.status !== 2 /* DESTROYED */ && f.node.fiber === f) {
5462
+ f.render();
5258
5463
  }
5259
- catch { }
5260
- throw new Error(`Missing template: "${name}"${extraInfo}`);
5261
5464
  }
5262
- const templateFn = this._compileTemplate(name, rawTemplate);
5263
- // first add a function to lazily get the template, in case there is a
5264
- // recursive call to the template name
5265
- const templates = this.templates;
5266
- this.templates[name] = function (context, parent) {
5267
- return templates[name].call(this, context, parent);
5268
- };
5269
- const template = templateFn(bdom, this.helpers);
5270
- this.templates[name] = template;
5271
5465
  }
5272
- return this.templates[name];
5466
+ if (this.frame === 0) {
5467
+ this.frame = this.requestAnimationFrame(() => {
5468
+ this.frame = 0;
5469
+ this.tasks.forEach((fiber) => this.processFiber(fiber));
5470
+ for (let task of this.tasks) {
5471
+ if (task.node.status === 2 /* DESTROYED */) {
5472
+ this.tasks.delete(task);
5473
+ }
5474
+ }
5475
+ });
5476
+ }
5273
5477
  }
5274
- _compileTemplate(name, template) {
5275
- return compile(template, {
5276
- name,
5277
- dev: this.dev,
5278
- translateFn: this.translateFn,
5279
- translatableAttributes: this.translatableAttributes,
5280
- });
5478
+ processFiber(fiber) {
5479
+ if (fiber.root !== fiber) {
5480
+ this.tasks.delete(fiber);
5481
+ return;
5482
+ }
5483
+ const hasError = fibersInError.has(fiber);
5484
+ if (hasError && fiber.counter !== 0) {
5485
+ this.tasks.delete(fiber);
5486
+ return;
5487
+ }
5488
+ if (fiber.node.status === 2 /* DESTROYED */) {
5489
+ this.tasks.delete(fiber);
5490
+ return;
5491
+ }
5492
+ if (fiber.counter === 0) {
5493
+ if (!hasError) {
5494
+ fiber.complete();
5495
+ }
5496
+ this.tasks.delete(fiber);
5497
+ }
5281
5498
  }
5282
- }
5499
+ }
5500
+ // capture the value of requestAnimationFrame as soon as possible, to avoid
5501
+ // interactions with other code, such as test frameworks that override them
5502
+ Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
5283
5503
 
5284
5504
  let hasBeenLogged = false;
5285
5505
  const DEV_MSG = () => {
@@ -5298,6 +5518,7 @@ class App extends TemplateSet {
5298
5518
  if (config.test) {
5299
5519
  this.dev = true;
5300
5520
  }
5521
+ this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
5301
5522
  if (this.dev && !config.test && !hasBeenLogged) {
5302
5523
  console.info(DEV_MSG());
5303
5524
  hasBeenLogged = true;
@@ -5309,13 +5530,16 @@ class App extends TemplateSet {
5309
5530
  }
5310
5531
  mount(target, options) {
5311
5532
  App.validateTarget(target);
5533
+ if (this.dev) {
5534
+ validateProps(this.Root, this.props, { __owl__: { app: this } });
5535
+ }
5312
5536
  const node = this.makeNode(this.Root, this.props);
5313
5537
  const prom = this.mountNode(node, target, options);
5314
5538
  this.root = node;
5315
5539
  return prom;
5316
5540
  }
5317
5541
  makeNode(Component, props) {
5318
- return new ComponentNode(Component, props, this);
5542
+ return new ComponentNode(Component, props, this, null, null);
5319
5543
  }
5320
5544
  mountNode(node, target, options) {
5321
5545
  const promise = new Promise((resolve, reject) => {
@@ -5333,10 +5557,7 @@ class App extends TemplateSet {
5333
5557
  nodeErrorHandlers.set(node, handlers);
5334
5558
  }
5335
5559
  handlers.unshift((e) => {
5336
- if (isResolved) {
5337
- console.error(e);
5338
- }
5339
- else {
5560
+ if (!isResolved) {
5340
5561
  reject(e);
5341
5562
  }
5342
5563
  throw e;
@@ -5347,9 +5568,62 @@ class App extends TemplateSet {
5347
5568
  }
5348
5569
  destroy() {
5349
5570
  if (this.root) {
5571
+ this.scheduler.flush();
5350
5572
  this.root.destroy();
5351
5573
  }
5352
5574
  }
5575
+ createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, hasNoProp) {
5576
+ const isDynamic = !isStatic;
5577
+ function _arePropsDifferent(props1, props2) {
5578
+ for (let k in props1) {
5579
+ if (props1[k] !== props2[k]) {
5580
+ return true;
5581
+ }
5582
+ }
5583
+ return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length;
5584
+ }
5585
+ const arePropsDifferent = hasSlotsProp
5586
+ ? (_1, _2) => true
5587
+ : hasNoProp
5588
+ ? (_1, _2) => false
5589
+ : _arePropsDifferent;
5590
+ const updateAndRender = ComponentNode.prototype.updateAndRender;
5591
+ const initiateRender = ComponentNode.prototype.initiateRender;
5592
+ return (props, key, ctx, parent, C) => {
5593
+ let children = ctx.children;
5594
+ let node = children[key];
5595
+ if (isDynamic && node && node.component.constructor !== C) {
5596
+ node = undefined;
5597
+ }
5598
+ const parentFiber = ctx.fiber;
5599
+ if (node) {
5600
+ if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {
5601
+ node.forceNextRender = false;
5602
+ updateAndRender.call(node, props, parentFiber);
5603
+ }
5604
+ }
5605
+ else {
5606
+ // new component
5607
+ if (isStatic) {
5608
+ C = parent.constructor.components[name];
5609
+ if (!C) {
5610
+ throw new OwlError(`Cannot find the definition of component "${name}"`);
5611
+ }
5612
+ else if (!(C.prototype instanceof Component)) {
5613
+ throw new OwlError(`"${name}" is not a Component. It must inherit from the Component class`);
5614
+ }
5615
+ }
5616
+ node = new ComponentNode(C, props, this, ctx, key);
5617
+ children[key] = node;
5618
+ initiateRender.call(node, new Fiber(node, parentFiber));
5619
+ }
5620
+ parentFiber.childrenMap[key] = node;
5621
+ return node;
5622
+ };
5623
+ }
5624
+ handleError(...args) {
5625
+ return handleError(...args);
5626
+ }
5353
5627
  }
5354
5628
  App.validateTarget = validateTarget;
5355
5629
  async function mount(C, target, config = {}) {
@@ -5412,10 +5686,6 @@ function useChildSubEnv(envExtension) {
5412
5686
  const node = getCurrent();
5413
5687
  node.childEnv = extendEnv(node.childEnv, envExtension);
5414
5688
  }
5415
- // -----------------------------------------------------------------------------
5416
- // useEffect
5417
- // -----------------------------------------------------------------------------
5418
- const NO_OP = () => { };
5419
5689
  /**
5420
5690
  * This hook will run a callback when a component is mounted and patched, and
5421
5691
  * will run a cleanup function before patching and before unmounting the
@@ -5433,18 +5703,20 @@ function useEffect(effect, computeDependencies = () => [NaN]) {
5433
5703
  let dependencies;
5434
5704
  onMounted(() => {
5435
5705
  dependencies = computeDependencies();
5436
- cleanup = effect(...dependencies) || NO_OP;
5706
+ cleanup = effect(...dependencies);
5437
5707
  });
5438
5708
  onPatched(() => {
5439
5709
  const newDeps = computeDependencies();
5440
5710
  const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
5441
5711
  if (shouldReapply) {
5442
5712
  dependencies = newDeps;
5443
- cleanup();
5444
- cleanup = effect(...dependencies) || NO_OP;
5713
+ if (cleanup) {
5714
+ cleanup();
5715
+ }
5716
+ cleanup = effect(...dependencies);
5445
5717
  }
5446
5718
  });
5447
- onWillUnmount(() => cleanup());
5719
+ onWillUnmount(() => cleanup && cleanup());
5448
5720
  }
5449
5721
  // -----------------------------------------------------------------------------
5450
5722
  // useExternalListener
@@ -5488,10 +5760,19 @@ const blockDom = {
5488
5760
  };
5489
5761
  const __info__ = {};
5490
5762
 
5491
- export { App, Component, EventBus, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, whenReady, xml };
5763
+ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, template) {
5764
+ return compile(template, {
5765
+ name,
5766
+ dev: this.dev,
5767
+ translateFn: this.translateFn,
5768
+ translatableAttributes: this.translatableAttributes,
5769
+ });
5770
+ };
5771
+
5772
+ export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, whenReady, xml };
5492
5773
 
5493
5774
 
5494
- __info__.version = '2.0.0-beta.3';
5495
- __info__.date = '2022-03-08T11:47:49.105Z';
5496
- __info__.hash = 'c356351';
5775
+ __info__.version = '2.0.0';
5776
+ __info__.date = '2022-10-07T13:28:10.216Z';
5777
+ __info__.hash = 'a1f2282';
5497
5778
  __info__.url = 'https://github.com/odoo/owl';