mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
841 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc04f727ac | |||
| 0bc9573a8a | |||
| 73f94fba3f | |||
| 7a16449724 | |||
| 718c765e3b | |||
| 150d620b8e | |||
| 307b936d01 | |||
| 6950f8e628 | |||
| 4e3b7c74da | |||
| 98bb2842d8 | |||
| 6e185f987b | |||
| c0f495661d | |||
| 0f7a8289a6 | |||
| 1c3b04f6a8 | |||
| 64db7777dd | |||
| 37313c47a3 | |||
| 3f563de9c0 | |||
| 97564a7612 | |||
| f1abf7f2ea | |||
| 8464a1b04e | |||
| c0a62dfd05 | |||
| 600f1e35d4 | |||
| b3181f119d | |||
| 1e1e05350a | |||
| b242230e20 | |||
| 9fe2da704e | |||
| 21b1661d39 | |||
| ec05b1f5e3 | |||
| 8083678f03 | |||
| 61c2ec5d83 | |||
| e579a993fd | |||
| ea3c6f7bf0 | |||
| 9cfafc30b5 | |||
| ad42c583c6 | |||
| 6faaa6c361 | |||
| bff539cabf | |||
| 88fd1cf483 | |||
| f0b75b6890 | |||
| a1552117f9 | |||
| 17ae1d06c4 | |||
| 27629cedfa | |||
| 3a93370ab6 | |||
| a6bb4d8ee1 | |||
| caf842c482 | |||
| 1b513a1637 | |||
| 52a200878a | |||
| f4994a20d8 | |||
| 7419c1982a | |||
| a8c8f6affe | |||
| dabe7f9bfa | |||
| d08ea63565 | |||
| 7ebe0da962 | |||
| 3e0e8475a6 | |||
| 2e83c739b9 | |||
| 922eb7cd98 | |||
| d12115554c | |||
| 1a6b26c8c6 | |||
| d67d295eaa | |||
| 9cbcf20b33 | |||
| 3a461e1dd1 | |||
| e8387810e6 | |||
| acac9d1741 | |||
| 968a5460bb | |||
| e646eb697e | |||
| 7dfa901332 | |||
| 0290f63ba3 | |||
| 370fae4e1a | |||
| 76735410f8 | |||
| 19a47a7001 | |||
| 398f9f4e53 | |||
| af426aa902 | |||
| 490cf18079 | |||
| dfc782599b | |||
| 71f545058b | |||
| 1a20cc57de | |||
| 25738a1bf0 | |||
| 4a96eff3c6 | |||
| d043d47754 | |||
| 3a10468f7b | |||
| 144b323d2b | |||
| bb64e87634 | |||
| 9a87b9a4a0 | |||
| 2a53a9592e | |||
| f54b9a4a0c | |||
| abb9d0b364 | |||
| 8483cc805e | |||
| 142f47ac82 | |||
| c8a27aa1a3 | |||
| 323cb61d2c | |||
| ac9cc91701 | |||
| d83cfc12ee | |||
| cb07c99d40 | |||
| d615ffd81b | |||
| 8d2b250fef | |||
| 7626cc01b3 | |||
| 392185ab67 | |||
| 392dc0131a | |||
| 4e8e9dee3e | |||
| 25d4cb2aab | |||
| 4724652533 | |||
| b08cc1d084 | |||
| a68d7774c6 | |||
| ed9d820d9e | |||
| 4a2d019161 | |||
| 89eae191b1 | |||
| 3fbc02986c | |||
| a28ce440dc | |||
| 8e018fbbda | |||
| 33f7fc8e83 | |||
| 6a833c54ec | |||
| cb38d795f9 | |||
| 8da68e925b | |||
| 474ffa1cd9 | |||
| e73fb462c5 | |||
| 38c7ad9629 | |||
| d0c76c5854 | |||
| e032314739 | |||
| b2db7f21ed | |||
| 7b8ac13d3f | |||
| 06d852fcf9 | |||
| 7e4baf668a | |||
| fe34ba00a6 | |||
| e5e7790530 | |||
| 3bf91afc3f | |||
| 2529aa3ef2 | |||
| 8d25bddda4 | |||
| 81af21a025 | |||
| 9baea2c1cd | |||
| 8e03f9cd9c | |||
| c5a2f52afb | |||
| 9fd8315c53 | |||
| 85318b3ae6 | |||
| 4b961cbffe | |||
| 2af8cccd65 | |||
| b8d3618afa | |||
| 559fadb62a | |||
| c36333dbbc | |||
| b5c3422b4d | |||
| 23ce19e57a | |||
| aaf40e9bec | |||
| 211f6ebdd6 | |||
| 1707bd240d | |||
| 142b69823f | |||
| 94c8bce810 | |||
| ae172d42e7 | |||
| ddf30a8a97 | |||
| 8d0d8538ad | |||
| 2b0315c03f | |||
| c469cac315 | |||
| b4ad14edc0 | |||
| 718e5264ae | |||
| f5d019bb69 | |||
| afa36f52a0 | |||
| fd6327b068 | |||
| d5098dd138 | |||
| ce052e0992 | |||
| b00188c1ce | |||
| 2c01802b8e | |||
| 20eb848262 | |||
| 8b479749b7 | |||
| f7728b93bd | |||
| 03585d8fea | |||
| 047a9c8993 | |||
| 69d35abe4f | |||
| 1d7503913e | |||
| ee956a1977 | |||
| 3fdc7a48f3 | |||
| d212309f1b | |||
| 8866905f33 | |||
| 50b116c56d | |||
| a823373220 | |||
| e402ee62de | |||
| 5711eb065d | |||
| 6739e79aac | |||
| 1c8dc97fca | |||
| f9cac94dc7 | |||
| c62c3fea19 | |||
| 3845607feb | |||
| 3fcbb9ad16 | |||
| e04b160bff | |||
| bc234d2053 | |||
| 3d0f046d9a | |||
| 4c75bae0e8 | |||
| 54f5819ef9 | |||
| 0c9c5b877b | |||
| b7b0e38fca | |||
| 74bd119bf5 | |||
| 4a0d04e4ae | |||
| 49931104a1 | |||
| 21d1306ec2 | |||
| 21737d33fa | |||
| 23012f3e7c | |||
| 55889febd5 | |||
| 0af46bc123 | |||
| b63cd4c9d9 | |||
| 953778dc50 | |||
| 4f61d9f1e0 | |||
| bc2c7edff4 | |||
| c8d9c0b50e | |||
| 15b25fd838 | |||
| 1db0f5ac9b | |||
| 6465665550 | |||
| 9216c5c24b | |||
| d9bf4284c6 | |||
| ef6fd457f8 | |||
| 455e45c148 | |||
| 5e7f1d5e6d | |||
| 0b05ad619f | |||
| 29a80d0b28 | |||
| 0931a4dc5b | |||
| 4e22dbcad6 | |||
| b283e65ad4 | |||
| f517d44e32 | |||
| 08022b49df | |||
| b96ea79f2b | |||
| 97d8b3ed8c | |||
| 9921ae07b1 | |||
| 41cb5f1abd | |||
| 0762eeb010 | |||
| bc455be720 | |||
| 9145799ae9 | |||
| ce9c2f8613 | |||
| b890e7ceae | |||
| 124211c18b | |||
| be8c3bbf6f | |||
| e162a6fb6c | |||
| 7ec05ff8cf | |||
| a0bfbf6dd0 | |||
| b67cb71048 | |||
| f5b3ef5cf3 | |||
| ecfccf2448 | |||
| f12d3373c9 | |||
| 286090efca | |||
| 2b4ff7b2fd | |||
| c43178a45b | |||
| 033a431e18 | |||
| 42aa9d3ae1 | |||
| 85f26a0286 | |||
| ed4ab51c17 | |||
| 556a4644f0 | |||
| 821bd0b4b8 | |||
| 06a6d890d7 | |||
| c1269288f5 | |||
| 8c17bb0411 | |||
| 7f6782d009 | |||
| 6e5d6aa226 | |||
| 9400c0adad | |||
| 85d4393242 | |||
| 6c8b401092 | |||
| 12be815342 | |||
| 65344dbf1f | |||
| a6f9b26057 | |||
| 6c753bb49f | |||
| 1f079b883e | |||
| 8fb35ed969 | |||
| a3f2d07b40 | |||
| c993278c80 | |||
| 3aa586db43 | |||
| 14db513f3b | |||
| 7bb04185c7 | |||
| 48744cfa87 | |||
| ff76747a05 | |||
| 7e6b1a28a0 | |||
| 58f6724194 | |||
| 3c0f8ac76f | |||
| cb11c0118c | |||
| 27b4eece66 | |||
| 2e3e8cd603 | |||
| 28ee790b3e | |||
| 2922cee6ea | |||
| 7749fd3b96 | |||
| f44b9a38ae | |||
| c7773bfd2a | |||
| bd39797f17 | |||
| 94a595ef5b | |||
| f83846e054 | |||
| a97144366d | |||
| 79983de4ed | |||
| 54e1734f2f | |||
| 4aea093ed9 | |||
| 56087b95cd | |||
| e5940b4b6b | |||
| bbdc9d90d7 | |||
| f2b3ebd1ec | |||
| 55bb09ba1a | |||
| d249f50d09 | |||
| 749f0063ea | |||
| 263f31fea9 | |||
| b891ae7de4 | |||
| c5a80497ac | |||
| e47f604449 | |||
| 7e721a96b5 | |||
| af4f372506 | |||
| 5731358607 | |||
| 1226f015d7 | |||
| b211e75140 | |||
| 8d1dd06340 | |||
| bd3c1265d3 | |||
| 2413c98f50 | |||
| d74b5a03db | |||
| 83532db48f | |||
| 534152eff7 | |||
| 05a678c039 | |||
| 8fbf2172c5 | |||
| ea1376d0ca | |||
| ba483b6e2c | |||
| 2fc71cfb62 | |||
| 08cb83149e | |||
| 7d249d6f09 | |||
| 0addca63a0 | |||
| 5ba73cc09d | |||
| 9106c19066 | |||
| 9f93da4765 | |||
| 9e37b968e8 | |||
| fa6801b523 | |||
| 9edf29a3a1 | |||
| 6a434310ee | |||
| e7967d0779 | |||
| b0e2ef82f7 | |||
| c9c2b3fa6d | |||
| 62608cbb0d | |||
| 3035a9f009 | |||
| c2adb429bd | |||
| da9dda5eca | |||
| 5614c85b04 | |||
| 5d57a7bf13 | |||
| 4ebe419c56 | |||
| 9779cd196c | |||
| 9cee12d7b4 | |||
| 2c563ee380 | |||
| 2aa705f5c8 | |||
| 5911c8e3f6 | |||
| 2279dafbec | |||
| f07ec21a07 | |||
| b5b6b7342e | |||
| 97b914b9cd | |||
| 9dfcfda365 | |||
| e026f537ae | |||
| f0b5a55ad1 | |||
| 690d8edf67 | |||
| 3c38bbc076 | |||
| 9c5cad15c1 | |||
| 2bed1cfbd1 | |||
| caa9ac5cb2 | |||
| 67349eb0f0 | |||
| 1ca401811f | |||
| 2beb12678e | |||
| 0ea1091692 | |||
| 71827c3ba8 | |||
| c2f42d3b82 | |||
| a3317ab997 | |||
| 403935a41e | |||
| 39af9ec938 | |||
| 13128ed425 | |||
| be556a970e | |||
| da6c24bbca | |||
| f6d6da8393 | |||
| 918945c11e | |||
| 29c2c5b9c9 | |||
| 1bb4577ec1 | |||
| 0aeebd7b6e | |||
| 4bc49e7241 | |||
| 2b5783d9bd | |||
| 1f40f113aa | |||
| 9e65b0686f | |||
| 1d72164015 | |||
| 2825a5a55d | |||
| d40ff53f5a | |||
| 23246c42aa | |||
| 895fe7c60f | |||
| 19b10f7c1d | |||
| 0788d9fe75 | |||
| 4a346d0615 | |||
| 1337aa0107 | |||
| 3e8f60cefa | |||
| 41b0618c93 | |||
| 0928c189f4 | |||
| 2f27768be2 | |||
| aa1b0f502d | |||
| da1824f4dd | |||
| 07dac970f7 | |||
| 5f6f081ae2 | |||
| 1c3c2e5c51 | |||
| 17ce73e6a9 | |||
| cedb1f411f | |||
| e7516be95c | |||
| 683993dbc4 | |||
| 50d569411c | |||
| c3453d35b9 | |||
| b0dd0252a4 | |||
| e2b2e54e29 | |||
| 766c378ac9 | |||
| 022b29b6a0 | |||
| 367725d194 | |||
| e82ed7dc8c | |||
| 63c3d4f6b0 | |||
| b859fcb8f0 | |||
| 3005756d89 | |||
| f6820bb8ac | |||
| 090880f478 | |||
| 5cdaa7b473 | |||
| 91b9e78182 | |||
| 5335fb8fba | |||
| 4513e3f31c | |||
| ecf12f6eee | |||
| 82a6961b3a | |||
| 78a4550ebb | |||
| cc82b3ffcd | |||
| bbbaf95c8e | |||
| caa63628dc | |||
| 7a49b9d94e | |||
| 01eb338e69 | |||
| 3048c6f961 | |||
| 8edc637033 | |||
| c9910077a4 | |||
| 6bf70f016c | |||
| 905619f80f | |||
| 14ceb382ef | |||
| 9846b2e997 | |||
| cfc2fb2ba2 | |||
| d57017b6ed | |||
| 5bf7a5c671 | |||
| 9b589af75a | |||
| 471fd49063 | |||
| 4fc3423682 | |||
| ffb3263c79 | |||
| e1acf66143 | |||
| 2a40907e35 | |||
| 1c0dd79955 | |||
| 9fad164c79 | |||
| 53a8841914 | |||
| 19f0863aa3 | |||
| 28f7b44f9e | |||
| fe46a6ccee | |||
| 3c23a7b246 | |||
| c52799de2c | |||
| 4edb90f44b | |||
| 0c1a7aabae | |||
| cfdce29ce7 | |||
| b344d73e07 | |||
| 4cd08d25c8 | |||
| f9861040c8 | |||
| 40d8090232 | |||
| 55be6437f1 | |||
| 6ff4dd74bf | |||
| 3628b49c37 | |||
| f289cb17ec | |||
| e6a3ede4a6 | |||
| 10cfe0b740 | |||
| a3d563ebb4 | |||
| 3b1247d476 | |||
| beb6c2bc82 | |||
| f0cd6f61cd | |||
| c950b385a0 | |||
| c172b31481 | |||
| 9b3a97e732 | |||
| 670c98916c | |||
| c59abef374 | |||
| bb4c2506c0 | |||
| 9843d16585 | |||
| 34e463ad0a | |||
| 2294c1967f | |||
| 4b53e48e91 | |||
| 70a70161e7 | |||
| e690991a6a | |||
| a621a14028 | |||
| 9754cc7521 | |||
| aa268937a7 | |||
| 3db6f0a643 | |||
| 635a7fc72c | |||
| 6cd5ec10f0 | |||
| 0a276fbf1d | |||
| d593131cc1 | |||
| 3c3dcb44a9 | |||
| 6ecd1d6ea2 | |||
| 1a931a2a08 | |||
| bf705fcd99 | |||
| bb46346767 | |||
| cd0762b436 | |||
| ca920a0091 | |||
| e5b1ba24d8 | |||
| eaf912bed7 | |||
| fd0fb5be00 | |||
| f9f730ab7b | |||
| b30b72fc36 | |||
| ab08e84bff | |||
| cd9cac1246 | |||
| f6369f9750 | |||
| f18c73e59f | |||
| 93d0a7dd46 | |||
| b1d022b583 | |||
| 5382e824b1 | |||
| 15a3f59744 | |||
| 4a90ec7ff0 | |||
| 9f9b6b174e | |||
| 545ceefc3d | |||
| 591508e769 | |||
| d9cbd23cd9 | |||
| f4cd111cc8 | |||
| ea015af741 | |||
| bfc7c81c0d | |||
| e24ff8f8aa | |||
| 9f1e64d399 | |||
| 8e1aa71436 | |||
| be2fc965b1 | |||
| 7245ccf8f9 | |||
| 1c8e1af86d | |||
| 60a6cca960 | |||
| ad55b42ccb | |||
| 50c0a4b126 | |||
| 2ca42e7470 | |||
| d381e85d94 | |||
| af7520d869 | |||
| 0095bfa61f | |||
| 1b12cf9b91 | |||
| dbfc7e4acd | |||
| e838e879c0 | |||
| ee63f6bb0f | |||
| 16bbb8bc9f | |||
| 4a5db0c283 | |||
| cc50a2e3bb | |||
| 1e2b204fdd | |||
| 4a889b7b6b | |||
| 34695883c2 | |||
| 3d2e2a1873 | |||
| af6aca83a2 | |||
| 8c8ffb6a6b | |||
| 63a8fcd7e2 | |||
| e6a5934162 | |||
| 5524c2e323 | |||
| 35c1de26b8 | |||
| d1cf6b1b8d | |||
| bfa2c681cd | |||
| 609c108607 | |||
| a56939f13a | |||
| 5ff501f8b8 | |||
| 60917bb102 | |||
| b201ef8162 | |||
| 9cacc78ad7 | |||
| 0d933b8570 | |||
| 72f6daa695 | |||
| 567d78a9ac | |||
| f013c57050 | |||
| b9fae87273 | |||
| 1f0457f69b | |||
| c481a73a76 | |||
| e64e415b3b | |||
| aa406b53e2 | |||
| 57027bd0fa | |||
| 671c662fee | |||
| 0ae70b1d63 | |||
| 2701a7d861 | |||
| 3ebe985c3c | |||
| 1c0d4b5832 | |||
| 2840794fa2 | |||
| 710f42d4e4 | |||
| 45a2b0122d | |||
| 4c41f62364 | |||
| c2ab9774fb | |||
| 22e48e3be0 | |||
| 1ff3c32fe4 | |||
| 9a3e1ed99e | |||
| 438e2fbc2e | |||
| b3db085745 | |||
| b948c59c6d | |||
| 6426fe98f9 | |||
| 4dd9de349e | |||
| e1bcea77b5 | |||
| 2a19aeb6a3 | |||
| b53a34b7d1 | |||
| f881640d3d | |||
| 54c3dd76e4 | |||
| 7b887447da | |||
| 438b21df3f | |||
| d1094f647a | |||
| 49baf0de6e | |||
| b14d069ee7 | |||
| e14a4fe338 | |||
| 1f9ec46236 | |||
| 1464a3631b | |||
| 7d3c374b78 | |||
| 1dd257f33c | |||
| 1206afe6be | |||
| 5842ed51b0 | |||
| d22219f084 | |||
| 0be1a54e03 | |||
| 32921ecc20 | |||
| 1c290c1172 | |||
| 2f9d7ea58f | |||
| fe23e76341 | |||
| 73023d4869 | |||
| ff51443b8e | |||
| f8e07f6d30 | |||
| 00da778753 | |||
| 187eb922c0 | |||
| 466c12a0e6 | |||
| 8dc3ec94bf | |||
| a554436d03 | |||
| 34b5049dee | |||
| 0eccfd08fe | |||
| 98b063fd70 | |||
| e9ba94ae55 | |||
| 4d72a4f240 | |||
| a08deb895d | |||
| 60cea14b01 | |||
| f60904b740 | |||
| 376f801541 | |||
| 26f14180e2 | |||
| bced9f3d04 | |||
| 94208d9cbb | |||
| 681983142d | |||
| a525b42537 | |||
| a4cc085b6d | |||
| 837dacf1cb | |||
| 7cdd1aa9a1 | |||
| 24c569e6d5 | |||
| 050694414c | |||
| 639908366a | |||
| 59f401303a | |||
| 649e20b956 | |||
| c2280ff1bb | |||
| 8a32d823cd | |||
| 13b31458da | |||
| 92b3ab32e2 | |||
| 4ca0a157e3 | |||
| 1dec8df715 | |||
| 8f29cfc996 | |||
| da275e74fd | |||
| 3d9f4e028c | |||
| 13577997bc | |||
| 671965026d | |||
| bc7fe9023d | |||
| c2c7997df3 | |||
| a1d7e6c987 | |||
| a1e982aba5 | |||
| 2ce027a0a6 | |||
| 61f21511a5 | |||
| 5682577870 | |||
| 25b633cc63 | |||
| f16b267931 | |||
| 30ec952588 | |||
| 46ba77d0cc | |||
| 042e376c2b | |||
| b9f1af2ad5 | |||
| 7df0fdcc0a | |||
| 73d743a1c3 | |||
| 8758dfcedf | |||
| 0a10bbc023 | |||
| 9c1d369e94 | |||
| 9556696fa8 | |||
| 04eb720619 | |||
| 8e098e8ed6 | |||
| c836cead58 | |||
| 361ecbe90b | |||
| ae8d7d3be2 | |||
| ed4f92211c | |||
| f8804d6501 | |||
| de7c680387 | |||
| 6dd7391cdc | |||
| dfa7d5932d | |||
| 53911de00a | |||
| 1760d0d336 | |||
| 0aba9748c9 | |||
| 8ee0973dab | |||
| e04c5b1c6b | |||
| f52a15d7f6 | |||
| 147f6fced7 | |||
| d928927da8 | |||
| 29cc94d567 | |||
| 7f33f9b68e | |||
| 80ffa37773 | |||
| 2f70a7a352 | |||
| f7f8c34e1c | |||
| edc7d2b635 | |||
| 7f0b236747 | |||
| b7860a95b0 | |||
| f7495dc897 | |||
| ba506be58d | |||
| 91896942dd | |||
| dfab8c7333 | |||
| bd29e4f761 | |||
| b9591cfecd | |||
| 51320204f9 | |||
| ec0bafaa8e | |||
| eb508b7660 | |||
| 7c8fc323e1 | |||
| ff2393f5df | |||
| ab2a98426e | |||
| 84b3cc42cf | |||
| d28b854e8d | |||
| 6c8c69affa | |||
| faec7d4ad9 | |||
| 5b9abb6785 | |||
| eb4ca0a8d5 | |||
| 641adc6e5c | |||
| 10ab97e3c6 | |||
| e060130655 | |||
| 4e982f5460 | |||
| d3197e9865 | |||
| 0da44033ac | |||
| 6350d9598d | |||
| 9df6c0aede | |||
| 896efca43f | |||
| efe82b6cd0 | |||
| eb910193d7 | |||
| 40e486a9eb | |||
| e56580efb2 | |||
| 46e05f1c96 | |||
| 4a748d2ea4 | |||
| 8f388e712e | |||
| 0a5b6aa0ec | |||
| fc1ea10243 | |||
| d52177560b | |||
| 1701d5aa86 | |||
| b08e876136 | |||
| 005b727328 | |||
| b44f274e37 | |||
| b6bd0f92a7 | |||
| 78108e8d2c | |||
| c405dadc1b | |||
| 2bd0719b9a | |||
| 9544034e6f | |||
| cf47f8459a | |||
| 8f7d06a5bd | |||
| d1708fc252 | |||
| f23a26426d | |||
| 40613e0a76 | |||
| aa11229641 | |||
| 754272b436 | |||
| d509373228 | |||
| 379cd2a90f | |||
| 4c2115d365 | |||
| 94b48cb389 | |||
| 3ecb772a56 | |||
| b14d264718 | |||
| 1a778922af | |||
| 79b97c6d24 | |||
| a8e919f382 | |||
| 20b33f50fa | |||
| 9170882010 | |||
| ae36cca2ef | |||
| f5ce05c81e | |||
| 2fbf4f2c22 | |||
| 8a4fd5015a | |||
| de04ed2339 | |||
| ae263730d9 | |||
| 85418043f4 | |||
| 4fb2733321 | |||
| d9d109c9a2 | |||
| 4ee39282ca | |||
| 3b3fd8a6c9 | |||
| 29111a5c10 | |||
| 4b30d0b412 | |||
| d07f396578 | |||
| 2c218c4463 | |||
| af83a8249e | |||
| 4807389012 | |||
| 7a479baf63 | |||
| ce87dd705e | |||
| 94ed6ebe60 | |||
| 619e455dd5 | |||
| 94e7d9aa39 | |||
| c47c4ccc60 | |||
| b89327c831 | |||
| e19540fb04 | |||
| c67ab3d712 | |||
| 8163e2b0e4 | |||
| e5e6c50fff | |||
| a22dcdaddf | |||
| eac49562fa | |||
| 460ae4bb34 | |||
| c52e86feaf | |||
| cef12acfe0 | |||
| 515b91fe8a | |||
| 0ca4cc1596 | |||
| 7092f5e142 | |||
| 6640644d34 | |||
| 7974ce4963 | |||
| 9dd8a36b28 | |||
| ed326d34fd | |||
| 480de10ff3 | |||
| 73fb8f8998 | |||
| 559c51a717 | |||
| 9f77344c3a | |||
| 85eb30626d | |||
| 3d18359dd8 | |||
| e005090296 | |||
| 7e7226213a | |||
| 9ee85e220b | |||
| 7b87470760 | |||
| f5bbf25d4f | |||
| 4ea0875fd9 | |||
| 3733734ae5 | |||
| d7bed96667 | |||
| a0f0a24d1d | |||
| caed076f2a | |||
| 2ea01455cb | |||
| f300e921d6 | |||
| bba9e5fb20 | |||
| 4f1d976206 | |||
| 8ca806eb28 | |||
| e0608aa7fd | |||
| 111080fdbf | |||
| ab12ac8a9f | |||
| 63069d96a5 | |||
| f1cc32de8f | |||
| 03511ce9ea | |||
| d49f5670fe | |||
| afc94c51a8 | |||
| 90f6808f57 | |||
| f960149ba2 | |||
| 74ce6a671e | |||
| bc07c49a5f | |||
| 816ffe1b44 | |||
| 94c33af8e9 | |||
| 6e0ef0fd5c | |||
| 370a92b67e | |||
| c10b84cd99 | |||
| 7ea782bd60 | |||
| bf4cbaea33 | |||
| cde85269fd | |||
| 3340401f54 | |||
| 7027ff1eb6 | |||
| d10426c720 | |||
| a951c484be | |||
| 14d3eb83eb | |||
| ca73ed07a6 | |||
| f2fc20f8e3 | |||
| 8badcbd21e | |||
| 5a75e5ec19 | |||
| 3e8682b741 | |||
| fd26775a0f | |||
| a86d0b5b02 | |||
| 7ebea5a486 | |||
| 56be87f26e | |||
| 990a08dc04 | |||
| e7a4ad99de | |||
| d6e61c2203 | |||
| e6b0d06e3e | |||
| 5cd45b59af | |||
| 1e58c7b12b |
@@ -0,0 +1,27 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm run test
|
||||
- run: npm run check-formatting
|
||||
+18
-1
@@ -1,5 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
|
||||
npm-debug.log
|
||||
|
||||
# misc
|
||||
@@ -14,4 +15,20 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
package-lock.json
|
||||
.vscode
|
||||
yarn.lock
|
||||
|
||||
#ide's
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
|
||||
# Extras temp file
|
||||
/tools/owl.js
|
||||
|
||||
release-notes.md
|
||||
|
||||
.rpt2_cache
|
||||
|
||||
# useful in some cases
|
||||
/temp
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
Most of the files are
|
||||
|
||||
Copyright (c) 2004-2015 Odoo S.A.
|
||||
|
||||
Many files also contain contributions from third
|
||||
parties. In this case the original copyright of
|
||||
the contributions can be traced through the
|
||||
history of the source version control system.
|
||||
|
||||
When that is not the case, the files contain a prominent
|
||||
notice stating the original copyright and applicable
|
||||
license, or come with their own dedicated COPYRIGHT
|
||||
and/or LICENSE file.
|
||||
@@ -0,0 +1,856 @@
|
||||
OWL is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
|
||||
(LGPLv3), as included below. Since the LGPL is a set of additional
|
||||
permissions on top of the GPL, the text of the GPL is included at the
|
||||
bottom as well.
|
||||
|
||||
Some external libraries and contributions bundled with OWL may be published
|
||||
under other GPL-compatible licenses. For these, please refer to the relevant
|
||||
source files and/or license files, in the source code tree.
|
||||
|
||||
**************************************************************************
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
|
||||
**************************************************************************
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
||||
|
||||
**************************************************************************
|
||||
@@ -0,0 +1,131 @@
|
||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">OWL Framework</a> 🦉</h1>
|
||||
|
||||
[](https://www.gnu.org/licenses/lgpl-3.0)
|
||||
[](https://badge.fury.io/js/@odoo%2Fowl)
|
||||
[](https://www.npmjs.com/package/@odoo/owl)
|
||||
|
||||
_Class based components with hooks, reactive state and concurrent mode_
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Odoo Web Library (OWL) is a smallish (~<20kb gzipped) UI framework intended to
|
||||
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
|
||||
framework, written in Typescript, taking the best ideas from React and Vue in a
|
||||
simple and consistent way. Owl's main features are:
|
||||
|
||||
- a declarative component system,
|
||||
- a reactivity system based on hooks,
|
||||
- concurrent mode by default,
|
||||
- a store and a frontend router
|
||||
|
||||
Owl components are defined with ES6 classes, they use QWeb templates, an
|
||||
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
|
||||
asynchronous.
|
||||
|
||||
**Try it online!** An online playground is available at
|
||||
[https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground)
|
||||
to let you experiment with the Owl framework. There are some code examples to
|
||||
showcase some interesting features.
|
||||
|
||||
Owl is currently stable. Possible future changes are explained in the
|
||||
[roadmap](roadmap.md).
|
||||
|
||||
## Why Owl?
|
||||
|
||||
Why did Odoo decide to make Yet Another Framework? This is really a question
|
||||
that deserves [a long answer](doc/miscellaneous/why_owl.md). But in short, we believe that
|
||||
while the current state of the art frameworks are excellent, they are not
|
||||
optimized for our use case, and there is still room for something else.
|
||||
|
||||
If you are interested in a comparison with React or Vue, you will
|
||||
find some more additional information [here](doc/miscellaneous/comparison.md).
|
||||
|
||||
## Example
|
||||
|
||||
Here is a short example to illustrate interactive components:
|
||||
|
||||
```javascript
|
||||
const { Component, useState, mount } = owl;
|
||||
const { xml } = owl.tags;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="state.value++">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span>Hello Owl</span>
|
||||
<Counter />
|
||||
</div>`;
|
||||
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
mount(App, { target: document.body });
|
||||
```
|
||||
|
||||
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
|
||||
Also, all examples here uses the [`xml` helper](doc/reference/tags.md#xml-tag) to define inline templates.
|
||||
But this is not mandatory, many applications will load templates separately.
|
||||
|
||||
More interesting examples can be found on the
|
||||
[playground](https://odoo.github.io/owl/playground) application.
|
||||
|
||||
## Design Principles
|
||||
|
||||
OWL is designed to be used in highly dynamic applications where changing
|
||||
requirements are common and code needs to be maintained by large teams.
|
||||
|
||||
- **XML based**: templates are based on the XML format, which allows interesting
|
||||
applications. For example, they could be stored in a database and modified
|
||||
dynamically with `xpaths`.
|
||||
- **templates compilation in the browser**: this may not be a good fit for all
|
||||
applications, but if you need to generate dynamically user interfaces in the
|
||||
browser, this is very powerful. For example, a generic form view component
|
||||
could generate a specific form user interface for each various models, from a XML view.
|
||||
- **no toolchain required**: this is extremely useful for some applications, if,
|
||||
for various reasons (security/deployment/dynamic modules/specific assets tools),
|
||||
it is not ok to use standard web tools based on `npm`.
|
||||
|
||||
Owl is not designed to be fast nor small (even though it is quite good on those
|
||||
two topics). It is a no nonsense framework to build applications. There is only
|
||||
one way to define components (with classes). There is no black magic. It just
|
||||
works.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
A complete documentation for Owl can be found here:
|
||||
|
||||
- [Main documentation page](doc/readme.md).
|
||||
|
||||
Some of the most important pages are:
|
||||
|
||||
- [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
|
||||
- [How to start an Owl project](doc/learning/quick_start.md)
|
||||
- [QWeb templating language](doc/reference/qweb_templating_language.md)
|
||||
- [Component](doc/reference/component.md)
|
||||
- [Hooks](doc/reference/hooks.md)
|
||||
|
||||
|
||||
## Installing Owl
|
||||
|
||||
Owl is available on `npm` and can be installed with the following command:
|
||||
|
||||
```
|
||||
npm install @odoo/owl
|
||||
```
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-1.4.10](https://github.com/odoo/owl/releases/tag/v1.4.10)
|
||||
|
||||
## License
|
||||
|
||||
OWL is [LGPL licensed](./LICENSE).
|
||||
File diff suppressed because one or more lines are too long
@@ -1,72 +0,0 @@
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
|
||||
/* Tomorrow Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #8e908c;
|
||||
}
|
||||
|
||||
/* Tomorrow Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
color: #c82829;
|
||||
}
|
||||
|
||||
/* Tomorrow Orange */
|
||||
.hljs-number,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
color: #f5871f;
|
||||
}
|
||||
|
||||
/* Tomorrow Yellow */
|
||||
.hljs-attribute {
|
||||
color: #eab700;
|
||||
}
|
||||
|
||||
/* Tomorrow Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
color: #718c00;
|
||||
}
|
||||
|
||||
/* Tomorrow Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #4271ae;
|
||||
}
|
||||
|
||||
/* Tomorrow Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #8959a8;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
background: white;
|
||||
color: #4d4d4c;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
html, body {
|
||||
background-color: #fefefe;
|
||||
font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-gap: 3rem;
|
||||
grid-template-areas: "header" "nav" "main" "footer";
|
||||
grid-template-columns: 1fr;
|
||||
height: 100%;
|
||||
grid-template-rows: 1fr 3rem 1fr 3rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
body {
|
||||
grid-template-rows: 1fr 3rem 1fr 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00A09D;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:visited {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
header {
|
||||
grid-area: header;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
text-align: center;
|
||||
padding-top: 3rem !important;
|
||||
}
|
||||
|
||||
hgroup > p {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
nav {
|
||||
grid-area: nav;
|
||||
}
|
||||
|
||||
main {
|
||||
grid-area: main;
|
||||
/*justify-self: center;*/
|
||||
grid-column: 1 / -1;
|
||||
/*max-width: 80rem !important;*/
|
||||
/*margin: 3rem 0;*/
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-area: footer;
|
||||
padding-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
header, nav, footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
article h3 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
article.brand {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
article.design {
|
||||
background-color: #875A7B;
|
||||
color: #f0eeee;
|
||||
}
|
||||
|
||||
header .homepage-logo {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
margin-left: initial;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row .column {
|
||||
margin-bottom: initial;
|
||||
padding: initial;
|
||||
}
|
||||
}
|
||||
@media (min-width: 60rem) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
margin-left: -1.0rem;
|
||||
width: calc(100% + 2.0rem);
|
||||
}
|
||||
.row .column {
|
||||
margin-bottom: inherit;
|
||||
padding: 0 1.0rem;
|
||||
}
|
||||
}
|
||||
@@ -1,602 +0,0 @@
|
||||
/*!
|
||||
* Milligram v1.3.0
|
||||
* https://milligram.github.io
|
||||
*
|
||||
* Copyright (c) 2017 CJ Patoilo
|
||||
* Licensed under the MIT license
|
||||
*/
|
||||
|
||||
*,
|
||||
*:after,
|
||||
*:before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #606c76;
|
||||
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||
font-size: 1.6em;
|
||||
font-weight: 300;
|
||||
letter-spacing: .01em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.3rem solid #d1d1d1;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
blockquote *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
input[type='button'],
|
||||
input[type='reset'],
|
||||
input[type='submit'] {
|
||||
background-color: #9b4dca;
|
||||
border: 0.1rem solid #9b4dca;
|
||||
border-radius: .4rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
height: 3.8rem;
|
||||
letter-spacing: .1rem;
|
||||
line-height: 3.8rem;
|
||||
padding: 0 3.0rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button:focus, .button:hover,
|
||||
button:focus,
|
||||
button:hover,
|
||||
input[type='button']:focus,
|
||||
input[type='button']:hover,
|
||||
input[type='reset']:focus,
|
||||
input[type='reset']:hover,
|
||||
input[type='submit']:focus,
|
||||
input[type='submit']:hover {
|
||||
background-color: #606c76;
|
||||
border-color: #606c76;
|
||||
color: #fff;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.button[disabled],
|
||||
button[disabled],
|
||||
input[type='button'][disabled],
|
||||
input[type='reset'][disabled],
|
||||
input[type='submit'][disabled] {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.button[disabled]:focus, .button[disabled]:hover,
|
||||
button[disabled]:focus,
|
||||
button[disabled]:hover,
|
||||
input[type='button'][disabled]:focus,
|
||||
input[type='button'][disabled]:hover,
|
||||
input[type='reset'][disabled]:focus,
|
||||
input[type='reset'][disabled]:hover,
|
||||
input[type='submit'][disabled]:focus,
|
||||
input[type='submit'][disabled]:hover {
|
||||
background-color: #9b4dca;
|
||||
border-color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-outline,
|
||||
button.button-outline,
|
||||
input[type='button'].button-outline,
|
||||
input[type='reset'].button-outline,
|
||||
input[type='submit'].button-outline {
|
||||
background-color: transparent;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-outline:focus, .button.button-outline:hover,
|
||||
button.button-outline:focus,
|
||||
button.button-outline:hover,
|
||||
input[type='button'].button-outline:focus,
|
||||
input[type='button'].button-outline:hover,
|
||||
input[type='reset'].button-outline:focus,
|
||||
input[type='reset'].button-outline:hover,
|
||||
input[type='submit'].button-outline:focus,
|
||||
input[type='submit'].button-outline:hover {
|
||||
background-color: transparent;
|
||||
border-color: #606c76;
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
|
||||
button.button-outline[disabled]:focus,
|
||||
button.button-outline[disabled]:hover,
|
||||
input[type='button'].button-outline[disabled]:focus,
|
||||
input[type='button'].button-outline[disabled]:hover,
|
||||
input[type='reset'].button-outline[disabled]:focus,
|
||||
input[type='reset'].button-outline[disabled]:hover,
|
||||
input[type='submit'].button-outline[disabled]:focus,
|
||||
input[type='submit'].button-outline[disabled]:hover {
|
||||
border-color: inherit;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-clear,
|
||||
button.button-clear,
|
||||
input[type='button'].button-clear,
|
||||
input[type='reset'].button-clear,
|
||||
input[type='submit'].button-clear {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
.button.button-clear:focus, .button.button-clear:hover,
|
||||
button.button-clear:focus,
|
||||
button.button-clear:hover,
|
||||
input[type='button'].button-clear:focus,
|
||||
input[type='button'].button-clear:hover,
|
||||
input[type='reset'].button-clear:focus,
|
||||
input[type='reset'].button-clear:hover,
|
||||
input[type='submit'].button-clear:focus,
|
||||
input[type='submit'].button-clear:hover {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
|
||||
button.button-clear[disabled]:focus,
|
||||
button.button-clear[disabled]:hover,
|
||||
input[type='button'].button-clear[disabled]:focus,
|
||||
input[type='button'].button-clear[disabled]:hover,
|
||||
input[type='reset'].button-clear[disabled]:focus,
|
||||
input[type='reset'].button-clear[disabled]:hover,
|
||||
input[type='submit'].button-clear[disabled]:focus,
|
||||
input[type='submit'].button-clear[disabled]:hover {
|
||||
color: #9b4dca;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f4f5f6;
|
||||
border-radius: .4rem;
|
||||
font-size: 86%;
|
||||
margin: 0 .2rem;
|
||||
padding: .2rem .5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f4f5f6;
|
||||
border-left: 0.3rem solid #9b4dca;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 0.1rem solid #f4f5f6;
|
||||
margin: 3.0rem 0;
|
||||
}
|
||||
|
||||
input[type='email'],
|
||||
input[type='number'],
|
||||
input[type='password'],
|
||||
input[type='search'],
|
||||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='url'],
|
||||
textarea,
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0.1rem solid #d1d1d1;
|
||||
border-radius: .4rem;
|
||||
box-shadow: none;
|
||||
box-sizing: inherit;
|
||||
height: 3.8rem;
|
||||
padding: .6rem 1.0rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type='email']:focus,
|
||||
input[type='number']:focus,
|
||||
input[type='password']:focus,
|
||||
input[type='search']:focus,
|
||||
input[type='tel']:focus,
|
||||
input[type='text']:focus,
|
||||
input[type='url']:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: #9b4dca;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||
padding-right: 3.0rem;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 6.5rem;
|
||||
}
|
||||
|
||||
label,
|
||||
legend {
|
||||
display: block;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.label-inline {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 112.0rem;
|
||||
padding: 0 2.0rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row.row-no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row.row-no-padding > .column {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.row.row-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row.row-top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.row.row-bottom {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.row.row-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row.row-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.row.row-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.row .column {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
margin-left: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-10 {
|
||||
margin-left: 10%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-20 {
|
||||
margin-left: 20%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-25 {
|
||||
margin-left: 25%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-33, .row .column.column-offset-34 {
|
||||
margin-left: 33.3333%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-50 {
|
||||
margin-left: 50%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-66, .row .column.column-offset-67 {
|
||||
margin-left: 66.6666%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-75 {
|
||||
margin-left: 75%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-80 {
|
||||
margin-left: 80%;
|
||||
}
|
||||
|
||||
.row .column.column-offset-90 {
|
||||
margin-left: 90%;
|
||||
}
|
||||
|
||||
.row .column.column-10 {
|
||||
flex: 0 0 10%;
|
||||
max-width: 10%;
|
||||
}
|
||||
|
||||
.row .column.column-20 {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.row .column.column-25 {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.row .column.column-33, .row .column.column-34 {
|
||||
flex: 0 0 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
|
||||
.row .column.column-40 {
|
||||
flex: 0 0 40%;
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
.row .column.column-50 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.row .column.column-60 {
|
||||
flex: 0 0 60%;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.row .column.column-66, .row .column.column-67 {
|
||||
flex: 0 0 66.6666%;
|
||||
max-width: 66.6666%;
|
||||
}
|
||||
|
||||
.row .column.column-75 {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.row .column.column-80 {
|
||||
flex: 0 0 80%;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.row .column.column-90 {
|
||||
flex: 0 0 90%;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.row .column .column-top {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.row .column .column-bottom {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.row .column .column-center {
|
||||
-ms-grid-row-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
margin-left: -1.0rem;
|
||||
width: calc(100% + 2.0rem);
|
||||
}
|
||||
.row .column {
|
||||
margin-bottom: inherit;
|
||||
padding: 0 1.0rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #9b4dca;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:focus, a:hover {
|
||||
color: #606c76;
|
||||
}
|
||||
|
||||
dl,
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
dl dl,
|
||||
dl ol,
|
||||
dl ul,
|
||||
ol dl,
|
||||
ol ol,
|
||||
ol ul,
|
||||
ul dl,
|
||||
ul ol,
|
||||
ul ul {
|
||||
font-size: 90%;
|
||||
margin: 1.5rem 0 1.5rem 3.0rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal inside;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: circle inside;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
dd,
|
||||
dt,
|
||||
li {
|
||||
margin-bottom: 1.0rem;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
form,
|
||||
ol,
|
||||
p,
|
||||
pre,
|
||||
table,
|
||||
ul {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 0.1rem solid #e1e1e1;
|
||||
padding: 1.2rem 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
td:last-child,
|
||||
th:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 300;
|
||||
letter-spacing: -.1rem;
|
||||
margin-bottom: 2.0rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.6rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.6rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.8rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: -.08rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.8rem;
|
||||
letter-spacing: -.05rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=milligram.css.map */
|
||||
Vendored
-349
@@ -1,349 +0,0 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,43 @@
|
||||
# 🦉 How to debug Owl applications 🦉
|
||||
|
||||
Non trivial applications become quickly more difficult to understand. It is then
|
||||
useful to have a solid understanding of what is going on. To help with that,
|
||||
logging useful information is extremely valuable. There is a [javascript file](../../tools/debug.js) which can be evaluated in an application.
|
||||
|
||||
Once it is executed, it will log a lot of information on each component main hooks. The following code is a minified version to make it easier to copy/paste:
|
||||
|
||||
```
|
||||
function debugOwl(t,e){let n,o="[OWL_DEBUG]";function r(t){let e;try{e=JSON.stringify(t||{})}catch(t){e="<JSON error>"}return e.length>200&&(e=e.slice(0,200)+"..."),e}if(Object.defineProperty(t.Component,"current",{get:()=>n,set(s){n=s;const i=s.constructor.name;if(e.componentBlackList&&e.componentBlackList.test(i))return;if(e.componentWhiteList&&!e.componentWhiteList.test(i))return;let l;Object.defineProperty(n,"__owl__",{get:()=>l,set(n){!function(n,s,i){let l=`${s}<id=${i}>`,c=t=>console.log(`${o} ${l} ${t}`),u=t=>(!e.methodBlackList||!e.methodBlackList.includes(t))&&!(e.methodWhiteList&&!e.methodWhiteList.includes(t));u("constructor")&&c(`constructor, props=${r(n.props)}`);u("willStart")&&t.hooks.onWillStart(()=>{c("willStart")});u("mounted")&&t.hooks.onMounted(()=>{c("mounted")});u("willUpdateProps")&&t.hooks.onWillUpdateProps(t=>{c(`willUpdateProps, nextprops=${r(t)}`)});u("willPatch")&&t.hooks.onWillPatch(()=>{c("willPatch")});u("patched")&&t.hooks.onPatched(()=>{c("patched")});u("willUnmount")&&t.hooks.onWillUnmount(()=>{c("willUnmount")});const d=n.__render.bind(n);n.__render=function(...t){c("rendering template"),d(...t)};const h=n.render.bind(n);n.render=function(...t){const e=n.__owl__;let o="render";return e.isMounted||e.currentFiber||(o+=" (warning: component is not mounted, this render has no effect)"),c(o),h(...t)};const p=n.mount.bind(n);n.mount=function(...t){return c("mount"),p(...t)}}(s,i,(l=n).id)}})}}),e.logScheduler){let e=t.Component.scheduler.start,n=t.Component.scheduler.stop;t.Component.scheduler.start=function(){this.isRunning||console.log(`${o} scheduler: start running tasks queue`),e.call(this)},t.Component.scheduler.stop=function(){this.isRunning&&console.log(`${o} scheduler: stop running tasks queue`),n.call(this)}}if(e.logStore){let e=t.Store.prototype.dispatch;t.Store.prototype.dispatch=function(t,...n){return console.log(`${o} store: action '${t}' dispatched. Payload: '${r(n)}'`),e.call(this,t,...n)}}}
|
||||
debugOwl(owl, {
|
||||
// componentBlackList: /App/, // regexp
|
||||
// componentWhiteList: /SomeComponent/, // regexp
|
||||
// methodBlackList: ["mounted"], // list of method names
|
||||
// methodWhiteList: ["willStart"], // list of method names
|
||||
logScheduler: false, // display/mute scheduler logs
|
||||
logStore: true, // display/mute store logs
|
||||
});
|
||||
```
|
||||
|
||||
The above code, once pasted somewhere in the main javascript file of an owl
|
||||
application, will log information looking like this:
|
||||
|
||||
```
|
||||
[OWL_DEBUG] TodoApp<id=1> constructor, props={}
|
||||
[OWL_DEBUG] TodoApp<id=1> mount
|
||||
[OWL_DEBUG] TodoApp<id=1> willStart
|
||||
[OWL_DEBUG] TodoApp<id=1> rendering template
|
||||
[OWL_DEBUG] TodoItem<id=2> constructor, props={"id":2,"completed":false,"title":"hey"}
|
||||
[OWL_DEBUG] TodoItem<id=2> willStart
|
||||
[OWL_DEBUG] TodoItem<id=3> constructor, props={"id":4,"completed":false,"title":"aaa"}
|
||||
[OWL_DEBUG] TodoItem<id=3> willStart
|
||||
[OWL_DEBUG] TodoItem<id=2> rendering template
|
||||
[OWL_DEBUG] TodoItem<id=3> rendering template
|
||||
[OWL_DEBUG] TodoItem<id=3> mounted
|
||||
[OWL_DEBUG] TodoItem<id=2> mounted
|
||||
[OWL_DEBUG] TodoApp<id=1> mounted
|
||||
```
|
||||
|
||||
Each component has an internal `id`, which is very useful when debugging.
|
||||
|
||||
Note that it is certainly useful to run this code at some point in an application,
|
||||
just to get a feel of what each user action implies, for the framework.
|
||||
@@ -0,0 +1,130 @@
|
||||
# 🦉 How to test Components 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Unit Tests](#unit-tests)
|
||||
|
||||
## Overview
|
||||
|
||||
It is a good practice to test applications and components to ensure that they
|
||||
behave as expected. There are many ways to test a user interface: manual
|
||||
testing, integration testing, unit testing, ...
|
||||
|
||||
In this section, we will discuss how to write unit tests for components.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
Writing unit tests for Owl components really depends on the testing framework
|
||||
used in a project. But usually, it involves the following steps:
|
||||
|
||||
- create a test file: for example `SomeComponent.test.js`,
|
||||
- in that file, import the code for `SomeComponent`,
|
||||
- add a test case:
|
||||
- create a real DOM element to use as test fixture,
|
||||
- create a test environment
|
||||
- create an instance of `SomeComponent`, mount it to the fixture
|
||||
- interact with the component and assert some properties.
|
||||
|
||||
To help with this, it is useful to have a `helper.js` file that contains some
|
||||
common utility functions:
|
||||
|
||||
```js
|
||||
export function makeTestFixture() {
|
||||
let fixture = document.createElement("div");
|
||||
document.body.appendChild(fixture);
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export function nextTick() {
|
||||
let requestAnimationFrame = owl.Component.scheduler.requestAnimationFrame;
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
export function makeTestEnv() {
|
||||
// application specific. It needs a way to load actual templates
|
||||
const templates = ...;
|
||||
|
||||
return {
|
||||
qweb: new QWeb(templates),
|
||||
..., // each service can be mocked here
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
With such a file, a typical test suite for Jest will look like this:
|
||||
|
||||
```js
|
||||
// in SomeComponent.test.js
|
||||
import { SomeComponent } from "../../src/ui/SomeComponent";
|
||||
import { nextTick, makeTestFixture, makeTestEnv} from '../helpers';
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup
|
||||
//------------------------------------------------------------------------------
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
// we set here the default environment for each component created in the test
|
||||
Component.env = env;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
describe("SomeComponent", () => {
|
||||
test("component behaves as expected", async () => {
|
||||
const props = {...}; // depends on the component
|
||||
const comp = await mount(SomeComponent, { target: fixture, props });
|
||||
|
||||
// do some assertions
|
||||
expect(...).toBe(...);
|
||||
|
||||
fixture.querySelector('button').click();
|
||||
await nextTick();
|
||||
|
||||
// some other assertions
|
||||
expect(...).toBe(...);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note that Owl does wait for the next animation frame to actually update the DOM.
|
||||
This is why it is necessary to wait with the `nextTick` (or other methods) to
|
||||
make sure that the DOM is up-to-date.
|
||||
|
||||
It is sometimes useful to wait until Owl is completely done updating components
|
||||
(in particular, if we have a highly concurrent user interface). This next
|
||||
helper simply polls every 20ms the internal Owl task queue and returns a promise
|
||||
which resolves when it is empty:
|
||||
|
||||
```js
|
||||
function afterUpdates() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timer = setTimeout(poll, 20);
|
||||
let counter = 0;
|
||||
function poll() {
|
||||
counter++;
|
||||
if (owl.Component.scheduler.tasks.length) {
|
||||
if (counter > 10) {
|
||||
reject(new Error("timeout"));
|
||||
} else {
|
||||
timer = setTimeout(poll);
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
# 🦉 How to write Single File Components 🦉
|
||||
|
||||
It is very useful to group code by feature instead of by type of file. It makes
|
||||
it easier to scale application to larger size.
|
||||
|
||||
To do so, Owl has two small helpers that make it easy to define a
|
||||
template or a stylesheet inside a javascript (or typescript) file: the
|
||||
[`xml`](../reference/tags.md#xml-tag) and [`css`](../reference/tags.md#css-tag)
|
||||
helper.
|
||||
|
||||
This means that the template, the style and the javascript code can be defined in
|
||||
the same file. For example:
|
||||
|
||||
```js
|
||||
const { Component } = owl;
|
||||
const { xml, css } = owl.tags;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TEMPLATE
|
||||
// -----------------------------------------------------------------------------
|
||||
const TEMPLATE = xml/* xml */ `
|
||||
<div class="main">
|
||||
<Sidebar/>
|
||||
<Content />
|
||||
</div>`;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// STYLE
|
||||
// -----------------------------------------------------------------------------
|
||||
const STYLE = css/* css */ `
|
||||
.main {
|
||||
display: grid;
|
||||
grid-template-columns: 200px auto;
|
||||
}
|
||||
`;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// CODE
|
||||
// -----------------------------------------------------------------------------
|
||||
class Main extends Component {
|
||||
static template = TEMPLATE;
|
||||
static style = STYLE;
|
||||
static components = { Sidebar, Content };
|
||||
|
||||
// rest of component...
|
||||
}
|
||||
```
|
||||
|
||||
Note that the above example has an inline xml comment, just after the `xml` call.
|
||||
This is useful for some editor plugins, such as the VS Code addon
|
||||
`Comment tagged template`, which, if installed, add syntax highlighting to the
|
||||
content of the template string.
|
||||
@@ -0,0 +1,133 @@
|
||||
# 🦉 Quick Overview 🦉
|
||||
|
||||
Owl components in an application are used to define a (dynamic) tree of components.
|
||||
|
||||
```
|
||||
Root
|
||||
/ \
|
||||
A B
|
||||
/ \
|
||||
C D
|
||||
```
|
||||
|
||||
**State:** each component can manage its own local state. It is a simple ES6
|
||||
class, there are no special rules:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = { value: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The example above shows a component with a local state. Note that since there
|
||||
is nothing magical to the `state` object, we need to manually call the `render`
|
||||
function whenever we update it. This can quickly become annoying (and not
|
||||
efficient if we do it too much). There is a better way: using the `useState`
|
||||
hook, which transforms an object into a reactive version of itself:
|
||||
|
||||
```js
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the `t-on-click` handler can even be replaced by an inline statement:
|
||||
|
||||
```xml
|
||||
<button t-on-click="state.value++">
|
||||
```
|
||||
|
||||
**Props:** sub components often needs some information from their parents. This
|
||||
is done by adding the required information to the template. This will then be
|
||||
accessible by the sub component in the `props` object. Note that there is an
|
||||
important rule here: the information contained in the `props` object is not
|
||||
owned by the sub component, and should never be modified.
|
||||
|
||||
```js
|
||||
class Child extends Component {
|
||||
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child name="'Owl'" />
|
||||
<Child name="'Framework'" />
|
||||
</div>`;
|
||||
static components = { Child };
|
||||
}
|
||||
```
|
||||
|
||||
**Communication:** there are multiple ways to communicate information between
|
||||
components. However, the two most important ways are the following:
|
||||
|
||||
- from parent to children: by using `props`,
|
||||
- from a children to one of its parent: by triggering events.
|
||||
|
||||
The following example illustrate both mechanisms:
|
||||
|
||||
```js
|
||||
class OrderLine extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="add">
|
||||
<div><t t-esc="props.line.name"/></div>
|
||||
<div>Quantity: <t t-esc="props.line.quantity"/></div>
|
||||
</div>`;
|
||||
|
||||
add() {
|
||||
this.trigger("add-to-order", { line: this.props.line });
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div t-on-add-to-order="addToOrder">
|
||||
<OrderLine
|
||||
t-foreach="orders"
|
||||
t-as="line"
|
||||
line="line" />
|
||||
</div>`;
|
||||
static components = { OrderLine };
|
||||
orders = useState([
|
||||
{ id: 1, name: "Coffee", quantity: 0 },
|
||||
{ id: 2, name: "Tea", quantity: 0 },
|
||||
]);
|
||||
|
||||
addToOrder(event) {
|
||||
const line = event.detail.line;
|
||||
line.quantity++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `OrderLine` component trigger a `add-to-order` event. This
|
||||
will generate a DOM event which will bubble along the DOM tree. It will then be
|
||||
intercepted by the parent component, which will then get the line (from the
|
||||
`detail` key) and then increment its quantity. See the page on [event handling](../reference/event_handling.md)
|
||||
for more details on how events work.
|
||||
|
||||
Note that this example would have also worked if the `OrderLine` component
|
||||
directly modifies the `line` object. However, this is not a good practice: this
|
||||
only works because the `props` object received by the child component is reactive,
|
||||
so the child component is then coupled to the parents implementation.
|
||||
@@ -0,0 +1,408 @@
|
||||
# 🦉 How to start an Owl project 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Simple html file](#simple-html-file)
|
||||
- [With a static server](#with-a-static-server)
|
||||
- [Standard Javascript project](#standard-javascript-project)
|
||||
|
||||
## Overview
|
||||
|
||||
Each software project has its specific needs. Many of these needs can be solved
|
||||
with some tooling: `webpack`, `gulp`, css preprocessor, bundlers, transpilers, ...
|
||||
|
||||
Because of that, it is usually not simple to just start a project. Some
|
||||
frameworks provide their own tooling to help with that. But then, you have to
|
||||
integrate and learn how these applications work.
|
||||
|
||||
Owl is designed to be used with no tooling at all. Because of that, Owl can
|
||||
"easily" be integrated in a modern build toolchain. In this section, we will
|
||||
discuss a few different setups to start a project. Each of these setups has
|
||||
advantages and disadvantages in different situations.
|
||||
|
||||
## Simple html file
|
||||
|
||||
The simplest possible setup is the following: a simple javascript file with your
|
||||
code. To do that, let us create the following file structure:
|
||||
|
||||
```
|
||||
hello_owl/
|
||||
index.html
|
||||
owl.js
|
||||
app.js
|
||||
```
|
||||
|
||||
The file `owl.js` can be downloaded from the last release published at
|
||||
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
|
||||
is a single javascript file which export all Owl into the global `owl` object.
|
||||
|
||||
Now, `index.html` should contain the following:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
<script src="owl.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
And `app.js` should look like this:
|
||||
|
||||
```js
|
||||
const { Component, mount } = owl;
|
||||
const { xml } = owl.tags;
|
||||
const { whenReady } = owl.utils;
|
||||
|
||||
// Owl Components
|
||||
class App extends Component {
|
||||
static template = xml`<div>Hello Owl</div>`;
|
||||
}
|
||||
|
||||
// Setup code
|
||||
function setup() {
|
||||
mount(App, target: { document.body })
|
||||
}
|
||||
|
||||
whenReady(setup);
|
||||
```
|
||||
|
||||
Now, simply loading this html file in a browser should display a welcome message.
|
||||
This setup is not fancy, but it is extremely simple. There are no tooling at
|
||||
all required. It can be slightly optimized by using the minified build of Owl.
|
||||
|
||||
## With a static server
|
||||
|
||||
The previous setup has a big disadvantage: the application code is located in a
|
||||
single file. Obviously, we could split it in several files and add multiple
|
||||
`<script>` tags in the html page, but then we need to make sure the script are
|
||||
inserted in the proper order, we need to export each file content in global
|
||||
variables and we lose autocompletion across files.
|
||||
|
||||
There is a low tech solution to this issue: using native javascript modules.
|
||||
This however has a requirement: for security reasons, browsers will not accept
|
||||
modules on content served through the `file` protocol. This means that we need
|
||||
to use a static server.
|
||||
|
||||
Let us start a new project with the following file structure:
|
||||
|
||||
```
|
||||
hello_owl/
|
||||
src/
|
||||
app.js
|
||||
index.html
|
||||
main.js
|
||||
owl.js
|
||||
```
|
||||
|
||||
As previously, the file `owl.js` can be downloaded from the last release published at
|
||||
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
|
||||
|
||||
Now, `index.html` should contain the following:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
<script src="owl.js"></script>
|
||||
<script src="main.js" type="module"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Not that the `main.js` script tag has the `type="module"` attribute. This means
|
||||
that the browser will parse the script as a module, and load all its dependencies.
|
||||
|
||||
Here is the content of `app.js` and `main.js`:
|
||||
|
||||
```js
|
||||
// app.js ----------------------------------------------------------------------
|
||||
const { Component, mount } = owl;
|
||||
const { xml } = owl.tags;
|
||||
|
||||
export class App extends Component {
|
||||
static template = xml`<div>Hello Owl</div>`;
|
||||
}
|
||||
|
||||
// main.js ---------------------------------------------------------------------
|
||||
import { App } from "./app.js";
|
||||
|
||||
function setup() {
|
||||
mount(App, { target: document.body });
|
||||
}
|
||||
|
||||
owl.utils.whenReady(setup);
|
||||
```
|
||||
|
||||
The `main.js` file import the `app.js` file. Note that the import statement has
|
||||
a `.js` suffix, which is important. Most text editor can understand this syntax
|
||||
and will provide autocompletion.
|
||||
|
||||
Now, to execute this code, we need to serve the `src` folder statically. A low
|
||||
tech way to do that is to use for example the python `SimpleHTTPServer` feature:
|
||||
|
||||
```
|
||||
$ cd src
|
||||
$ python -m SimpleHTTPServer 8022 # now content is available at localhost:8022
|
||||
```
|
||||
|
||||
Another more "javascripty" way to do it is to create a `npm` application. To do
|
||||
that, we can add the following `package.json` file at the root of the project:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "hello_owl",
|
||||
"version": "0.1.0",
|
||||
"description": "Starting Owl app",
|
||||
"main": "src/index.html",
|
||||
"scripts": {
|
||||
"serve": "serve src"
|
||||
},
|
||||
"author": "John",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"serve": "^11.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can now install the `serve` tool with the command `npm install`, and then,
|
||||
start a static server with the simple `npm run serve` command.
|
||||
|
||||
## Standard Javascript project
|
||||
|
||||
The previous setup works, and is certainly good for some usecases, including
|
||||
quick prototyping. However, it lacks some useful features, such as livereload,
|
||||
a test suite, or bundling the code in a single file.
|
||||
|
||||
Each of these features, and many others, can be done in many different ways.
|
||||
Since it is really not trivial to configure such a project, we provide here an
|
||||
example that can be used as a starting point.
|
||||
|
||||
Our standard Owl project has the following file structure:
|
||||
|
||||
```
|
||||
hello_owl/
|
||||
public/
|
||||
index.html
|
||||
src/
|
||||
components/
|
||||
App.js
|
||||
main.js
|
||||
tests/
|
||||
components/
|
||||
App.test.js
|
||||
helpers.js
|
||||
.gitignore
|
||||
package.json
|
||||
webpack.config.js
|
||||
```
|
||||
|
||||
This project as a `public` folder, meant to contain all static assets, such as
|
||||
images and styles. The `src` folder has the javascript source code, and finally,
|
||||
`tests` contains the test suite.
|
||||
|
||||
Here is the content of `index.html`:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Note that there are no `<script>` tag here. They will be injected by webpack.
|
||||
Now, let's have a look at the javascript files:
|
||||
|
||||
```js
|
||||
// src/components/App.js -------------------------------------------------------
|
||||
import { Component, tags, useState } from "@odoo/owl";
|
||||
|
||||
const { xml } = tags;
|
||||
|
||||
export class App extends Component {
|
||||
static template = xml`<div t-on-click="update">Hello <t t-esc="state.text"/></div>`;
|
||||
state = useState({ text: "Owl" });
|
||||
update() {
|
||||
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
|
||||
}
|
||||
}
|
||||
|
||||
// src/main.js -----------------------------------------------------------------
|
||||
import { utils, mount } from "@odoo/owl";
|
||||
import { App } from "./components/App";
|
||||
|
||||
function setup() {
|
||||
mount(App, { target: document.body });
|
||||
}
|
||||
|
||||
utils.whenReady(setup);
|
||||
|
||||
// tests/components/App.test.js ------------------------------------------------
|
||||
import { App } from "../../src/components/App";
|
||||
import { makeTestFixture, nextTick, click } from "../helpers";
|
||||
import { mount } from "@odoo/owl";
|
||||
|
||||
let fixture;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
describe("App", () => {
|
||||
test("Works as expected...", async () => {
|
||||
await mount(App, { target: fixture });
|
||||
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
|
||||
|
||||
click(fixture, "div");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
|
||||
});
|
||||
});
|
||||
|
||||
// tests/helpers.js ------------------------------------------------------------
|
||||
import { Component } from "@odoo/owl";
|
||||
import "regenerator-runtime/runtime";
|
||||
|
||||
export async function nextTick() {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
export function makeTestFixture() {
|
||||
let fixture = document.createElement("div");
|
||||
document.body.appendChild(fixture);
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export function click(elem, selector) {
|
||||
elem.querySelector(selector).dispatchEvent(new Event("click"));
|
||||
}
|
||||
```
|
||||
|
||||
Finally, here is the configuration files `.gitignore`, `package.json` and
|
||||
`webpack.config.js`:
|
||||
|
||||
```
|
||||
node_modules/
|
||||
package-lock.json
|
||||
dist/
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "hello_owl",
|
||||
"version": "0.1.0",
|
||||
"description": "Demo app",
|
||||
"main": "src/index.html",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack-dev-server --mode development"
|
||||
},
|
||||
"author": "Someone",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"babel-jest": "^25.1.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "^25.1.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"serve": "^11.3.0",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.10.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@odoo/owl": "^1.0.4"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["transform-es2015-modules-commonjs"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"verbose": false,
|
||||
"testRegex": "(/tests/.*(test|spec))\\.js?$",
|
||||
"moduleFileExtensions": ["js"],
|
||||
"transform": {
|
||||
"^.+\\.[t|j]sx?$": "babel-jest"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
const host = process.env.HOST || "localhost";
|
||||
|
||||
module.exports = function (env, argv) {
|
||||
const mode = argv.mode || "development";
|
||||
return {
|
||||
mode: mode,
|
||||
entry: "./src/main.js",
|
||||
output: {
|
||||
filename: "main.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
loader: "babel-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".jsx"],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.resolve(__dirname, "public/index.html"),
|
||||
compress: true,
|
||||
hot: true,
|
||||
host,
|
||||
port: 3000,
|
||||
publicPath: "/",
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: path.resolve(__dirname, "public/index.html"),
|
||||
}),
|
||||
],
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
With this setup, we can now use the following script commands:
|
||||
|
||||
```
|
||||
npm run build # build the full application in prod mode in dist/
|
||||
|
||||
npm run dev # start a dev server with livereload
|
||||
|
||||
npm run test # run the jest test suite
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,345 @@
|
||||
# Comparison with Vue/React
|
||||
|
||||
OWL, React and Vue have the same main feature: they allow developers to build
|
||||
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
|
||||
|
||||
In this page, we try to highlight some of these differences. Obviously, a lot of
|
||||
effort was put to be fair. However, if you disagree with some of the points
|
||||
discussed, feel free to open an issue/submit a PR to correct this text.
|
||||
|
||||
## Content
|
||||
|
||||
- [Size](#size)
|
||||
- [Class Based](#class-based)
|
||||
- [Tooling/Build Step](#toolingbuild-step)
|
||||
- [Templating](#templating)
|
||||
- [Asynchronous rendering](#asynchronous-rendering)
|
||||
- [Reactiveness](#reactiveness)
|
||||
- [State Management](#state-management)
|
||||
- [Hooks](#hooks)
|
||||
|
||||
## Size
|
||||
|
||||
OWL is intended to be small and to work at a slightly lower level of abstraction
|
||||
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
|
||||
|
||||
| Framework | Size (minified, gzipped) |
|
||||
| ------------------------ | ------------------------ |
|
||||
| OWL | 18kb |
|
||||
| Vue + VueX | 30kb |
|
||||
| Vue + VueX + Vue Router | 39kb |
|
||||
| React + ReactDOM + Redux | 40kb |
|
||||
| jQuery | 30kb |
|
||||
|
||||
Note that those comparisons are not entirely fair, because we do not compare
|
||||
the same exact set of features. For example, VueX and Vue Router support more
|
||||
advanced use cases.
|
||||
|
||||
## Class Based
|
||||
|
||||
Both React and Vue moved away from defining components with classes. They prefer
|
||||
a more functional approach, in particular, with the new `hooks` mechanisms.
|
||||
|
||||
This has some advantages and disadvantages. But the end result is that React
|
||||
and Vue both offers multiple different ways of defining new components. In
|
||||
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
||||
components are fast enough for all our usecases, and making it as simple as
|
||||
possible for developers is more valuable (for us).
|
||||
|
||||
Also, functions or class based components are more than just syntax. Functions
|
||||
come with a mindset of composition and class are about inheritance. Clearly,
|
||||
both of these are important mechanisms for reusing code. Also, one does not
|
||||
exclude the other.
|
||||
|
||||
It certainly looks like the world of UI frameworks is moving toward composition,
|
||||
for many very good reasons. Owl is still good at composition (for example,
|
||||
Owl supports slots, which is the primary mechanism to make generic reusable
|
||||
components). But it can also use inheritance (and this is very important since
|
||||
templates can also be inherited with `xpaths` transformations).
|
||||
|
||||
## Tooling/Build step
|
||||
|
||||
OWL is designed to be easy to use in a standalone way. For various reasons,
|
||||
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
|
||||
be used by simply adding a script tag to a page.
|
||||
|
||||
```html
|
||||
<script src="owl.min.js" />
|
||||
```
|
||||
|
||||
In comparison, React encourages using JSX, which necessitate a build step, and
|
||||
most Vue applications uses single file components, which also necessitate a build step.
|
||||
|
||||
On the flipside, external tooling may make it harder to use in some case, but it
|
||||
also brings a lot of benefits. And React/Vue have both a large ecosystem.
|
||||
|
||||
Note that since Owl is not dependant on any external tool nor libraries, it is
|
||||
very easy to integrate into any build toolchain. Also, since we cannot rely on
|
||||
additional tools, we made a lot of effort to make the most of the web platform.
|
||||
|
||||
For example, Owl uses the standard `xml` parser that comes with every browser.
|
||||
Because of that, Owl did not have to write its own template parser. Another
|
||||
example is the [`xml`](../reference/tags.md#xml-tag) tag helper function, which makes use of
|
||||
native template literals to allow in a natural way to write `xml` templates
|
||||
directly in the javascript code. This can be easily integrated with editor
|
||||
plugins to have autocompletion inside the template.
|
||||
|
||||
## Templating
|
||||
|
||||
OWL uses its own QWeb engine, which compiles templates on the
|
||||
frontend, as they are needed. This is extremely convenient for our use case, in
|
||||
particular because templates are described in XML files, and can be modified by
|
||||
XPaths. Since Odoo is at its heart a modular application, this is an important
|
||||
feature for us.
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<button t-on-click="increment">Click Me! [<t t-esc="state.value"/>]</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
Vue is actually kind of similar. Its template language is kind of close to QWeb,
|
||||
with the `v` replaced by the `t`. However, it is also more fully featured. For
|
||||
example, Vue templates have slots, or event modifiers. A large difference is that
|
||||
most Vue applications will need to be built ahead of time, to compile the templates
|
||||
into javascript functions. Note that Vue has a separate build which includes the
|
||||
template compiler.
|
||||
|
||||
In contrast, most React applications do not use a templating language, but write
|
||||
some JSX code, which is precompiled into plain JavaScript by a build step. This
|
||||
example is done with the (kind of outdated) React class system:
|
||||
|
||||
```jsx
|
||||
class Clock extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello, world!</h1>
|
||||
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This has the advantage of having the full power of Javascript, but is less
|
||||
structured than a template language. Note that the tooling is quite impressive:
|
||||
there is a syntax highlighter for jsx here on github!
|
||||
|
||||
By comparison, here is the equivalent Owl component, written with the
|
||||
[`xml`](../reference/tags.md#xml-tag) tag helper:
|
||||
|
||||
```js
|
||||
class Clock extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<h1>Hello, world!</h1>
|
||||
<h2>It is {props.date.toLocaleTimeString()}.</h2>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## Asynchronous Rendering
|
||||
|
||||
This is actually a big difference between OWL and React/Vue: components in OWL
|
||||
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
|
||||
|
||||
- `willStart` (before the component starts rendering)
|
||||
- `willUpdateProps` (before new props are set)
|
||||
|
||||
Both these methods can be implemented and return a promise. The rendering will
|
||||
then wait for these promises to be completed before patching the DOM. This is
|
||||
useful for some use cases: for example, a component may want to fetch an external
|
||||
library (a calendar component may need a specialized calendar rendering library),
|
||||
in its willStart hook.
|
||||
|
||||
```javascript
|
||||
class MyCalendarComponent extends owl.Component {
|
||||
...
|
||||
|
||||
willStart() {
|
||||
return utils.lazyLoad('static/libs/fullcalendar/fullcalendar.js');
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This may be dangerous (to stop the rendering waiting for the network), but it is
|
||||
extremely powerful as well, as demonstrated by the Odoo Web Client.
|
||||
|
||||
Lazy loading static libraries can obviously be done with React/Vue, but it is
|
||||
more convoluted. For example, in Vue, you need to use a dynamic import keyword
|
||||
that needs to be transpiled at build time in order for the component to be loaded
|
||||
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
|
||||
|
||||
## Reactiveness
|
||||
|
||||
React has a simple model: whenever the state changes, it is
|
||||
replaced with a new state (via the `setState` method). Then, the DOM is patched.
|
||||
This is simple, efficient, and a little bit awkward to write.
|
||||
|
||||
Vue is a little bit different: it replace magically the properties in the state
|
||||
by getters/setters. With that, it can notify components whenever the state that
|
||||
they read was changed.
|
||||
|
||||
Owl is closer to vue: it also tracks magically the state properties, but it does
|
||||
only increment an internal counter whenever it changes. Note that it is done
|
||||
with a `Proxy`, which means that it is totally transparent to the developers.
|
||||
Adding new keys is supported. Once any part of the state has been changed, a
|
||||
rendering is scheduled in the next microtask tick (promise queue).
|
||||
|
||||
## State Management
|
||||
|
||||
Managing the state of an application is a tricky issue. Many solutions have
|
||||
been proposed these last few years. It also depends on the kind of application we
|
||||
are talking about. A small application may not need much more than a simple
|
||||
object to contain its state.
|
||||
|
||||
However, there are some common solutions for React and Vue: redux and vuex.
|
||||
Both of them are a centralized store that own the state, and they dictate how
|
||||
the state can be mutated.
|
||||
|
||||
**Redux**
|
||||
|
||||
In Redux, the state is mutated by reducers. Reducers are functions
|
||||
that modify the state by returning a different object:
|
||||
|
||||
```javascript
|
||||
...
|
||||
switch (action.type) {
|
||||
case ADD_TODO: {
|
||||
const { id, content } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
allIds: [...state.allIds, id],
|
||||
byIds: {
|
||||
...state.byIds,
|
||||
[id]: {
|
||||
content,
|
||||
completed: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This is a little bit awkward to write, but this allows the component system to
|
||||
check if a part of the state was changed. This is exactly what is done by the
|
||||
`connect` function: it create a _connected_ component, which is subscribed to
|
||||
the state and triggers a rerender if some part of the state was modified.
|
||||
|
||||
**VueX**
|
||||
|
||||
VueX is based on a different principle: the state is mutated through
|
||||
some special functions (the mutations), which modify the state in place:
|
||||
|
||||
```javascript
|
||||
function ({state}, payload) {
|
||||
const { id, content } = payload;
|
||||
const message = {id, content, completed: false};
|
||||
state.messages.push(message)
|
||||
}
|
||||
```
|
||||
|
||||
This is simpler, but there is a little bit more happening behind the scene:
|
||||
each key from the state is silently replaced by getters and setters, and VueX
|
||||
keeps track of who get data, and retrigger a render when it was changed.
|
||||
|
||||
**Owl**
|
||||
|
||||
Owl store is a little bit like a mix of redux and vuex: it has actions (but not
|
||||
mutations), and like VueX, it keeps track of the state changes. However, it does
|
||||
not notify a component when the state changes. Instead, components need to connect
|
||||
to the store like in redux, with the `useStore` hook (see the [store documentation](../reference/store.md#connecting-a-component)).
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
increment({ state }, val) {
|
||||
state.counter.value += val;
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
counter: { value: 0 },
|
||||
};
|
||||
const store = new owl.Store({ state, actions });
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||
Click Me! [<t t-esc="counter.value"/>]
|
||||
</button>`;
|
||||
counter = useStore((state) => state.counter);
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
|
||||
Counter.env.store = store;
|
||||
const counter = new Counter();
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
|
||||
the React world. They solve a lot of seemingly unconnected problems: attach
|
||||
reusable behavior to a component, in a composable way, extract stateful logic
|
||||
from a component or reuse stateful logic between component, without changing your
|
||||
component hierarchy.
|
||||
|
||||
Here is an example of the React `useState` hook:
|
||||
|
||||
```js
|
||||
import React, { useState } from "react";
|
||||
|
||||
function Example() {
|
||||
// Declare a new state variable, which we'll call "count"
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>You clicked {count} times</p>
|
||||
<button onClick={() => setCount(count + 1)}>Click me</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Because of the way React designed the hooks API, they only work for functional
|
||||
components. But in that case, they really are powerful. Every major React library
|
||||
is in the process of redesigning their API with hooks (for example,
|
||||
[Redux](https://react-redux.js.org/next/api/hooks)).
|
||||
|
||||
Vue 2 does not have hooks, but the Vue project is working on its next version,
|
||||
which will feature its new [composition API](https://vue-composition-api-rfc.netlify.com/).
|
||||
This work is based on the new ideas introduced by React hooks.
|
||||
|
||||
From the way React and Vue introduce their hooks, it may look like hooks are not
|
||||
compatible with class components. However, this is not the case, as shown by
|
||||
Owl [hooks](../reference/hooks.md). They are inspired by both React and Vue. For example,
|
||||
the `useState` hook is named after React, but its API is closer to the `reactive`
|
||||
Vue hook.
|
||||
|
||||
Here is what the `Counter` example above look like in Owl:
|
||||
|
||||
```js
|
||||
import { Component, Owl } from "owl";
|
||||
import { xml } from "owl/tags";
|
||||
|
||||
class Example extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<p>You clicked {count.value} times</p>
|
||||
<button t-on-click="increment">Click me</button>
|
||||
</div>`;
|
||||
|
||||
count = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since the Owl framework had hooks from early in its life, its main APIs
|
||||
are designed to be interacted with hooks from the start. For example, the
|
||||
`Context` and `Store` abstractions.
|
||||
@@ -0,0 +1,32 @@
|
||||
# 🦉 Rendering Pipeline 🦉
|
||||
|
||||
We explain here how Owl is designed, from the perspective of its rendering
|
||||
pipeline.
|
||||
|
||||
Warning: these notes are technical by nature, and intended for people working
|
||||
on Owl (or interested in understanding its design).
|
||||
|
||||
## Overview
|
||||
|
||||
A rendering occurs in two phases:
|
||||
|
||||
- virtual rendering: this generates the virtual dom in memory, asynchronously
|
||||
- patch: applies a virtual tree to the screen (synchronously)
|
||||
|
||||
There are several classes involved in a rendering:
|
||||
|
||||
- components
|
||||
- a scheduler
|
||||
- fibers: small objects containing some metadata, associated with a rendering of
|
||||
a specific component
|
||||
|
||||
Components are organized in a dynamic component tree, visible in the user
|
||||
interface. Whenever a rendering is initiated in a component `C`:
|
||||
|
||||
- a fiber is created on `C` with the rendering props information
|
||||
- the virtual rendering phase starts on C (will asynchronously render all the
|
||||
child components)
|
||||
- the fiber is added to the scheduler, which will poll continuously, every
|
||||
animation frame, if the fiber is done
|
||||
- once it is done, the scheduler will call the task callback, which will apply
|
||||
the patch (if it was not cancelled in the meantime).
|
||||
@@ -0,0 +1,18 @@
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
Owl is a declarative component system: we declare the structure of the component
|
||||
tree, and Owl will translate that to a list of imperative operations. This
|
||||
translation is done by a virtual dom. This is the low level layer of Owl, most
|
||||
developer will not need to call directly the virtual dom functions.
|
||||
|
||||
The main idea behind a virtual dom is to keep a in-memory representation of the
|
||||
DOM (called a virtual node), and whenever some change is needed, to regenerate
|
||||
a new representation, compute the difference between the old and the new, then
|
||||
apply the changes.
|
||||
|
||||
`vdom` exports two functions:
|
||||
|
||||
- `h`: create a new virtual node
|
||||
- `patch`: compare two virtual nodes, and apply the difference.
|
||||
|
||||
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
@@ -0,0 +1,180 @@
|
||||
# 🦉 Why Owl ? 🦉
|
||||
|
||||
The common wisdom is that one should not reinvent the wheel, because that would
|
||||
waste effort and resources. It is certainly true in many cases. A javascript
|
||||
framework is a considerable investment, so it is quite logical to ask the question:
|
||||
why did Odoo decide to make OWL instead of using a standard/well known framework,
|
||||
such as React or Vue?
|
||||
|
||||
As you might expect, the answer to that question is not simple. But most of the
|
||||
reasons discussed in this page are a consequence from a single fact: Odoo is
|
||||
extremely modular.
|
||||
|
||||
This means, for example, that the core parts of Odoo are not aware, before runtime,
|
||||
of what files will be loaded/executed, or what will be the state of the UI. Because
|
||||
of that, Odoo cannot rely on a standard build toolchain. Also, this implies that
|
||||
the core parts of Odoo need to be extremely generic. In other words, Odoo is not
|
||||
really an application with a user interface. It is an application which generates
|
||||
a dynamic user interface. And most frameworks are not up to the task.
|
||||
|
||||
Betting on Owl was not an easy choice to make, because there certainly are a lot
|
||||
of conflicting needs that we want to carefully balance. Choosing anything other
|
||||
than a well known framework is bound to be controversial. This page will explain
|
||||
some of the reason why we still believe that building Owl is a worthwile
|
||||
endeavour.
|
||||
|
||||
## Strategy
|
||||
|
||||
It is true that we want to keep control of our technology, in the sense that we
|
||||
do not want to depend on Facebook or Google, or any other large (or small)
|
||||
company. If they decide to change their license, or to go in a direction that
|
||||
will not work for us, this may be a problem. This is even more true because
|
||||
Odoo is not a conventional javascript application, and our needs are probably
|
||||
quite different as most other applications.
|
||||
|
||||
## Class components
|
||||
|
||||
It is clear that the biggest frameworks are moving away from class components.
|
||||
There is an implicit assumption that class components are terrible, and that
|
||||
functional programming is the way to go. React even goes as far as to say that
|
||||
classes are confusing for developers.
|
||||
|
||||
While there is some truth to that, and to the fact that composition is certainly
|
||||
a good mechanism for code reuse, we believe that classes and inheritance are
|
||||
important tools.
|
||||
|
||||
Sharing code between generic components with inheritance is the way Odoo built
|
||||
its web client. And it is clear that inheritance is not the root of all evils.
|
||||
It is often a perfectly simple and appropriate solution. What matter most is
|
||||
the architectural decisions.
|
||||
|
||||
Also, Odoo has another specific use out of class components: each method of a
|
||||
class provides an extension point for addons. This may not be a clean architecture
|
||||
pattern, but it is a pragmatic decision that served Odoo well: classes are
|
||||
sometimes monkey-patched to add behaviour from the outside. A little bit like
|
||||
mixins, but from the outside.
|
||||
|
||||
Using React or Vue would make it significantly harder to monkey patch components,
|
||||
because a lot of the state is hidden in their internals.
|
||||
|
||||
## Tooling
|
||||
|
||||
React or Vue have a huge community, and a lot of effort have been made into their
|
||||
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
|
||||
since the assets are totally dynamic (and could change whenever the user installs
|
||||
or removes an addon), we need to have all that kind of tooling on the production
|
||||
servers. This is certainly not ideal.
|
||||
|
||||
Also, this makes it very complicated to setup Vue or React tools: Odoo code is
|
||||
not a simple file that import other files. It changes all the time, assets
|
||||
are bundled differently in different contexts. This is the reason why Odoo has
|
||||
its own module system, which are resolved at runtime, by the browser. The
|
||||
dynamic nature of Odoo means that we often need to delay work as late as possible
|
||||
(in other word, we want a JIT user interface!)
|
||||
|
||||
Our ideal framework has minimal (mandatory) tooling, which makes it easier to
|
||||
deploy. Using React without JSX, or Vue without vue file is not very appealing.
|
||||
|
||||
At the same time, Owl is designed to solve this issue: it compiles templates
|
||||
by the browser, it doesn't need much code for that, since we use the XML parser
|
||||
built into each browser. Owl works with or without any additional tooling. It
|
||||
can use template strings to write single file components, and is easy to integrate
|
||||
in any html page, with a simple `<script>` tag.
|
||||
|
||||
## Template based
|
||||
|
||||
Odoo stores templates as XML documents in a database. This is very powerful, since
|
||||
this allow the use of xpaths to customize other templates. This is a very
|
||||
important feature of odoo, and one of the key to Odoo modularity.
|
||||
|
||||
Because of that, we still expect to write our templates in an XML document.
|
||||
Weirdly enough, no major framework uses XML to store templates, even though it
|
||||
is extremely convenient.
|
||||
|
||||
So, using React or Vue means that we need to make a template compiler. For React,
|
||||
that would be a compiler that would take a QWeb template, and convert it to a
|
||||
React render function. For Vue, it would convert it to a Vue template. Then
|
||||
we need to bundle the vue template compiler as well.
|
||||
|
||||
Not only this would be complex (compiling a templating language into another is
|
||||
not an easy task), but it would negatively impact the developer experience as
|
||||
well. Writing Vue or React components in a QWeb template would certainly be
|
||||
awkward, and very confusing.
|
||||
|
||||
## Developer Experience
|
||||
|
||||
This brings us to the following point: developer experience. We see this choice
|
||||
as an investment for the future, and we want to make onboarding developers as
|
||||
easy as possible.
|
||||
|
||||
While many javascript professionals clearly think that react/vue is not difficult
|
||||
(which is true to some extent), it is alsy true that many non js specialists are
|
||||
overwhelmed with the frontend world: functional components, hooks, and many other
|
||||
fancy words. Also, what is available in the compilation context may be difficult,
|
||||
there is a lot of black magic going on in pretty much every framework. Vue
|
||||
somehow join various namespaces into one, under the hood, and add various internal
|
||||
keys. Svelte transform the code. React require that state transformations are
|
||||
deep, and not shallow.
|
||||
|
||||
Owl is trying very hard to have a simple and familiar API. It uses classes. Its
|
||||
reactivity system is explicit, not implicit. The scoping rules are obvious. In
|
||||
case of doubt, we err on the side of not implementing a feature.
|
||||
|
||||
It is certainly different from React or Vue, but at the same time, kind of
|
||||
familiar for experienced developers.
|
||||
|
||||
## JIT compilation
|
||||
|
||||
There is also a clear trend in the frontend world to compile code
|
||||
as much as possible ahead of time. Most frameworks will compile templates ahead
|
||||
of time. And now Svelte is trying to compile the JS code away, so it can remove
|
||||
itself from the bundle.
|
||||
|
||||
This is certainly reasonable for many usecases. However, this is not what Odoo
|
||||
needs: Odoo will fetch templates from the database and need to compile them only
|
||||
at the last possible moment, so we can apply all necessary xpaths.
|
||||
|
||||
Even more: Odoo needs to be able to generate (and compile) templates at runtime.
|
||||
Currently, Odoo form views interpret an xml description. But the form view code
|
||||
then needs to do a lot of complicated operations. With Owl, we will be able to
|
||||
transform a view description into a QWeb template, then compile that and use it
|
||||
immediately.
|
||||
|
||||
## Reactivity
|
||||
|
||||
There are other design choices that we feel are not optimal in other frameworks.
|
||||
For example, the reactivity system. We like the way Vue did it, but it has a
|
||||
flaw: it is not really optional. There is actually a way to opt out of the reactivity
|
||||
system by freezing the state, but then, it is freezed.
|
||||
|
||||
And there certainly are situations where we need a state, which is not read-only,
|
||||
and not observed. For example, imagine a spreadsheet component. It may have a
|
||||
very large internal state, and it knows exactly when it needs to be rendered
|
||||
(basically, whenever the user performs some action). Then, observing its state
|
||||
is a net performance loss, both for the CPU and the memory.
|
||||
|
||||
## Concurrency
|
||||
|
||||
Many applications are happy to simply display a spinner whenever a new asynchronous
|
||||
action is performed, but Odoo wants a different user experience: most asynchronous
|
||||
state changes are not displayed until ready. This is sometimes called a concurrent
|
||||
mode: the UI is rendered in memory, and displayed only when it is ready (and
|
||||
only if it has not been cancelled by subsequent user actions).
|
||||
|
||||
React has now an experimental concurrent mode, but it was not ready when Owl
|
||||
started. Vue has not really an equivalent API (suspense is not what we need).
|
||||
|
||||
Also, React concurrent mode is complex to use. Concurrency was one of the rare
|
||||
strong point of the former Odoo js framework (widgets), and we feel that Owl has
|
||||
now a very strong concurrent mode, which is simple and powerful at the same time.
|
||||
|
||||
## Conclusion
|
||||
|
||||
This lengthy discussion showed that there are many small and not so small reasons
|
||||
that current standard frameworks are not tailored to our needs. It is perfectly
|
||||
fine, because they each chose a different set of tradeoffs.
|
||||
|
||||
However, we feel that there is still room in the framework world for something
|
||||
that is different. For a framework that makes choices compatible with Odoo.
|
||||
|
||||
And that is why we built Owl 🦉.
|
||||
@@ -0,0 +1,57 @@
|
||||
# 🦉 OWL Documentation 🦉
|
||||
|
||||
## Learning Owl
|
||||
|
||||
Are you new to Owl? This is the place to start!
|
||||
|
||||
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
|
||||
- [Quick Overview](learning/overview.md)
|
||||
- [How to start an Owl project](learning/quick_start.md)
|
||||
- [How to test Components](learning/how_to_test.md)
|
||||
- [How to write Single File Components](learning/how_to_write_sfc.md)
|
||||
- [How to write debug Owl applications](learning/how_to_debug.md)
|
||||
|
||||
## Reference
|
||||
|
||||
You will find here a complete reference of every feature, class or object
|
||||
provided by Owl.
|
||||
|
||||
- [Animations](reference/animations.md)
|
||||
- [Browser](reference/browser.md)
|
||||
- [Component](reference/component.md)
|
||||
- [Content](reference/content.md)
|
||||
- [Concurrency Model](reference/concurrency_model.md)
|
||||
- [Configuration](reference/config.md)
|
||||
- [Context](reference/context.md)
|
||||
- [Environment](reference/environment.md)
|
||||
- [Event Bus](reference/event_bus.md)
|
||||
- [Event Handling](reference/event_handling.md)
|
||||
- [Error Handling](reference/error_handling.md)
|
||||
- [Hooks](reference/hooks.md)
|
||||
- [Mounting a component](reference/mounting.md)
|
||||
- [Miscellaneous Components](reference/misc.md)
|
||||
- [Observer](reference/observer.md)
|
||||
- [Props](reference/props.md)
|
||||
- [Props Validation](reference/props_validation.md)
|
||||
- [QWeb Templating Language](reference/qweb_templating_language.md)
|
||||
- [QWeb Engine](reference/qweb_engine.md)
|
||||
- [Router](reference/router.md)
|
||||
- [Store](reference/store.md)
|
||||
- [Slots](reference/slots.md)
|
||||
- [Tags](reference/tags.md)
|
||||
- [Utils](reference/utils.md)
|
||||
|
||||
## Other Topics
|
||||
|
||||
This section provides miscellaneous document that explains some topics
|
||||
which cannot be considered either a tutorial, or reference documentation.
|
||||
|
||||
- [Owl architecture: the Virtual DOM](miscellaneous/vdom.md)
|
||||
- [Owl architecture: the rendering pipeline](miscellaneous/rendering.md)
|
||||
- [Comparison with React/Vue](miscellaneous/comparison.md)
|
||||
- [Why did Odoo built Owl?](miscellaneous/why_owl.md)
|
||||
|
||||
---
|
||||
|
||||
Found an issue in the documentation? A broken link? Some outdated information?
|
||||
Please open an issue or submit a PR!
|
||||
@@ -0,0 +1,127 @@
|
||||
# 🦉 Animations 🦉
|
||||
|
||||
Animation is a complex topic. There are many different use cases, and many
|
||||
solutions and technologies. Owl only supports some basic use cases.
|
||||
|
||||
## Simple CSS effects
|
||||
|
||||
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
|
||||
necessary: it just needs to render a DOM element with a specific class. For
|
||||
example:
|
||||
|
||||
```xml
|
||||
<a class="btn flash" t-on-click="doSomething">Click</a>
|
||||
```
|
||||
|
||||
with the following CSS:
|
||||
|
||||
```css
|
||||
btn {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
.flash {
|
||||
transition: background 0.5s;
|
||||
}
|
||||
|
||||
.flash:active {
|
||||
background-color: #41454a;
|
||||
transition: background 0s;
|
||||
}
|
||||
```
|
||||
|
||||
will produce a nice flash effect whenever the user clicks (or activates with the
|
||||
keyboard) the button.
|
||||
|
||||
## CSS Transitions
|
||||
|
||||
A more complex situation occurs when we want to transition an element in or out
|
||||
of the page. For example, we may want a fade-in and fade-out effect.
|
||||
|
||||
The `t-transition` directive is here to help us. It works on html elements and
|
||||
on components, by adding and removing some css classes.
|
||||
|
||||
To perform useful transition effects, whenever an element appears or disappears,
|
||||
it is necessary to add/remove some css style or class at some precise moment in
|
||||
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
|
||||
directive is there to help.
|
||||
|
||||
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||
sequence of events will happen:
|
||||
|
||||
At node insertion:
|
||||
|
||||
- the css classes `name-enter` and `name-enter-active` will be added directly
|
||||
when the node is inserted into the DOM.
|
||||
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||
class `name-enter-to` will be added (so they can be used to trigger css
|
||||
transition effects).
|
||||
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed.
|
||||
|
||||
At node destruction:
|
||||
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM.
|
||||
- on the next animation frame: the css class `name-leave` will be removed and the
|
||||
class `name-leave-to` will be added (so they can be used to trigger css
|
||||
transition effects).
|
||||
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed.
|
||||
|
||||
For example, a simple fade in/out effect can be done with this:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-transition` directive can be applied on a node element or on a component.
|
||||
|
||||
Notes:
|
||||
|
||||
Owl does not support more than one transition on a single node, so the
|
||||
`t-transition` expression must be a single value (i.e. no space allowed).
|
||||
|
||||
## SCSS Mixins
|
||||
|
||||
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
|
||||
|
||||
```scss
|
||||
@mixin animation-fade($time, $name) {
|
||||
.#{$name}_fade-enter-active,
|
||||
.#{$name}_fade-active {
|
||||
transition: all $time;
|
||||
}
|
||||
|
||||
.#{$name}_fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.#{$name}_fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```scss
|
||||
@include animation-fade(0.5s, "o_notification");
|
||||
```
|
||||
|
||||
You can now have in your template:
|
||||
|
||||
```xml
|
||||
<SomeTag t-transition="o_notification_fade"/>
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# 🦉 Browser 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Browser Content](#browser-content)
|
||||
|
||||
## Overview
|
||||
|
||||
The browser object contains some browser native APIs, such as `setTimeout`, that
|
||||
are used by Owl and its utility functions. They are exposed with the intent of
|
||||
making them mockable if necessary.
|
||||
|
||||
```js
|
||||
owl.browser.setTimeout === window.setTimeout; // return true
|
||||
```
|
||||
|
||||
For now, this object contains some functions that are not used by Owl. They
|
||||
will eventually be removed in Owl 2.0.
|
||||
|
||||
## Browser Content
|
||||
|
||||
More specifically, the `browser` object contains the following methods and objects:
|
||||
|
||||
- `setTimeout`
|
||||
- `clearTimeout`
|
||||
- `setInterval`
|
||||
- `clearInterval`
|
||||
- `requestAnimationFrame`
|
||||
- `random`
|
||||
- `Date`
|
||||
- `fetch`
|
||||
- `localStorage`
|
||||
@@ -0,0 +1,855 @@
|
||||
# 🦉 OWL Component 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Reactive System](#reactive-system)
|
||||
- [Properties](#properties)
|
||||
- [Static Properties](#static-properties)
|
||||
- [Methods](#methods)
|
||||
- [Lifecycle](#lifecycle)
|
||||
- [`constructor(parent, props)`](#constructorparent-props)
|
||||
- [`setup()`](#setup)
|
||||
- [`willStart()`](#willstart)
|
||||
- [`mounted()`](#mounted)
|
||||
- [`willUpdateProps(nextProps)`](#willupdatepropsnextprops)
|
||||
- [`willPatch()`](#willpatch)
|
||||
- [`patched(snapshot)`](#patchedsnapshot)
|
||||
- [`willUnmount()`](#willunmount)
|
||||
- [`catchError(error)`](#catcherrorerror)
|
||||
- [Root Component](#root-component)
|
||||
- [Composition](#composition)
|
||||
- [Form Input Bindings](#form-input-bindings)
|
||||
- [References](#references)
|
||||
- [Dynamic sub components](#dynamic-sub-components)
|
||||
- [Functional Components](#functional-components)
|
||||
- [SVG Components](#svg-components)
|
||||
|
||||
## Overview
|
||||
|
||||
OWL components are the building blocks for user interface. They are designed to be:
|
||||
|
||||
1. **declarative:** the user interface should be described in terms of the state
|
||||
of the application, not as a sequence of imperative steps.
|
||||
|
||||
2. **composable:** each component can seamlessly be created in a parent component by
|
||||
a simple tag or directive in its template.
|
||||
|
||||
3. **asynchronous rendering:** the framework will transparently wait for each
|
||||
sub components to be ready before applying the rendering. It uses native promises
|
||||
under the hood.
|
||||
|
||||
4. **uses QWeb as a template system:** the templates are described in XML
|
||||
and follow the QWeb specification. This is a requirement for Odoo.
|
||||
|
||||
OWL components are defined as a subclass of Component. The rendering is
|
||||
exclusively done by a [QWeb](qweb_templating_language.md) template (which needs to be preloaded in QWeb).
|
||||
Rendering a component generates a virtual dom representation
|
||||
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
|
||||
|
||||
## Example
|
||||
|
||||
Let us have a look at a simple component:
|
||||
|
||||
```javascript
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class ClickCounter extends owl.Component {
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<button t-name="ClickCounter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
```
|
||||
|
||||
Note that this code is written in ESNext style, so it will only run on the
|
||||
latest browsers without a transpilation step.
|
||||
|
||||
This example shows how a component should be defined: it simply subclasses the
|
||||
Component class. If no static `template` key is defined, then
|
||||
Owl will use the component's name as template name. Here,
|
||||
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
|
||||
[observed](observer.md), and any change to it will cause a rerendering.
|
||||
|
||||
## Reference
|
||||
|
||||
An Owl component is a small class which represents a component or some UI element.
|
||||
It exists in the context of an [environment](environment.md) (`env`), which is propagated from a
|
||||
parent to its children. The environment needs to have a [QWeb](qweb_templating_language.md) instance, which
|
||||
will be used to render the component template.
|
||||
|
||||
Be aware that the name of the component may be significant: if a component does
|
||||
not define a `template` key, then Owl will lookup in QWeb to
|
||||
find a template with the component name (or one of its ancestors).
|
||||
|
||||
### Reactive system
|
||||
|
||||
OWL components are normal javascript classes. So, changing a component internal
|
||||
state does nothing more:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
|
||||
state = { value: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clicking on the `Counter` component defined above will call the `increment`
|
||||
method, but it will not rerender the component. To fix that, one could add an
|
||||
explicit call to `render` in `increment`:
|
||||
|
||||
```js
|
||||
increment() {
|
||||
this.state.value++;
|
||||
this.render();
|
||||
}
|
||||
```
|
||||
|
||||
However, it may be simple in this case, but it quickly become cumbersome, as a
|
||||
component get more complex, and its internal state is modified by more than one
|
||||
method.
|
||||
|
||||
A better way is to use the reactive system: by using the `useState` hook (see the
|
||||
[hooks](hooks.md) section for more details), one can make Owl react to state
|
||||
changes. The `useState` hook generates a proxy version of an object
|
||||
(this is done by an [observer](observer.md)), which allows the component to
|
||||
react to any change. So, the `Counter` example above can be improved like this:
|
||||
|
||||
```js
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Obviously, we can call the `useState` hook more than once:
|
||||
|
||||
```js
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-on-click="increment(counter1)"><t t-esc="counter1.value"/></span>
|
||||
<span t-on-click="increment(counter2)"><t t-esc="counter2.value"/></span>
|
||||
</div>`;
|
||||
counter1 = useState({ value: 0 });
|
||||
counter2 = useState({ value: 0 });
|
||||
|
||||
increment(counter) {
|
||||
counter.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that hooks are subject to one important [rule](hooks.md#one-rule): they need
|
||||
to be called in the constructor.
|
||||
|
||||
### Properties
|
||||
|
||||
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
|
||||
component is not mounted.
|
||||
|
||||
- **`env`** (Object): the component [environment](environment.md), which contains a QWeb instance.
|
||||
|
||||
- **`props`** (Object): this is an object containing all the properties given by
|
||||
the parent to a child component. For example, in the following situation,
|
||||
the parent component gives a `user` and a `color` value to the `ChildComponent`.
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<ChildComponent user="state.user" color="color">
|
||||
</div>
|
||||
```
|
||||
|
||||
Note that `props` are owned by the parent, not by the component.
|
||||
As such, it should not ever be modified by the component (otherwise you risk
|
||||
unintended effects, since the parent may not be aware of the change)!!
|
||||
|
||||
The `props` can be modified dynamically by the parent. In that case, the
|
||||
component will go through the following lifecycle methods: `willUpdateProps`,
|
||||
`willPatch` and `patched`.
|
||||
|
||||
### Static Properties
|
||||
|
||||
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
|
||||
make it easy to define an inline template.
|
||||
|
||||
* **`components`** (Object, optional): if given, this is an object that contains
|
||||
the classes of any sub components needed by the template. This is the main way
|
||||
used by Owl to be able to create sub components.
|
||||
|
||||
```js
|
||||
class ParentComponent extends owl.Component {
|
||||
static components = { SubComponent };
|
||||
}
|
||||
```
|
||||
|
||||
* **`props`** (Object, optional): if given, this is an object that describes the
|
||||
type and shape of the (actual) props given to the component. If Owl mode is
|
||||
`dev`, this will be used to validate the props each time the component is
|
||||
created/updated. See [Props Validation](props_validation.md) for more information.
|
||||
|
||||
```js
|
||||
class Counter extends owl.Component {
|
||||
static props = {
|
||||
initialValue: Number,
|
||||
optional: true,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- **`defaultProps`** (Object, optional): if given, this object define default
|
||||
values for (top-level) props. Whenever `props` are given to the object, they
|
||||
will be altered to add default value (if missing). Note that it does not
|
||||
change the initial object, a new object will be created instead.
|
||||
|
||||
```js
|
||||
class Counter extends owl.Component {
|
||||
static defaultProps = {
|
||||
initialValue: 0,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- **`style`** (string, optional): it should be the return value of the [`css` tag](tags.md#css-tag),
|
||||
which is used to inject stylesheet whenever the component is visible on the
|
||||
screen.
|
||||
|
||||
There is another static property defined on the `Component` class: `current`.
|
||||
This property is set to the currently being defined component (in the constructor).
|
||||
This is the way [hooks](hooks.md) are able to get a reference to the target
|
||||
component.
|
||||
|
||||
### Methods
|
||||
|
||||
We explain here all the public methods of the `Component` class.
|
||||
|
||||
- **`mount(target, options)`** (async): this is the main way a
|
||||
component is added to the DOM: the root component is mounted to a target
|
||||
HTMLElement (or document fragment). Obviously, this is asynchronous, since each children need to be
|
||||
created as well. Most applications will need to call `mount` exactly once, on
|
||||
the root component.
|
||||
|
||||
The `options` argument is an optional object with a `position` key. The
|
||||
`position` key can have three possible values: `first-child`, `last-child`, `self`.
|
||||
|
||||
- `first-child`: with this option, the component will be prepended inside the target,
|
||||
- `last-child` (default value): with this option, the component will be
|
||||
appended in the target element,
|
||||
- `self`: the target will be used as the root element for the component. This
|
||||
means that the target has to be an HTMLElement (and not a document fragment).
|
||||
In this situation, it is possible that the component cannot be unmounted. For
|
||||
example, if its target is `document.body`.
|
||||
|
||||
Note that if a component is mounted, unmounted and remounted, it will be
|
||||
automatically re-rendered to ensure that changes in its state (or something
|
||||
in the environment, or in the store, or ...) will be taken into account.
|
||||
|
||||
If a component is mounted inside an element or a fragment which is not in the
|
||||
DOM, then it will be rendered fully, but not active: the `mounted` hooks will
|
||||
not be called. This is sometimes useful if we want to load an application in
|
||||
memory. In that case, we need to mount the root component again in an element
|
||||
which is in the DOM:
|
||||
|
||||
```js
|
||||
const app = new App();
|
||||
await app.mount(document.createDocumentFragment());
|
||||
// app is rendered in memory, but not active
|
||||
await app.mount(document.body);
|
||||
// app is now visible
|
||||
```
|
||||
|
||||
Note that the normal way of mounting an application is by using the `mount`
|
||||
method on a component class, not by creating the instance by hand. See the
|
||||
documentation on [mounting applications](mounting.md).
|
||||
|
||||
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
|
||||
method can be used. Most applications should not call `unmount`, this is more
|
||||
useful to the underlying component system.
|
||||
|
||||
* **`render()`** (async): calling this method directly will cause a rerender. Note
|
||||
that this should be very rare to have to do it manually, the Owl framework is
|
||||
most of the time responsible for doing that at an appropriate moment.
|
||||
|
||||
Note that the render method is asynchronous, so one cannot observe the updated
|
||||
DOM in the same stack frame.
|
||||
|
||||
* **`shouldUpdate(nextProps)`**: this method is called each time a component's props
|
||||
are updated. It returns a boolean, which indicates if the component should
|
||||
ignore a props update. If it returns false, then `willUpdateProps` will not
|
||||
be called, and no rendering will occur. Its default implementation is to
|
||||
always return true. Note that this is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
|
||||
can be useful if we are handling large number of components. Since this is an
|
||||
optimization, Owl has the freedom to ignore the result of `shouldUpdate` in
|
||||
some cases (for example, if a component is remounted, or if we want to force
|
||||
a full rerender of the UI). However, if `shouldUpdate` returns true, then Owl
|
||||
provides the guarantee that the component will be rendered at some point in
|
||||
the future (except if the component is destroyed or if some part of the UI crashes).
|
||||
|
||||
* **`destroy()`**. As its name suggests, this method will remove the component,
|
||||
and perform all necessary cleanup, such as unmounting the component, its children,
|
||||
removing the parent/children relationship. This method should almost never be
|
||||
called directly (except maybe on the root component), but should be done by the
|
||||
framework instead.
|
||||
|
||||
Obviously, these methods are reserved for Owl, and should not be used by Owl
|
||||
users, unless they want to override them. Also, Owl reserves all method names
|
||||
starting with `__`, in order to prevent possible future conflicts with user code
|
||||
whenever Owl needs to change.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
A solid and robust component system needs useful hooks/methods to help
|
||||
developers write components. Here is a complete description of the lifecycle of
|
||||
a owl component:
|
||||
|
||||
| Method | Description |
|
||||
| ------------------------------------------------ | ----------------------------------------------------------- |
|
||||
| **[setup](#setup)** | setup |
|
||||
| **[willStart](#willstart)** | async, before first rendering |
|
||||
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
|
||||
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
|
||||
| **[willPatch](#willpatch)** | just before the DOM is patched |
|
||||
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
|
||||
| **[willUnmount](#willunmount)** | just before removing component from DOM |
|
||||
| **[catchError](#catcherrorerror)** | catch errors (see [error handling page](error_handling.md)) |
|
||||
|
||||
Notes:
|
||||
|
||||
- hooks call order is precisely defined: `[willX]` hooks are called first on parent,
|
||||
then on children, and `[Xed]` are called in the reverse order: first children,
|
||||
then parent.
|
||||
- no hook method should ever be called manually. They are supposed to be
|
||||
called by the owl framework whenever it is required.
|
||||
|
||||
#### `constructor(parent, props)`
|
||||
|
||||
The constructor is not exactly a hook, it is the regular,
|
||||
normal, constructor of the component. Since it is not a hook, you need to make
|
||||
sure that `super` is called.
|
||||
|
||||
This is usually where you would set the initial state and the template of the
|
||||
component.
|
||||
|
||||
```javascript
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = useState({someValue: true});
|
||||
this.template = 'mytemplate';
|
||||
}
|
||||
```
|
||||
|
||||
Note that with ESNext class fields, the constructor method does not need to be
|
||||
implemented in most cases:
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
state = useState({ value: 0 });
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Hook functions can be called in the constructor.
|
||||
|
||||
#### `setup()`
|
||||
|
||||
_setup_ is run just after the component is constructed. It is a lifecycle method,
|
||||
very similar to the _constructor_, except that it does not receive any argument.
|
||||
|
||||
It is a valid method to call hook functions. Note that one of the main reason to
|
||||
have the `setup` hook in the component lifecycle is to make it possible to
|
||||
monkey patch it. It is a common need in the Odoo ecosystem.
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
useSetupAutofocus();
|
||||
}
|
||||
```
|
||||
|
||||
#### `willStart()`
|
||||
|
||||
willStart is an asynchronous hook that can be implemented to
|
||||
perform some action before the initial rendering of a component.
|
||||
|
||||
It will be called exactly once before the initial rendering. It is useful
|
||||
in some cases, for example, to load external assets (such as a JS library)
|
||||
before the component is rendered. Another use case is to load data from a server.
|
||||
|
||||
```javascript
|
||||
async willStart() {
|
||||
await owl.utils.loadJS("my-awesome-lib.js");
|
||||
}
|
||||
```
|
||||
|
||||
At this point, the component is not yet rendered. Note that a slow `willStart` method will slow down the rendering of the user
|
||||
interface. Therefore, some care should be made to make this method as
|
||||
fast as possible.
|
||||
|
||||
#### `mounted()`
|
||||
|
||||
`mounted` is called each time a component is attached to the
|
||||
DOM, after the initial rendering and possibly later if the component was unmounted
|
||||
and remounted. At this point, the component is considered _active_. This is a good place to add some listeners, or to interact with the
|
||||
DOM, if the component needs to perform some measure for example.
|
||||
|
||||
It is the opposite of `willUnmount`. If a component has been mounted, it will
|
||||
always be unmounted at some point in the future.
|
||||
|
||||
The mounted method will be called recursively on each of its children. First,
|
||||
the parent, then all its children.
|
||||
|
||||
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
|
||||
Doing so will cause a rerender, which will not be perceptible by the user, but
|
||||
will slightly slow down the component.
|
||||
|
||||
#### `willUpdateProps(nextProps)`
|
||||
|
||||
The willUpdateProps is an asynchronous hook, called just before new props
|
||||
are set. This is useful if the component needs to perform an asynchronous task,
|
||||
depending on the props (for example, assuming that the props are
|
||||
some record Id, fetching the record data).
|
||||
|
||||
```javascript
|
||||
willUpdateProps(nextProps) {
|
||||
return this.loadData({id: nextProps.id});
|
||||
}
|
||||
```
|
||||
|
||||
This hook is not called during the first render (but willStart is called
|
||||
and performs a similar job).
|
||||
|
||||
#### `willPatch()`
|
||||
|
||||
The willPatch hook is called just before the DOM patching process starts.
|
||||
It is not called on the initial render. This is useful to read
|
||||
information from the DOM. For example, the current position of the
|
||||
scrollbar.
|
||||
|
||||
Note that modifying the state is not allowed here. This method is called just
|
||||
before an actual DOM patch, and is only intended to be used to save some local
|
||||
DOM state. Also, it will not be called if the component is not in the DOM.
|
||||
|
||||
#### `patched(snapshot)`
|
||||
|
||||
This hook is called whenever a component did actually update its DOM (most
|
||||
likely via a change in its state/props or environment).
|
||||
|
||||
This method is not called on the initial render. It is useful to interact
|
||||
with the DOM (for example, through an external library) whenever the
|
||||
component was patched. Note that this hook will not be called if the component is
|
||||
not in the DOM.
|
||||
|
||||
Updating the component state in this hook is possible, but not encouraged.
|
||||
One needs to be careful, because updates here will create an additional rendering, which in
|
||||
turn will cause other calls to the `patched` method. So, we need to be particularly
|
||||
careful at avoiding endless cycles.
|
||||
|
||||
#### `willUnmount()`
|
||||
|
||||
willUnmount is a hook that is called each time just before a component is unmounted from
|
||||
the DOM. This is a good place to remove listeners, for example.
|
||||
|
||||
```javascript
|
||||
mounted() {
|
||||
this.env.bus.on('someevent', this, this.doSomething);
|
||||
}
|
||||
willUnmount() {
|
||||
this.env.bus.off('someevent', this, this.doSomething);
|
||||
}
|
||||
```
|
||||
|
||||
This is the opposite method of `mounted`.
|
||||
|
||||
#### `catchError(error)`
|
||||
|
||||
The `catchError` method is useful when we need to intercept and properly react
|
||||
to (rendering) errors that occur in some sub components. See the page on
|
||||
[error handling](error_handling.md).
|
||||
|
||||
### Root Component
|
||||
|
||||
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
|
||||
directive) in a template. There is however an obvious exception: the root component
|
||||
of an Owl application has to be created manually:
|
||||
|
||||
```js
|
||||
class App extends owl.Component { ... }
|
||||
|
||||
const app = new App();
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
The root component does not have a parent nor `props` (see note below). It will be setup with an
|
||||
[environment](environment.md) (either the `env` defined on its class, or a
|
||||
default empty environment).
|
||||
|
||||
Note: a root component can however be given a `props` object in its constructor,
|
||||
like this: `new App(null, {some: 'object'});`. It will not be a true `props`
|
||||
object, managed by Owl (so, for example, it will never be updated).
|
||||
|
||||
### Composition
|
||||
|
||||
The example above shows a QWeb template with a sub component. In a template,
|
||||
components are declared with a tagname corresponding to the class name. It has
|
||||
to be capitalized.
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<span>some text</span>
|
||||
<MyComponent info="13" />
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentComponent extends owl.Component {
|
||||
static components = { MyComponent: MyComponent};
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `ParentComponent`'s template creates a component `MyComponent` just
|
||||
after the span. The `info` key will be added to the subcomponent's `props`. Each
|
||||
`props` is a string which represents a javascript (QWeb) expression, so it is
|
||||
dynamic. If it is necessary to give a string, this can be done by quoting it:
|
||||
`someString="'somevalue'"`.
|
||||
|
||||
Note that the rendering context for the template is the component itself. This means
|
||||
that the template can access `state` (if it exists), `props`, `env`, or any
|
||||
methods defined in the component.
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<ChildComponent count="state.val" />
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
static components = { ChildComponent };
|
||||
state = useState({ val: 4 });
|
||||
}
|
||||
```
|
||||
|
||||
Whenever the template is rendered, it will automatically create the subcomponent
|
||||
`ChildComponent` at the correct place. It needs to find the reference to the
|
||||
actual component class in the special static `components` key, or the class registered in
|
||||
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
||||
the static `components` key, then fallbacks on the global registry.
|
||||
|
||||
_Props_: In this example, the child component will receive the object `{count: 4}` in its
|
||||
constructor. This will be assigned to the `props` variable, which can be accessed
|
||||
on the component (and also, in the template). Whenever the state is updated, then
|
||||
the sub component will also be updated automatically. See the [props section](props.md)
|
||||
for more information.
|
||||
|
||||
**CSS and style:** Owl allows the parent to declare
|
||||
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
||||
root component element.
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<MyComponent class="someClass" style="font-weight:bold;" info="13" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Warning: there is a small caveat with dynamic class attributes: since Owl needs
|
||||
to be able to add/remove proper classes whenever necessary, it needs to be aware
|
||||
of the possible classes. Otherwise, it will not be able to make the difference
|
||||
between a valid css class added by the component, or other custom code, and a
|
||||
class that need to be removed. This is why we only support the explicit syntax
|
||||
with a class object:
|
||||
|
||||
```xml
|
||||
<MyComponent t-att-class="{a: state.flagA, b: state.flagB}" />
|
||||
```
|
||||
|
||||
### Form Input Bindings
|
||||
|
||||
It is very common to need to be able to read the value out of an html `input` (or
|
||||
`textarea`, or `select`) in order to use it (note: it does not need to be in a
|
||||
form!). A possible way to do this is to do it by hand:
|
||||
|
||||
```js
|
||||
class Form extends owl.Component {
|
||||
state = useState({ text: "" });
|
||||
|
||||
_updateInputValue(event) {
|
||||
this.state.text = event.target.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-on-input="_updateInputValue" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
|
||||
This works. However, this requires a little bit of _plumbing_ code. Also, the
|
||||
plumbing code is slightly different if you need to interact with a checkbox,
|
||||
or with radio buttons, or with select tags.
|
||||
|
||||
To help with this situation, Owl has a builtin directive `t-model`: its value
|
||||
should be an observed value in the component (usually `state.someValue`). With
|
||||
the `t-model` directive, we can write a shorter code, equivalent to the previous
|
||||
example:
|
||||
|
||||
```js
|
||||
class Form extends owl.Component {
|
||||
state = { text: "" };
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-model="state.text" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
|
||||
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
|
||||
`<input type="radio">`, `<textarea>` and `<select>`:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div>Text in an input: <input t-model="state.someVal"/></div>
|
||||
<div>Textarea: <textarea t-model="state.otherVal"/></div>
|
||||
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
|
||||
<div>Selection:
|
||||
<select t-model="state.color">
|
||||
<option value="">Select a color</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
Selection with radio buttons:
|
||||
<span>
|
||||
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
|
||||
<label for="red">Red</label>
|
||||
</span>
|
||||
<span>
|
||||
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
|
||||
<label for="blue">Blue</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Like event handling, the `t-model` directive accepts the following modifiers:
|
||||
|
||||
| Modifier | Description |
|
||||
| --------- | -------------------------------------------------------------------- |
|
||||
| `.lazy` | update the value on the `change` event (default is on `input` event) |
|
||||
| `.number` | try to parse the value to a number (using `parseFloat`) |
|
||||
| `.trim` | trim the resulting value |
|
||||
|
||||
For example:
|
||||
|
||||
```xml
|
||||
<input t-model.lazy="state.someVal" />
|
||||
```
|
||||
|
||||
These modifiers can be combined. For instance, `t-model.lazy.number` will only
|
||||
update a number whenever the change is done.
|
||||
|
||||
Note: the online playground has an example to show how it works.
|
||||
|
||||
### References
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||
tagged by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
|
||||
more detail.
|
||||
|
||||
As a short example, here is how we could set the focus on a given input:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-ref="input"/>
|
||||
<button t-on-click="focusInput">Click</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
import { useRef } from "owl/hooks";
|
||||
|
||||
class SomeComponent extends Component {
|
||||
inputRef = useRef("input");
|
||||
|
||||
focusInput() {
|
||||
this.inputRef.el.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `useRef` hook can also be used to get a reference to an instance of a sub
|
||||
component rendered by Owl. In that case, we need to access it with the `comp`
|
||||
property instead of `el`:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<SubComponent t-ref="sub"/>
|
||||
<button t-on-click="doSomething">Click</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
import { useRef } from "owl/hooks";
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static components = { SubComponent };
|
||||
subRef = useRef("sub");
|
||||
|
||||
doSomething() {
|
||||
this.subRef.comp.doSomeThingElse();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that these two examples uses the suffix `ref` to name the reference. This
|
||||
is not mandatory, but it is a useful convention, so we do not forget to access
|
||||
it with the `el` or `comp` suffix.
|
||||
|
||||
### Dynamic sub components
|
||||
|
||||
It is not common, but sometimes we need a dynamic component name. In this case,
|
||||
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) directive):
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<t t-component="ChildComponent{{id}}" />
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
static components = { ChildComponent1, ChildComponent2 };
|
||||
state = { id: 1 };
|
||||
}
|
||||
```
|
||||
|
||||
There is an even more dynamic way to use `t-component`: its value can be an
|
||||
expression evaluating to an actual component class. In that case, this is the
|
||||
class that will be used to create the component:
|
||||
|
||||
```js
|
||||
class A extends Component<any, any, any> {
|
||||
static template = xml`<span>child a</span>`;
|
||||
}
|
||||
class B extends Component<any, any, any> {
|
||||
static template = xml`<span>child b</span>`;
|
||||
}
|
||||
class App extends Component<any, any, any> {
|
||||
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
|
||||
|
||||
state = { child: "a" };
|
||||
|
||||
get myComponent() {
|
||||
return this.state.child === "a" ? A : B;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the component `App` selects dynamically the concrete sub
|
||||
component class.
|
||||
|
||||
Note that the `t-component` directive can only be used on `<t>` nodes.
|
||||
|
||||
### Functional Components
|
||||
|
||||
Owl does not exactly have functional components. However, there is an extremely
|
||||
close alternative: calling sub templates.
|
||||
|
||||
A stateless functional component in react is usually some kind of function that
|
||||
maps props to a virtual dom (often with `jsx`). So, basically, almost like a
|
||||
template rendered with `props`. In Owl, this can be done by
|
||||
simply defining a template, that will access the `props` object:
|
||||
|
||||
```js
|
||||
const Welcome = xml`<h1>Hello, <t t-esc="props.name"/></h1>`;
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-call=${Welcome}/>
|
||||
<div>something</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
The way this works is that sub templates are inlined, and have access to the
|
||||
ambient context. They can therefore access `props`, and any other part of the
|
||||
caller component.
|
||||
|
||||
### SVG Components
|
||||
|
||||
Owl components can be used to generate dynamic SVG graphs:
|
||||
|
||||
```js
|
||||
class Node extends Component {
|
||||
static template = xml`
|
||||
<g>
|
||||
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
|
||||
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
|
||||
<t t-set="childx" t-value="props.x + 100"/>
|
||||
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
|
||||
<t t-foreach="props.node.children || []" t-as="child">
|
||||
<t t-set="childy" t-value="props.y + child_index*height"/>
|
||||
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
|
||||
<Node x="childx" y="childy" node="child" height="height"/>
|
||||
</t>
|
||||
</g>
|
||||
`;
|
||||
static components = { Node };
|
||||
}
|
||||
|
||||
class RootNode extends Component {
|
||||
static template = xml`
|
||||
<svg height="180">
|
||||
<Node node="graph" x="10" y="20" height="180"/>
|
||||
</svg>
|
||||
`;
|
||||
static components = { Node };
|
||||
graph = {
|
||||
label: "a",
|
||||
children: [
|
||||
{ label: "b" },
|
||||
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
|
||||
{ label: "f", children: [{ label: "g" }] },
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This `RootNode` component will then display a live SVG representation of the
|
||||
graph described by the `graph` property. Note that there is a recursive structure
|
||||
here: the `Node` component uses itself as a subcomponent.
|
||||
|
||||
Note that since SVG needs to be handled in a specific way (its namespace needs
|
||||
to be properly set), there is a small constraint for Owl components: if an owl
|
||||
component is supposed to be a part of an svg graph, then its root node needs to
|
||||
be a `g` tag, so Owl can properly set the namespace.
|
||||
@@ -0,0 +1,183 @@
|
||||
# 🦉 Concurrency Model 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Rendering Components](#rendering-components)
|
||||
- [Semantics](#semantics)
|
||||
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||
|
||||
## Overview
|
||||
|
||||
Owl was designed from the very beginning with asynchronous components. This comes
|
||||
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
|
||||
methods, it is possible to build complex highly concurrent applications.
|
||||
|
||||
Owl concurrent mode has several benefits: it makes it possible to delay the
|
||||
rendering until some asynchronous operation is complete, it makes it possible
|
||||
to lazy load libraries, while keeping the previous screen completely functional.
|
||||
It is also good for performance reasons: Owl uses it to only apply the result of
|
||||
many different renderings only once in an animation frame. Owl can cancel
|
||||
a rendering that is no longer relevant, restart it, reuse it in some cases.
|
||||
|
||||
But even though using concurrency is quite simple (and is the default behaviour),
|
||||
asynchrony is difficult, because it introduces an additional dimension that
|
||||
vastly increase the complexity of an application. This section will explain
|
||||
how Owl manages this complexity, how concurrent rendering works in a general way.
|
||||
|
||||
## Rendering Components
|
||||
|
||||
The word _rendering_ is a little vague, so, let us explain more precisely the
|
||||
process by which Owl components are displayed on a screen.
|
||||
|
||||
When a component is mounted or updated, a new rendering is started. It has
|
||||
two phases: _virtual rendering_ and _patching_.
|
||||
|
||||
### Virtual rendering
|
||||
|
||||
This phase represent the process of rendering a template, in memory, which create a virtual representation of the desired component html). The output of this phase is a
|
||||
virtual DOM.
|
||||
|
||||
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
|
||||
will need to be called), or updated (which is done with the `willUpdateProps`
|
||||
method). This is completely a recursive process: a component is the root of a
|
||||
component tree, and each sub component needs to be (virtually) rendered.
|
||||
|
||||
### Patching
|
||||
|
||||
Once a rendering is complete, it will be applied on the next animation frame.
|
||||
This is done synchronously: the whole component tree is patched to the real
|
||||
DOM.
|
||||
|
||||
## Semantics
|
||||
|
||||
We give here an informal description of the way components are created/updated
|
||||
in an application. Here, ordered lists describe actions that are executed
|
||||
sequentially, bullet lists describe actions that are executed in parallel.
|
||||
|
||||
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
|
||||
|
||||
```
|
||||
A
|
||||
/ \
|
||||
B C
|
||||
/ \
|
||||
D E
|
||||
```
|
||||
|
||||
Here is what happen whenever we mount the root
|
||||
component (with some code like `app.mount(document.body)`).
|
||||
|
||||
1. `willStart` is called on `A`
|
||||
|
||||
2. when it is done, template `A` is rendered.
|
||||
|
||||
- component `B` is created
|
||||
1. `willStart` is called on `B`
|
||||
2. template `B` is rendered
|
||||
- component `C` is created
|
||||
1. `willStart` is called on `C`
|
||||
2. template `C` is rendered
|
||||
- component `D` is created
|
||||
1. `willStart` is called on `D`
|
||||
2. template `D` is rendered
|
||||
- component `E` is created
|
||||
1. `willStart` is called on `E`
|
||||
2. template `E` is rendered
|
||||
|
||||
3. each components are patched into a detached DOM element, in the following order:
|
||||
`E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created
|
||||
in one pass)
|
||||
|
||||
4. the component `A` root element is actually appended to `document.body`
|
||||
|
||||
5. The method `mounted` is called recursively on all components in the following
|
||||
order: `E`, `D`, `C`, `B`, `A`.
|
||||
|
||||
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
|
||||
button in `C`, and this results in a state update, which is supposed to:
|
||||
|
||||
- update `D`,
|
||||
- remove `E`,
|
||||
- add new component `F`.
|
||||
|
||||
So, the component tree should look like this:
|
||||
|
||||
```
|
||||
A
|
||||
/ \
|
||||
B C
|
||||
/ \
|
||||
D F
|
||||
```
|
||||
|
||||
Here is what Owl will do:
|
||||
|
||||
1. because of a state change, the method `render` is called on `C`
|
||||
2. template `C` is rendered again
|
||||
|
||||
- component `D` is updated:
|
||||
1. hook `willUpdateProps` is called on `D` (async)
|
||||
2. template `D` is rerendered
|
||||
- component `F` is created:
|
||||
1. hook `willStart` is called on `F` (async)
|
||||
2. template `F` is rendered
|
||||
|
||||
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
|
||||
because it is not mounted yet)
|
||||
|
||||
4. components `F`, `D` are patched in that order
|
||||
|
||||
5. component `C` is patched, which will cause recursively:
|
||||
|
||||
1. `willUnmount` hook on `E`
|
||||
2. destruction of `E`,
|
||||
|
||||
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
|
||||
|
||||
Tags are very small helpers to make it easy to write inline templates. There is
|
||||
only one currently available tag: `xml`, but we plan to add other tags later,
|
||||
such as a `css` tag, which will be used to write [single file components](../learning/how_to_write_sfc.md).
|
||||
|
||||
### Asynchronous Rendering
|
||||
|
||||
Working with asynchronous code always adds a lot of complexity to a system. Whenever
|
||||
different parts of a system are active at the same time, one needs to think
|
||||
carefully about all possible interactions. Clearly, this is also true for Owl
|
||||
components.
|
||||
|
||||
There are two different common problems with Owl asynchronous rendering model:
|
||||
|
||||
- any component can delay the rendering (initial and subsequent) of the whole
|
||||
application
|
||||
- for a given component, there are two independant situations that will trigger an
|
||||
asynchronous rerendering: a change in the state, or a change in the props.
|
||||
These changes may be done at different times, and Owl has no way of knowing
|
||||
how to reconcile the resulting renderings.
|
||||
|
||||
Here are a few tips on how to work with asynchronous components:
|
||||
|
||||
1. Minimize the use of asynchronous components!
|
||||
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
|
||||
synchronous renderings
|
||||
3. Lazy loading external libraries is a good use case for async rendering. This
|
||||
is mostly fine, because we can assume that it will only takes a fraction of a
|
||||
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
|
||||
4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
|
||||
this component is met, a new rendering
|
||||
sub tree is created, such that the rendering of that component (and its
|
||||
children) is not tied to the rendering of the rest of the interface. It can
|
||||
be used on an asynchronous component, to prevent it from delaying the
|
||||
rendering of the whole interface, or on a synchronous one, such that its
|
||||
rendering isn't delayed by other (asynchronous) components. Note that this
|
||||
directive has no effect on the first rendering, but only on subsequent ones
|
||||
(triggered by state or props changes).
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<SyncChild />
|
||||
<AsyncRoot>
|
||||
<AsyncChild/>
|
||||
</AsyncRoot>
|
||||
</div>
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
# 🦉 Config 🦉
|
||||
|
||||
The Owl framework is designed to work in many situations. However, it is
|
||||
sometimes necessary to customize some behaviour. This is done by using the
|
||||
global `config` object. It provides two settings:
|
||||
|
||||
- [`mode`](#mode) (default value: `prod`),
|
||||
- [`enableTransitions`](#enabletransitions) (default value: `true`).
|
||||
|
||||
## Mode
|
||||
|
||||
By default, Owl is in _production_ mode, this means that it will try to do its
|
||||
job fast, and skip some expensive operations. However, it is sometimes necessary
|
||||
to have better information on what is going on, this is the purpose
|
||||
of the `dev` mode.
|
||||
|
||||
Owl has a mode flag, in `owl.config.mode`. Its default value is `prod`, but
|
||||
it can be set to `dev`:
|
||||
|
||||
```js
|
||||
owl.config.mode = "dev";
|
||||
```
|
||||
|
||||
Note that templates compiled with the `prod` settings will not be recompiled.
|
||||
So, changing this setting is best done at startup.
|
||||
|
||||
An important job done by the `dev` mode is to validate props for each component
|
||||
creation and update. Also, extra props will cause an error.
|
||||
|
||||
## `enableTransitions`
|
||||
|
||||
Transitions are usually nice, but they can cause issues in some specific cases,
|
||||
such as automated tests. It is uncomfortable having to wait for a transition
|
||||
to end before moving to the next step.
|
||||
|
||||
To solve this issue, Owl can be configured to ignore the `t-transition` directive.
|
||||
To do that, one only needs to set the `enableTransitions` flag to false:
|
||||
|
||||
```js
|
||||
owl.config.enableTransitions = false;
|
||||
```
|
||||
|
||||
Note that it suffers from the same drawback as the "dev" mode: all compiled
|
||||
templates, if any, will keep their current behaviours.
|
||||
@@ -0,0 +1,40 @@
|
||||
# 🦉 Owl Content 🦉
|
||||
|
||||
Here is a complete visual representation of everything exported by the `owl`
|
||||
global object.
|
||||
|
||||
For example, `Component` is available at `owl.Component` and `EventBus` is
|
||||
exported as `owl.core.EventBus`.
|
||||
|
||||
```
|
||||
browser
|
||||
Component misc
|
||||
Context AsyncRoot
|
||||
QWeb Portal
|
||||
mount router
|
||||
Store Link
|
||||
useState RouteComponent
|
||||
config Router
|
||||
mode
|
||||
core tags
|
||||
EventBus css
|
||||
Observer xml
|
||||
hooks utils
|
||||
onWillStart debounce
|
||||
onMounted escape
|
||||
onWillUpdateProps loadJS
|
||||
onWillPatch loadFile
|
||||
onPatched shallowEqual
|
||||
onWillUnmount whenReady
|
||||
useContext
|
||||
useState
|
||||
useRef
|
||||
useComponent
|
||||
useEnv
|
||||
useSubEnv
|
||||
useStore
|
||||
useDispatch
|
||||
useGetters
|
||||
```
|
||||
|
||||
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||
@@ -0,0 +1,105 @@
|
||||
# 🦉 Context 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [`Context`](#context)
|
||||
- [`useContext`](#usecontext)
|
||||
|
||||
## Overview
|
||||
|
||||
The `Context` object provides a way to share data between an arbitrary number
|
||||
of components. Usually, data is passed from a parent to its children component,
|
||||
but when we have to deal with some mostly global information, this can be
|
||||
annoying, since each component will need to pass the information to each children,
|
||||
even though some or most of them will not use the information.
|
||||
|
||||
With a `Context` object, each component can subscribe (with the `useContext` hook)
|
||||
to its state, and will be updated whenever the context state is updated.
|
||||
|
||||
## Example
|
||||
|
||||
Assume that we have an application with various components which needs to render
|
||||
differently depending on the size of the device. Here is how we could proceed
|
||||
to make sure that the information is properly shared. First, let us create a
|
||||
context, and add it to the environment:
|
||||
|
||||
```js
|
||||
const deviceContext = new Context({ isMobile: true });
|
||||
App.env.deviceContext = deviceContext;
|
||||
```
|
||||
|
||||
If we want to make it completely responsive, we need to update its value whenever
|
||||
the size of the screen is updated:
|
||||
|
||||
```js
|
||||
const isMobile = () => window.innerWidth <= 768;
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
owl.utils.debounce(() => {
|
||||
const state = deviceContext.state;
|
||||
if (state.isMobile !== isMobile()) {
|
||||
state.isMobile = !state.isMobile;
|
||||
}
|
||||
}, 15)
|
||||
);
|
||||
```
|
||||
|
||||
Then, each component that want can subscribe and render differently depending on the
|
||||
fact that we are in a mobile or desktop mode.
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if=device.isMobile>
|
||||
some simplified user interface
|
||||
</t>
|
||||
<t t-else="">
|
||||
a more advanced user interface
|
||||
</t>
|
||||
</div>`;
|
||||
device = useContext(this.env.deviceContext);
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### `Context`
|
||||
|
||||
A `Context` object should be created with a state object:
|
||||
|
||||
```js
|
||||
const someContext = new Context({ some: "key" });
|
||||
```
|
||||
|
||||
Its state is now available in the `state` key:
|
||||
|
||||
```js
|
||||
someContext.state.some = "other key";
|
||||
```
|
||||
|
||||
This is the way some global code (such as the responsive code above) should
|
||||
read and update the context state. However, components should not ever read the
|
||||
context state directly from the context, they should instead use the `useContext`
|
||||
hook to properly register themselves to state changes.
|
||||
|
||||
Note that the `Context` hook is different from the React version. For example,
|
||||
there is no concept of provider/consumer. So, the `Context` feature does not
|
||||
by itself allow the use of a different context state depending on the component
|
||||
place in the component tree. However, this functionality can be obtained, if
|
||||
necessary, with the use of sub environment.
|
||||
|
||||
### `useContext`
|
||||
|
||||
The `useContext` hook is the normal way for a component to register themselve
|
||||
to context state changes. The `useContext` method returns the context state:
|
||||
|
||||
```js
|
||||
device = useContext(this.env.deviceContext);
|
||||
```
|
||||
|
||||
It is a simple observed state (with an owl `Observer`), which contains the shared
|
||||
information.
|
||||
@@ -0,0 +1,136 @@
|
||||
# 🦉 Environment 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Setting an Environment](#setting-an-environment)
|
||||
- [Using a sub environment](#using-a-sub-environment)
|
||||
- [Content of an Environment](#content-of-an-environment)
|
||||
- [Special keys](#special-keys)
|
||||
|
||||
## Overview
|
||||
|
||||
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
|
||||
a root component is created, it is assigned an environment (see
|
||||
[below](#setting-an-environment) for more info on this). This environment is
|
||||
then automatically given to each sub component (and accessible in the `this.env`
|
||||
property).
|
||||
|
||||
```
|
||||
Root
|
||||
/ \
|
||||
A B
|
||||
```
|
||||
|
||||
This way, all components share the same `QWeb` instance. Owl internally requires
|
||||
that the environment has a `qweb` key which maps to a
|
||||
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
|
||||
render each templates in this specific component tree. Note that if no `QWeb`
|
||||
instance is provided, Owl will simply generate it on the fly.
|
||||
|
||||
The environment is mostly static. Each application is free to add anything to
|
||||
the environment, which is very useful, since this can be accessed by each sub
|
||||
component.
|
||||
|
||||
## Setting an environment
|
||||
|
||||
An Owl application needs an [environment](environment.md) to be executed. The
|
||||
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
|
||||
all templates.
|
||||
|
||||
Whenever a root component `App` is mounted, Owl will setup a valid environment by
|
||||
following the next steps:
|
||||
|
||||
- take the `env` object defined on `App.env` (if no `env` was explicitly setup,
|
||||
this will return the empty `env` object defined on `Component`)
|
||||
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
|
||||
|
||||
The correct way to customize an environment is to simply set it up on the root
|
||||
component class, before the first component is created:
|
||||
|
||||
```js
|
||||
const env = {
|
||||
_t: myTranslateFunction,
|
||||
user: {...},
|
||||
services: {
|
||||
...
|
||||
},
|
||||
};
|
||||
mount(App, { target: document.body, env });
|
||||
```
|
||||
|
||||
It is also possible to simply share an environment between all root components,
|
||||
by simply doing this:
|
||||
|
||||
```js
|
||||
Component.env = myEnv; // will be the default env for all components
|
||||
```
|
||||
|
||||
Note that this environment is the global owl environment for an application. The
|
||||
next section explains how to extend an environment for a specific sub component
|
||||
and its children.
|
||||
|
||||
## Using a sub environment
|
||||
|
||||
It is sometimes useful to add one (or more) specific keys to the environment,
|
||||
from the perspective of a specific component and its children. In that case, the
|
||||
solution presented above will not work, since it sets the global environment.
|
||||
|
||||
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv).
|
||||
|
||||
```js
|
||||
class FormComponent extends Component {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
useSubEnv({ myKey: someValue });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Content of an Environment
|
||||
|
||||
Some good use cases for additional keys in the environment are:
|
||||
|
||||
- some configuration keys,
|
||||
- session information,
|
||||
- generic services (such as doing rpcs).
|
||||
|
||||
Doing it this way means that components are easily testable: we can simply
|
||||
create a test environment with mock services.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
async function myEnv() {
|
||||
const templates = await loadTemplates();
|
||||
const qweb = new QWeb({ templates });
|
||||
const session = getSession();
|
||||
|
||||
return {
|
||||
_t: myTranslateFunction,
|
||||
session: session,
|
||||
qweb: qweb,
|
||||
services: {
|
||||
localStorage: localStorage,
|
||||
rpc: rpc,
|
||||
},
|
||||
debug: false,
|
||||
inMobileMode: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const env = await myEnv();
|
||||
mount(App, { target: document.body, env });
|
||||
}
|
||||
```
|
||||
|
||||
## Special Keys
|
||||
|
||||
There are two special key/value added by Owl if not provided in the environment:
|
||||
the `QWeb` instance and a `browser` object:
|
||||
|
||||
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
|
||||
for Owl to be able to render anything
|
||||
- `browser`: this is an object that contains some common access points to the
|
||||
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0.
|
||||
@@ -0,0 +1,81 @@
|
||||
# 🦉 Error Handling 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Overview
|
||||
|
||||
By default, whenever an error occurs in the rendering of an Owl application, we
|
||||
destroy the whole application. Otherwise, we cannot offer any guarantee on the
|
||||
state of the resulting component tree. It might be hopelessly corrupted, but
|
||||
without any user-visible state.
|
||||
|
||||
Clearly, it sometimes is a little bit extreme to destroy the application. This
|
||||
is why we have a builtin mechanism to handle rendering errors (and errors coming
|
||||
from lifecycle hooks): the `catchError` hook.
|
||||
|
||||
## Example
|
||||
|
||||
For example, here is how we could implement an `ErrorBoundary` component:
|
||||
|
||||
```xml
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">
|
||||
Error handled
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-slot="default" />
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ErrorBoundary extends Component {
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using the `ErrorBoundary` is then extremely simple:
|
||||
|
||||
```xml
|
||||
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
|
||||
```
|
||||
|
||||
Note that we need to be careful here: the fallback UI should not throw any
|
||||
error, otherwise we risk going into an infinite loop (also, see the page on
|
||||
[slots](slots.md) for more information on the `t-slot` directive).
|
||||
|
||||
## Reference
|
||||
|
||||
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
|
||||
sub components rendering and/or lifecycle method calls will be caught and given
|
||||
to the `catchError` method. This allows us to properly handle the error, and to
|
||||
not break the application.
|
||||
|
||||
There are important things to know:
|
||||
|
||||
- If an error that occured in the internal rendering cycle is not caught, then
|
||||
Owl will destroy the full application. This is done on purpose, because Owl
|
||||
cannot guarantee that the state is not corrupted from this point on.
|
||||
|
||||
- errors coming from event handlers are NOT managed by `catchError` or any other
|
||||
owl mechanism. This is up to the application developer to properly recover
|
||||
from an error
|
||||
|
||||
Also, it may be useful to know that whenever an error is caught, it is then
|
||||
broadcasted to the application by an event on the `qweb` instance. It may be
|
||||
useful, for example, to log the error somewhere.
|
||||
|
||||
```js
|
||||
env.qweb.on("error", null, function (error) {
|
||||
// do something
|
||||
// react to the error
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
# 🦉 Event Bus 🦉
|
||||
|
||||
It is sometimes useful to use a `Bus` to communicate informations between various
|
||||
parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
||||
triggering events, and callbacks.
|
||||
|
||||
```js
|
||||
const bus = new owl.core.EventBus();
|
||||
|
||||
bus.on("some-event", null, function (...args) {
|
||||
console.log(...args);
|
||||
});
|
||||
|
||||
bus.trigger("some-event", 1, 2, 3);
|
||||
// [1,2,3] will be logged to the console
|
||||
```
|
||||
|
||||
Its API is:
|
||||
|
||||
| Method | Description |
|
||||
| -------------------------------- | --------------------------------- |
|
||||
| `on(eventType, owner, callback)` | add a listener |
|
||||
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||
| `trigger(eventType, ...args)` | trigger an event |
|
||||
| `clear` | remove all subscriptions |
|
||||
|
||||
Note that the [`Store`](store.md) is an example of an `EventBus`.
|
||||
@@ -0,0 +1,151 @@
|
||||
# 🦉 Event Handling 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Event Handling](#event-handling)
|
||||
- [Business DOM Events](#business-dom-events)
|
||||
- [Inline Event Handlers](#inline-event-handlers)
|
||||
- [Modifiers](#modifiers)
|
||||
|
||||
## Event Handling
|
||||
|
||||
In a component's template, it is useful to be able to register handlers on DOM
|
||||
elements to some specific events. This is what makes a template _alive_. There
|
||||
are four different use cases.
|
||||
|
||||
1. Register an event handler on a DOM node (_pure_ DOM event)
|
||||
2. Register an event handler on a component (_pure_ DOM event)
|
||||
3. Register an event handler on a DOM node (_business_ DOM event)
|
||||
4. Register an event handler on a component (_business_ DOM event)
|
||||
|
||||
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
This will be roughly translated in javascript like this:
|
||||
|
||||
```js
|
||||
button.addEventListener("click", component.someMethod.bind(component));
|
||||
```
|
||||
|
||||
The suffix (`click` in this example) is simply the name of the actual DOM
|
||||
event.
|
||||
|
||||
## Business DOM Events
|
||||
|
||||
A _business_ DOM event is triggered by a call to `trigger` on a component.
|
||||
|
||||
```xml
|
||||
<MyComponent t-on-menu-loaded="someMethod" />
|
||||
```
|
||||
|
||||
```js
|
||||
class MyComponent {
|
||||
someWhere() {
|
||||
const payload = ...;
|
||||
this.trigger('menu-loaded', payload);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
|
||||
with an additional attribute `originalComponent` (the component that triggered
|
||||
the event). The generated event is of type `menu-loaded` and dispatches it on
|
||||
the component's DOM element (`this.el`). The event bubbles and is cancelable.
|
||||
The parent component listening to event `menu-loaded` will receive the payload
|
||||
in its `someMethod` handler (in the `detail` property of the event), whenever
|
||||
the event is triggered.
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
someMethod(ev) {
|
||||
const payload = ev.detail;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By convention, we use KebabCase for the name of _business_ events.
|
||||
|
||||
The `t-on` directive allows to prebind its arguments. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod(expr)">Do something</button>
|
||||
```
|
||||
|
||||
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
|
||||
from the rendering context.
|
||||
|
||||
### Type Hinting
|
||||
|
||||
Note that if you work with Typescript, the `trigger` method is generic on the type of the payload.
|
||||
|
||||
You can then describe the type of the event, so you will see typing errors...
|
||||
|
||||
```typescript
|
||||
this.trigger<MyCustomPayload>("my-custom-event", payload);
|
||||
```
|
||||
|
||||
```typescript
|
||||
myCustomEventHandler(ev: OwlEvent<MyCustomPayload>) { ... }
|
||||
```
|
||||
|
||||
## Inline Event Handlers
|
||||
|
||||
One can also directly specify inline statements. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click="state.counter++">Increment counter</button>
|
||||
```
|
||||
|
||||
Here, `state` must be defined in the rendering context (typically the component)
|
||||
as it will be translated to:
|
||||
|
||||
```js
|
||||
button.addEventListener("click", () => {
|
||||
context.state.counter++;
|
||||
});
|
||||
```
|
||||
|
||||
Warning: inline expressions are evaluated in the context of the template. This
|
||||
means that they can access the component methods and properties. But if they set
|
||||
a key, the inline statement will actually not modify the component, but a key in
|
||||
a sub scope.
|
||||
|
||||
```xml
|
||||
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
|
||||
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
|
||||
```
|
||||
|
||||
## Modifiers
|
||||
|
||||
In order to remove the DOM event details from the event handlers (like calls to
|
||||
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
|
||||
specified as additional suffixes of the `t-on` directive.
|
||||
|
||||
| Modifier | Description |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `.stop` | calls `event.stopPropagation()` before calling the method |
|
||||
| `.prevent` | calls `event.preventDefault()` before calling the method |
|
||||
| `.self` | calls the method only if the `event.target` is the element itself |
|
||||
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
|
||||
|
||||
```xml
|
||||
<button t-on-click.stop="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
|
||||
the order may matter. For instance `t-on-click.prevent.self` will prevent all
|
||||
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
|
||||
itself.
|
||||
|
||||
Finally, empty handlers are tolerated as they could be defined only to apply
|
||||
modifiers. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click.stop="">Do something</button>
|
||||
```
|
||||
|
||||
This will simply stop the propagation of the event.
|
||||
@@ -0,0 +1,464 @@
|
||||
# 🦉 Hooks 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
- [Example: Autofocus](#example-autofocus)
|
||||
- [Reference](#reference)
|
||||
- [One Rule](#one-rule)
|
||||
- [`useState`](#usestate)
|
||||
- [`onMounted`](#onmounted)
|
||||
- [`onWillUnmount`](#onwillunmount)
|
||||
- [`onWillPatch`](#onwillpatch)
|
||||
- [`onPatched`](#onpatched)
|
||||
- [`onWillStart`](#onwillstart)
|
||||
- [`onWillUpdateProps`](#onwillupdateprops)
|
||||
- [`useContext`](#usecontext)
|
||||
- [`useRef`](#useref)
|
||||
- [`useSubEnv`](#usesubenv)
|
||||
- [`useExternalListener`](#useexternallistener)
|
||||
- [`useStore`](#usestore)
|
||||
- [`useDispatch`](#usedispatch)
|
||||
- [`useGetters`](#usegetters)
|
||||
- [`useComponent`](#usecomponent)
|
||||
- [`useEnv`](#useenv)
|
||||
- [Making customized hooks](#making-customized-hooks)
|
||||
|
||||
## Overview
|
||||
|
||||
Hooks were popularised by React as a way to solve the following issues:
|
||||
|
||||
- help reusing stateful logic between components
|
||||
- help organizing code by feature in complex components
|
||||
- use state in functional components, without writing a class.
|
||||
|
||||
Owl hooks serve the same purpose, except that they work for class components
|
||||
(note: React hooks do not work on class components, and maybe because of that,
|
||||
there seems to be the misconception that hooks are in opposition to class. This
|
||||
is clearly not true, as shown by Owl hooks).
|
||||
|
||||
Hooks work beautifully with Owl components: they solve the problems mentioned
|
||||
above, and in particular, they are the perfect way to make your component
|
||||
reactive.
|
||||
|
||||
## Example: mouse position
|
||||
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
```js
|
||||
const { useState, onMounted, onWillUnmount } = owl.hooks;
|
||||
|
||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||
// position
|
||||
function useMouse() {
|
||||
const position = useState({ x: 0, y: 0 });
|
||||
|
||||
function update(e) {
|
||||
position.x = e.clientX;
|
||||
position.y = e.clientY;
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener("mousemove", update);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
window.removeEventListener("mousemove", update);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class App extends owl.Component {
|
||||
static template = xml`
|
||||
<div t-name="App">
|
||||
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||
</div>`;
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
mouse = useMouse();
|
||||
}
|
||||
```
|
||||
|
||||
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||
a convention.
|
||||
|
||||
## Example: autofocus
|
||||
|
||||
Hooks can be combined to create the desired effect. For example, the following
|
||||
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
|
||||
to create an easy way to focus an input whenever it appears in the DOM:
|
||||
|
||||
```js
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
let isInDom = false;
|
||||
function updateFocus() {
|
||||
if (!isInDom && ref.el) {
|
||||
isInDom = true;
|
||||
ref.el.focus();
|
||||
} else if (isInDom && !ref.el) {
|
||||
isInDom = false;
|
||||
}
|
||||
}
|
||||
onPatched(updateFocus);
|
||||
onMounted(updateFocus);
|
||||
}
|
||||
```
|
||||
|
||||
This hook takes the name of a valid `t-ref` directive, which should be present
|
||||
in the template. It then checks whenever the component is mounted or patched if
|
||||
the reference is not valid, and in this case, it will focus the node element.
|
||||
This hook can be used like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input />
|
||||
<input t-ref="myinput"/>
|
||||
</div>`;
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
useAutofocus("myinput");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### One rule
|
||||
|
||||
There is only one rule: every hook for a component has to be called in the
|
||||
constructor, in the _setup_ method, or in class fields:
|
||||
|
||||
```js
|
||||
// ok
|
||||
class SomeComponent extends Component {
|
||||
state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// not ok: this is executed after the constructor is called
|
||||
class SomeComponent extends Component {
|
||||
async willStart() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, the `useState` hook does not need to be given a reference to
|
||||
the component. This is possible because there is a way to get a reference to the
|
||||
current component: the `Component.current` static property is the reference to the
|
||||
component instance that is currently being created.
|
||||
|
||||
Hooks need to be called in the constructor to ensure that this reference is
|
||||
properly set. This is also a good thing for performance reasons (Owl can use
|
||||
this to optimize its implementation), and for a clean architecture (this makes
|
||||
it easier for developers to understand what is really happening in a component).
|
||||
|
||||
### `useState`
|
||||
|
||||
The `useState` hook is certainly the most important hook for Owl components:
|
||||
this is what allows a component to be reactive, to react to state change.
|
||||
|
||||
The `useState` hook has to be given an object or an array, and will return
|
||||
an observed version of it (using a `Proxy`).
|
||||
|
||||
```javascript
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
class Counter extends owl.Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It is important to remember that `useState` only works with objects or arrays. It
|
||||
is necessary, since Owl needs to react to a change in state.
|
||||
|
||||
### `onMounted`
|
||||
|
||||
`onMounted` is not a user hook, but is a building block designed to help make useful
|
||||
abstractions. `onMounted` registers a callback, which will be called when the component
|
||||
is mounted (see example on top of this page).
|
||||
|
||||
### `onWillUnmount`
|
||||
|
||||
`onWillUnmount` is not a user hook, but is a building block designed to help make useful
|
||||
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
||||
is unmounted (see example on top of this page).
|
||||
|
||||
### `onWillPatch`
|
||||
|
||||
`onWillPatch` is not a user hook, but is a building block designed to help make useful
|
||||
abstractions. `onWillPatch` registers a callback, which will be called just
|
||||
before the component patched.
|
||||
|
||||
### `onPatched`
|
||||
|
||||
`onPatched` is not a user hook, but is a building block designed to help make useful
|
||||
abstractions. `onPatched` registers a callback, which will be called just
|
||||
after the component patched.
|
||||
|
||||
### `onWillStart`
|
||||
|
||||
`onWillStart` is an asynchronous hook. This means that the function registered
|
||||
in the hook will be run just before the component is first rendered and can return a
|
||||
promise, to express the fact that it is an asynchronous operation.
|
||||
|
||||
Note that if there are more than one `onWillStart` registered callback, then they
|
||||
will all be run in parallel.
|
||||
|
||||
It can be used to load some initial data. For example, the following hook will
|
||||
automatically load some data from the server, and return an object that will
|
||||
be ready whenever the component is rendered:
|
||||
|
||||
```js
|
||||
function useLoader() {
|
||||
const component = Component.current;
|
||||
const record = useState({});
|
||||
onWillStart(async () => {
|
||||
const recordId = component.props.id;
|
||||
Object.assign(record, await fetchSomeRecord(recordId));
|
||||
});
|
||||
return record;
|
||||
}
|
||||
```
|
||||
|
||||
Note that this example does not update the record value whenever props are
|
||||
updated. For that situation, we need to use the `onWillUpdateProps` hook.
|
||||
|
||||
### `onWillUpdateProps`
|
||||
|
||||
Just like `onWillStart`, `onWillUpdateProps` is an asynchronous hook. It is
|
||||
designed to be run whenever the component props are updated. This could be
|
||||
useful to perform some asynchronous task such as fetching updated data.
|
||||
|
||||
```js
|
||||
function useLoader() {
|
||||
const component = Component.current;
|
||||
const record = useState({});
|
||||
|
||||
async function updateRecord(id) {
|
||||
Object.assign(record, await fetchSomeRecord(id));
|
||||
}
|
||||
|
||||
onWillStart(() => updateRecord(component.props.id));
|
||||
onWillUpdateProps((nextProps) => updateRecord(nextProps.id));
|
||||
|
||||
return record;
|
||||
}
|
||||
```
|
||||
|
||||
Note that if there are more than one `onWillUpdateProps` registered callback,
|
||||
then they will all be run in parallel.
|
||||
|
||||
### `useContext`
|
||||
|
||||
See [`useContext`](context.md#usecontext) for reference documentation.
|
||||
|
||||
### `useRef`
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||
tagged by the `t-ref` directive:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div t-ref="someDiv"/>
|
||||
<SubComponent t-ref="someComponent"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component will be able to access the `div` and the component
|
||||
`SubComponent` using the `useRef` hook:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
subRef = useRef("someComponent");
|
||||
divRef = useRef("someDiv");
|
||||
|
||||
someMethod() {
|
||||
// here, if component is mounted, refs are active:
|
||||
// - this.divRef.el is the div HTMLElement
|
||||
// - this.subRef.comp is the instance of the sub component
|
||||
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As shown by the example above, html elements are accessed by using the `el`
|
||||
key, and components references are accessed with `comp`.
|
||||
|
||||
Notes:
|
||||
|
||||
- if used on a component, the reference will be set in the `refs`
|
||||
variable between `willPatch` and `patched`,
|
||||
- on a component, accessing `ref.el` will get the root node of the component.
|
||||
|
||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) and
|
||||
`t-component` directives). For example,
|
||||
|
||||
```xml
|
||||
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||
```
|
||||
|
||||
Here, the references need to be set like this:
|
||||
|
||||
```js
|
||||
this.ref1 = useRef("component_1");
|
||||
this.ref2 = useRef("component_2");
|
||||
```
|
||||
|
||||
References are only guaranteed to be active while the parent component is mounted.
|
||||
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||
|
||||
### `useSubEnv`
|
||||
|
||||
The environment is sometimes useful to share some common information between
|
||||
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
|
||||
|
||||
For example, if we have a form view component, maybe we would like to make some
|
||||
`model` object available to all sub components, but not to the whole application.
|
||||
This is where the `useSubEnv` hook may be useful: it lets a component add some
|
||||
information to the environment in a way that only the component and its children
|
||||
can access it:
|
||||
|
||||
```js
|
||||
class FormComponent extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
const model = makeModel();
|
||||
useSubEnv({ model });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `useSubEnv` takes one argument: an object which contains some key/value that
|
||||
will be added to the parent environment. Note that it will extend, not replace
|
||||
the parent environment. And of course, the parent environment will not be
|
||||
affected.
|
||||
|
||||
### `useExternalListener`
|
||||
|
||||
The `useExternalListener` hook helps solve a very common problem: adding and removing
|
||||
a listener on some target whenever a component is mounted/unmounted. For example,
|
||||
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
|
||||
to be closed:
|
||||
|
||||
```js
|
||||
useExternalListener(window, "click", this.closeMenu);
|
||||
```
|
||||
|
||||
### `useStore`
|
||||
|
||||
The `useStore` hook is the entry point for a component to connect to the store.
|
||||
See the [store documentation](store.md) for more information.
|
||||
|
||||
### `useDispatch`
|
||||
|
||||
The `useDispatch` hook is the way for components to get a reference to the store
|
||||
`dispatch` function. See the [store documentation](store.md) for more information.
|
||||
|
||||
### `useGetters`
|
||||
|
||||
The `useGetters` hook is the way for components to get a reference to the store
|
||||
getters. See the [store documentation](store.md) for more information.
|
||||
|
||||
### `useComponent`
|
||||
|
||||
The `useComponent` hook is useful as a building block for some customized hooks,
|
||||
that may need a reference to the component calling them.
|
||||
|
||||
### `useEnv`
|
||||
|
||||
The `useEnv` hook is useful as a building block for some customized hooks,
|
||||
that may need a reference to the env of the component calling them.
|
||||
|
||||
### Making customized hooks
|
||||
|
||||
Hooks are a wonderful way to organize the code of a complex component by feature
|
||||
instead of by lifecycle methods. They are like mixins, except that they can be
|
||||
easily composed together.
|
||||
|
||||
But, like every good things in life, hooks should be used with moderation. They are
|
||||
not the solution to every problem.
|
||||
|
||||
- they may be overkill: if your component needs to perform some action specific
|
||||
to itself (so, the specific code does not need to be shared), there is nothing
|
||||
wrong with a simple class method:
|
||||
|
||||
```js
|
||||
// maybe overkill
|
||||
class A extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
useMySpecificHook();
|
||||
}
|
||||
}
|
||||
|
||||
// ok
|
||||
class B extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.performSpecificTask();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the second solution is easier to extend in sub components.
|
||||
|
||||
- they may be harder to test: if a customized hook injects some external side
|
||||
effect dependency, then it is harder to test without doing some non obvious
|
||||
manipulation. For example, assume that we want to give a reference to a
|
||||
router in a `useRouter` hook. We could do this:
|
||||
|
||||
```js
|
||||
const router = new Router(...);
|
||||
|
||||
function useRouter() {
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, this does not _hook_ into the internal of the component. It
|
||||
simply returns a global object, which is difficult to mock.
|
||||
|
||||
A better way would be to do something like this: get the reference from the
|
||||
environment.
|
||||
|
||||
```js
|
||||
function useRouter() {
|
||||
const env = useEnv();
|
||||
return env.router;
|
||||
}
|
||||
```
|
||||
|
||||
This means that we give control to the application developer to create the
|
||||
router, which is good, so they can set it up, subclass it, ... And then, to
|
||||
test our components, we can just add a mock router in the environment.
|
||||
@@ -0,0 +1,133 @@
|
||||
# 🦉 Miscellaneous 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Portal](#portal)
|
||||
- [AsyncRoot](#asyncroot)
|
||||
|
||||
## `Portal`
|
||||
|
||||
### Overview
|
||||
|
||||
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
|
||||
of DOM to the node represented by its sole `target` props.
|
||||
|
||||
This component aims at helping the implementation of the needed infrastructure
|
||||
for modals (as in `bootstrap-modal`).
|
||||
|
||||
### Usage
|
||||
|
||||
The content it will teleport is defined within the `<Portal>` node and
|
||||
internally uses the `default` [Slot](slots.md).
|
||||
|
||||
This slot must contain only **one** node, which in turn can have as many children as necessary.
|
||||
|
||||
The element under which the content will be teleported is represented as a selector
|
||||
by the `target` props which only accepts a string as value.
|
||||
|
||||
The `target` props only supports static selector, and is not meant to be passed to `Portal`
|
||||
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
|
||||
By contrast, `<Portal target="state.target" />` is not supported.
|
||||
|
||||
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
|
||||
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
|
||||
|
||||
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
|
||||
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
|
||||
|
||||
### Example
|
||||
|
||||
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
|
||||
workflow to help the user put in some data, which it could use later on.
|
||||
|
||||
JavaScript:
|
||||
|
||||
```js
|
||||
const { Component, mount } = owl;
|
||||
const { Portal } = owl.misc;
|
||||
|
||||
class TeleportedComponent extends Component {}
|
||||
class App extends Component {
|
||||
static components = { Portal, TeleportedComponent };
|
||||
}
|
||||
|
||||
mount(App, { target: document.body });
|
||||
```
|
||||
|
||||
XML:
|
||||
|
||||
```xml
|
||||
<templates>
|
||||
<div t-name="TeleportedComponent">
|
||||
<span>I will move soon enough</span>
|
||||
</div>
|
||||
|
||||
<div t-name="App">
|
||||
<span>I am like the rest of us</span>
|
||||
<Portal target="'body'">
|
||||
<TeleportedComponent />
|
||||
</Portal>
|
||||
</div>
|
||||
</templates>
|
||||
```
|
||||
|
||||
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
|
||||
`TeleportedComponent` is acting as a Dialog here.
|
||||
|
||||
The resulting DOM will look like:
|
||||
|
||||
```xml
|
||||
<body>
|
||||
<div>
|
||||
<span>I am like the rest of us</span>
|
||||
<portal></portal>
|
||||
</div>
|
||||
<div>
|
||||
<span>I will move soon enough</span>
|
||||
</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
### Expected Behaviors
|
||||
|
||||
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
|
||||
Namely the teleported piece will be updated in function of its parents components, and patched as
|
||||
a normal child.
|
||||
|
||||
The [_business_ events](event_handling.md#business-dom-events) triggered by a child component will be stopped
|
||||
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
|
||||
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
|
||||
|
||||
Beware that those re-directed events are copies of the original event.
|
||||
They have:
|
||||
|
||||
- The same payload.
|
||||
- The same `originalComponent` than their original counterpart,
|
||||
that is the actual Component that triggered it.
|
||||
- A **different** `target` property than their original counterpart.
|
||||
The `target` of a re-directed event is necessarily the `Portal`'s root node.
|
||||
|
||||
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
|
||||
up to the `body`.
|
||||
|
||||
## `AsyncRoot`
|
||||
|
||||
When this component is used, a new rendering sub tree is created, such that the
|
||||
rendering of that component (and its children) is not tied to the rendering of
|
||||
the rest of the interface. It can be used on an asynchronous component, to
|
||||
prevent it from delaying the rendering of the whole interface, or on a
|
||||
synchronous one, such that its rendering isn't delayed by other (asynchronous)
|
||||
components. Note that this directive has no effect on the first rendering, but
|
||||
only on subsequent ones (triggered by state or props changes).
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<SyncChild />
|
||||
<AsyncRoot>
|
||||
<AsyncChild/>
|
||||
</AsyncRoot>
|
||||
</div>
|
||||
```
|
||||
|
||||
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
|
||||
be a dom node or a component.
|
||||
@@ -0,0 +1,60 @@
|
||||
# 🦉 Mounting an application 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [API](#api)
|
||||
|
||||
## Overview
|
||||
|
||||
Mounting an Owl application is done by using the `mount` method (available in
|
||||
`owl.mount` if you are using the iife build, or it can be directly imported
|
||||
from `owl` if you are using a module system):
|
||||
|
||||
```js
|
||||
const mount = { owl }; // if owl is available as an object
|
||||
|
||||
const env = { ... };
|
||||
const app = await mount(MyComponent, { target: document.body, env });
|
||||
```
|
||||
|
||||
Another example:
|
||||
|
||||
```js
|
||||
const config = {
|
||||
env: ...,
|
||||
props: ...,
|
||||
target: document.body,
|
||||
position: "self",
|
||||
};
|
||||
const app = await mount(App, config);
|
||||
```
|
||||
|
||||
A common way to initialize an application is to first setup an environment,
|
||||
then to call the `mount` method.
|
||||
|
||||
## API
|
||||
|
||||
Mount takes two parameters:
|
||||
|
||||
- `C`, which should be a component class (NOT instance),
|
||||
- `params`, which is an object with the following keys:
|
||||
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
|
||||
- `env (optional, Env)` an environment
|
||||
- `position (optional, "first-child" | "last-child" | "self")` the position
|
||||
where it should be mounted (see below for more informations)
|
||||
- `props (optional, any)`: some initial values that are given as props. Useful
|
||||
when the root component is configurable, or when testing sub components
|
||||
|
||||
Here are the various positions supported by Owl:
|
||||
|
||||
- `first-child`: with this option, the component will be prepended inside the target,
|
||||
- `last-child` (default value): with this option, the component will be
|
||||
appended in the target element,
|
||||
- `self`: the target will be used as the root element for the component. This
|
||||
means that the target has to be an HTMLElement (and not a document fragment).
|
||||
In this situation, it is possible that the component cannot be unmounted. For
|
||||
example, if its target is `document.body`.
|
||||
|
||||
The `mount` method returns a promise that resolves to the instance of the created
|
||||
component.
|
||||
@@ -0,0 +1,52 @@
|
||||
# 🦉 Observer 🦉
|
||||
|
||||
Owl needs to be able to react to state changes. For example, whenever the state
|
||||
of a component is changed, Owl needs to rerender it. To help with that, there is
|
||||
an Observer class. Its job is to observe the state of an object (or array), and
|
||||
to react to any change. The observer is implemented with the native `Proxy`
|
||||
object. Note that this means that it will not work on older browsers.
|
||||
|
||||
Note that the `Observer` is used by the `useState` and `useContext` hooks. This
|
||||
is the way most Owl applications will create observers. For the majority of
|
||||
use cases, there is no need to directly instantiate an observer.
|
||||
|
||||
## Example
|
||||
|
||||
For example, this code will display `update` in the console:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.core.Observer();
|
||||
observer.notifyCB = () => console.log("update");
|
||||
const obj = observer.observe({ a: { b: 1 } });
|
||||
|
||||
obj.a.b = 2;
|
||||
```
|
||||
|
||||
This example shows that an observer can observe nested properties.
|
||||
|
||||
## Reference
|
||||
|
||||
**observe** An observer can observe multiple values with the `observe` method.
|
||||
This method takes an object or an array as its argument and will return a proxy
|
||||
(which is mapped to the initial object/array). With this proxy, the observer
|
||||
can detect whenever any internal value is changed.
|
||||
|
||||
**Registering a callback** Whenever an observer sees a state change, it will
|
||||
call its `notifyCB` method. No additional information is given to the callback.
|
||||
|
||||
**deepRevNumber** Each observed value has an internal revision number, which
|
||||
is incremented every time the value is observed. Sometimes, it can be useful
|
||||
to obtain that number:
|
||||
|
||||
```js
|
||||
const observer = new owl.core.Observer();
|
||||
const obj = observer.observe({ a: { b: 1 } });
|
||||
|
||||
observer.revNumber(obj.a); // 1
|
||||
obj.a.b = 2;
|
||||
|
||||
observer.revNumber(obj.a); // 2
|
||||
```
|
||||
|
||||
The `revNumber` can also return 0, which indicates that the value is not
|
||||
observed.
|
||||
@@ -0,0 +1,97 @@
|
||||
# 🦉 Props 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Definition](#definition)
|
||||
- [Good Practices](#good-practices)
|
||||
- [Dynamic Props](#dynamic-props)
|
||||
|
||||
## Overview
|
||||
|
||||
In Owl, `props` (short for _properties_) is an object which contains every piece
|
||||
of data given to a component by its parent.
|
||||
|
||||
```js
|
||||
class Child extends Component {
|
||||
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
|
||||
static components = { Child };
|
||||
state = useState({ a: "fromparent" });
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `Child` component receives two props from its parent: `a`
|
||||
and `b`. They are collected into a `props` object by Owl, with each value being
|
||||
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
|
||||
`props.b` is equal to `'string'`.
|
||||
|
||||
Note that `props` is an object that only makes sense from the perspective of the
|
||||
child component.
|
||||
|
||||
## Definition
|
||||
|
||||
The `props` object is made of every attributes defined on the template, with the
|
||||
following exceptions:
|
||||
|
||||
- every attribute starting with `t-` are not props (they are QWeb directives),
|
||||
- `style` and `class` attributes are excluded as well (they are applied by Owl on
|
||||
the root element of the component).
|
||||
|
||||
In the following example:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<ComponentA a="state.a" b="'string'"/>
|
||||
<ComponentB t-if="state.flag" model="model"/>
|
||||
<ComponentC style="color:red;" class="left-pane" />
|
||||
</div>
|
||||
```
|
||||
|
||||
the `props` object contains the following keys:
|
||||
|
||||
- for `ComponentA`: `a` and `b`,
|
||||
- for `ComponentB`: `model`,
|
||||
- for `ComponentC`: empty object
|
||||
|
||||
## Good Practices
|
||||
|
||||
A `props` object is a collection of values that come from the parent. As such,
|
||||
they are owned by the parent, and should never be modified by the child:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
props.a.b = 43; // Never do that!!!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Props should be considered readonly, from the perspective of the child component.
|
||||
If there is a need to modify them, then the request to update them should be
|
||||
sent to the parent (for example, with an event).
|
||||
|
||||
Any value can go in a props. Strings, objects, classes, or even callbacks could
|
||||
be given to a child component (but then, in the case of callbacks, communicating
|
||||
with events seems more appropriate).
|
||||
|
||||
## Dynamic Props
|
||||
|
||||
The `t-props` directive can be used to specify totally dynamic props:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<Child t-props="some.obj"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
static components = { Child };
|
||||
some = { obj: { a: 1, b: 2 } };
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,103 @@
|
||||
# 🦉 Props Validation 🦉
|
||||
|
||||
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
|
||||
|
||||
- hard to tell how a component should be used, by looking at its code.
|
||||
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
|
||||
|
||||
A props type system solves both issues, by describing the types and shapes
|
||||
of the props. Here is how it works in Owl:
|
||||
|
||||
- `props` key is a static key (so, different from `this.props` in a component instance)
|
||||
- it is optional: it is ok for a component to not define a `props` key.
|
||||
- props are validated whenever a component is created/updated
|
||||
- props are only validated in `dev` mode (see [config page](config.md#mode))
|
||||
- if a key does not match the description, an error is thrown
|
||||
- it validates keys defined in (static) `props`. Additional keys given by the
|
||||
parent will cause an error.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
class ComponentA extends owl.Component {
|
||||
static props = ['id', 'url'];
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
class ComponentB extends owl.Component {
|
||||
static props = {
|
||||
count: {type: Number},
|
||||
messages: {
|
||||
type: Array,
|
||||
element: {type: Object, shape: {id: Boolean, text: String }
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean]
|
||||
};
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- it is an object or a list of strings
|
||||
- a list of strings is a simplified props definition, which only lists the name
|
||||
of the props. Also, if the name ends with `?`, it is considered optional.
|
||||
- all props are by default required, unless they are defined with `optional: true`
|
||||
(in that case, validation is only done if there is a value)
|
||||
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
|
||||
constructor functions (so, if you have a `Person` class, it can be used as a type)
|
||||
- arrays are homogeneous (all elements have the same type/shape)
|
||||
|
||||
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
|
||||
|
||||
- a boolean: indicate that the props exists, and is mandatory.
|
||||
- a constructor: this should describe the type, for example: `id: Number` describe
|
||||
the props `id` as a number
|
||||
- a list of constructors. In that case, this means that we allow more than one
|
||||
type. For example, `id: [Number, String]` means that `id` can be either a string
|
||||
or a number.
|
||||
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
|
||||
- `type`: the main type of the prop being validated
|
||||
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||
- `validate`: this is a function which should return a boolean to determine if
|
||||
the value is valid or not. Useful for custom validation logic.
|
||||
|
||||
Examples:
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented
|
||||
static props = ['message', 'id', 'date'];
|
||||
```
|
||||
|
||||
```js
|
||||
// size is optional
|
||||
static props = ['message', 'size?'];
|
||||
```
|
||||
|
||||
```js
|
||||
static props = {
|
||||
messageIds: {type: Array, element: Number}, // list of number
|
||||
otherArr: {type: Array}, // just array. no validation is made on sub elements
|
||||
otherArr2: Array, // same as otherArr
|
||||
someObj: {type: Object}, // just an object, no internal validation
|
||||
someObj2: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||
someVal: [Boolean, Date], // either a boolean or a date
|
||||
otherValue: true, // indicates that it is a prop
|
||||
kindofsmallnumber: {
|
||||
type: Number,
|
||||
validate: n => (0 <= n && n <= 10)
|
||||
},
|
||||
size: {
|
||||
validate: e => ["small", "medium", "large"].includes(e)
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,151 @@
|
||||
# 🦉 QWeb Engine 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Overview
|
||||
|
||||
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
|
||||
templating engine used by Odoo. The QWeb class in the OWL project is an
|
||||
implementation of that specification with a few interesting points:
|
||||
|
||||
- it compiles templates into functions that output a virtual DOM instead of a
|
||||
string. This is necessary for the component system.
|
||||
- it has a few extra directives: `t-component`, `t-on`, ...
|
||||
|
||||
We present in this section the engine, not the templating language.
|
||||
|
||||
## Reference
|
||||
|
||||
This section is about the javascript code that implements the `QWeb` specification.
|
||||
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
|
||||
instantiated:
|
||||
|
||||
```js
|
||||
const qweb = new owl.QWeb();
|
||||
```
|
||||
|
||||
Its API is quite simple:
|
||||
|
||||
- **`constructor(config)`**: constructor. Takes an optional configuration object
|
||||
with an optional `templates` string to add initial
|
||||
templates (see `addTemplates` for more information on format of the string)
|
||||
and an optional `translateFn` translate function (see the section on
|
||||
[translations](#translations)).
|
||||
|
||||
```js
|
||||
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
|
||||
```
|
||||
|
||||
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
|
||||
|
||||
```js
|
||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||
```
|
||||
|
||||
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
|
||||
ignore templates added for a second time. Otherwise, `QWeb` will crash.
|
||||
|
||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||
attribute).
|
||||
|
||||
```js
|
||||
const TEMPLATES = `
|
||||
<templates>
|
||||
<div t-name="App" class="main">main</div>
|
||||
<div t-name="OtherComponent">other component</div>
|
||||
</templates>`;
|
||||
qweb.addTemplates(TEMPLATES);
|
||||
```
|
||||
|
||||
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
|
||||
which is a virtual representation of the DOM (see [vdom doc](../miscellaneous/vdom.md)).
|
||||
|
||||
```js
|
||||
const vnode = qweb.render("App", component);
|
||||
```
|
||||
|
||||
- **`renderToString(name, context)`**: renders a template, but returns an html
|
||||
string.
|
||||
|
||||
```js
|
||||
const str = qweb.renderToString("someTemplate", somecontext);
|
||||
```
|
||||
|
||||
- **`registerTemplate(name, template)`**: static function to register a global
|
||||
QWeb template. This is useful for commonly used components accross the
|
||||
application, and for making a template available to an application without
|
||||
having a reference to the actual QWeb instance.
|
||||
|
||||
```js
|
||||
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
|
||||
```
|
||||
|
||||
- **`registerComponent(name, Component)`**: static function to register an OWL Component
|
||||
to QWeb's global registry. Globally registered Components can be used in
|
||||
templates (see the `t-component` directive). This is useful for commonly used
|
||||
components accross the application.
|
||||
|
||||
```js
|
||||
class Dialog extends owl.Component { ... }
|
||||
QWeb.registerComponent("Dialog", Dialog);
|
||||
|
||||
...
|
||||
|
||||
class ParentComponent extends owl.Component { ... }
|
||||
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
||||
```
|
||||
|
||||
In some way, a `QWeb` instance is the core of an Owl application. It is the only
|
||||
mandatory element of an [environment](environment.md). As such, it
|
||||
has an extra responsibility: it can act as an event bus for internal communication
|
||||
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
|
||||
|
||||
### Translations
|
||||
|
||||
If properly setup, Owl QWeb engine can translate all rendered templates. To do
|
||||
so, it needs a translate function, which takes a string and returns a string.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const translations = {
|
||||
hello: "bonjour",
|
||||
yes: "oui",
|
||||
no: "non",
|
||||
};
|
||||
const translateFn = (str) => translations[str] || str;
|
||||
|
||||
const qweb = new QWeb({ translateFn });
|
||||
```
|
||||
|
||||
Once setup, all rendered templates will be translated using `translateFn`:
|
||||
|
||||
- each text node will be replaced with its translation,
|
||||
- each of the following attribute values will be translated as well: `title`,
|
||||
`placeholder`, `label` and `alt`,
|
||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||
if its value is `off`.
|
||||
|
||||
So, with the above `translateFn`, the following templates:
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
<div t-translation="off">hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="hello" other="yes"/>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<div>bonjour</div>
|
||||
<div>hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="bonjour" other="yes"/>
|
||||
```
|
||||
|
||||
Note that the translation is done during the compilation of the template, not
|
||||
when it is rendered.
|
||||
@@ -0,0 +1,613 @@
|
||||
# 🦉 QWeb Templating Language🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Directives](#directives)
|
||||
- [Reference](#reference)
|
||||
- [White Spaces](#white-spaces)
|
||||
- [Root Nodes](#root-nodes)
|
||||
- [Expression Evaluation](#expression-evaluation)
|
||||
- [Static html Nodes](#static-html-nodes)
|
||||
- [Outputting Data](#outputting-data)
|
||||
- [Setting Variables](#setting-variables)
|
||||
- [Conditionals](#conditionals)
|
||||
- [Dynamic Attributes](#dynamic-attributes)
|
||||
- [Dynamic Class Attribute](#dynamic-class-attribute)
|
||||
- [Dynamic Tag Names](#dynamic-tag-names)
|
||||
- [Loops](#loops)
|
||||
- [Rendering Sub Templates](#rendering-sub-templates)
|
||||
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
||||
- [Translations](#translations)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
## Overview
|
||||
|
||||
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
|
||||
templating engine used by Odoo. It is based on the XML format, and used
|
||||
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
||||
generate a virtual dom representation of the HTML.
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<span t-if="somecondition">Some string</span>
|
||||
<ul t-else="">
|
||||
<li t-foreach="messages" t-as="message">
|
||||
<t t-esc="message"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
Template directives are specified as XML attributes prefixed with `t-`, for
|
||||
instance `t-if` for conditionals, with elements and other attributes being
|
||||
rendered directly.
|
||||
|
||||
To avoid element rendering, a placeholder element `<t>` is also available, which
|
||||
executes its directive but doesn’t generate any output in and of itself.
|
||||
|
||||
We present in this section the templating language, including its Owl specific
|
||||
extensions.
|
||||
|
||||
## Directives
|
||||
|
||||
For reference, here is a list of all standard QWeb directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------ | -------------------------------------------------------------- |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-translation` | [Disabling the translation of a node](#translations) |
|
||||
| `t-name` | [Defining a template (not really a directive)](qweb_engine.md) |
|
||||
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------------- |
|
||||
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
|
||||
| `t-on-*` | [Event handling](event_handling.md) |
|
||||
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||
| `t-slot` | [Rendering a slot](slots.md) |
|
||||
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
||||
|
||||
## Reference
|
||||
|
||||
### White Spaces
|
||||
|
||||
White spaces in a template are handled in a special way:
|
||||
|
||||
- consecutive whitespaces are always condensed to a single whitespace
|
||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||
- the previous rules do not apply if we are in a `<pre>` tag
|
||||
|
||||
### Root Nodes
|
||||
|
||||
For many reasons, Owl QWeb templates should have a single root node. More
|
||||
precisely, the result of a template rendering should have a single root node:
|
||||
|
||||
```xml
|
||||
<!–– not ok: two root nodes ––>
|
||||
<t>
|
||||
<div>foo</div>
|
||||
<div>bar</div>
|
||||
</t>
|
||||
|
||||
<!–– ok: result has one single root node ––>
|
||||
<t>
|
||||
<div t-if="someCondition">foo</div>
|
||||
<span t-else="">bar</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
Extra root nodes will actually be ignored (even though they will be rendered
|
||||
in memory).
|
||||
|
||||
Note: this does not apply to subtemplates (see the `t-call` directive). In that
|
||||
case, they will be inlined in the main template, and can actually have many
|
||||
root nodes.
|
||||
|
||||
### Expression Evaluation
|
||||
|
||||
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||
the javascript expression will be replaced with a lookup in the context (so, the
|
||||
component). For example, `a + b.c(d)` will be converted into:
|
||||
|
||||
```js
|
||||
context["a"] + context["b"].c(context["d"]);
|
||||
```
|
||||
|
||||
It is useful to explain the various rules that apply on these expressions:
|
||||
|
||||
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||
|
||||
```xml
|
||||
<div><p t-if="1 + 2 === 3">ok</p></div>
|
||||
```
|
||||
|
||||
is valid, but the following is not valid:
|
||||
|
||||
```xml
|
||||
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||
```
|
||||
|
||||
2. it can use anything in the rendering context (typically, the component):
|
||||
|
||||
```xml
|
||||
<p t-if="user.birthday === today()">Happy bithday!</p>
|
||||
```
|
||||
|
||||
is valid, and will read the `user` object from the context, and call the
|
||||
`today` function.
|
||||
|
||||
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
||||
`&` or `|`. This is useful to make sure that we still write valid XML.
|
||||
|
||||
| Word | replaced with |
|
||||
| ----- | ------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
```xml
|
||||
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||
```
|
||||
|
||||
### Static Html Nodes
|
||||
|
||||
Normal, regular html nodes are rendered into themselves:
|
||||
|
||||
```xml
|
||||
<div>hello</div> <!–– rendered as itself ––>
|
||||
```
|
||||
|
||||
### Outputting Data
|
||||
|
||||
The `t-esc` directive is necessary whenever you want to add a dynamic text
|
||||
expression in a template. The text is escaped to avoid security issues.
|
||||
|
||||
```xml
|
||||
<p><t t-esc="value"/></p>
|
||||
```
|
||||
|
||||
rendered with the value `value` set to `42` in the rendering context yields:
|
||||
|
||||
```html
|
||||
<p>42</p>
|
||||
```
|
||||
|
||||
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
|
||||
This is mostly useful to inject a raw html string somewhere. Obviously, this
|
||||
is unsafe to do in general, and should only be used for strings known to be safe.
|
||||
|
||||
```xml
|
||||
<p><t t-raw="value"/></p>
|
||||
```
|
||||
|
||||
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
|
||||
|
||||
```html
|
||||
<p><span>foo</span></p>
|
||||
```
|
||||
|
||||
Note that since the content of the expression is not known beforehand, the `t-raw`
|
||||
directive has to parse the html (and convert it to a virtual dom structure) for
|
||||
each rendering. So, it will be much slower than a regular template. It is
|
||||
therefore advised to limit the use of `t-raw` whenever possible.
|
||||
|
||||
### Setting Variables
|
||||
|
||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||
|
||||
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
|
||||
|
||||
1. a `t-value` attribute containing an expression, and the result of its
|
||||
evaluation will be set:
|
||||
|
||||
```xml
|
||||
<t t-set="foo" t-value="2 + 1"/>
|
||||
<t t-esc="foo"/>
|
||||
```
|
||||
|
||||
will print `3`. Note that the evaluation is done at rendering time, not at
|
||||
compilte time.
|
||||
|
||||
2. if there is no `t-value` attribute, the node’s body is saved and its value is
|
||||
set as the variable’s value:
|
||||
|
||||
```xml
|
||||
<t t-set="foo">
|
||||
<li>ok</li>
|
||||
</t>
|
||||
<t t-esc="foo"/>
|
||||
```
|
||||
|
||||
will generate `<li>ok</li>` (the content is escaped as we used the `t-esc` directive)
|
||||
|
||||
The `t-set` directive acts like a regular variable in most programming language.
|
||||
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
|
||||
|
||||
### Conditionals
|
||||
|
||||
The `t-if` directive is useful to conditionally render something. It evaluates
|
||||
the expression given as attribute value, and then acts accordingly.
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<t t-if="condition">
|
||||
<p>ok</p>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
The element is rendered if the condition (evaluated with the current rendering
|
||||
context) is true:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p>ok</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
but if the condition is false it is removed from the result:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
</div>
|
||||
```
|
||||
|
||||
The conditional rendering applies to the bearer of the directive, which does not
|
||||
have to be `<t>`:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p t-if="condition">ok</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
will give the same results as the previous example.
|
||||
|
||||
Extra conditional branching directives `t-elif` and `t-else` are also available:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p t-if="user.birthday == today()">Happy bithday!</p>
|
||||
<p t-elif="user.login == 'root'">Welcome master!</p>
|
||||
<p t-else="">Welcome!</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dynamic Attributes
|
||||
|
||||
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
|
||||
evaluate an expression (at rendering time) and bind an attribute to its result:
|
||||
|
||||
For example, if we have `id` set to 32 in the rendering context,
|
||||
|
||||
```xml
|
||||
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
|
||||
```
|
||||
|
||||
If an expression evaluates to a falsy value, it will not be set at all:
|
||||
|
||||
```xml
|
||||
<div t-att-foo="false"/> <!-- result: <div></div> -->
|
||||
```
|
||||
|
||||
It is sometimes convenient to format an attribute with string interpolation. In
|
||||
that case, the `t-attf-` directive can be used. It is useful when we need to mix
|
||||
literal and dynamic elements, such as css classes.
|
||||
|
||||
```xml
|
||||
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
|
||||
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||
```
|
||||
|
||||
If we need completely dynamic attribute names, then there is an additional
|
||||
directive: `t-att`, which takes either an object (with keys mapping to their
|
||||
values) or a pair `[key, value]`. For example:
|
||||
|
||||
```xml
|
||||
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></div> -->
|
||||
|
||||
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
|
||||
```
|
||||
|
||||
### Dynamic class attribute
|
||||
|
||||
For convenience, Owl supports a special case for the `t-att-class` case: one can
|
||||
use an object with keys describing the classes, and values boolean value denoting
|
||||
if the class is or is not present:
|
||||
|
||||
```xml
|
||||
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
|
||||
|
||||
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
|
||||
```
|
||||
|
||||
Note that it can be combined with normal class attribute:
|
||||
|
||||
```xml
|
||||
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
|
||||
```
|
||||
|
||||
### Dynamic tag names
|
||||
|
||||
When writing generic components or templates, the specific concrete tag for an
|
||||
HTML element is not known yet. In those situations, the `t-tag` directive is
|
||||
useful. It simply evaluates dynamically an expression to use as a tag name. The
|
||||
template:
|
||||
|
||||
```xml
|
||||
<t t-tag="tag">
|
||||
<span>content</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
will be rendered as `<div><span>content</span></div>` if the `tag` context key
|
||||
is set to `div`.
|
||||
|
||||
### Loops
|
||||
|
||||
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
||||
collection to iterate on, and a second parameter `t-as` providing the name to use
|
||||
for the current item of the iteration:
|
||||
|
||||
```xml
|
||||
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||
<p><t t-esc="i"/></p>
|
||||
</t>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<p>1</p>
|
||||
<p>2</p>
|
||||
<p>3</p>
|
||||
```
|
||||
|
||||
Like conditions, `t-foreach` applies to the element bearing the directive’s attribute, and
|
||||
|
||||
```xml
|
||||
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
```
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||
or an object (the current item will be the current key).
|
||||
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||
variables for various data points (note: `$as` will be replaced with the name
|
||||
passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||
- `$as_first`: whether the current item is the first of the iteration
|
||||
(equivalent to `$as_index == 0`)
|
||||
- `$as_last`: whether the current item is the last of the iteration
|
||||
(equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be
|
||||
available
|
||||
|
||||
These extra variables provided and all new variables created into the `t-foreach`
|
||||
are only available in the scope of the `t-foreach`. If the variable exists outside
|
||||
the context of the `t-foreach`, the value is copied at the end of the foreach
|
||||
into the global context.
|
||||
|
||||
```xml
|
||||
<t t-set="existing_variable" t-value="false"/>
|
||||
<!-- existing_variable now False -->
|
||||
|
||||
<p t-foreach="Array(3)" t-as="i">
|
||||
<t t-set="existing_variable" t-value="true"/>
|
||||
<t t-set="new_variable" t-value="true"/>
|
||||
<!-- existing_variable and new_variable now true -->
|
||||
</p>
|
||||
|
||||
<!-- existing_variable always true -->
|
||||
<!-- new_variable undefined -->
|
||||
```
|
||||
|
||||
Even though Owl tries to be as declarative as possible, the DOM does not fully
|
||||
expose its state declaratively in the DOM tree. For example, the scrolling state,
|
||||
the current user selection, the focused element or the state of an input are not
|
||||
set as attribute in the DOM tree. This is why we use a virtual dom
|
||||
algorithm to keep the actual DOM node as much as possible.
|
||||
|
||||
However, in some situations, this is not enough, and we need to help Owl decide
|
||||
if an element is actually the same, or is a different element with the same
|
||||
properties.
|
||||
|
||||
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
|
||||
and we render them in this template:
|
||||
|
||||
```xml
|
||||
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
|
||||
```
|
||||
|
||||
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
|
||||
and rerender the template, Owl needs to know what the intent is:
|
||||
|
||||
- should Owl actually swap the DOM nodes,
|
||||
- or should it keep the DOM nodes, but with an updated text content?
|
||||
|
||||
This might look trivial, but it actually matters. These two possibilities lead
|
||||
to different results in some cases. For example, if the user selected the text
|
||||
of the first `p`, swapping them will keep the selection while updating the
|
||||
text content will not.
|
||||
|
||||
There are many other cases where this is important: `input` tags with their
|
||||
value, css classes and animations, scroll position...
|
||||
|
||||
So, the `t-key` directive is used to give an identity to an element. It allows
|
||||
Owl to understand if different elements of a list are actually different or not.
|
||||
|
||||
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
|
||||
Then, the template could look like this:
|
||||
|
||||
```xml
|
||||
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
|
||||
```
|
||||
|
||||
The `t-key` directive is useful for lists (`t-foreach`). A key should be
|
||||
a unique number or string (objects will not work: they will be cast to the
|
||||
`"[object Object]"` string, which is obviously not unique).
|
||||
|
||||
Also, the key can be set on a `t` tag or on its children. The following variations
|
||||
are all equivalent:
|
||||
|
||||
```xml
|
||||
<p t-foreach="items" t-as="item" t-key="item.id">
|
||||
<t t-esc="item.text"/>
|
||||
</p>
|
||||
|
||||
<t t-foreach="items" t-as="item" t-key="item.id">
|
||||
<p t-esc="item.text"/>
|
||||
</t>
|
||||
|
||||
<t t-foreach="items" t-as="item">
|
||||
<p t-key="item.id" t-esc="item.text"/>
|
||||
</t>
|
||||
```
|
||||
|
||||
If there is no `t-key` directive, Owl will use the index as a default key.
|
||||
|
||||
Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
|
||||
not work with other iterables, such as `Set`. However, it is only a matter of
|
||||
using the `...` javascript operator. For example:
|
||||
|
||||
```xml
|
||||
<t t-foreach="...items" t-as="item">...</t>
|
||||
```
|
||||
|
||||
The `...` operator will convert the `Set` (or any other iterables) into a list,
|
||||
which will work with Owl QWeb.
|
||||
|
||||
### Rendering Sub Templates
|
||||
|
||||
QWeb templates can be used for top level rendering, but they can also be used
|
||||
from within another template (to avoid duplication or give names to parts of
|
||||
templates), using the `t-call` directive:
|
||||
|
||||
```xml
|
||||
<div t-name="other-template">
|
||||
<p><t t-value="var"/></p>
|
||||
</div>
|
||||
|
||||
<div t-name="main-template">
|
||||
<t t-set="var" t-value="owl"/>
|
||||
<t t-call="other-template"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
|
||||
template is rendered with the execution context of the parent. The sub template
|
||||
is actually inlined in the main template, but in a sub scope: variables defined
|
||||
in the sub template do not escape.
|
||||
|
||||
Sometimes, one might want to pass information to the sub template. In that case,
|
||||
the content of the body of the `t-call` directive is available as a special
|
||||
magic variable `0`:
|
||||
|
||||
```xml
|
||||
<t t-name="other-template">
|
||||
This template was called with content:
|
||||
<t t-raw="0"/>
|
||||
</t>
|
||||
|
||||
<div t-name="main-template">
|
||||
<t t-call="other-template">
|
||||
<em>content</em>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
will result in :
|
||||
|
||||
```xml
|
||||
<div>
|
||||
This template was called with content:
|
||||
<em>content</em>
|
||||
</div>
|
||||
```
|
||||
|
||||
This can be used to define variables scoped to a sub template:
|
||||
|
||||
```xml
|
||||
<t t-call="other-template">
|
||||
<t t-set="var" t-value="1"/>
|
||||
</t>
|
||||
<!-- "var" does not exist here -->
|
||||
```
|
||||
|
||||
### Dynamic sub templates
|
||||
|
||||
The `t-call` directive can also be used to dynamically call a sub template,
|
||||
using string interpolation. For example:
|
||||
|
||||
```xml
|
||||
<div t-name="main-template">
|
||||
<t t-call="{{template}}">
|
||||
<em>content</em>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
Here, the name of the template is obtained from the `template` value in the
|
||||
template rendering context.
|
||||
|
||||
### Translations
|
||||
|
||||
By default, QWeb specify that templates should be translated. If this behaviour
|
||||
is not wanted, there is a `t-translation` directive which can turn off
|
||||
translations (if it is set to the `off` value), with the following rules:
|
||||
|
||||
- each text node will be replaced with its translation,
|
||||
- each of the following attribute values will be translated as well: `title`,
|
||||
`placeholder`, `label` and `alt`,
|
||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||
if its value is `off`.
|
||||
|
||||
See [here](qweb_engine.md#translations) for more information on how to setup a
|
||||
translate function in Owl QWeb.
|
||||
|
||||
### Debugging
|
||||
|
||||
The javascript QWeb implementation provides two useful debugging directives:
|
||||
|
||||
`t-debug` adds a debugger statement during template rendering:
|
||||
|
||||
```xml
|
||||
<t t-if="a_test">
|
||||
<t t-debug=""/>
|
||||
</t>
|
||||
```
|
||||
|
||||
will stop execution if the browser dev tools are open.
|
||||
|
||||
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
|
||||
|
||||
```xml
|
||||
<t t-set="foo" t-value="42"/>
|
||||
<t t-log="foo"/>
|
||||
```
|
||||
|
||||
will print 42 to the console.
|
||||
@@ -0,0 +1,181 @@
|
||||
# 🦉 Router 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Route Definition](#route-definition)
|
||||
- [Router](#router)
|
||||
- [Navigation Guards](#navigation-guards)
|
||||
- [RouteComponent](#routecomponent)
|
||||
- [Link](#link)
|
||||
|
||||
## Overview
|
||||
|
||||
It is often useful to organize an application around urls. If the application is
|
||||
a single page application, then we need a way to manage those urls in the browser.
|
||||
This is why there are many different routers for different frameworks. A generic
|
||||
router can do the job just fine, but a specialized router for Owl can give a
|
||||
better developer experience.
|
||||
|
||||
The Owl router support the following features:
|
||||
|
||||
- `history` or `hash` mode
|
||||
- declarative routes
|
||||
- route redirection
|
||||
- navigation guards
|
||||
- parameterized routes
|
||||
- a `<Link/>` component
|
||||
- a `<RouteComponent/>` component
|
||||
|
||||
Note that it is still in early stage of developments, and there are probably
|
||||
still some issues.
|
||||
|
||||
## Example
|
||||
|
||||
To use the Owl router, there are some steps that needs to be done:
|
||||
|
||||
- declare some routes
|
||||
- create a router
|
||||
- add it to the environment
|
||||
|
||||
```js
|
||||
async function protectRoute({ env, to }) {
|
||||
if (!env.session.authUser) {
|
||||
env.session.setNextRoute(to.name);
|
||||
return { to: "SIGN_IN" };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const ROUTES = [
|
||||
{ name: "LANDING", path: "/", component: Landing },
|
||||
{ name: "TASK", path: "/tasks/{{id}}", component: Task },
|
||||
{ name: "SIGN_UP", path: "/signup", component: SignUp },
|
||||
{ name: "SIGN_IN", path: "/signin", component: SignIn },
|
||||
{ name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute },
|
||||
{ name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute },
|
||||
{ name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } }
|
||||
];
|
||||
|
||||
function makeEnvironment() {
|
||||
...
|
||||
const env = { qweb };
|
||||
env.session = new Session(env);
|
||||
env.router = new owl.router.Router(env, ROUTES);
|
||||
await env.router.start();
|
||||
return env;
|
||||
}
|
||||
|
||||
App.env = makeEnvironment();
|
||||
// create root component here
|
||||
```
|
||||
|
||||
Notice that the router needs to be started. This is an asynchronous operation
|
||||
because it needs to apply the potential navigation guards on the current route
|
||||
(which may or may not mean that the application is redirected to another route).
|
||||
|
||||
## Reference
|
||||
|
||||
### Route definition
|
||||
|
||||
A route need to be defined as an object with the following keys:
|
||||
|
||||
- `name` (optional): a (unique) string useful to identify the current route. If not
|
||||
given, it will be assigned an automatic name,
|
||||
- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`.
|
||||
It also can be `*`, to catch all remaining routes.
|
||||
- `component` (optional): an Owl component that will be used by the `t-routecomponent`
|
||||
directive if the route is active
|
||||
- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route
|
||||
- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards).
|
||||
|
||||
### `Router`
|
||||
|
||||
The `Router` constructor takes three arguments:
|
||||
|
||||
- `env`: a valid environment,
|
||||
- a list of routes,
|
||||
- an optional object (with the only key `mode` which can be `history` (default
|
||||
value) or `hash`).
|
||||
|
||||
`history` will use the browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) as the mechanism to manage URL.\
|
||||
Example: `https://yourdomain.tld/my_custom_route`.\
|
||||
For this mechanism to work, you need a way to configure your web server accordingly.
|
||||
|
||||
`hash` will manipulate the hash of the URL.\
|
||||
Example: `https://yourdomain.tld/index.html#/my_custom_route`.
|
||||
|
||||
```js
|
||||
const ROUTES = [...];
|
||||
const router = new owl.router.Router(env, ROUTES, {mode: 'history'});
|
||||
```
|
||||
|
||||
Note that the route are defined in a list, and the order matters: the router
|
||||
tries to find a match by going down the list.
|
||||
|
||||
The router needs to be added to the environment in the `router` sub key.
|
||||
|
||||
Once a router is created, it needs to be started. This is necessary to initialize
|
||||
its current state to the current URL (and also, to potentially apply any
|
||||
navigation guards and/or redirecting).
|
||||
|
||||
```js
|
||||
await router.start();
|
||||
```
|
||||
|
||||
Once started, the router will keep track of the current url and reflect its
|
||||
value in two keys:
|
||||
|
||||
- `router.currentRoute`
|
||||
- `router.currentParams`
|
||||
|
||||
The router also has a `navigate` method, useful to programmatically change the
|
||||
application to another state (and the url):
|
||||
|
||||
```js
|
||||
router.navigate({ to: "USER", params: { id: 51 } });
|
||||
```
|
||||
|
||||
### Navigation Guards
|
||||
|
||||
Navigation guards are very useful to be able to execute some business logic/
|
||||
perform some actions or redirect to other routes whenever the application is
|
||||
entering a new route. For example, the following guard checks if there is an
|
||||
authenticated user, and if it is not the case, redirect to the sign in route.
|
||||
|
||||
```js
|
||||
async function protectRoute({ env, to }) {
|
||||
if (!env.session.authUser) {
|
||||
env.session.setNextRoute(to.name);
|
||||
return { to: "SIGN_IN" };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
A navigation guard is a function that returns a promise, which either resolves
|
||||
to `true` (the navigation is accepted), or to another destination object.
|
||||
|
||||
### `RouteComponent`
|
||||
|
||||
The `RouteComponent` component directs Owl to render the component associated
|
||||
to the currently active route (if any):
|
||||
|
||||
```xml
|
||||
<div t-name="App">
|
||||
<NavBar />
|
||||
<RouteComponent />
|
||||
</div>
|
||||
```
|
||||
|
||||
### `Link`
|
||||
|
||||
The `Link` component is a Owl component which render as a `<a>` tag with any
|
||||
content. It will compute the proper href from its props, and allow Owl to
|
||||
properly navigate to a given url if clicked on it.
|
||||
|
||||
```xml
|
||||
<Link to="'HOME'">Home</Link>
|
||||
```
|
||||
@@ -0,0 +1,111 @@
|
||||
# 🦉 Slots 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Overview
|
||||
|
||||
Owl is a template based component system. There is therefore a need to be able
|
||||
to make generic components. For example, imagine a generic `Dialog`
|
||||
component, which is able to display some arbitrary content.
|
||||
|
||||
Obviously, we want to use this component everywhere in our application, to
|
||||
display various different content. The `Dialog` component is technically the
|
||||
owner of its content, but is only a container. The user of the `Dialog` is
|
||||
the component that want to _inject_ something inside the `Dialog`. This is
|
||||
exactly what slots are for.
|
||||
|
||||
## Example
|
||||
|
||||
To make generic components, it is useful to be able for a parent component to _inject_
|
||||
some sub template, but still be the owner. For example, a generic dialog component
|
||||
will need to render some content, some footer, but with the parent as the
|
||||
rendering context.
|
||||
|
||||
Slots are inserted with the `t-slot` directive:
|
||||
|
||||
```xml
|
||||
<div t-name="Dialog" class="modal">
|
||||
<div class="modal-title"><t t-esc="props.title"/></div>
|
||||
<div class="modal-content">
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<t t-slot="footer"/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Slots are defined by the caller, with the `t-set-slot` directive:
|
||||
|
||||
```xml
|
||||
<div t-name="SomeComponent">
|
||||
<div>some component</div>
|
||||
<Dialog title="'Some Dialog'">
|
||||
<t t-set-slot="content">
|
||||
<div>hey</div>
|
||||
</t>
|
||||
<t t-set-slot="footer">
|
||||
<button t-on-click="doSomething">ok</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component `Dialog` will render the slots `content` and `footer`
|
||||
with its parent as rendering context. This means that clicking on the button
|
||||
will execute the `doSomething` method on the parent, not on the dialog.
|
||||
|
||||
Note: Owl previously used the `t-set` directive to define the content of a slot.
|
||||
This is deprecated and should no longer be used in new code.
|
||||
|
||||
## Reference
|
||||
|
||||
### Default Slot
|
||||
|
||||
The first element inside the component which is not a named slot will
|
||||
be considered the `default` slot. For example:
|
||||
|
||||
```xml
|
||||
<div t-name="Parent">
|
||||
<Child>
|
||||
<span>some content</span>
|
||||
</Child>
|
||||
</div>
|
||||
|
||||
<div t-name="Child">
|
||||
<t t-slot="default"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Default content
|
||||
|
||||
Slots can define a default content, in case the parent did not define them:
|
||||
|
||||
```xml
|
||||
<div t-name="Parent">
|
||||
<Child/>
|
||||
</div>
|
||||
|
||||
<span t-name="Child">
|
||||
<t t-slot="default">default content</t>
|
||||
</span>
|
||||
<!-- will be rendered as: <div><span>default content</span></div> -->
|
||||
```
|
||||
|
||||
Rendering context: the content of the slots is actually rendered with the
|
||||
rendering context corresponding to where it was defined, not where it is
|
||||
positioned. This allows the user to define event handlers that will be bound
|
||||
to the correct component (usually, the grandparent of the slot content).
|
||||
|
||||
### Dynamic Slots
|
||||
|
||||
The `t-slot` directive is actually able to use any expressions, using string
|
||||
interplolation:
|
||||
|
||||
```xml
|
||||
<t t-slot="{{current}}" />
|
||||
```
|
||||
@@ -0,0 +1,349 @@
|
||||
# 🦉 Store 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Reference](#reference)
|
||||
- [Store](#store)
|
||||
- [Actions](#actions)
|
||||
- [Getters](#getters)
|
||||
- [Connecting a Component](#connecting-a-component)
|
||||
- [`useStore`](#usestore)
|
||||
- [`useDispatch`](#usedispatch)
|
||||
- [`useGetters`](#usegetters)
|
||||
- [Semantics](#semantics)
|
||||
- [Good Practices](#good-practices)
|
||||
|
||||
## Overview
|
||||
|
||||
Managing the state in an application is not an easy task. In some cases, the
|
||||
state of an application can be part of the component tree, in a natural way.
|
||||
However, there are situations where some parts of the state need to be displayed
|
||||
in various parts of the user interface, and then, it is not obvious which
|
||||
component should own which part of the state.
|
||||
|
||||
Owl's solution to this issue is a centralized store. It is a class that owns
|
||||
some (or all) state, and lets the developer update it in a structured way, with
|
||||
`actions`. Owl components can then connect to the store to read their relevant
|
||||
state, and they will be rerendered if the state is updated.
|
||||
|
||||
Note: Owl store is inspired by React Redux and VueX.
|
||||
|
||||
## Example
|
||||
|
||||
Here is what a simple store looks like:
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
addTodo({ state }, message) {
|
||||
state.todos.push({
|
||||
id: state.nextId++,
|
||||
message,
|
||||
isCompleted: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
todos: [],
|
||||
nextId: 1,
|
||||
};
|
||||
|
||||
const store = new owl.Store({ state, actions });
|
||||
store.on("update", null, () => console.log(store.state));
|
||||
|
||||
// updating the state
|
||||
store.dispatch("addTodo", "fix all bugs");
|
||||
```
|
||||
|
||||
This example shows how a store can be defined and used. Note that in most cases,
|
||||
actions will be dispatched by connected components.
|
||||
|
||||
## Reference
|
||||
|
||||
### `Store`
|
||||
|
||||
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events
|
||||
whenever its state is changed. Note that these events are triggered only after a
|
||||
microtask tick, so only one event will be triggered for any number of state changes in a
|
||||
call stack.
|
||||
|
||||
Also, it is important to mention that the state is observed (with an `owl.Observer`),
|
||||
which is the reason why it is able to know if it was changed. See the
|
||||
[Observer](observer.md)'s documentation for more details.
|
||||
|
||||
The `Store` class is quite small. It has two public methods:
|
||||
|
||||
- its constructor
|
||||
- `dispatch`
|
||||
|
||||
The constructor takes a configuration object with four (optional) keys:
|
||||
|
||||
- the initial state
|
||||
- the actions
|
||||
- the getters
|
||||
- the environment
|
||||
|
||||
```javascript
|
||||
const config = {
|
||||
state,
|
||||
actions,
|
||||
getters,
|
||||
env,
|
||||
};
|
||||
const store = new Store(config);
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
Actions are used to coordinate state changes. It can be used for both synchronous
|
||||
and asynchronous logic.
|
||||
|
||||
```js
|
||||
const actions = {
|
||||
async login({ state }, info) {
|
||||
state.loginState = "pending";
|
||||
try {
|
||||
const loginInfo = await doSomeRPC("/login/", info);
|
||||
state.loginState = loginInfo;
|
||||
} catch (e) {
|
||||
state.loginState = "error";
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The first argument to an action method is an object with four keys:
|
||||
|
||||
- `state`: the current state of the store content,
|
||||
- `dispatch`: a function that can be used to dispatch other actions,
|
||||
- `getters`: an object containing all getters defined in the store,
|
||||
- `env`: the current environment. This is useful sometimes, in particular if
|
||||
an action needs to apply some side effects (such as performing an rpc), and
|
||||
the `rpc` method is located in the environment.
|
||||
|
||||
Actions are called with the `dispatch` method on the store, and can receive an
|
||||
arbitrary number of arguments.
|
||||
|
||||
```js
|
||||
store.dispatch("login", someInfo);
|
||||
```
|
||||
|
||||
Note that anything returned by an action will also be returned by the `dispatch`
|
||||
call.
|
||||
|
||||
Also, it is important to be aware that we need to be careful with asynchronous
|
||||
logic. Each state change will potentially trigger a rerendering, so we need to
|
||||
make sure that we do not have a partially corrupted state. Here is an example that
|
||||
is likely not a good idea:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
async fetchSomeData({ state }, recordId) {
|
||||
state.recordId = recordId;
|
||||
const data = await doSomeRPC("/read/", recordId);
|
||||
state.recordData = data;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
In the previous example, there is a period of time in which the state has a
|
||||
`recordId` which does not correspond to the `recordData`. It is more likely that
|
||||
we want an atomic update: updating the `recordId` at the same time as the `recordData`
|
||||
values:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
async fetchSomeData({ state }, recordId) {
|
||||
const data = await doSomeRPC("/read/", recordId);
|
||||
state.recordId = recordId;
|
||||
state.recordData = data;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Getters
|
||||
|
||||
Usually, data contained in the store will be stored in a normalized way. For
|
||||
example,
|
||||
|
||||
```js
|
||||
{
|
||||
posts: [{id: 11, authorId: 4, content: 'Greetings'}],
|
||||
authors: [{id: 4, name: 'John'}]
|
||||
}
|
||||
```
|
||||
|
||||
However, the user interface will probably need some denormalized data like
|
||||
|
||||
```js
|
||||
{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'}
|
||||
```
|
||||
|
||||
This is what `getters` are for: they give a centralized way to process and
|
||||
transform the data contained in the store.
|
||||
|
||||
```js
|
||||
const getters = {
|
||||
getPost({ state }, id) {
|
||||
const post = state.posts.find((p) => p.id === id);
|
||||
const author = state.authors.find((a) => a.id === post.id);
|
||||
return {
|
||||
id,
|
||||
author,
|
||||
content: post.content,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// somewhere else
|
||||
const post = store.getters.getPost(id);
|
||||
```
|
||||
|
||||
Getters take _at most_ one argument.
|
||||
|
||||
Note that getters are not cached.
|
||||
|
||||
### Connecting a Component
|
||||
|
||||
At some point, we need a way to interact with the store from a component. This
|
||||
means that the component needs a reference to the store. By default, it looks
|
||||
for it in the `env.store` key. However, this can be configured with the `useStore`
|
||||
hook.
|
||||
|
||||
Every component-store interactions are done with the help of the three store hooks:
|
||||
|
||||
- [`useStore`](#usestore) to subscribe a component to some part of the store state,
|
||||
- [`useDispatch`](#usedispatch) to get a reference to a dispatch function,
|
||||
- [`useGetters`](#usegetters) to get a reference to the getters defined in the store.
|
||||
|
||||
Assume we have this store:
|
||||
|
||||
```javascript
|
||||
const actions = {
|
||||
increment({ state }, val) {
|
||||
state.counter.value += val;
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
counter: { value: 0 },
|
||||
};
|
||||
const store = new owl.Store({ state, actions });
|
||||
```
|
||||
|
||||
To make it accessible to the complete application, we will put it in the
|
||||
environment:
|
||||
|
||||
```js
|
||||
// in this example, the root component is App
|
||||
App.env.store = store;
|
||||
```
|
||||
|
||||
A counter component can then select this value and dispatch an action like this:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
counter = useStore((state) => state.counter);
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
|
||||
const counter = new Counter({ store, qweb });
|
||||
```
|
||||
|
||||
```xml
|
||||
<button t-name="Counter" t-on-click="dispatch('increment')">
|
||||
Click Me! [<t t-esc="counter.value"/>]
|
||||
</button>
|
||||
```
|
||||
|
||||
### `useStore`
|
||||
|
||||
The `useStore` hook is used to select some part of the store state. It accepts
|
||||
two arguments:
|
||||
|
||||
- a selector function, which takes the store state as first argument (and the
|
||||
component props as second argument) and which must return the part of the
|
||||
store state that will be made available and observed for changes,
|
||||
- optionally, an object which can have the following optional keys:
|
||||
- a `store` key containing a store object if we want to use another store than
|
||||
the default store,
|
||||
- an `isEqual` key containing an equality function if we want to specialize
|
||||
the comparison (the function must accept two arguments: the previous result
|
||||
and the new result, and must return whether they are equal),
|
||||
- and an `onUpdate` key containing an update function if we want to execute an
|
||||
arbitrary code every time the selected state changes (the function will
|
||||
receive one argument, the new result, and can execute arbitrary code).
|
||||
|
||||
If the `useStore` selector returns a sub part of the store state, the component
|
||||
will only be rerendered whenever this part of the state changes. Otherwise, it
|
||||
will perform a strict equality check (unless the `isEqual` option is defined,
|
||||
then it will call it) and will update the component every time this check fails.
|
||||
|
||||
Note that if the selector function returns a primitive type, the result of
|
||||
`useStore` will be immutable and it will not react to changes. In this case, it
|
||||
is important to define the `onUpdate` option to properly update the value
|
||||
manually when it changes.
|
||||
|
||||
Also, the return value from `useStore` is not supposed to be modified. The store
|
||||
state should only be updated with actions.
|
||||
|
||||
### `useDispatch`
|
||||
|
||||
The `useDispatch` hook is useful when a component needs to be able to dispatch
|
||||
actions. It takes an optional argument, which is a store. If not given, it will
|
||||
use the store in the environment.
|
||||
|
||||
Note that a component does not need to be connected in any other way to the store.
|
||||
For example:
|
||||
|
||||
```js
|
||||
class DoSomethingButton extends Component {
|
||||
static template = xml`<button t-on-click="dispatch('something')">Click</button>`;
|
||||
dispatch = useDispatch();
|
||||
}
|
||||
```
|
||||
|
||||
### `useGetters`
|
||||
|
||||
The `useGetters` hook is useful when a component needs to be able to use the
|
||||
getters defined in a store. It takes an optional argument, which is a store. If
|
||||
not given, it will use the store in the environment.
|
||||
|
||||
Note that a component does not need to be connected in any other way to the store.
|
||||
For example:
|
||||
|
||||
```js
|
||||
class InfoButton extends Component {
|
||||
static template = xml`<span><t t-esc="getters.somevalue()"></span>`;
|
||||
getters = useGetters();
|
||||
}
|
||||
```
|
||||
|
||||
### Semantics
|
||||
|
||||
The `Store` class and the `useStore` hook try to be smart and to optimize as much
|
||||
as possible the rendering and update process. What is important to know is:
|
||||
|
||||
- components are always updated in the order of their creation (so, parent
|
||||
before children),
|
||||
- they are updated only if they are in the DOM,
|
||||
- if a parent is asynchronous, the system will wait for it to complete its
|
||||
update before updating other components,
|
||||
- in general, updates are not coordinated. This is not a problem for synchronous
|
||||
components, but if there are many asynchronous components, this could lead to
|
||||
a situation where some part of the UI is updated and some other part of the UI is
|
||||
not updated.
|
||||
|
||||
### Good Practices
|
||||
|
||||
- avoid asynchronous components as much as possible. Asynchronous components
|
||||
lead to situations where parts of the UI is not updated immediately,
|
||||
- do not be afraid to connect many components, parent or children if needed. For
|
||||
example, a `MessageList` component could get a list of ids in its `useStore`
|
||||
call and a `Message` component could get the data of its own
|
||||
message,
|
||||
- since the `useStore` function is called for each connected component,
|
||||
for each state update, it is important to make sure that these functions are
|
||||
as fast as possible.
|
||||
@@ -0,0 +1,184 @@
|
||||
# 🦉 Tags 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [`xml` tag](#xml-tag)
|
||||
- [`css` tag](#css-tag)
|
||||
|
||||
## Overview
|
||||
|
||||
Tags are very small helpers intended to make it easy to write inline templates
|
||||
or styles. There are currently two tags: `css` and `xml`. With these functions,
|
||||
it is possible to write [single file components](../learning/how_to_write_sfc.md).
|
||||
|
||||
## XML tag
|
||||
|
||||
The `xml` tag is certainly the most useful tag. It is used to define an inline
|
||||
QWeb template for a component. Without tags, creating a standalone component
|
||||
would look like this:
|
||||
|
||||
```js
|
||||
import { Component } from 'owl'
|
||||
|
||||
const name = 'some-unique-name';
|
||||
const template = `
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
QWeb.registerTemplate(name, template);
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = name;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
With tags, this process is slightly simplified. The name is uniquely generated,
|
||||
and the template is automatically registered:
|
||||
|
||||
```js
|
||||
const { Component } = owl;
|
||||
const { xml } = owl.tags;
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## CSS tag
|
||||
|
||||
The CSS tag is useful to define a css stylesheet in the javascript file:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div class="my-component">some template</div>
|
||||
`;
|
||||
static style = css`
|
||||
.my-component {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
The `css` tag registers internally the css information. Then, whenever the first
|
||||
instance of the component is created, will add a `<style>` tag to the document
|
||||
`<head>`.
|
||||
|
||||
Note that to make it more useful, like other css preprocessors, the `css` tag
|
||||
accepts a small extension of the css specification: css scopes can be nested,
|
||||
and the rules will then be expanded by the `css` helper:
|
||||
|
||||
```scss
|
||||
.my-component {
|
||||
display: block;
|
||||
.sub-component h {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
will be formatted as:
|
||||
|
||||
```css
|
||||
.my-component {
|
||||
display: block;
|
||||
}
|
||||
.my-component .sub-component h {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
This extension brings another useful feature: the `&` selector which refers to
|
||||
the parent selector. For example, we want our component to be red when hovered.
|
||||
We would like to write something like:
|
||||
|
||||
```scss
|
||||
.my-component {
|
||||
display: block;
|
||||
:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
but it will be formatted as:
|
||||
|
||||
```css
|
||||
.my-component {
|
||||
display: block;
|
||||
}
|
||||
.my-component :hover {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
The `&` selector can be used to solve this problem:
|
||||
|
||||
```scss
|
||||
.my-component {
|
||||
display: block;
|
||||
&:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
will be formatted as:
|
||||
|
||||
```css
|
||||
.my-component {
|
||||
display: block;
|
||||
}
|
||||
.my-component:hover {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
Now, there is no additional processing done by the `css` tag. However, since it
|
||||
is done in javascript at runtime, we actually have more power. For example:
|
||||
|
||||
1. sharing values between javascript and css:
|
||||
|
||||
```js
|
||||
import { theme } from "./theme";
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div class="my-component">...</div>`;
|
||||
static style = css`
|
||||
.my-component {
|
||||
color: ${theme.MAIN_COLOR};
|
||||
background-color: ${theme.SECONDARY_color};
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
2. scoping rules to the current component:
|
||||
|
||||
```js
|
||||
import { generateUUID } from "./utils";
|
||||
|
||||
const uuid = generateUUID();
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`<div data-o-${uuid}="">...</div>`;
|
||||
static style = css`
|
||||
[data-o-${uuid}] {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,148 @@
|
||||
# 🦉 Utils 🦉
|
||||
|
||||
Owl export a few useful utility functions, to help with common issues. Those
|
||||
functions are all available in the `owl.utils` namespace.
|
||||
|
||||
## Content
|
||||
|
||||
- [`whenReady`](#whenready): executing code when DOM is ready
|
||||
- [`loadJS`](#loadjs): loading script files
|
||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||
- [`escape`](#escape): sanitizing strings
|
||||
- [`debounce`](#debounce): limiting rate of function calls
|
||||
- [`shallowEqual`](#shallowequal): shallow object comparison
|
||||
|
||||
## `whenReady`
|
||||
|
||||
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
|
||||
not ready yet, resolved directly otherwise). If called with a callback as
|
||||
argument, it executes it as soon as the DOM ready (or directly).
|
||||
|
||||
```js
|
||||
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
|
||||
const qweb = new owl.QWeb({ templates });
|
||||
const env = { qweb };
|
||||
await mount(App, { env, target: document.body });
|
||||
});
|
||||
```
|
||||
|
||||
or alternatively:
|
||||
|
||||
```js
|
||||
owl.utils.whenReady(function () {
|
||||
const qweb = new owl.QWeb();
|
||||
const env = { qweb };
|
||||
await mount(App, { env, target: document.body });
|
||||
});
|
||||
```
|
||||
|
||||
## `loadJS`
|
||||
|
||||
`loadJS` takes a url (string) for a javascript resource, and loads it (by adding
|
||||
a script tag in the document head). It returns a promise, so the caller can
|
||||
properly reacts when it is ready. Also, it is smart: it maintains a list of urls
|
||||
previously loaded (or currently being loaded), and prevent doing twice the work.
|
||||
|
||||
For example, it is useful for lazy loading external libraries:
|
||||
|
||||
```js
|
||||
class MyComponent extends owl.Component {
|
||||
willStart() {
|
||||
return owl.utils.loadJS("/static/libs/someLib.js");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `loadFile`
|
||||
|
||||
`loadFile` is a helper function to fetch a file. It simply
|
||||
performs a `GET` request and returns the resulting string in a promise. The
|
||||
initial usecase for this function is to load a template file. For example:
|
||||
|
||||
```js
|
||||
async function makeEnv() {
|
||||
const templates = await owl.utils.loadFile("templates.xml");
|
||||
const qweb = new owl.QWeb({ templates });
|
||||
return { qweb };
|
||||
}
|
||||
```
|
||||
|
||||
Note that unlike `loadJS`, this function returns the content of the file as a
|
||||
string. It does not add a `script` tag or any other side effect.
|
||||
|
||||
## `escape`
|
||||
|
||||
Sometimes, we need to display dynamic data (for example user-generated data) in
|
||||
the user interface. If this is done by a `QWeb` template, it is not an issue:
|
||||
|
||||
```xml
|
||||
<div><t t-esc="user.data"/></div>
|
||||
```
|
||||
|
||||
The `QWeb` engine will create a `div` node and add the content of the `user.data`
|
||||
string as a text node, so the web browser will not parse it as html. However,
|
||||
it may be a problem if this is done with some javascript code like this:
|
||||
|
||||
```js
|
||||
class BadComponent extends Component {
|
||||
// some template with a ref to a div
|
||||
// some code ...
|
||||
|
||||
mounted() {
|
||||
this.divRef.el.innerHTML = this.state.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this case, the content of the `div` will be parsed as html, which may inject
|
||||
unwanted behaviour. To fix this, the `escape` function will simply transform a
|
||||
string into an escaped version of the same string, which will be properly displayed
|
||||
by the browser, but which will not be parsed as html (for example, `"<ok>"` is
|
||||
escaped to the string: `"<ok>"`). So, the bad example above can be fixed
|
||||
with the following change:
|
||||
|
||||
```js
|
||||
this.divRef.el.innerHTML = owl.utils.escape(this.state.value);
|
||||
```
|
||||
|
||||
## `debounce`
|
||||
|
||||
The `debounce` function is useful when we want to limit the number of times some
|
||||
function/action is perfomed. For example, this may be useful to prevent issue
|
||||
with people double clicking on a button.
|
||||
|
||||
It takes three arguments:
|
||||
|
||||
- `func` (function): this is the function that will be rate limited
|
||||
- `wait` (number): this is the number of milliseconds that we want to use to
|
||||
rate limit the function `func`
|
||||
- `immediate` (optional, boolean, default=false): if `immediate` is true, the
|
||||
function will be triggered immediately (leading edge of the interval). If false,
|
||||
the function will be triggered at the end (trailing edge).
|
||||
|
||||
It returns a function. For example:
|
||||
|
||||
```js
|
||||
const debounce = owl.utils.debounce;
|
||||
window.addEventListener("mousemove", debounce(doSomething, 100));
|
||||
```
|
||||
|
||||
As this example shows, it is usualy useful for event handlers which are triggered
|
||||
very quickly, such as `scroll` or `mousemove` events.
|
||||
|
||||
## `shallowEqual`
|
||||
|
||||
This function checks if two objects have the same values assigned to each keys:
|
||||
|
||||
```js
|
||||
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
|
||||
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 }); // false
|
||||
```
|
||||
|
||||
However, for performance reasons, it assumes that the two objects have the same
|
||||
keys. If we are in a situation where this is not guaranteed, the following code
|
||||
will work:
|
||||
|
||||
```js
|
||||
const completeShallowEqual = (a, b) => shallowEqual(a, b) && shallowEqual(b, a);
|
||||
```
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OWL Framework</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
|
||||
<link rel="stylesheet" href="assets/normalize.css">
|
||||
<link rel="stylesheet" href="assets/milligram.css">
|
||||
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
|
||||
<link rel="stylesheet" href="assets/main.css">
|
||||
<script src="./owl.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="container">
|
||||
<img class="homepage-logo" src="assets/owl_1f989.png" alt="OWL Logo">
|
||||
<hgroup>
|
||||
<h1>OWL Framework</h1>
|
||||
<p>Mysterious OWL: A web framework for structured, dynamic and maintainable applications</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
<nav class="container">
|
||||
<p>
|
||||
<a href="https://github.com/odoo/owl" title="https://github.com/odoo/owl">Github</a>
|
||||
•
|
||||
<a href="https://github.com/odoo/owl/blob/master/doc/readme.md">Documentation</a>
|
||||
•
|
||||
<a href="playground/">Playground</a>
|
||||
</p>
|
||||
</nav>
|
||||
<main>
|
||||
<article class="design">
|
||||
<div class="container">
|
||||
<h2>Why OWL?</h2>
|
||||
<div class="row">
|
||||
<section class="column">
|
||||
<h3>XML based</h3>
|
||||
<p>Templates are based on the XML format, which allows interesting applications. For example, they could be stored in a database and modified dynamically with <tt>xpaths</tt>.</p>
|
||||
</section>
|
||||
<section class="column">
|
||||
<h3>Templates compiled in the browser</h3>
|
||||
<p>This may not be a good fit for all applications, but if you need to generate dynamically user interfaces in the browser, this is very powerful. For example, a generic form view component could generate a specific form user interface for each various models, from a XML view.</p>
|
||||
</section>
|
||||
<section class="column">
|
||||
<h3>No toolchain required</h3>
|
||||
<p>This is extremely useful for some applications, if, for various reasons (security/deployment/dynamic modules/specific assets tools), it is not ok to use standard web tools based on <tt>npm</tt>.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="example">
|
||||
<div class="container">
|
||||
<h2>Example</h2>
|
||||
<div class="row">
|
||||
<section class="column">
|
||||
<pre><code class="javascript">import { mount, Component, useState } from "owl";
|
||||
|
||||
class Counter extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
mount(Counter, document.body);</code></pre>
|
||||
</section>
|
||||
<section class="column">
|
||||
<pre><code class="xml"><templates>
|
||||
<t t-name="Counter">
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
</t>
|
||||
</templates></code></pre>
|
||||
<div id="app-container"></div>
|
||||
<script>
|
||||
const { mount, Component, useState, xml } = owl;
|
||||
|
||||
class Counter extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
Counter.template = xml`<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
mount(Counter, document.getElementById("app-container"));
|
||||
</script>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
<footer>
|
||||
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
|
||||
</footer>
|
||||
<script src="assets/highlight.pack.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "1.4.10",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"browser": "dist/owl.iife.js",
|
||||
"module": "dist/owl.es.js",
|
||||
"types": "dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.18.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build:bundle": "rollup -c",
|
||||
"build": "npm run build:bundle",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"tools:serve": "python3 tools/server.py || python tools/server.py",
|
||||
"tools": "npm run build && npm run tools:serve",
|
||||
"pretools:watch": "npm run build",
|
||||
"tools:watch": "npm-run-all --parallel tools:serve \"build:* -- --watch\"",
|
||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
|
||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
|
||||
"publish": "npm run build && npm publish",
|
||||
"release": "node tools/release.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/odoo/owl.git"
|
||||
},
|
||||
"author": "Odoo",
|
||||
"license": "LGPL-3.0-only",
|
||||
"bugs": {
|
||||
"url": "https://github.com/odoo/owl/issues"
|
||||
},
|
||||
"homepage": "https://github.com/odoo/owl#readme",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^14.11.8",
|
||||
"chalk": "^3.0.0",
|
||||
"cpx": "^1.5.0",
|
||||
"current-git-branch": "^1.1.0",
|
||||
"git-rev-sync": "^1.12.0",
|
||||
"github-api": "^3.3.0",
|
||||
"jest": "^27.1.0",
|
||||
"jest-environment-jsdom": "^27.1.0",
|
||||
"live-server": "^1.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.0.4",
|
||||
"rollup": "^2.56.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.30.0",
|
||||
"sass": "^1.16.1",
|
||||
"source-map-support": "^0.5.10",
|
||||
"ts-jest": "^27.0.5",
|
||||
"typescript": "^3.7.2",
|
||||
"uglify-es": "^3.3.9"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
"roots": [
|
||||
"<rootDir>/src",
|
||||
"<rootDir>/tests"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.ts?$": "ts-jest"
|
||||
},
|
||||
"verbose": false,
|
||||
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"json",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { loadJS } from "./utils.js";
|
||||
|
||||
const sourceCache = new Map();
|
||||
async function getSourceCode(filePath) {
|
||||
if (sourceCache.has(filePath)) {
|
||||
return sourceCache.get(filePath);
|
||||
}
|
||||
let source = await fetch(`${filePath}.js`);
|
||||
source = await source.text();
|
||||
sourceCache.set(filePath, source);
|
||||
return source;
|
||||
}
|
||||
|
||||
const DEFAULT_HTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OWL App</title>
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<script src="owl.js"></script>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<script type="module" src="app.js"></script>
|
||||
<script type="module" src="utils.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const APP_PY = `#!/usr/bin/env python3
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
|
||||
def start_server():
|
||||
SimpleHTTPRequestHandler.extensions_map['.js'] = 'application/javascript'
|
||||
httpd = HTTPServer(('0.0.0.0', 3600), SimpleHTTPRequestHandler)
|
||||
httpd.serve_forever()
|
||||
|
||||
url = 'http://127.0.0.1:3600'
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Owl Application")
|
||||
print("---------------")
|
||||
print("Server running on: {}".format(url))
|
||||
threading.Thread(target=start_server, daemon=True).start()
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
httpd.server_close()
|
||||
quit(0)
|
||||
`;
|
||||
|
||||
/**
|
||||
* Make a zip file containing a functioning application
|
||||
*/
|
||||
export async function exportStandaloneApp(js, css, xml) {
|
||||
await loadJS("libs/jszip.min.js");
|
||||
|
||||
const zip = new JSZip();
|
||||
const processedJS = js
|
||||
.split("\n")
|
||||
.map(l => (l === "" ? "" : " " + l))
|
||||
.join("\n");
|
||||
|
||||
const JS = `
|
||||
/**
|
||||
* This is the javascript code defined in the playground.
|
||||
* In a larger application, this code should probably be moved in different
|
||||
* sub files.
|
||||
*/
|
||||
import * as utils from "./utils.js";
|
||||
async function app() {
|
||||
${processedJS}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization code
|
||||
* This code load templates, and make sure everything is properly connected.
|
||||
*/
|
||||
function prepareOWLApp(templates) {
|
||||
const _configure = owl.App.prototype.configure;
|
||||
owl.App.prototype.configure = function configureOverriden(config) {
|
||||
config = Object.assign({ dev: true }, config);
|
||||
this.addTemplates(templates);
|
||||
return _configure.call(this, config);
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
let templates;
|
||||
try {
|
||||
templates = await owl.loadFile('app.xml');
|
||||
} catch(e) {
|
||||
console.error(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
|
||||
return;
|
||||
}
|
||||
prepareOWLApp(templates);
|
||||
app();
|
||||
}
|
||||
|
||||
start();
|
||||
`;
|
||||
|
||||
const [owlSrc, utilsSrc] = await Promise.all([
|
||||
getSourceCode("../owl"),
|
||||
getSourceCode("./utils"),
|
||||
]);
|
||||
|
||||
zip.file("app.js", JS);
|
||||
zip.file("utils.js", utilsSrc);
|
||||
zip.file("app.css", css);
|
||||
zip.file("app.py", APP_PY);
|
||||
zip.file("app.xml", xml);
|
||||
zip.file("index.html", DEFAULT_HTML);
|
||||
zip.file("owl.js", owlSrc);
|
||||
return zip.generateAsync({ type: "blob" });
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
export function debounce(func, wait, immediate) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
function later() {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
}
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useBus(bus, evName, cb) {
|
||||
bus.addEventListener(evName, cb);
|
||||
owl.onWillUnmount(() => {
|
||||
bus.removeEventListener(evName, cb);
|
||||
});
|
||||
}
|
||||
|
||||
const loadedScripts = {};
|
||||
|
||||
export function loadJS(url) {
|
||||
if (url in loadedScripts) {
|
||||
return loadedScripts[url];
|
||||
}
|
||||
const promise = new Promise(function (resolve, reject) {
|
||||
const script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = url;
|
||||
script.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
script.onerror = function () {
|
||||
reject(`Error loading file '${url}'`);
|
||||
};
|
||||
const head = document.head || document.getElementsByTagName("head")[0];
|
||||
head.appendChild(script);
|
||||
});
|
||||
loadedScripts[url] = promise;
|
||||
return promise;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# v1.0.5
|
||||
|
||||
- lots of doc improvements
|
||||
- qweb: ignore comments between t-if/t-elif/t-else
|
||||
- qweb: support attributes with inside quotes
|
||||
- component: concurrency bug fix
|
||||
- component: cancel previous mounting operations if necessary
|
||||
- transition: no crash if element added/removed quickly
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# 🦉 OWL Roadmap 🦉
|
||||
|
||||
- Current version: 1.4.10
|
||||
- Status: stable
|
||||
|
||||
This roadmap is only an attempt at predicting Owl's future. Everything may
|
||||
change!
|
||||
|
||||
|
||||
### 1.x
|
||||
|
||||
- add chrome and firefox devtools,
|
||||
- fix every bugs,
|
||||
- improve documentation,
|
||||
- small backward compatible improvements.
|
||||
|
||||
### 2.x (2020? 2021? 2022?)
|
||||
|
||||
- stop support for `t-set` directive to define the content of a slot
|
||||
|
||||
Maybe:
|
||||
|
||||
- reimplement vdom to use *block* system, like Vue 3, which should make Owl
|
||||
much faster
|
||||
- refactor `QWeb` to use an intermediate representation (some kind of AST) to
|
||||
allow additional optimisations.
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import pkg from "./package.json";
|
||||
import git from "git-rev-sync";
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
|
||||
const name = "owl";
|
||||
const extend = true;
|
||||
|
||||
/**
|
||||
* Meta data to be added on the __info__ object.
|
||||
* Used to let external tools know the current owl version.
|
||||
*/
|
||||
const outro = `
|
||||
__info__.version = '${pkg.version}';
|
||||
__info__.date = '${new Date().toISOString()}';
|
||||
__info__.hash = '${git.short()}';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
`;
|
||||
|
||||
/**
|
||||
* Generate from a string depicting a path a new path for the minified version.
|
||||
* @param {string} pkgFileName file name
|
||||
*/
|
||||
function generateMinifiedNameFromPkgName(pkgFileName) {
|
||||
const parts = pkgFileName.split('.');
|
||||
parts.splice(parts.length - 1, 0, "min");
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rollup config based on the arguments
|
||||
* @param {string} format format of the bundle
|
||||
* @param {string} generatedFileName generated file name
|
||||
* @param {boolean} minified should it be minified
|
||||
*/
|
||||
function getConfigForFormat(format, generatedFileName, minified = false) {
|
||||
return {
|
||||
file: minified ? generateMinifiedNameFromPkgName(generatedFileName) : generatedFileName,
|
||||
format: format,
|
||||
name: name,
|
||||
extend: extend,
|
||||
outro: outro,
|
||||
plugins: minified ? [terser()] : [],
|
||||
indent: ' ', // indent with 4 spaces
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
input: "src/index.ts",
|
||||
output: [
|
||||
|
||||
/**
|
||||
* Read about module formats:
|
||||
* https://auth0.com/blog/javascript-module-systems-showdown/
|
||||
* https://medium.com/@kelin2025/so-you-wanna-use-es6-modules-714f48b3a953
|
||||
*/
|
||||
|
||||
getConfigForFormat('esm', pkg.module),
|
||||
getConfigForFormat('esm', pkg.module, true),
|
||||
getConfigForFormat('cjs', pkg.main),
|
||||
getConfigForFormat('cjs', pkg.main, true),
|
||||
getConfigForFormat('iife', pkg.browser),
|
||||
getConfigForFormat('iife', pkg.browser, true),
|
||||
],
|
||||
plugins: [
|
||||
typescript({
|
||||
useTsconfigDeclarationDir: true
|
||||
}),
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export interface Browser {
|
||||
setTimeout: Window["setTimeout"];
|
||||
clearTimeout: Window["clearTimeout"];
|
||||
setInterval: Window["setInterval"];
|
||||
clearInterval: Window["clearInterval"];
|
||||
requestAnimationFrame: Window["requestAnimationFrame"];
|
||||
random: Math["random"];
|
||||
Date: typeof Date;
|
||||
fetch: Window["fetch"];
|
||||
localStorage: Window["localStorage"];
|
||||
}
|
||||
|
||||
let localStorage: Window["localStorage"] | null = null;
|
||||
|
||||
export const browser: Browser = {
|
||||
setTimeout: window.setTimeout.bind(window),
|
||||
clearTimeout: window.clearTimeout.bind(window),
|
||||
setInterval: window.setInterval.bind(window),
|
||||
clearInterval: window.clearInterval.bind(window),
|
||||
requestAnimationFrame: window.requestAnimationFrame.bind(window),
|
||||
random: Math.random,
|
||||
Date: window.Date,
|
||||
fetch: (window.fetch || (() => {})).bind(window),
|
||||
get localStorage() {
|
||||
return localStorage || window.localStorage;
|
||||
},
|
||||
set localStorage(newLocalStorage: Window["localStorage"]) {
|
||||
localStorage = newLocalStorage;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,781 @@
|
||||
import { Observer } from "../core/observer";
|
||||
import { OwlEvent } from "../core/owl_event";
|
||||
import { CompiledTemplate, QWeb } from "../qweb/index";
|
||||
import { patch, VNode } from "../vdom/index";
|
||||
import "./directive";
|
||||
import { Fiber } from "./fiber";
|
||||
import "./props_validation";
|
||||
import { Scheduler, scheduler } from "./scheduler";
|
||||
import { activateSheet } from "./styles";
|
||||
import { Browser, browser } from "../browser";
|
||||
|
||||
/**
|
||||
* Owl Component System
|
||||
*
|
||||
* This file introduces a declarative and composable component system. It
|
||||
* contains:
|
||||
*
|
||||
* - the Env interface (generic type for the environment)
|
||||
* - the Internal interface (the owl specific metadata attached to a component)
|
||||
* - the Component class
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* An Env (environment) is an object that will be (mostly) shared between all
|
||||
* components of an Owl application. It is the location which should contain
|
||||
* the qweb instance necessary to render all components.
|
||||
*
|
||||
* Note that it is totally fine to extend the environment with application
|
||||
* specific keys/objects/whatever. For example, a key `isMobile` (to declare
|
||||
* if we are in "mobile" mode), or a shared bus could be useful.
|
||||
*/
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
browser: Browser;
|
||||
}
|
||||
|
||||
export type MountPosition = "first-child" | "last-child" | "self";
|
||||
|
||||
interface MountOptions {
|
||||
position?: MountPosition;
|
||||
}
|
||||
|
||||
export const enum STATUS {
|
||||
CREATED,
|
||||
WILLSTARTED, // willstart has been called
|
||||
RENDERED, // first render is completed (so, vnode is now defined)
|
||||
MOUNTED, // is ready, and in DOM. It has a valid el
|
||||
UNMOUNTED, // has a valid el, but is not in DOM
|
||||
DESTROYED,
|
||||
}
|
||||
|
||||
/**
|
||||
* This is mostly an internal detail of implementation. The Meta interface is
|
||||
* useful to typecheck and describe the internal keys used by Owl to manage the
|
||||
* component tree.
|
||||
*/
|
||||
interface Internal<T extends Env> {
|
||||
// each component has a unique id, useful mostly to handle parent/child
|
||||
// relationships
|
||||
readonly id: number;
|
||||
depth: number;
|
||||
vnode: VNode | null;
|
||||
pvnode: VNode | null;
|
||||
status: STATUS;
|
||||
|
||||
// parent and children keys are obviously useful to setup the parent-children
|
||||
// relationship.
|
||||
parent: Component<any, T> | null;
|
||||
children: { [key: number]: Component<any, T> };
|
||||
// children mapping: from templateID to componentID. templateID identifies a
|
||||
// place in a template. The t-component directive needs it to be able to get
|
||||
// the component instance back whenever the template is rerendered.
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
currentFiber: Fiber | null;
|
||||
// parentLastFiberId is there to help the parent component to detect, among
|
||||
// its children, those that are not used anymore and thus can be destroyed
|
||||
parentLastFiberId: number;
|
||||
|
||||
// when a rendering is initiated by a parent, it may set variables in 'scope'
|
||||
// (typically when the component is rendered in a slot). We need to
|
||||
// store that information in case the component would be re-rendered later on.
|
||||
scope: any;
|
||||
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer: Observer | null;
|
||||
renderFn: CompiledTemplate;
|
||||
mountedCB: Function | null;
|
||||
willUnmountCB: Function | null;
|
||||
willPatchCB: Function | null;
|
||||
patchedCB: Function | null;
|
||||
willStartCB: Function | null;
|
||||
willUpdatePropsCB: Function | null;
|
||||
classObj: { [key: string]: boolean } | null;
|
||||
refs: { [key: string]: Component<any, T> | HTMLElement | undefined } | null;
|
||||
}
|
||||
|
||||
export const portalSymbol = Symbol("portal"); // FIXME
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Component
|
||||
//------------------------------------------------------------------------------
|
||||
let nextId = 1;
|
||||
|
||||
export class Component<Props extends {} = any, T extends Env = Env> {
|
||||
readonly __owl__: Internal<T>;
|
||||
static template?: string | null = null;
|
||||
static _template?: string | null = null;
|
||||
static current: Component | null = null;
|
||||
static components = {};
|
||||
static props?: any;
|
||||
static defaultProps?: any;
|
||||
static env: any = {};
|
||||
// expose scheduler s.t. it can be mocked for testing purposes
|
||||
static scheduler: Scheduler = scheduler;
|
||||
|
||||
/**
|
||||
* The `el` is the root element of the component. Note that it could be null:
|
||||
* this is the case if the component is not mounted yet, or is destroyed.
|
||||
*/
|
||||
get el(): HTMLElement | null {
|
||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||
}
|
||||
|
||||
env: T;
|
||||
props: Props;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates an instance of Component.
|
||||
*
|
||||
* Note that most of the time, only the root component needs to be created by
|
||||
* hand. Other components should be created automatically by the framework (with
|
||||
* the t-component directive in a template)
|
||||
*/
|
||||
constructor(parent?: Component<any, T> | null, props?: Props) {
|
||||
Component.current = this;
|
||||
|
||||
let constr = this.constructor as any;
|
||||
const defaultProps = constr.defaultProps;
|
||||
if (defaultProps) {
|
||||
props = props || ({} as Props);
|
||||
this.__applyDefaultProps(props, defaultProps);
|
||||
}
|
||||
this.props = <Props>props;
|
||||
if (QWeb.dev) {
|
||||
QWeb.utils.validateProps(constr, this.props);
|
||||
}
|
||||
|
||||
const id: number = nextId++;
|
||||
let depth;
|
||||
if (parent) {
|
||||
this.env = parent.env;
|
||||
const __powl__ = parent.__owl__;
|
||||
__powl__.children[id] = this;
|
||||
depth = __powl__.depth + 1;
|
||||
} else {
|
||||
// we are the root component
|
||||
this.env = (this.constructor as any).env;
|
||||
if (!this.env.qweb) {
|
||||
this.env.qweb = new QWeb();
|
||||
}
|
||||
// TODO: remove this in owl 2.0
|
||||
if (!this.env.browser) {
|
||||
this.env.browser = browser;
|
||||
}
|
||||
this.env.qweb.on("update", this, () => {
|
||||
switch (this.__owl__.status) {
|
||||
case STATUS.MOUNTED:
|
||||
this.render(true);
|
||||
break;
|
||||
case STATUS.DESTROYED:
|
||||
// this is unlikely to happen, but if a root widget is destroyed,
|
||||
// we want to remove our subscription. The usual way to do that
|
||||
// would be to perform some check in the destroy method, but since
|
||||
// it is very performance sensitive, and since this is a rare event,
|
||||
// we simply do it lazily
|
||||
this.env.qweb.off("update", this);
|
||||
break;
|
||||
}
|
||||
});
|
||||
depth = 0;
|
||||
}
|
||||
|
||||
const qweb = this.env.qweb;
|
||||
const template = constr.template || this.__getTemplate(qweb);
|
||||
this.__owl__ = {
|
||||
id: id,
|
||||
depth: depth,
|
||||
vnode: null,
|
||||
pvnode: null,
|
||||
status: STATUS.CREATED,
|
||||
parent: parent || null,
|
||||
children: {},
|
||||
cmap: {},
|
||||
currentFiber: null,
|
||||
parentLastFiberId: 0,
|
||||
boundHandlers: {},
|
||||
mountedCB: null,
|
||||
willUnmountCB: null,
|
||||
willPatchCB: null,
|
||||
patchedCB: null,
|
||||
willStartCB: null,
|
||||
willUpdatePropsCB: null,
|
||||
observer: null,
|
||||
renderFn: qweb.render.bind(qweb, template),
|
||||
classObj: null,
|
||||
refs: null,
|
||||
scope: null,
|
||||
};
|
||||
if (constr.style) {
|
||||
this.__applyStyles(constr);
|
||||
}
|
||||
this.setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* setup is run just after the component is constructed. This is the standard
|
||||
* location where the component can setup its hooks. It has some advantages
|
||||
* over the constructor:
|
||||
* - it can be patched (useful in odoo ecosystem)
|
||||
* - it does not need to propagate the arguments to the super call
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*/
|
||||
setup() {}
|
||||
|
||||
/**
|
||||
* willStart is an asynchronous hook that can be implemented to perform some
|
||||
* action before the initial rendering of a component.
|
||||
*
|
||||
* It will be called exactly once before the initial rendering. It is useful
|
||||
* in some cases, for example, to load external assets (such as a JS library)
|
||||
* before the component is rendered.
|
||||
*
|
||||
* Note that a slow willStart method will slow down the rendering of the user
|
||||
* interface. Therefore, some effort should be made to make this method as
|
||||
* fast as possible.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*/
|
||||
async willStart() {}
|
||||
|
||||
/**
|
||||
* mounted is a hook that is called each time a component is attached to the
|
||||
* DOM. This is a good place to add some listeners, or to interact with the
|
||||
* DOM, if the component needs to perform some measure for example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
* @see willUnmount
|
||||
*/
|
||||
mounted() {}
|
||||
|
||||
/**
|
||||
* The willUpdateProps is an asynchronous hook, called just before new props
|
||||
* are set. This is useful if the component needs some asynchronous task
|
||||
* performed, depending on the props (for example, assuming that the props are
|
||||
* some record Id, fetching the record data).
|
||||
*
|
||||
* This hook is not called during the first render (but willStart is called
|
||||
* and performs a similar job).
|
||||
*/
|
||||
async willUpdateProps(nextProps: Props) {}
|
||||
|
||||
/**
|
||||
* The willPatch hook is called just before the DOM patching process starts.
|
||||
* It is not called on the initial render. This is useful to get some
|
||||
* information which are in the DOM. For example, the current position of the
|
||||
* scrollbar
|
||||
*/
|
||||
willPatch(): any {}
|
||||
|
||||
/**
|
||||
* This hook is called whenever a component did actually update its props,
|
||||
* state or env.
|
||||
*
|
||||
* This method is not called on the initial render. It is useful to interact
|
||||
* with the DOM (for example, through an external library) whenever the
|
||||
* component was updated.
|
||||
*
|
||||
* Updating the component state in this hook is possible, but not encouraged.
|
||||
* One need to be careful, because updates here will cause rerender, which in
|
||||
* turn will cause other calls to updated. So, we need to be particularly
|
||||
* careful at avoiding endless cycles.
|
||||
*/
|
||||
patched() {}
|
||||
|
||||
/**
|
||||
* willUnmount is a hook that is called each time just before a component is
|
||||
* unmounted from the DOM. This is a good place to remove some listeners, for
|
||||
* example.
|
||||
*
|
||||
* Note: this method should not be called manually.
|
||||
*
|
||||
* @see mounted
|
||||
*/
|
||||
willUnmount() {}
|
||||
|
||||
/**
|
||||
* catchError is a method called whenever some error happens in the rendering or
|
||||
* lifecycle hooks of a child.
|
||||
*
|
||||
* It needs to be implemented by a component that is designed to handle the
|
||||
* error properly.
|
||||
*/
|
||||
catchError?(error?: Error): void;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Mount the component to a target element.
|
||||
*
|
||||
* This should only be done if the component was created manually. Components
|
||||
* created declaratively in templates are managed by the Owl system.
|
||||
*
|
||||
* Note that a component can be mounted an unmounted several times
|
||||
*/
|
||||
async mount(target: HTMLElement | DocumentFragment, options: MountOptions = {}): Promise<void> {
|
||||
if (!(target instanceof HTMLElement || target instanceof DocumentFragment)) {
|
||||
let message = `Component '${this.constructor.name}' cannot be mounted: the target is not a valid DOM node.`;
|
||||
message += `\nMaybe the DOM is not ready yet? (in that case, you can use owl.utils.whenReady)`;
|
||||
throw new Error(message);
|
||||
}
|
||||
const position = options.position || "last-child";
|
||||
const __owl__ = this.__owl__;
|
||||
const currentFiber = __owl__.currentFiber;
|
||||
|
||||
switch (__owl__.status) {
|
||||
case STATUS.CREATED: {
|
||||
const fiber = new Fiber(null, this, true, target, position);
|
||||
fiber.shouldPatch = false;
|
||||
this.__prepareAndRender(fiber, () => {});
|
||||
return scheduler.addFiber(fiber);
|
||||
}
|
||||
case STATUS.WILLSTARTED:
|
||||
case STATUS.RENDERED:
|
||||
currentFiber.target = target;
|
||||
currentFiber.position = position;
|
||||
return scheduler.addFiber(currentFiber);
|
||||
|
||||
case STATUS.UNMOUNTED: {
|
||||
const fiber = new Fiber(null, this, true, target, position);
|
||||
fiber.shouldPatch = false;
|
||||
this.__render(fiber);
|
||||
return scheduler.addFiber(fiber);
|
||||
}
|
||||
|
||||
case STATUS.MOUNTED: {
|
||||
if (position !== "self" && this.el!.parentNode !== target) {
|
||||
const fiber = new Fiber(null, this, true, target, position);
|
||||
fiber.shouldPatch = false;
|
||||
this.__render(fiber);
|
||||
return scheduler.addFiber(fiber);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
case STATUS.DESTROYED:
|
||||
throw new Error("Cannot mount a destroyed component");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The unmount method is the opposite of the mount method. It is useful
|
||||
* to call willUnmount calls and remove the component from the DOM.
|
||||
*/
|
||||
unmount() {
|
||||
if (this.__owl__.status === STATUS.MOUNTED) {
|
||||
this.__callWillUnmount();
|
||||
this.el!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The render method is the main entry point to render a component (once it
|
||||
* is ready. This method is not initially called when the component is
|
||||
* rendered the first time).
|
||||
*
|
||||
* This method will cause all its sub components to potentially rerender
|
||||
* themselves. Note that `render` is not called if a component is updated via
|
||||
* its props.
|
||||
*/
|
||||
async render(force: boolean = false): Promise<void> {
|
||||
const __owl__ = this.__owl__;
|
||||
const currentFiber = __owl__.currentFiber;
|
||||
if (!__owl__.vnode && !currentFiber) {
|
||||
return;
|
||||
}
|
||||
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
|
||||
return scheduler.addFiber(currentFiber.root);
|
||||
}
|
||||
|
||||
// if we aren't mounted at this point, it implies that there is a
|
||||
// currentFiber that is already rendered (isRendered is true), so we are
|
||||
// about to be mounted
|
||||
const status = __owl__.status;
|
||||
const fiber = new Fiber(null, this, force, null, null);
|
||||
Promise.resolve().then(() => {
|
||||
if (__owl__.status === STATUS.MOUNTED || status !== STATUS.MOUNTED) {
|
||||
if (fiber.isCompleted || fiber.isRendered) {
|
||||
return;
|
||||
}
|
||||
this.__render(fiber);
|
||||
} else {
|
||||
// we were mounted when render was called, but we aren't anymore, so we
|
||||
// were actually about to be unmounted ; we can thus forget about this
|
||||
// fiber
|
||||
fiber.isCompleted = true;
|
||||
__owl__.currentFiber = null;
|
||||
}
|
||||
});
|
||||
return scheduler.addFiber(fiber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the component. This operation is quite complex:
|
||||
* - it recursively destroy all children
|
||||
* - call the willUnmount hooks if necessary
|
||||
* - remove the dom node from the dom
|
||||
*
|
||||
* This should only be called manually if you created the component. Most
|
||||
* components will be automatically destroyed.
|
||||
*/
|
||||
destroy() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.status !== STATUS.DESTROYED) {
|
||||
const el = this.el;
|
||||
this.__destroy(__owl__.parent);
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the component system whenever its props are
|
||||
* updated. If it returns true, then the component will be rendered.
|
||||
* Otherwise, it will skip the rendering (also, its props will not be updated)
|
||||
*/
|
||||
shouldUpdate(nextProps: Props): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a custom event of type 'eventType' with the given 'payload' on the
|
||||
* component's el, if it exists. However, note that the event will only bubble
|
||||
* up to the parent DOM nodes. Thus, it must be called between mounted() and
|
||||
* willUnmount().
|
||||
*/
|
||||
trigger<T = any>(eventType: string, payload?: T) {
|
||||
this.__trigger<T>(this, eventType, payload);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Private helper to perform a full destroy, from the point of view of an Owl
|
||||
* component. It does not remove the el (this is done only once on the top
|
||||
* level destroyed component, for performance reasons).
|
||||
*
|
||||
* The job of this method is mostly to call willUnmount hooks, and to perform
|
||||
* all necessary internal cleanup.
|
||||
*
|
||||
* Note that it does not call the __callWillUnmount method to avoid visiting
|
||||
* all children many times.
|
||||
*/
|
||||
__destroy(parent: Component | null) {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.status === STATUS.MOUNTED) {
|
||||
if (__owl__.willUnmountCB) {
|
||||
__owl__.willUnmountCB();
|
||||
}
|
||||
this.willUnmount();
|
||||
__owl__.status = STATUS.UNMOUNTED;
|
||||
}
|
||||
const children = __owl__.children;
|
||||
for (let key in children) {
|
||||
children[key].__destroy(this);
|
||||
}
|
||||
if (parent) {
|
||||
let id = __owl__.id;
|
||||
delete parent.__owl__.children[id];
|
||||
__owl__.parent = null;
|
||||
}
|
||||
__owl__.status = STATUS.DESTROYED;
|
||||
delete __owl__.vnode;
|
||||
if (__owl__.currentFiber) {
|
||||
__owl__.currentFiber.isCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
__callMounted() {
|
||||
const __owl__ = this.__owl__;
|
||||
|
||||
__owl__.status = STATUS.MOUNTED;
|
||||
this.mounted();
|
||||
if (__owl__.mountedCB) {
|
||||
__owl__.mountedCB();
|
||||
}
|
||||
}
|
||||
|
||||
__callWillUnmount() {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.willUnmountCB) {
|
||||
__owl__.willUnmountCB();
|
||||
}
|
||||
this.willUnmount();
|
||||
__owl__.status = STATUS.UNMOUNTED;
|
||||
if (__owl__.currentFiber) {
|
||||
__owl__.currentFiber.isCompleted = true;
|
||||
__owl__.currentFiber.root.counter = 0;
|
||||
}
|
||||
const children = __owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (comp.__owl__.status === STATUS.MOUNTED) {
|
||||
comp.__callWillUnmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Private trigger method, allows to choose the component which triggered
|
||||
* the event in the first place
|
||||
*/
|
||||
__trigger<T>(component: Component, eventType: string, payload?: T) {
|
||||
if (this.el) {
|
||||
const ev = new OwlEvent<T>(component, eventType, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: payload,
|
||||
});
|
||||
const triggerHook = this.env[portalSymbol as any];
|
||||
if (triggerHook) {
|
||||
triggerHook(ev);
|
||||
}
|
||||
this.el.dispatchEvent(ev);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The __updateProps method is called by the t-component directive whenever
|
||||
* it updates a component (so, when the parent template is rerendered).
|
||||
*/
|
||||
async __updateProps(nextProps: Props, parentFiber: Fiber, scope: any): Promise<void> {
|
||||
this.__owl__.scope = scope;
|
||||
const shouldUpdate = parentFiber.force || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
const __owl__ = this.__owl__;
|
||||
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
|
||||
if (!parentFiber.child) {
|
||||
parentFiber.child = fiber;
|
||||
} else {
|
||||
parentFiber.lastChild!.sibling = fiber;
|
||||
}
|
||||
parentFiber.lastChild = fiber;
|
||||
|
||||
const defaultProps = (<any>this.constructor).defaultProps;
|
||||
if (defaultProps) {
|
||||
this.__applyDefaultProps(nextProps, defaultProps);
|
||||
}
|
||||
if (QWeb.dev) {
|
||||
QWeb.utils.validateProps(this.constructor, nextProps);
|
||||
}
|
||||
await Promise.all([
|
||||
this.willUpdateProps(nextProps),
|
||||
__owl__.willUpdatePropsCB && __owl__.willUpdatePropsCB(nextProps),
|
||||
]);
|
||||
if (fiber.isCompleted) {
|
||||
return;
|
||||
}
|
||||
this.props = nextProps;
|
||||
|
||||
this.__render(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main patching method. We call the virtual dom patch method here to convert
|
||||
* a virtual dom vnode into some actual dom.
|
||||
*/
|
||||
__patch(target: HTMLElement | VNode | DocumentFragment, vnode: VNode) {
|
||||
this.__owl__.vnode = patch(target as any, vnode);
|
||||
}
|
||||
|
||||
/**
|
||||
* The __prepare method is only called by the t-component directive, when a
|
||||
* subcomponent is created. It gets its scope, if any, from the
|
||||
* parent template.
|
||||
*/
|
||||
__prepare(parentFiber: Fiber, scope: any, cb: CallableFunction): Fiber {
|
||||
this.__owl__.scope = scope;
|
||||
const fiber = new Fiber(parentFiber, this, parentFiber.force, null, null);
|
||||
fiber.shouldPatch = false;
|
||||
if (!parentFiber.child) {
|
||||
parentFiber.child = fiber;
|
||||
} else {
|
||||
parentFiber.lastChild!.sibling = fiber;
|
||||
}
|
||||
parentFiber.lastChild = fiber;
|
||||
this.__prepareAndRender(fiber, cb);
|
||||
return fiber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the stylesheets defined by the component. Note that we need to make
|
||||
* sure all inherited stylesheets are applied as well. We then delete the
|
||||
* `style` key from the constructor to make sure we do not apply it again.
|
||||
*/
|
||||
private __applyStyles(constr) {
|
||||
while (constr && constr.style) {
|
||||
if (constr.hasOwnProperty("style")) {
|
||||
activateSheet(constr.style, constr.name);
|
||||
delete constr.style;
|
||||
}
|
||||
constr = constr.__proto__;
|
||||
}
|
||||
}
|
||||
__getTemplate(qweb: QWeb): string {
|
||||
let p = (<any>this).constructor;
|
||||
if (!p.hasOwnProperty("_template")) {
|
||||
// here, the component and none of its superclasses defines a static `template`
|
||||
// key. So we fall back on looking for a template matching its name (or
|
||||
// one of its subclass).
|
||||
|
||||
let template: string = p.name;
|
||||
while (!(template in qweb.templates) && p !== Component) {
|
||||
p = p.__proto__;
|
||||
template = p.name;
|
||||
}
|
||||
if (p === Component) {
|
||||
throw new Error(`Could not find template for component "${this.constructor.name}"`);
|
||||
} else {
|
||||
p._template = template;
|
||||
}
|
||||
}
|
||||
return p._template;
|
||||
}
|
||||
|
||||
async __prepareAndRender(fiber: Fiber, cb: CallableFunction) {
|
||||
try {
|
||||
const proms = Promise.all([
|
||||
this.willStart(),
|
||||
this.__owl__.willStartCB && this.__owl__.willStartCB(),
|
||||
]);
|
||||
this.__owl__.status = STATUS.WILLSTARTED;
|
||||
await proms;
|
||||
if (this.__owl__.status === <any>STATUS.DESTROYED) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
fiber.handleError(e);
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!fiber.isCompleted) {
|
||||
this.__render(fiber);
|
||||
this.__owl__.status = STATUS.RENDERED;
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
__render(fiber: Fiber) {
|
||||
const __owl__ = this.__owl__;
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = false;
|
||||
}
|
||||
let error;
|
||||
try {
|
||||
let vnode = __owl__.renderFn!(this, {
|
||||
handlers: __owl__.boundHandlers,
|
||||
fiber: fiber,
|
||||
});
|
||||
// we iterate over the children to detect those that no longer belong to the
|
||||
// current rendering: those ones, if not mounted yet, can (and have to) be
|
||||
// destroyed right now, because they are not in the DOM, and thus we won't
|
||||
// be notified later on (when patching), that they are removed from the DOM
|
||||
for (let childKey in __owl__.children) {
|
||||
const child = __owl__.children[childKey];
|
||||
const childOwl = child.__owl__;
|
||||
if (childOwl.status !== STATUS.MOUNTED && childOwl.parentLastFiberId < fiber.id) {
|
||||
// we only do here a "soft" destroy, meaning that we leave the child
|
||||
// dom node alone, without removing it. Most of the time, it does not
|
||||
// matter, because the child component is already unmounted. However,
|
||||
// if some of its parent have been unmounted, the child could actually
|
||||
// still be attached to its parent, and this may be important if we
|
||||
// want to remount the parent, because the vdom need to match the
|
||||
// actual DOM
|
||||
child.__destroy(childOwl.parent);
|
||||
if (childOwl.pvnode) {
|
||||
// we remove the key here to make sure that the patching algorithm
|
||||
// is able to make the difference between this pvnode and an eventual
|
||||
// other instance of the same component
|
||||
delete childOwl.pvnode.key;
|
||||
// Since the component has been unmounted, we do not want to actually
|
||||
// call a remove hook. This is pretty important, since the t-component
|
||||
// directive actually disabled it, so the vdom algorithm will just
|
||||
// not remove the child elm if we don't remove the hook.
|
||||
delete childOwl.pvnode.data!.hook!.remove;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!vnode) {
|
||||
throw new Error(`Rendering '${this.constructor.name}' did not return anything`);
|
||||
}
|
||||
fiber.vnode = vnode;
|
||||
// we apply here the class information described on the component by the
|
||||
// template (so, something like <MyComponent class="..."/>) to the actual
|
||||
// root vnode
|
||||
if (__owl__.classObj) {
|
||||
const data = vnode.data!;
|
||||
data.class = Object.assign(data.class || {}, __owl__.classObj);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
if (__owl__.observer) {
|
||||
__owl__.observer.allowMutations = true;
|
||||
}
|
||||
|
||||
fiber.root.counter--;
|
||||
fiber.isRendered = true;
|
||||
if (error) {
|
||||
fiber.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default props (only top level).
|
||||
*
|
||||
* Note that this method does modify in place the props
|
||||
*/
|
||||
__applyDefaultProps(props: Object, defaultProps: Object) {
|
||||
for (let propName in defaultProps) {
|
||||
if (props![propName] === undefined) {
|
||||
props![propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface MountParameters {
|
||||
env?: Env;
|
||||
target: HTMLElement | DocumentFragment;
|
||||
props?: any;
|
||||
position?: MountOptions["position"];
|
||||
}
|
||||
|
||||
interface Type<T> extends Function {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
export async function mount<T extends Type<Component>>(
|
||||
C: T,
|
||||
params: MountParameters
|
||||
): Promise<InstanceType<T>> {
|
||||
const { env, props, target } = params;
|
||||
let origEnv = C.hasOwnProperty("env") ? (C as any).env : null;
|
||||
if (env) {
|
||||
(C as any as typeof Component).env = env;
|
||||
}
|
||||
const component: Component = new C(null, props);
|
||||
if (origEnv) {
|
||||
(C as any).env = origEnv;
|
||||
} else {
|
||||
delete (C as any).env;
|
||||
}
|
||||
const position = params.position || "last-child";
|
||||
await component.mount(target, { position });
|
||||
return component as any;
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { INTERP_REGEXP } from "../qweb/compilation_context";
|
||||
import { makeHandlerCode, MODS_CODE } from "../qweb/extensions";
|
||||
import { STATUS } from "./component";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-component
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const T_COMPONENT_MODS_CODE = Object.assign({}, MODS_CODE, {
|
||||
self: "if (e.target !== vn.elm) {return}",
|
||||
});
|
||||
|
||||
QWeb.utils.defineProxy = function defineProxy(target, source) {
|
||||
for (let k in source) {
|
||||
Object.defineProperty(target, k, {
|
||||
get() {
|
||||
return source[k];
|
||||
},
|
||||
set(val) {
|
||||
source[k] = val;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
QWeb.utils.assignHooks = function assignHooks(dataObj, hooks) {
|
||||
if ("hook" in dataObj) {
|
||||
const hookObject = dataObj.hook;
|
||||
for (let name in hooks) {
|
||||
const current = hookObject[name];
|
||||
const fn = hooks[name];
|
||||
if (current) {
|
||||
hookObject[name] = (...args) => {
|
||||
current(...args);
|
||||
fn(...args);
|
||||
};
|
||||
} else {
|
||||
hookObject[name] = fn;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dataObj.hook = hooks;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The t-component directive is certainly a complicated and hard to maintain piece
|
||||
* of code. To help you, fellow developer, if you have to maintain it, I offer
|
||||
* you this advice: Good luck...
|
||||
*
|
||||
* Since it is not 'direct' code, but rather code that generates other code, it
|
||||
* is not easy to understand. To help you, here is a detailed and commented
|
||||
* explanation of the code generated by the t-component directive for the following
|
||||
* situation:
|
||||
* ```xml
|
||||
* <Child
|
||||
* t-key="'somestring'"
|
||||
* flag="state.flag"
|
||||
* t-transition="fade"/>
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* // we assign utils on top of the function because it will be useful for
|
||||
* // each components
|
||||
* let utils = this.utils;
|
||||
*
|
||||
* // this is the virtual node representing the parent div
|
||||
* let c1 = [], p1 = { key: 1 };
|
||||
* var vn1 = h("div", p1, c1);
|
||||
*
|
||||
* // t-component directive: we start by evaluating the expression given by t-key:
|
||||
* let key5 = "somestring";
|
||||
*
|
||||
* // def3 is the promise that will contain later either the new component
|
||||
* // creation, or the props update...
|
||||
* let def3;
|
||||
*
|
||||
* // this is kind of tricky: we need here to find if the component was already
|
||||
* // created by a previous rendering. This is done by checking the internal
|
||||
* // `cmap` (children map) of the parent component: it maps keys to component ids,
|
||||
* // and, then, if there is an id, we look into the children list to get the
|
||||
* // instance
|
||||
* let w4 =
|
||||
* key5 in context.__owl__.cmap
|
||||
* ? context.__owl__.children[context.__owl__.cmap[key5]]
|
||||
* : false;
|
||||
*
|
||||
* // We keep the index of the position of the component in the closure. We push
|
||||
* // null to reserve the slot, and will replace it later by the component vnode,
|
||||
* // when it will be ready (do not forget that preparing/rendering a component is
|
||||
* // asynchronous)
|
||||
* let _2_index = c1.length;
|
||||
* c1.push(null);
|
||||
*
|
||||
* // we evaluate here the props given to the component. It is done here to be
|
||||
* // able to easily reference it later, and also, it might be an expensive
|
||||
* // computation, so it is certainly better to do it only once
|
||||
* let props4 = { flag: context["state"].flag };
|
||||
*
|
||||
* // If we have a component, currently rendering, but not ready yet, we do not want
|
||||
* // to wait for it to be ready if we can avoid it
|
||||
* if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
|
||||
* // we check if the props are the same. In that case, we can simply reuse
|
||||
* // the previous rendering and skip all useless work
|
||||
* if (utils.shallowEqual(props4, w4.__owl__.renderProps)) {
|
||||
* def3 = w4.__owl__.renderPromise;
|
||||
* } else {
|
||||
* // if the props are not the same, we destroy the component and starts anew.
|
||||
* // this will be faster than waiting for its rendering, then updating it
|
||||
* w4.destroy();
|
||||
* w4 = false;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* if (!w4) {
|
||||
* // in this situation, we need to create a new component. First step is
|
||||
* // to get a reference to the class, then create an instance with
|
||||
* // current context as parent, and the props.
|
||||
* let W4 = context.component && context.components[componentKey4] || QWeb.component[componentKey4];
|
||||
|
||||
* if (!W4) {
|
||||
* throw new Error("Cannot find the definition of component 'child'");
|
||||
* }
|
||||
* w4 = new W4(owner, props4);
|
||||
*
|
||||
* // Whenever we rerender the parent component, we need to be sure that we
|
||||
* // are able to find the component instance. To do that, we register it to
|
||||
* // the parent cmap (children map). Note that the 'template' key is
|
||||
* // used here, since this is what identify the component from the template
|
||||
* // perspective.
|
||||
* context.__owl__.cmap[key5] = w4.__owl__.id;
|
||||
*
|
||||
* // __prepare is called, to basically call willStart, then render the
|
||||
* // component
|
||||
* def3 = w4.__prepare();
|
||||
*
|
||||
* def3 = def3.then(vnode => {
|
||||
* // we create here a virtual node for the parent (NOT the component). This
|
||||
* // means that the vdom of the parent will be stopped here, and from
|
||||
* // the parent's perspective, it simply is a vnode with no children.
|
||||
* // However, it shares the same dom element with the component root
|
||||
* // vnode.
|
||||
* let pvnode = h(vnode.sel, { key: key5 });
|
||||
*
|
||||
* // we add hooks to the parent vnode so we can interact with the new
|
||||
* // component at the proper time
|
||||
* pvnode.data.hook = {
|
||||
* insert(vn) {
|
||||
* // the __mount method will patch the component vdom into the elm vn.elm,
|
||||
* // then call the mounted hooks. However, suprisingly, the snabbdom
|
||||
* // patch method actually replace the elm by a new elm, so we need
|
||||
* // to synchronise the pvnode elm with the resulting elm
|
||||
* let nvn = w4.__mount(vnode, vn.elm);
|
||||
* pvnode.elm = nvn.elm;
|
||||
* // what follows is only present if there are animations on the component
|
||||
* utils.transitionInsert(vn, "fade");
|
||||
* },
|
||||
* remove() {
|
||||
* // override with empty function to prevent from removing the node
|
||||
* // directly. It will be removed when destroy is called anyway, which
|
||||
* // delays the removal if there are animations.
|
||||
* },
|
||||
* destroy() {
|
||||
* // if there are animations, we delay the call to destroy on the
|
||||
* // component, if not, we call it directly.
|
||||
* let finalize = () => {
|
||||
* w4.destroy();
|
||||
* };
|
||||
* utils.transitionRemove(vn, "fade", finalize);
|
||||
* }
|
||||
* };
|
||||
* // the pvnode is inserted at the correct position in the div's children
|
||||
* c1[_2_index] = pvnode;
|
||||
*
|
||||
* // we keep here a reference to the parent vnode (representing the
|
||||
* // component, so we can reuse it later whenever we update the component
|
||||
* w4.__owl__.pvnode = pvnode;
|
||||
* });
|
||||
* } else {
|
||||
* // this is the 'update' path of the directive.
|
||||
* // the call to __updateProps is the actual component update
|
||||
* // Note that we only update the props if we cannot reuse the previous
|
||||
* // rendering work (in the case it was rendered with the same props)
|
||||
* def3 = def3 || w4.__updateProps(props4, extra.forceUpdate, extra.patchQueue);
|
||||
* def3 = def3.then(() => {
|
||||
* // if component was destroyed in the meantime, we do nothing (so, this
|
||||
* // means that the parent's element children list will have a null in
|
||||
* // the component's position, which will cause the pvnode to be removed
|
||||
* // when it is patched.
|
||||
* if (w4.__owl__.isDestroyed) {
|
||||
* return;
|
||||
* }
|
||||
* // like above, we register the pvnode to the children list, so it
|
||||
* // will not be patched out of the dom.
|
||||
* let pvnode = w4.__owl__.pvnode;
|
||||
* c1[_2_index] = pvnode;
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* // we register the deferred here so the parent can coordinate its patch operation
|
||||
* // with all the children.
|
||||
* extra.promises.push(def3);
|
||||
* return vn1;
|
||||
* ```
|
||||
*/
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "component",
|
||||
extraNames: ["props"],
|
||||
priority: 100,
|
||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||
ctx.addLine(`// Component '${value}'`);
|
||||
ctx.rootContext.shouldDefineQWeb = true;
|
||||
ctx.rootContext.shouldDefineParent = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
ctx.rootContext.shouldDefineScope = true;
|
||||
let hasDynamicProps = node.getAttribute("t-props") ? true : false;
|
||||
|
||||
// t-on- events and t-transition
|
||||
const events: [string, string][] = [];
|
||||
let transition: string = "";
|
||||
const attributes = (<Element>node).attributes;
|
||||
const props: { [key: string]: string } = {};
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const name = attributes[i].name;
|
||||
const value = attributes[i].textContent!;
|
||||
if (name.startsWith("t-on-")) {
|
||||
events.push([name, value]);
|
||||
} else if (name === "t-transition") {
|
||||
if (QWeb.enableTransitions) {
|
||||
transition = value;
|
||||
}
|
||||
} else if (!name.startsWith("t-")) {
|
||||
if (name !== "class" && name !== "style") {
|
||||
// this is a prop!
|
||||
if (value.includes("=>")) {
|
||||
props[name] = ctx.captureExpression(value);
|
||||
} else {
|
||||
props[name] = ctx.formatExpression(value) || "undefined";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// computing the props string representing the props object
|
||||
let propStr = Object.keys(props)
|
||||
.map((k) => k + ":" + props[k])
|
||||
.join(",");
|
||||
let componentID = ctx.generateID();
|
||||
|
||||
let hasDefinedKey = false;
|
||||
let templateKey;
|
||||
if (node.tagName === "t" && !node.hasAttribute("t-key") && value.match(INTERP_REGEXP)) {
|
||||
defineComponentKey();
|
||||
const id = ctx.generateID();
|
||||
// the ___ is to make sure we have no possible conflict with normal
|
||||
// template keys
|
||||
ctx.addLine(`let k${id} = '___' + componentKey${componentID}`);
|
||||
templateKey = `k${id}`;
|
||||
} else {
|
||||
templateKey = ctx.generateTemplateKey();
|
||||
}
|
||||
let ref = node.getAttribute("t-ref");
|
||||
let refExpr = "";
|
||||
let refKey: string = "";
|
||||
if (ref) {
|
||||
ctx.rootContext.shouldDefineRefs = true;
|
||||
refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(ref)};`);
|
||||
refExpr = `context.__owl__.refs[${refKey}] = w${componentID};`;
|
||||
}
|
||||
let finalizeComponentCode = `w${componentID}.destroy();`;
|
||||
if (ref) {
|
||||
finalizeComponentCode += `delete context.__owl__.refs[${refKey}];`;
|
||||
}
|
||||
if (transition) {
|
||||
finalizeComponentCode = `let finalize = () => {
|
||||
${finalizeComponentCode}
|
||||
};
|
||||
delete w${componentID}.__owl__.transitionInserted;
|
||||
utils.transitionRemove(vn, '${transition}', finalize);`;
|
||||
}
|
||||
|
||||
let createHook = "";
|
||||
let classAttr = node.getAttribute("class");
|
||||
let tattClass = node.getAttribute("t-att-class");
|
||||
let styleAttr = node.getAttribute("style");
|
||||
let tattStyle = node.getAttribute("t-att-style");
|
||||
if (tattStyle) {
|
||||
const attVar = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
|
||||
tattStyle = attVar;
|
||||
}
|
||||
let classObj = "";
|
||||
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
|
||||
if (classAttr) {
|
||||
let classDef = classAttr
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((a) => `'${a}':true`)
|
||||
.join(",");
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||
}
|
||||
if (tattClass) {
|
||||
let tattExpr = ctx.formatExpression(tattClass);
|
||||
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
|
||||
tattExpr = `utils.toClassObj(${tattExpr})`;
|
||||
}
|
||||
if (classAttr) {
|
||||
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = ${tattExpr};`);
|
||||
}
|
||||
}
|
||||
let eventsCode = events
|
||||
.map(function ([name, value]) {
|
||||
const capture = name.match(/\.capture/);
|
||||
name = capture ? name.replace(/\.capture/, "") : name;
|
||||
const { event, handler } = makeHandlerCode(
|
||||
ctx,
|
||||
name,
|
||||
value,
|
||||
false,
|
||||
T_COMPONENT_MODS_CODE
|
||||
);
|
||||
if (capture) {
|
||||
return `vn.elm.addEventListener('${event}', ${handler}, true);`;
|
||||
}
|
||||
return `vn.elm.addEventListener('${event}', ${handler});`;
|
||||
})
|
||||
.join("");
|
||||
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
|
||||
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
|
||||
createHook = `utils.assignHooks(vnode.data, {create(_, vn){${styleCode}${eventsCode}}});`;
|
||||
}
|
||||
|
||||
ctx.addLine(
|
||||
`let w${componentID} = ${templateKey} in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[${templateKey}]] : false;`
|
||||
);
|
||||
let shouldProxy = !ctx.parentNode;
|
||||
if (shouldProxy) {
|
||||
let id = ctx.generateID();
|
||||
ctx.rootContext.rootNode = id;
|
||||
shouldProxy = true;
|
||||
ctx.rootContext.shouldDefineResult = true;
|
||||
ctx.addLine(`let vn${id} = {};`);
|
||||
ctx.addLine(`result = vn${id};`);
|
||||
}
|
||||
if (hasDynamicProps) {
|
||||
const dynamicProp = ctx.formatExpression(node.getAttribute("t-props")!);
|
||||
ctx.addLine(`let props${componentID} = Object.assign({}, ${dynamicProp}, {${propStr}});`);
|
||||
} else {
|
||||
ctx.addLine(`let props${componentID} = {${propStr}};`);
|
||||
}
|
||||
ctx.addIf(
|
||||
`w${componentID} && w${componentID}.__owl__.currentFiber && !w${componentID}.__owl__.vnode`
|
||||
);
|
||||
ctx.addLine(`w${componentID}.destroy();`);
|
||||
ctx.addLine(`w${componentID} = false;`);
|
||||
ctx.closeIf();
|
||||
|
||||
let registerCode = "";
|
||||
if (shouldProxy) {
|
||||
registerCode = `utils.defineProxy(vn${ctx.rootNode}, pvnode);`;
|
||||
}
|
||||
|
||||
// SLOTS
|
||||
const hasSlots = node.childNodes.length;
|
||||
|
||||
let scope = hasSlots ? `utils.combine(context, scope)` : "undefined";
|
||||
|
||||
ctx.addIf(`w${componentID}`);
|
||||
|
||||
// need to update component
|
||||
let styleCode = "";
|
||||
if (tattStyle) {
|
||||
styleCode = `.then(()=>{if (w${componentID}.__owl__.status === ${STATUS.DESTROYED}) {return};w${componentID}.el.style=${tattStyle};});`;
|
||||
}
|
||||
ctx.addLine(
|
||||
`w${componentID}.__updateProps(props${componentID}, extra.fiber, ${scope})${styleCode};`
|
||||
);
|
||||
ctx.addLine(`let pvnode = w${componentID}.__owl__.pvnode;`);
|
||||
if (registerCode) {
|
||||
ctx.addLine(registerCode);
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
|
||||
}
|
||||
|
||||
ctx.addElse();
|
||||
|
||||
// new component
|
||||
function defineComponentKey() {
|
||||
if (!hasDefinedKey) {
|
||||
const interpValue = ctx.interpolate(value);
|
||||
ctx.addLine(`let componentKey${componentID} = ${interpValue};`);
|
||||
hasDefinedKey = true;
|
||||
}
|
||||
}
|
||||
defineComponentKey();
|
||||
const contextualValue = value.match(INTERP_REGEXP) ? "false" : ctx.formatExpression(value);
|
||||
ctx.addLine(
|
||||
`let W${componentID} = ${contextualValue} || context.constructor.components[componentKey${componentID}] || QWeb.components[componentKey${componentID}];`
|
||||
);
|
||||
|
||||
// maybe only do this in dev mode...
|
||||
ctx.addLine(
|
||||
`if (!W${componentID}) {throw new Error('Cannot find the definition of component "' + componentKey${componentID} + '"')}`
|
||||
);
|
||||
ctx.addLine(`w${componentID} = new W${componentID}(parent, props${componentID});`);
|
||||
if (transition) {
|
||||
ctx.addLine(`const __patch${componentID} = w${componentID}.__patch;`);
|
||||
ctx.addLine(
|
||||
`w${componentID}.__patch = (t, vn) => {__patch${componentID}.call(w${componentID}, t, vn); if(!w${componentID}.__owl__.transitionInserted){w${componentID}.__owl__.transitionInserted = true;utils.transitionInsert(w${componentID}.__owl__.vnode, '${transition}');}};`
|
||||
);
|
||||
}
|
||||
ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`);
|
||||
|
||||
if (hasSlots) {
|
||||
const clone = <Element>node.cloneNode(true);
|
||||
|
||||
// The next code is a fallback for compatibility reason. It accepts t-set
|
||||
// elements that are direct children with a non empty body as nodes defining
|
||||
// the content of a slot.
|
||||
//
|
||||
// This is wrong, but is necessary to prevent breaking all existing Owl
|
||||
// code using slots. This will be removed in v2.0 someday. Meanwhile,
|
||||
// please use t-set-slot everywhere you need to set the content of a
|
||||
// slot.
|
||||
for (let node of clone.children) {
|
||||
if (node.hasAttribute("t-set") && node.hasChildNodes()) {
|
||||
node.setAttribute("t-set-slot", node.getAttribute("t-set")!);
|
||||
node.removeAttribute("t-set");
|
||||
}
|
||||
}
|
||||
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
|
||||
const slotNames = new Set<string>();
|
||||
const slotId = QWeb.nextSlotId++;
|
||||
ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`);
|
||||
if (slotNodes.length) {
|
||||
for (let i = 0, length = slotNodes.length; i < length; i++) {
|
||||
const slotNode = slotNodes[i];
|
||||
// check if this is defined in a sub component (in which case it should
|
||||
// be ignored)
|
||||
let el = slotNode.parentElement;
|
||||
let isInSubComponent = false;
|
||||
while (el !== clone) {
|
||||
if (
|
||||
el!.hasAttribute("t-component") ||
|
||||
el!.tagName[0] === el!.tagName[0].toUpperCase()
|
||||
) {
|
||||
isInSubComponent = true;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
if (isInSubComponent) {
|
||||
continue;
|
||||
}
|
||||
let key = slotNode.getAttribute("t-set-slot")!;
|
||||
if (slotNames.has(key)) {
|
||||
continue;
|
||||
}
|
||||
slotNames.add(key);
|
||||
slotNode.removeAttribute("t-set-slot");
|
||||
slotNode.parentElement!.removeChild(slotNode);
|
||||
|
||||
const slotFn = qweb._compile(`slot_${key}_template`, { elem: slotNode, hasParent: true });
|
||||
QWeb.slots[`${slotId}_${key}`] = slotFn;
|
||||
}
|
||||
}
|
||||
if (clone.childNodes.length) {
|
||||
let hasContent = false;
|
||||
const t = clone.ownerDocument!.createElement("t");
|
||||
for (let child of Object.values(clone.childNodes)) {
|
||||
hasContent =
|
||||
hasContent || (child instanceof Text ? Boolean(child.textContent.trim().length) : true);
|
||||
t.appendChild(child);
|
||||
}
|
||||
if (hasContent) {
|
||||
const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true });
|
||||
QWeb.slots[`${slotId}_default`] = slotFn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.addLine(
|
||||
`let fiber = w${componentID}.__prepare(extra.fiber, ${scope}, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; ${createHook}});`
|
||||
);
|
||||
// hack: specify empty remove hook to prevent the node from being removed from the DOM
|
||||
const insertHook = refExpr ? `insert(vn) {${refExpr}},` : "";
|
||||
ctx.addLine(
|
||||
`let pvnode = h('dummy', {key: ${templateKey}, hook: {${insertHook}remove() {},destroy(vn) {${finalizeComponentCode}}}});`
|
||||
);
|
||||
if (registerCode) {
|
||||
ctx.addLine(registerCode);
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(pvnode);`);
|
||||
}
|
||||
ctx.addLine(`w${componentID}.__owl__.pvnode = pvnode;`);
|
||||
|
||||
ctx.closeIf();
|
||||
|
||||
if (classObj) {
|
||||
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
|
||||
}
|
||||
|
||||
ctx.addLine(`w${componentID}.__owl__.parentLastFiberId = extra.fiber.id;`);
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,366 @@
|
||||
import { h, VNode } from "../vdom/index";
|
||||
import { Component, MountPosition, STATUS } from "./component";
|
||||
import { scheduler } from "./scheduler";
|
||||
|
||||
/**
|
||||
* Owl Fiber Class
|
||||
*
|
||||
* Fibers are small abstractions designed to contain all the internal state
|
||||
* associated with a "rendering work unit", relative to a specific component.
|
||||
*
|
||||
* A rendering will cause the creation of a fiber for each impacted components.
|
||||
*
|
||||
* Fibers capture all that necessary information, which is critical to owl
|
||||
* asynchronous rendering pipeline. Fibers can be cancelled, can be in different
|
||||
* states and in general determine the state of the rendering.
|
||||
*/
|
||||
|
||||
export class Fiber {
|
||||
static nextId: number = 1;
|
||||
id: number = Fiber.nextId++;
|
||||
|
||||
// The force attribute determines if a rendering should bypass the `shouldUpdate`
|
||||
// method potentially implemented by a component. It is usually set to false.
|
||||
force: boolean;
|
||||
|
||||
// isCompleted means that the rendering corresponding to this fiber's work is
|
||||
// done, either because the component has been mounted or patched, or because
|
||||
// fiber has been cancelled.
|
||||
isCompleted: boolean = false;
|
||||
|
||||
// the fibers corresponding to component updates (updateProps) need to call
|
||||
// the willPatch and patched hooks from the corresponding component. However,
|
||||
// fibers corresponding to a new component do not need to do that. So, the
|
||||
// shouldPatch hook is the boolean that we check whenever we need to apply
|
||||
// a patch.
|
||||
shouldPatch: boolean = true;
|
||||
|
||||
// isRendered is the last state of a fiber. If true, this means that it has
|
||||
// been rendered and is inert (so, it should not be taken into account when
|
||||
// counting the number of active fibers).
|
||||
isRendered: boolean = false;
|
||||
|
||||
// the counter number is a critical information. It is only necessary for a
|
||||
// root fiber. For that fiber, this number counts the number of active sub
|
||||
// fibers. When that number reaches 0, the fiber can be applied by the
|
||||
// scheduler.
|
||||
counter: number = 0;
|
||||
|
||||
target: HTMLElement | DocumentFragment | null;
|
||||
position: MountPosition | null;
|
||||
|
||||
scope: any;
|
||||
|
||||
component: Component;
|
||||
vnode: VNode | null = null;
|
||||
|
||||
root: Fiber;
|
||||
child: Fiber | null = null;
|
||||
sibling: Fiber | null = null;
|
||||
lastChild: Fiber | null = null;
|
||||
parent: Fiber | null = null;
|
||||
|
||||
error?: Error;
|
||||
|
||||
constructor(
|
||||
parent: Fiber | null,
|
||||
component: Component,
|
||||
force: boolean,
|
||||
target: HTMLElement | DocumentFragment | null,
|
||||
position: MountPosition | null
|
||||
) {
|
||||
this.component = component;
|
||||
this.force = force;
|
||||
this.target = target;
|
||||
this.position = position;
|
||||
|
||||
const __owl__ = component.__owl__;
|
||||
this.scope = __owl__.scope;
|
||||
|
||||
this.root = parent ? parent.root : this;
|
||||
this.parent = parent;
|
||||
|
||||
let oldFiber = __owl__.currentFiber;
|
||||
if (oldFiber && !oldFiber.isCompleted) {
|
||||
this.force = true;
|
||||
if (oldFiber.root === oldFiber && !parent) {
|
||||
// both oldFiber and this fiber are root fibers
|
||||
this._reuseFiber(oldFiber);
|
||||
return oldFiber;
|
||||
} else {
|
||||
this._remapFiber(oldFiber);
|
||||
}
|
||||
}
|
||||
|
||||
this.root.counter++;
|
||||
|
||||
__owl__.currentFiber = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the oldFiber is not completed yet, and both oldFiber and this fiber
|
||||
* are root fibers, we want to reuse the oldFiber instead of creating a new
|
||||
* one. Doing so will guarantee that the initiator(s) of those renderings will
|
||||
* be notified (the promise will resolve) when the last rendering will be done.
|
||||
*
|
||||
* This function thus assumes that oldFiber is a root fiber.
|
||||
*/
|
||||
_reuseFiber(oldFiber: Fiber) {
|
||||
oldFiber.cancel(); // cancel children fibers
|
||||
oldFiber.target = this.target || oldFiber.target;
|
||||
oldFiber.position = this.position || oldFiber.position;
|
||||
oldFiber.isCompleted = false; // keep the root fiber alive
|
||||
oldFiber.isRendered = false; // the fiber has to be re-rendered
|
||||
if (oldFiber.child) {
|
||||
// remove relation to children
|
||||
oldFiber.child.parent = null;
|
||||
oldFiber.child = null;
|
||||
oldFiber.lastChild = null;
|
||||
}
|
||||
oldFiber.counter = 1; // re-initialize counter
|
||||
oldFiber.id = Fiber.nextId++;
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases, a rendering initiated at some component can detect that it
|
||||
* should be part of a larger rendering initiated somewhere up the component
|
||||
* tree. In that case, it needs to cancel the previous rendering and
|
||||
* remap itself as a part of the current parent rendering.
|
||||
*/
|
||||
_remapFiber(oldFiber: Fiber) {
|
||||
oldFiber.cancel();
|
||||
this.shouldPatch = oldFiber.shouldPatch;
|
||||
if (oldFiber === oldFiber.root) {
|
||||
oldFiber.counter++;
|
||||
}
|
||||
if (oldFiber.parent && !this.parent) {
|
||||
// re-map links
|
||||
this.parent = oldFiber.parent;
|
||||
this.root = this.parent.root;
|
||||
this.sibling = oldFiber.sibling;
|
||||
if (this.parent.lastChild === oldFiber) {
|
||||
this.parent.lastChild = this;
|
||||
}
|
||||
if (this.parent.child === oldFiber) {
|
||||
this.parent.child = this;
|
||||
} else {
|
||||
let current = this.parent.child!;
|
||||
while (true) {
|
||||
if (current.sibling === oldFiber) {
|
||||
current.sibling = this;
|
||||
break;
|
||||
}
|
||||
current = current.sibling!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function has been taken from
|
||||
* https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7
|
||||
*/
|
||||
_walk(doWork: (f: Fiber) => Fiber | null) {
|
||||
let root = this;
|
||||
let current: Fiber = this;
|
||||
while (true) {
|
||||
const child = doWork(current);
|
||||
if (child) {
|
||||
current = child;
|
||||
continue;
|
||||
}
|
||||
if (current === root) {
|
||||
return;
|
||||
}
|
||||
while (!current.sibling) {
|
||||
if (!current.parent || current.parent === root) {
|
||||
return;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
current = current.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Successfully complete the work of the fiber: call the mount or patch hooks
|
||||
* and patch the DOM. This function is called once the fiber and its children
|
||||
* are ready, and the scheduler decides to process it.
|
||||
*/
|
||||
complete() {
|
||||
let component = this.component;
|
||||
this.isCompleted = true;
|
||||
const status = component.__owl__.status;
|
||||
if (status === STATUS.DESTROYED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// build patchQueue
|
||||
const patchQueue: Fiber[] = [];
|
||||
const doWork: (Fiber) => Fiber | null = function (f) {
|
||||
f.component.__owl__.currentFiber = null;
|
||||
patchQueue.push(f);
|
||||
return f.child;
|
||||
};
|
||||
this._walk(doWork);
|
||||
const patchLen = patchQueue.length;
|
||||
|
||||
// call willPatch hook on each fiber of patchQueue
|
||||
if (status === STATUS.MOUNTED) {
|
||||
for (let i = 0; i < patchLen; i++) {
|
||||
const fiber = patchQueue[i];
|
||||
if (fiber.shouldPatch) {
|
||||
component = fiber.component;
|
||||
if (component.__owl__.willPatchCB) {
|
||||
component.__owl__.willPatchCB();
|
||||
}
|
||||
component.willPatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call __patch on each fiber of (reversed) patchQueue
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
if (fiber.target && i === 0) {
|
||||
let target;
|
||||
if (fiber.position === "self") {
|
||||
target = fiber.target;
|
||||
if ((target as HTMLElement).tagName.toLowerCase() !== fiber.vnode!.sel) {
|
||||
throw new Error(
|
||||
`Cannot attach '${component.constructor.name}' to target node (not same tag name)`
|
||||
);
|
||||
}
|
||||
// In self mode, we *know* we are to take possession of the target
|
||||
// Hence we manually create the corresponding VNode and copy the "key" in data
|
||||
const selfVnodeData = fiber.vnode!.data ? { key: fiber.vnode!.data.key } : {};
|
||||
const selfVnode = h(fiber.vnode!.sel, selfVnodeData);
|
||||
selfVnode.elm = target;
|
||||
target = selfVnode;
|
||||
} else {
|
||||
target = component.__owl__.vnode || document.createElement(fiber.vnode!.sel!);
|
||||
}
|
||||
component.__patch(target!, fiber.vnode!);
|
||||
} else {
|
||||
const vnode = component.__owl__.vnode;
|
||||
if (fiber.shouldPatch && vnode) {
|
||||
component.__patch(vnode, fiber.vnode!);
|
||||
// When updating a Component's props (in directive),
|
||||
// the component has a pvnode AND should be patched.
|
||||
// However, its pvnode.elm may have changed if it is a High Order Component
|
||||
if (component.__owl__.pvnode) {
|
||||
component.__owl__.pvnode.elm = component.__owl__.vnode!.elm;
|
||||
}
|
||||
} else {
|
||||
component.__patch(document.createElement(fiber.vnode!.sel!), fiber.vnode!);
|
||||
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insert into the DOM (mount case)
|
||||
let inDOM = false;
|
||||
if (this.target) {
|
||||
switch (this.position) {
|
||||
case "first-child":
|
||||
this.target.prepend(this.component.el!);
|
||||
break;
|
||||
case "last-child":
|
||||
this.target.appendChild(this.component.el!);
|
||||
break;
|
||||
}
|
||||
inDOM = document.body.contains(this.component.el);
|
||||
this.component.env.qweb.trigger("dom-appended");
|
||||
}
|
||||
|
||||
// call patched/mounted hook on each fiber of (reversed) patchQueue
|
||||
if (status === STATUS.MOUNTED || inDOM) {
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
if (fiber.shouldPatch && !this.target) {
|
||||
component.patched();
|
||||
if (component.__owl__.patchedCB) {
|
||||
component.__owl__.patchedCB();
|
||||
}
|
||||
} else {
|
||||
component.__callMounted();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = patchLen - 1; i >= 0; i--) {
|
||||
const fiber = patchQueue[i];
|
||||
component = fiber.component;
|
||||
component.__owl__.status = STATUS.UNMOUNTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a fiber and all its children.
|
||||
*/
|
||||
cancel() {
|
||||
this._walk((f) => {
|
||||
if (!f.isRendered) {
|
||||
f.root.counter--;
|
||||
}
|
||||
f.isCompleted = true;
|
||||
return f.child;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the global error handler for errors occurring in Owl main lifecycle
|
||||
* methods. Caught errors are triggered on the QWeb instance, and are
|
||||
* potentially given to some parent component which implements `catchError`.
|
||||
*
|
||||
* If there are no such component, we destroy everything. This is better than
|
||||
* being in a corrupted state.
|
||||
*/
|
||||
handleError(error: Error) {
|
||||
let component = this.component;
|
||||
this.vnode = component.__owl__.vnode || h("div");
|
||||
|
||||
const qweb = component.env.qweb;
|
||||
let root = component;
|
||||
|
||||
function handle(error) {
|
||||
let canCatch = false;
|
||||
qweb.trigger("error", error);
|
||||
while (component && !(canCatch = !!component.catchError)) {
|
||||
root = component;
|
||||
component = component.__owl__.parent!;
|
||||
}
|
||||
if (canCatch) {
|
||||
try {
|
||||
component.catchError!(error);
|
||||
} catch (e) {
|
||||
root = component;
|
||||
component = component.__owl__.parent!;
|
||||
return handle(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let isHandled = handle(error);
|
||||
|
||||
if (!isHandled) {
|
||||
// the 3 next lines aim to mark the root fiber as being in error, and
|
||||
// to force it to end, without waiting for its children
|
||||
this.root.counter = 0;
|
||||
this.root.error = error;
|
||||
scheduler.flush();
|
||||
// at this point, the state of the application is corrupted and we could
|
||||
// have a lot of issues or crashes. So we destroy the application in a try
|
||||
// catch and swallow these errors because the fiber is already in error,
|
||||
// and this is the actual issue that needs to be solved, not those followup
|
||||
// errors.
|
||||
try {
|
||||
root.destroy();
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { QWeb } from "../qweb/index";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
|
||||
QWeb.utils.validateProps = function (Widget, props: Object) {
|
||||
const propsDef = (<any>Widget).props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
const propName = propsDef[i];
|
||||
if (propName[propName.length - 1] === "?") {
|
||||
// optional prop
|
||||
break;
|
||||
}
|
||||
if (!(propName in props)) {
|
||||
throw new Error(`Missing props '${propsDef[i]}' (component '${Widget.name}')`);
|
||||
}
|
||||
}
|
||||
for (let key in props) {
|
||||
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
|
||||
throw new Error(`Unknown prop '${key}' given to component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
} else if (propsDef) {
|
||||
// propsDef is an object now
|
||||
for (let propName in propsDef) {
|
||||
if (props[propName] === undefined) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${Widget.name}')`);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let isValid;
|
||||
try {
|
||||
isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
} catch (e) {
|
||||
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
|
||||
throw e;
|
||||
}
|
||||
if (!isValid) {
|
||||
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
for (let propName in props) {
|
||||
if (!(propName in propsDef)) {
|
||||
throw new Error(`Unknown prop '${propName}' given to component '${Widget.name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop, propDef): boolean {
|
||||
if (propDef === true) {
|
||||
return true;
|
||||
}
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
if (propDef.optional && prop === undefined) {
|
||||
return true;
|
||||
}
|
||||
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
|
||||
if (propDef.validate) {
|
||||
result = result && propDef.validate(prop);
|
||||
}
|
||||
if (propDef.type === Array && propDef.element) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object && propDef.shape) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
if (result) {
|
||||
for (let propName in prop) {
|
||||
if (!(propName in shape)) {
|
||||
throw new Error(`unknown prop '${propName}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Fiber } from "./fiber";
|
||||
import { browser } from "../browser";
|
||||
|
||||
/**
|
||||
* Owl Scheduler Class
|
||||
*
|
||||
* The scheduler is the part of Owl that will effectively apply a rendering
|
||||
* whenever a fiber is ready.
|
||||
*
|
||||
* Briefly, it can be used to register root fibers. Whenever there is an
|
||||
* active root fiber, it will poll continuously each animation frame (so, about
|
||||
* once every 16ms) and whenever a root fiber is ready, it will apply it.
|
||||
*/
|
||||
|
||||
interface Task {
|
||||
fiber: Fiber;
|
||||
callback: (err?: Error) => void;
|
||||
}
|
||||
|
||||
export class Scheduler {
|
||||
tasks: Task[] = [];
|
||||
isRunning: boolean = false;
|
||||
requestAnimationFrame: Window["requestAnimationFrame"];
|
||||
|
||||
constructor(requestAnimationFrame: Window["requestAnimationFrame"]) {
|
||||
this.requestAnimationFrame = requestAnimationFrame;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.isRunning = true;
|
||||
this.scheduleTasks();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
}
|
||||
|
||||
addFiber(fiber: Fiber): Promise<void> {
|
||||
// if the fiber was remapped into a larger rendering fiber, it may not be a
|
||||
// root fiber. But we only want to register root fibers
|
||||
fiber = fiber.root;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (fiber.error) {
|
||||
return reject(fiber.error);
|
||||
}
|
||||
this.tasks.push({
|
||||
fiber,
|
||||
callback: () => {
|
||||
if (fiber.error) {
|
||||
return reject(fiber.error);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
if (!this.isRunning) {
|
||||
this.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rejectFiber(fiber: Fiber, reason: string) {
|
||||
fiber = fiber.root;
|
||||
const index = this.tasks.findIndex((t) => t.fiber === fiber);
|
||||
if (index >= 0) {
|
||||
const [task] = this.tasks.splice(index, 1);
|
||||
fiber.cancel();
|
||||
fiber.error = new Error(reason);
|
||||
task.callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all current tasks. This only applies to the fibers that are ready.
|
||||
* Other tasks are left unchanged.
|
||||
*/
|
||||
flush() {
|
||||
let tasks = this.tasks;
|
||||
this.tasks = [];
|
||||
tasks = tasks.filter((task) => {
|
||||
if (task.fiber.isCompleted) {
|
||||
task.callback();
|
||||
return false;
|
||||
}
|
||||
if (task.fiber.counter === 0) {
|
||||
if (!task.fiber.error) {
|
||||
try {
|
||||
task.fiber.complete();
|
||||
} catch (e) {
|
||||
task.fiber.handleError(e);
|
||||
}
|
||||
}
|
||||
task.callback();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.tasks = tasks.concat(this.tasks);
|
||||
if (this.tasks.length === 0) {
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
scheduleTasks() {
|
||||
this.requestAnimationFrame(() => {
|
||||
this.flush();
|
||||
if (this.isRunning) {
|
||||
this.scheduleTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const scheduler = new Scheduler(browser.requestAnimationFrame);
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Owl Style System
|
||||
*
|
||||
* This files contains the Owl code related to processing (extended) css strings
|
||||
* and creating/adding <style> tags to the document head.
|
||||
*/
|
||||
|
||||
export const STYLESHEETS: { [id: string]: HTMLStyleElement } = {};
|
||||
|
||||
export function processSheet(str: string): string {
|
||||
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
||||
const selectorStack: string[][] = [];
|
||||
const parts: string[] = [];
|
||||
let rules: string[] = [];
|
||||
function generateSelector(stackIndex: number, parentSelector?: string) {
|
||||
const parts: string[] = [];
|
||||
for (const selector of selectorStack[stackIndex]) {
|
||||
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
||||
if (part.includes("&")) {
|
||||
part = selector.replace(/&/g, parentSelector || "");
|
||||
}
|
||||
if (stackIndex < selectorStack.length - 1) {
|
||||
part = generateSelector(stackIndex + 1, part);
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
return parts.join(", ");
|
||||
}
|
||||
function generateRules() {
|
||||
if (rules.length) {
|
||||
parts.push(generateSelector(0) + " {");
|
||||
parts.push(...rules);
|
||||
parts.push("}");
|
||||
rules = [];
|
||||
}
|
||||
}
|
||||
while (tokens.length) {
|
||||
let token = tokens.shift()!;
|
||||
if (token === "}") {
|
||||
generateRules();
|
||||
selectorStack.pop();
|
||||
} else {
|
||||
if (tokens[0] === "{") {
|
||||
generateRules();
|
||||
selectorStack.push(token.split(/\s*,\s*/));
|
||||
tokens.shift();
|
||||
}
|
||||
if (tokens[0] === ";") {
|
||||
rules.push(" " + token + ";");
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
export function registerSheet(id: string, css: string) {
|
||||
const sheet = document.createElement("style");
|
||||
sheet.innerHTML = processSheet(css);
|
||||
STYLESHEETS[id] = sheet;
|
||||
}
|
||||
|
||||
export function activateSheet(id, name) {
|
||||
const sheet = STYLESHEETS[id];
|
||||
if (!sheet) {
|
||||
throw new Error(
|
||||
`Invalid css stylesheet for component '${name}'. Did you forget to use the 'css' tag helper?`
|
||||
);
|
||||
}
|
||||
sheet.setAttribute("component", name);
|
||||
document.head.appendChild(sheet);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { QWeb } from "./qweb/index";
|
||||
import { TRANSLATABLE_ATTRS } from "./qweb/qweb";
|
||||
|
||||
/**
|
||||
* This file creates and exports the OWL 'config' object, with keys:
|
||||
* - 'mode': 'prod' or 'dev',
|
||||
* - 'env': the environment to use in root components.
|
||||
*/
|
||||
|
||||
interface Config {
|
||||
mode: string;
|
||||
enableTransitions: boolean;
|
||||
translatableAttributes: string[];
|
||||
}
|
||||
|
||||
export const config = {
|
||||
translatableAttributes: TRANSLATABLE_ATTRS,
|
||||
} as Config;
|
||||
|
||||
Object.defineProperty(config, "mode", {
|
||||
get() {
|
||||
return QWeb.dev ? "dev" : "prod";
|
||||
},
|
||||
set(mode: string) {
|
||||
QWeb.dev = mode === "dev";
|
||||
if (QWeb.dev) {
|
||||
console.info(`Owl is running in 'dev' mode.
|
||||
|
||||
This is not suitable for production use.
|
||||
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(config, "enableTransitions", {
|
||||
get() {
|
||||
return QWeb.enableTransitions;
|
||||
},
|
||||
set(value: boolean) {
|
||||
QWeb.enableTransitions = value;
|
||||
},
|
||||
});
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { Component } from "./component/component";
|
||||
import { scheduler } from "./component/scheduler";
|
||||
import { EventBus } from "./core/event_bus";
|
||||
import { Observer } from "./core/observer";
|
||||
|
||||
/**
|
||||
* The `Context` object provides a way to share data between an arbitrary number
|
||||
* of component. Usually, data is passed from a parent to its children component,
|
||||
* but when we have to deal with some mostly global information, this can be
|
||||
* annoying, since each component will need to pass the information to each
|
||||
* children, even though some or most of them will not use the information.
|
||||
*
|
||||
* With a `Context` object, each component can subscribe (with the `useContext`
|
||||
* hook) to its state, and will be updated whenever the context state is updated.
|
||||
*/
|
||||
|
||||
function partitionBy<T>(arr: T[], fn: (t: T) => boolean) {
|
||||
let lastGroup: T[] | false = false;
|
||||
let lastValue;
|
||||
return arr.reduce((acc: T[][], cur) => {
|
||||
let curVal = fn(cur);
|
||||
if (lastGroup) {
|
||||
if (curVal === lastValue) {
|
||||
lastGroup.push(cur);
|
||||
} else {
|
||||
lastGroup = false;
|
||||
}
|
||||
}
|
||||
if (!lastGroup) {
|
||||
lastGroup = [cur];
|
||||
acc.push(lastGroup);
|
||||
}
|
||||
lastValue = curVal;
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export class Context extends EventBus {
|
||||
state: any;
|
||||
observer: Observer;
|
||||
rev: number = 1;
|
||||
// mapping from component id to last observed context id
|
||||
mapping: { [componentId: number]: number } = {};
|
||||
|
||||
constructor(state: Object = {}) {
|
||||
super();
|
||||
this.observer = new Observer();
|
||||
this.observer.notifyCB = () => {
|
||||
// notify components in the next microtask tick to ensure that subscribers
|
||||
// are notified only once for all changes that occur in the same micro tick
|
||||
let rev = this.rev;
|
||||
return Promise.resolve().then(() => {
|
||||
if (rev === this.rev) {
|
||||
this.__notifyComponents();
|
||||
}
|
||||
});
|
||||
};
|
||||
this.state = this.observer.observe(state);
|
||||
this.subscriptions.update = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Instead of using trigger to emit an update event, we actually implement
|
||||
* our own function to do that. The reason is that we need to be smarter than
|
||||
* a simple trigger function: we need to wait for parent components to be
|
||||
* done before doing children components. More precisely, if an update
|
||||
* as an effect of destroying a children, we do not want to call any code
|
||||
* from the child, and certainly not render it.
|
||||
*
|
||||
* This method implements a simple grouping algorithm by depth. If we have
|
||||
* connected components of depths [2, 4,4,4,4, 3,8,8], the Context will notify
|
||||
* them in the following groups: [2], [4,4,4,4], [3], [8,8]. Each group will
|
||||
* be updated sequentially, but each components in a given group will be done in
|
||||
* parallel.
|
||||
*
|
||||
* This is a very simple algorithm, but it avoids checking if a given
|
||||
* component is a child of another.
|
||||
*/
|
||||
async __notifyComponents() {
|
||||
const rev = ++this.rev;
|
||||
const subscriptions = this.subscriptions.update;
|
||||
const groups = partitionBy(subscriptions, (s) => (s.owner ? s.owner.__owl__.depth : -1));
|
||||
for (let group of groups) {
|
||||
const proms = group.map((sub) => sub.callback.call(sub.owner, rev));
|
||||
// at this point, each component in the current group has registered a
|
||||
// top level fiber in the scheduler. It could happen that rendering these
|
||||
// components is done (if they have no children). This is why we manually
|
||||
// flush the scheduler. This will force the scheduler to check
|
||||
// immediately if they are done, which will cause their rendering
|
||||
// promise to resolve earlier, which means that there is a chance of
|
||||
// processing the next group in the same frame.
|
||||
scheduler.flush();
|
||||
await Promise.all(proms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The`useContext` hook is the normal way for a component to register themselve
|
||||
* to context state changes. The `useContext` method returns the context state
|
||||
*/
|
||||
export function useContext(ctx: Context): any {
|
||||
const component: Component = Component.current!;
|
||||
return useContextWithCB(ctx, component, component.render.bind(component));
|
||||
}
|
||||
|
||||
export function useContextWithCB(ctx: Context, component: Component, method): any {
|
||||
const __owl__ = component.__owl__;
|
||||
const id = __owl__.id;
|
||||
const mapping = ctx.mapping;
|
||||
if (id in mapping) {
|
||||
return ctx.state;
|
||||
}
|
||||
if (!__owl__.observer) {
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.notifyCB = component.render.bind(component);
|
||||
}
|
||||
|
||||
mapping[id] = 0;
|
||||
const renderFn = __owl__.renderFn;
|
||||
__owl__.renderFn = function (comp, params) {
|
||||
mapping[id] = ctx.rev;
|
||||
return renderFn(comp, params);
|
||||
};
|
||||
ctx.on("update", component, async (contextRev) => {
|
||||
if (mapping[id] < contextRev) {
|
||||
mapping[id] = contextRev;
|
||||
await method();
|
||||
}
|
||||
});
|
||||
const __destroy = component.__destroy;
|
||||
component.__destroy = (parent) => {
|
||||
ctx.off("update", component);
|
||||
delete mapping[id];
|
||||
__destroy.call(component, parent);
|
||||
};
|
||||
return ctx.state;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* We define here a simple event bus: it can
|
||||
* - emit events
|
||||
* - add/remove listeners.
|
||||
*
|
||||
* This is a useful pattern of communication in many cases. For OWL, each
|
||||
* components and stores are event buses.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export type Callback = (...args: any[]) => void;
|
||||
|
||||
export interface Subscription {
|
||||
owner: any;
|
||||
callback: Callback;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// EventBus
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class EventBus {
|
||||
subscriptions: { [eventType: string]: Subscription[] } = {};
|
||||
|
||||
/**
|
||||
* Add a listener for the 'eventType' events.
|
||||
*
|
||||
* Note that the 'owner' of this event can be anything, but will more likely
|
||||
* be a component or a class. The idea is that the callback will be called with
|
||||
* the proper owner bound.
|
||||
*
|
||||
* Also, the owner should be kind of unique. This will be used to remove the
|
||||
* listener.
|
||||
*/
|
||||
on(eventType: string, owner: any, callback: Callback) {
|
||||
if (!callback) {
|
||||
throw new Error("Missing callback");
|
||||
}
|
||||
if (!this.subscriptions[eventType]) {
|
||||
this.subscriptions[eventType] = [];
|
||||
}
|
||||
this.subscriptions[eventType].push({
|
||||
owner,
|
||||
callback,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener
|
||||
*/
|
||||
off(eventType: string, owner: any) {
|
||||
const subs = this.subscriptions[eventType];
|
||||
if (subs) {
|
||||
this.subscriptions[eventType] = subs.filter((s) => s.owner !== owner);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event of type 'eventType'. Any extra arguments will be passed to
|
||||
* the listeners callback.
|
||||
*/
|
||||
trigger(eventType: string, ...args: any[]) {
|
||||
const subs = this.subscriptions[eventType] || [];
|
||||
for (let i = 0, iLen = subs.length; i < iLen; i++) {
|
||||
const sub = subs[i];
|
||||
sub.callback.call(sub.owner, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all subscriptions.
|
||||
*/
|
||||
clear() {
|
||||
this.subscriptions = {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Owl Observer
|
||||
*
|
||||
* This code contains the logic that allows Owl to observe and react to state
|
||||
* changes.
|
||||
*
|
||||
* This is a Observer class that can observe any JS values. The way it works
|
||||
* can be summarized thusly:
|
||||
* - primitive values are not observed at all
|
||||
* - Objects and arrays are observed by replacing them with a Proxy
|
||||
* - each object/array metadata are tracked in a weakmap, and keep a revision
|
||||
* number
|
||||
*
|
||||
* Note that this code is loosely inspired by Vue.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Observer
|
||||
//------------------------------------------------------------------------------
|
||||
export class Observer {
|
||||
rev: number = 1;
|
||||
allowMutations: boolean = true;
|
||||
weakMap: WeakMap<any, any> = new WeakMap();
|
||||
|
||||
notifyCB() {}
|
||||
|
||||
observe<T>(value: T, parent?: any): T {
|
||||
if (
|
||||
value === null ||
|
||||
typeof value !== "object" ||
|
||||
value instanceof Date ||
|
||||
value instanceof Promise
|
||||
) {
|
||||
// fun fact: typeof null === 'object'
|
||||
return value;
|
||||
}
|
||||
let metadata = this.weakMap.get(value) || this._observe(value, parent);
|
||||
return metadata.proxy;
|
||||
}
|
||||
|
||||
revNumber(value): number {
|
||||
const metadata = this.weakMap.get(value);
|
||||
return metadata ? metadata.rev : 0;
|
||||
}
|
||||
|
||||
_observe(value, parent) {
|
||||
var self = this;
|
||||
|
||||
const proxy = new Proxy(value, {
|
||||
get(target, k) {
|
||||
const targetValue = target[k];
|
||||
return self.observe(targetValue, value);
|
||||
},
|
||||
set(target, key: string, newVal): boolean {
|
||||
const value = target[key];
|
||||
if (newVal !== value) {
|
||||
if (!self.allowMutations) {
|
||||
throw new Error(
|
||||
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
|
||||
);
|
||||
}
|
||||
self._updateRevNumber(target);
|
||||
target[key] = newVal;
|
||||
self.notifyCB();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
if (key in target) {
|
||||
delete target[key];
|
||||
self._updateRevNumber(target);
|
||||
self.notifyCB();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = {
|
||||
value,
|
||||
proxy,
|
||||
rev: this.rev,
|
||||
parent,
|
||||
};
|
||||
|
||||
this.weakMap.set(value, metadata);
|
||||
this.weakMap.set(metadata.proxy, metadata);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
_updateRevNumber(target: any) {
|
||||
this.rev++;
|
||||
let metadata = this.weakMap.get(target);
|
||||
let parent = target;
|
||||
do {
|
||||
metadata = this.weakMap.get(parent);
|
||||
metadata.rev++;
|
||||
} while ((parent = metadata.parent) && parent !== target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component } from "../component/component";
|
||||
|
||||
/**
|
||||
* We define here OwlEvent, a subclass of CustomEvent, with an additional
|
||||
* attribute:
|
||||
* - originalComponent: the component that triggered the event
|
||||
*/
|
||||
|
||||
export class OwlEvent<T> extends CustomEvent<T> {
|
||||
originalComponent: Component;
|
||||
constructor(component, eventType, options) {
|
||||
super(eventType, options);
|
||||
this.originalComponent = component;
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { Component, Env } from "./component/component";
|
||||
import { Observer } from "./core/observer";
|
||||
|
||||
/**
|
||||
* Owl Hook System
|
||||
*
|
||||
* This file introduces the concept of hooks, similar to React or Vue hooks.
|
||||
* We have currently an implementation of:
|
||||
* - useState (reactive state)
|
||||
* - onMounted
|
||||
* - onWillUnmount
|
||||
* - useRef
|
||||
*/
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useState
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the main way a component can be made reactive. The useState hook
|
||||
* will return an observed object (or array). Changes to that value will then
|
||||
* trigger a rerendering of the current component.
|
||||
*/
|
||||
export function useState<T>(state: T): T {
|
||||
const component: Component = Component.current!;
|
||||
const __owl__ = component.__owl__;
|
||||
if (!__owl__.observer) {
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.notifyCB = component.render.bind(component);
|
||||
}
|
||||
return __owl__.observer.observe(state);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Life cycle hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
function makeLifecycleHook(method: string, reverse: boolean = false) {
|
||||
if (reverse) {
|
||||
return function (cb) {
|
||||
const component: Component = Component.current!;
|
||||
if (component.__owl__[method]) {
|
||||
const current = component.__owl__[method];
|
||||
component.__owl__[method] = function () {
|
||||
current.call(component);
|
||||
cb.call(component);
|
||||
};
|
||||
} else {
|
||||
component.__owl__[method] = cb;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return function (cb) {
|
||||
const component: Component = Component.current!;
|
||||
if (component.__owl__[method]) {
|
||||
const current = component.__owl__[method];
|
||||
component.__owl__[method] = function () {
|
||||
cb.call(component);
|
||||
current.call(component);
|
||||
};
|
||||
} else {
|
||||
component.__owl__[method] = cb;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function makeAsyncHook(method: string) {
|
||||
return function (cb) {
|
||||
const component: Component = Component.current!;
|
||||
if (component.__owl__[method]) {
|
||||
const current = component.__owl__[method];
|
||||
component.__owl__[method] = function (...args) {
|
||||
return Promise.all([current.call(component, ...args), cb.call(component, ...args)]);
|
||||
};
|
||||
} else {
|
||||
component.__owl__[method] = cb;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const onMounted = makeLifecycleHook("mountedCB", true);
|
||||
export const onWillUnmount = makeLifecycleHook("willUnmountCB");
|
||||
export const onWillPatch = makeLifecycleHook("willPatchCB");
|
||||
export const onPatched = makeLifecycleHook("patchedCB", true);
|
||||
|
||||
export const onWillStart = makeAsyncHook("willStartCB");
|
||||
export const onWillUpdateProps = makeAsyncHook("willUpdatePropsCB");
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useRef
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The purpose of this hook is to allow components to get a reference to a sub
|
||||
* html node or component.
|
||||
*/
|
||||
interface Ref<C extends Component = Component> {
|
||||
el: HTMLElement | null;
|
||||
comp: C | null;
|
||||
}
|
||||
|
||||
export function useRef<C extends Component = Component>(name: string): Ref<C> {
|
||||
const __owl__ = Component.current!.__owl__;
|
||||
return {
|
||||
get el(): HTMLElement | null {
|
||||
const val = __owl__.refs && __owl__.refs[name];
|
||||
if (val instanceof HTMLElement) {
|
||||
return val;
|
||||
} else if (val instanceof Component) {
|
||||
return val.el;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
get comp(): C | null {
|
||||
const val = __owl__.refs && __owl__.refs[name];
|
||||
return val instanceof Component ? (val as C) : null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// "Builder" hooks
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This hook is useful as a building block for some customized hooks, that may
|
||||
* need a reference to the component calling them.
|
||||
*/
|
||||
export function useComponent<P, E extends Env>(): Component<P, E> {
|
||||
return Component.current as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is useful as a building block for some customized hooks, that may
|
||||
* need a reference to the env of the component calling them.
|
||||
*/
|
||||
export function useEnv<E extends Env>(): E {
|
||||
return Component.current.env as any;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useSubEnv
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This hook is a simple way to let components use a sub environment. Note that
|
||||
* like for all hooks, it is important that this is only called in the
|
||||
* constructor method.
|
||||
*/
|
||||
export function useSubEnv(nextEnv) {
|
||||
const component = Component.current!;
|
||||
component.env = Object.assign(Object.create(component.env), nextEnv);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// useExternalListener
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* When a component needs to listen to DOM Events on element(s) that are not
|
||||
* part of his hierarchy, we can use the `useExternalListener` hook.
|
||||
* It will correctly add and remove the event listener, whenever the
|
||||
* component is mounted and unmounted.
|
||||
*
|
||||
* Example:
|
||||
* a menu needs to listen to the click on window to be closed automatically
|
||||
*
|
||||
* Usage:
|
||||
* in the constructor of the OWL component that needs to be notified,
|
||||
* `useExternalListener(window, 'click', this._doSomething);`
|
||||
* */
|
||||
export function useExternalListener(
|
||||
target: HTMLElement | typeof window,
|
||||
eventName: string,
|
||||
handler,
|
||||
eventParams?
|
||||
) {
|
||||
const boundHandler = handler.bind(Component.current);
|
||||
|
||||
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
||||
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* This file is the main file packaged by rollup (see rollup.config.js). From
|
||||
* this file, we export all public owl elements.
|
||||
*
|
||||
* Note that dynamic values, such as a date or a commit hash are added by rollup
|
||||
*/
|
||||
import { EventBus } from "./core/event_bus";
|
||||
import { Observer } from "./core/observer";
|
||||
import { QWeb } from "./qweb/index";
|
||||
import { config } from "./config";
|
||||
import * as _store from "./store";
|
||||
import * as _utils from "./utils";
|
||||
import * as _tags from "./tags";
|
||||
import { AsyncRoot } from "./misc/async_root";
|
||||
import { Portal } from "./misc/portal";
|
||||
import * as _hooks from "./hooks";
|
||||
import * as _context from "./context";
|
||||
import { Link } from "./router/link";
|
||||
import { RouteComponent } from "./router/route_component";
|
||||
import { Router } from "./router/router";
|
||||
|
||||
export { Component, mount } from "./component/component";
|
||||
export { QWeb };
|
||||
export { config };
|
||||
export { browser } from "./browser";
|
||||
|
||||
export const Context = _context.Context;
|
||||
export const useState = _hooks.useState;
|
||||
export const core = { EventBus, Observer };
|
||||
export const router = { Router, RouteComponent, Link };
|
||||
export const Store = _store.Store;
|
||||
export const utils = _utils;
|
||||
export const tags = _tags;
|
||||
export const misc = { AsyncRoot, Portal };
|
||||
export const hooks = Object.assign({}, _hooks, {
|
||||
useContext: _context.useContext,
|
||||
useDispatch: _store.useDispatch,
|
||||
useGetters: _store.useGetters,
|
||||
useStore: _store.useStore,
|
||||
});
|
||||
|
||||
export const __info__ = {};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component } from "../component/component";
|
||||
import { xml } from "../tags";
|
||||
|
||||
/**
|
||||
* AsyncRoot
|
||||
*
|
||||
* Owl is by default asynchronous, and the user interface will wait for all its
|
||||
* subcomponents to be rendered before updating the DOM. This is most of the
|
||||
* time what we want, but in some cases, it makes sense to "detach" a component
|
||||
* from this coordination. This is the goal of the AsyncRoot component.
|
||||
*/
|
||||
|
||||
export class AsyncRoot extends Component {
|
||||
static template = xml`<t t-slot="default"/>`;
|
||||
|
||||
async __updateProps(nextProps, parentFiber) {
|
||||
this.render(parentFiber.force);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Component, portalSymbol } from "../component/component";
|
||||
import { VNode, patch } from "../vdom/index";
|
||||
import { xml } from "../tags";
|
||||
import { OwlEvent } from "../core/owl_event";
|
||||
import { useSubEnv } from "../hooks";
|
||||
|
||||
/**
|
||||
* Portal
|
||||
*
|
||||
* The Portal component allows to render a part of a component outside it's DOM.
|
||||
* It is for example useful for dialogs: for css reasons, dialogs are in general
|
||||
* placed in a specific spot of the DOM (e.g. directly in the body). With the
|
||||
* Portal, a component can conditionally specify in its tempate that it contains
|
||||
* a dialog, and where this dialog should be inserted in the DOM.
|
||||
*
|
||||
* The Portal component ensures that the communication between the content of
|
||||
* the Portal and its parent properly works: business events reaching the Portal
|
||||
* are re-triggered on an empty <portal> node located in the parent's DOM.
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
target: string;
|
||||
}
|
||||
|
||||
export class Portal extends Component<Props> {
|
||||
static template = xml`<portal><t t-slot="default"/></portal>`;
|
||||
static props = {
|
||||
target: {
|
||||
type: String,
|
||||
},
|
||||
};
|
||||
|
||||
// boolean to indicate whether or not we must listen to 'dom-appended' event
|
||||
// to hook on the moment when the target is inserted into the DOM (because it
|
||||
// is not when the portal is rendered)
|
||||
doTargetLookUp: boolean = true;
|
||||
// set of encountered events that need to be redirected
|
||||
_handledEvents: Set<string> = new Set();
|
||||
// function that will be the event's tunnel (needs to be an arrow function to
|
||||
// avoid having to rebind `this`)
|
||||
_handlerTunnel: (f: OwlEvent<any>) => void = (ev: OwlEvent<any>) => {
|
||||
ev.stopPropagation();
|
||||
this.__trigger(ev.originalComponent, ev.type, ev.detail);
|
||||
};
|
||||
// Storing the parent's env
|
||||
parentEnv: any = null;
|
||||
// represents the element that is moved somewhere else
|
||||
portal: VNode | null = null;
|
||||
// the target where we will move `portal`
|
||||
target: Element | null = null;
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.parentEnv = parent ? parent.env : {};
|
||||
// put a callback in the env that is propagated to children s.t. portal can
|
||||
// register an handler to those events just before children will trigger them
|
||||
useSubEnv({
|
||||
[portalSymbol]: (ev) => {
|
||||
if (!this._handledEvents.has(ev.type)) {
|
||||
this.portal!.elm!.addEventListener(ev.type, this._handlerTunnel);
|
||||
this._handledEvents.add(ev.type);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Override to revert back to a classic Component's structure
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
__callWillUnmount() {
|
||||
super.__callWillUnmount();
|
||||
this.el!.appendChild(this.portal!.elm!);
|
||||
this.doTargetLookUp = true;
|
||||
}
|
||||
/**
|
||||
* At each DOM change, we must ensure that the portal contains exactly one
|
||||
* child
|
||||
*/
|
||||
__checkVNodeStructure(vnode: VNode) {
|
||||
const children = vnode.children!;
|
||||
let countRealNodes = 0;
|
||||
for (let child of children) {
|
||||
if ((child as VNode).sel) {
|
||||
countRealNodes++;
|
||||
}
|
||||
}
|
||||
if (countRealNodes !== 1) {
|
||||
throw new Error(`Portal must have exactly one non-text child (has ${countRealNodes})`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Ensure the target is still there at whichever time we render
|
||||
*/
|
||||
__checkTargetPresence() {
|
||||
if (!this.target || !document.contains(this.target)) {
|
||||
throw new Error(`Could not find any match for "${this.props.target}"`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Move the portal's element to the target
|
||||
*/
|
||||
__deployPortal() {
|
||||
this.__checkTargetPresence();
|
||||
this.target!.appendChild(this.portal!.elm!);
|
||||
}
|
||||
/**
|
||||
* Override to remove from the DOM the element we have teleported
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
__destroy(parent) {
|
||||
if (this.portal && this.portal.elm) {
|
||||
const displacedElm = this.portal.elm!;
|
||||
const parent = displacedElm.parentNode;
|
||||
if (parent) {
|
||||
parent.removeChild(displacedElm);
|
||||
}
|
||||
}
|
||||
super.__destroy(parent);
|
||||
}
|
||||
/**
|
||||
* Override to patch the element that has been teleported
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
__patch(target, vnode) {
|
||||
if (this.doTargetLookUp) {
|
||||
const target = document.querySelector(this.props.target);
|
||||
if (!target) {
|
||||
this.env.qweb.on("dom-appended", this, () => {
|
||||
this.doTargetLookUp = false;
|
||||
this.env.qweb.off("dom-appended", this);
|
||||
this.target = document.querySelector(this.props.target);
|
||||
this.__deployPortal();
|
||||
});
|
||||
} else {
|
||||
this.doTargetLookUp = false;
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
this.__checkVNodeStructure(vnode);
|
||||
const shouldDeploy =
|
||||
(!this.portal || this.el!.contains(this.portal.elm!)) && !this.doTargetLookUp;
|
||||
|
||||
if (!this.doTargetLookUp && !shouldDeploy) {
|
||||
// Only on pure patching, provided the
|
||||
// this.target's parent has not been unmounted
|
||||
this.__checkTargetPresence();
|
||||
}
|
||||
|
||||
const portalPatch = this.portal ? this.portal : document.createElement(vnode.children[0].sel);
|
||||
this.portal = patch(portalPatch, vnode.children![0] as VNode);
|
||||
vnode.children = [];
|
||||
|
||||
super.__patch(target, vnode);
|
||||
|
||||
if (shouldDeploy) {
|
||||
this.__deployPortal();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Override to set the env
|
||||
*/
|
||||
__trigger(component: Component, eventType: string, payload?: any) {
|
||||
const env = this.env;
|
||||
this.env = this.parentEnv;
|
||||
super.__trigger(component, eventType, payload);
|
||||
this.env = env;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import { CompilationContext, INTERP_REGEXP } from "./compilation_context";
|
||||
import { QWeb } from "./qweb";
|
||||
import { htmlToVDOM } from "../vdom/html_to_vdom";
|
||||
import { QWebVar } from "./expression_parser";
|
||||
|
||||
/**
|
||||
* Owl QWeb Directives
|
||||
*
|
||||
* This file contains the implementation of most standard QWeb directives:
|
||||
* - t-esc
|
||||
* - t-raw
|
||||
* - t-set/t-value
|
||||
* - t-if/t-elif/t-else
|
||||
* - t-call
|
||||
* - t-foreach/t-as
|
||||
* - t-debug
|
||||
* - t-log
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-esc and t-raw
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.htmlToVDOM = htmlToVDOM;
|
||||
|
||||
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
|
||||
ctx.rootContext.shouldDefineScope = true;
|
||||
if (value === "0") {
|
||||
if (ctx.parentNode) {
|
||||
// the 'zero' magical symbol is where we can find the result of the rendering
|
||||
// of the body of the t-call.
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
const zeroArgs = ctx.escaping
|
||||
? `{text: utils.vDomToString(scope[utils.zero])}`
|
||||
: `...scope[utils.zero]`;
|
||||
ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let exprID: string;
|
||||
if (typeof value === "string") {
|
||||
exprID = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`);
|
||||
} else {
|
||||
exprID = `scope.${value.id}`;
|
||||
}
|
||||
ctx.addIf(`${exprID} != null`);
|
||||
|
||||
if (ctx.escaping) {
|
||||
let protectID;
|
||||
if (value.hasBody) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
protectID = ctx.startProtectScope();
|
||||
ctx.addLine(
|
||||
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
|
||||
);
|
||||
}
|
||||
if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
|
||||
} else if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
|
||||
} else {
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
|
||||
if (ctx.rootContext.shouldDefineResult) {
|
||||
ctx.addLine(`result = vn${nodeID}`);
|
||||
}
|
||||
}
|
||||
if (value.hasBody) {
|
||||
ctx.stopProtectScope(protectID);
|
||||
}
|
||||
} else {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
if (value.hasBody) {
|
||||
ctx.addLine(
|
||||
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
|
||||
);
|
||||
ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
|
||||
} else {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
|
||||
}
|
||||
}
|
||||
if (node.childNodes.length) {
|
||||
ctx.addElse();
|
||||
qweb._compileChildren(node, ctx);
|
||||
}
|
||||
|
||||
ctx.closeIf();
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "esc",
|
||||
priority: 70,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
||||
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "raw",
|
||||
priority: 80,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
||||
compileValueNode(value, node, qweb, ctx);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-set
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "set",
|
||||
extraNames: ["value"],
|
||||
priority: 60,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
ctx.rootContext.shouldDefineScope = true;
|
||||
const variable = node.getAttribute("t-set")!;
|
||||
let value = node.getAttribute("t-value")!;
|
||||
ctx.variables[variable] = ctx.variables[variable] || ({} as QWebVar);
|
||||
let qwebvar = ctx.variables[variable];
|
||||
const hasBody = node.hasChildNodes();
|
||||
|
||||
qwebvar.id = variable;
|
||||
qwebvar.expr = `scope.${variable}`;
|
||||
if (value) {
|
||||
const formattedValue = ctx.formatExpression(value);
|
||||
let scopeExpr = `scope`;
|
||||
if (ctx.protectedScopeNumber) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
scopeExpr = `utils.getScope(scope, '${variable}')`;
|
||||
}
|
||||
ctx.addLine(`${scopeExpr}.${variable} = ${formattedValue};`);
|
||||
qwebvar.value = formattedValue;
|
||||
}
|
||||
|
||||
if (hasBody) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
if (value) {
|
||||
ctx.addIf(`!(${qwebvar.expr})`);
|
||||
}
|
||||
const tempParentNodeID = ctx.generateID();
|
||||
const _parentNode = ctx.parentNode;
|
||||
ctx.parentNode = tempParentNodeID;
|
||||
|
||||
ctx.addLine(`let c${tempParentNodeID} = new utils.VDomArray();`);
|
||||
const nodeCopy = node.cloneNode(true) as Element;
|
||||
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
|
||||
nodeCopy.removeAttribute(attr);
|
||||
}
|
||||
qweb._compileNode(nodeCopy, ctx);
|
||||
|
||||
ctx.addLine(`${qwebvar.expr} = c${tempParentNodeID}`);
|
||||
qwebvar.value = `c${tempParentNodeID}`;
|
||||
qwebvar.hasBody = true;
|
||||
|
||||
ctx.parentNode = _parentNode;
|
||||
if (value) {
|
||||
ctx.closeIf();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-if, t-elif, t-else
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "if",
|
||||
priority: 20,
|
||||
atNodeEncounter({ node, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-if")!);
|
||||
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id!}`);
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
},
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "elif",
|
||||
priority: 30,
|
||||
atNodeEncounter({ node, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-elif")!);
|
||||
ctx.addLine(
|
||||
`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`
|
||||
);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
},
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "else",
|
||||
priority: 40,
|
||||
atNodeEncounter({ ctx }): boolean {
|
||||
ctx.addLine(`else {`);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-call
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "call",
|
||||
priority: 50,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
// Step 1: sanity checks
|
||||
// ------------------------------------------------
|
||||
ctx.rootContext.shouldDefineScope = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
const subTemplate = node.getAttribute("t-call")!;
|
||||
const isDynamic = INTERP_REGEXP.test(subTemplate);
|
||||
const nodeTemplate = qweb.templates[subTemplate];
|
||||
if (!isDynamic && !nodeTemplate) {
|
||||
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
|
||||
}
|
||||
|
||||
// Step 2: compile target template in sub templates
|
||||
// ------------------------------------------------
|
||||
let subIdstr: string;
|
||||
if (isDynamic) {
|
||||
const _id = ctx.generateID();
|
||||
ctx.addLine(`let tname${_id} = ${ctx.interpolate(subTemplate)};`);
|
||||
ctx.addLine(`let tid${_id} = this.subTemplates[tname${_id}];`);
|
||||
ctx.addIf(`!tid${_id}`);
|
||||
ctx.addLine(`tid${_id} = this.constructor.nextId++;`);
|
||||
ctx.addLine(`this.subTemplates[tname${_id}] = tid${_id};`);
|
||||
ctx.addLine(
|
||||
`this.constructor.subTemplates[tid${_id}] = this._compile(tname${_id}, {hasParent: true, defineKey: true});`
|
||||
);
|
||||
ctx.closeIf();
|
||||
subIdstr = `tid${_id}`;
|
||||
} else {
|
||||
let subId = qweb.subTemplates[subTemplate];
|
||||
if (!subId) {
|
||||
subId = QWeb.nextId++;
|
||||
qweb.subTemplates[subTemplate] = subId;
|
||||
const subTemplateFn = qweb._compile(subTemplate, { hasParent: true, defineKey: true });
|
||||
QWeb.subTemplates[subId] = subTemplateFn;
|
||||
}
|
||||
subIdstr = `'${subId}'`;
|
||||
}
|
||||
|
||||
// Step 3: compile t-call body if necessary
|
||||
// ------------------------------------------------
|
||||
let hasBody = node.hasChildNodes();
|
||||
const protectID = ctx.startProtectScope();
|
||||
if (hasBody) {
|
||||
// we add a sub scope to protect the ambient scope
|
||||
ctx.addLine(`{`);
|
||||
ctx.indent();
|
||||
const nodeCopy = node.cloneNode(true) as Element;
|
||||
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
|
||||
nodeCopy.removeAttribute(attr);
|
||||
}
|
||||
// this local scope is intended to trap c__0
|
||||
ctx.addLine(`{`);
|
||||
ctx.indent();
|
||||
ctx.addLine("let c__0 = [];");
|
||||
qweb._compileNode(nodeCopy, ctx.subContext("parentNode", "__0"));
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
ctx.addLine("scope[utils.zero] = c__0;");
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
}
|
||||
|
||||
// Step 4: add the appropriate function call to current component
|
||||
// ------------------------------------------------
|
||||
const parentComponent = ctx.rootContext.shouldDefineParent
|
||||
? `parent`
|
||||
: `utils.getComponent(context)`;
|
||||
const key = ctx.generateTemplateKey();
|
||||
const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result";
|
||||
const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`;
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`);
|
||||
} else {
|
||||
// this is a t-call with no parentnode, we need to extract the result
|
||||
ctx.rootContext.shouldDefineResult = true;
|
||||
ctx.addLine(`result = []`);
|
||||
ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`);
|
||||
ctx.addLine(`result = result[0]`);
|
||||
}
|
||||
|
||||
// Step 5: restore previous scope
|
||||
// ------------------------------------------------
|
||||
if (hasBody) {
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
}
|
||||
ctx.stopProtectScope(protectID);
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-foreach
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "foreach",
|
||||
extraNames: ["as"],
|
||||
priority: 10,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
ctx.rootContext.shouldDefineScope = true;
|
||||
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
|
||||
const elems = node.getAttribute("t-foreach")!;
|
||||
const name = node.getAttribute("t-as")!;
|
||||
let arrayID = ctx.generateID();
|
||||
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
|
||||
let keysID = ctx.generateID();
|
||||
let valuesID = ctx.generateID();
|
||||
ctx.addLine(`let _${keysID} = _${arrayID};`);
|
||||
ctx.addLine(`let _${valuesID} = _${arrayID};`);
|
||||
ctx.addIf(`!(_${arrayID} instanceof Array)`);
|
||||
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
|
||||
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
|
||||
ctx.closeIf();
|
||||
ctx.addLine(`let _length${keysID} = _${keysID}.length;`);
|
||||
let varsID = ctx.startProtectScope(true);
|
||||
const loopVar = `i${ctx.loopNumber}`;
|
||||
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
|
||||
ctx.indent();
|
||||
|
||||
ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
|
||||
ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
|
||||
ctx.addLine(`scope.${name}_index = ${loopVar}`);
|
||||
ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
|
||||
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
let shouldWarn =
|
||||
!nodeCopy.hasAttribute("t-key") &&
|
||||
node.children.length === 1 &&
|
||||
node.children[0].tagName !== "t" &&
|
||||
!node.children[0].hasAttribute("t-key");
|
||||
if (shouldWarn) {
|
||||
console.warn(
|
||||
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
|
||||
);
|
||||
}
|
||||
if (nodeCopy.hasAttribute("t-key")) {
|
||||
const expr = ctx.formatExpression(nodeCopy.getAttribute("t-key")!);
|
||||
ctx.addLine(`let key${ctx.loopNumber} = ${expr};`);
|
||||
nodeCopy.removeAttribute("t-key");
|
||||
} else {
|
||||
ctx.addLine(`let key${ctx.loopNumber} = i${ctx.loopNumber};`);
|
||||
}
|
||||
|
||||
nodeCopy.removeAttribute("t-foreach");
|
||||
qweb._compileNode(nodeCopy, ctx);
|
||||
ctx.dedent();
|
||||
ctx.addLine("}");
|
||||
ctx.stopProtectScope(varsID);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-debug
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "debug",
|
||||
priority: 1,
|
||||
atNodeEncounter({ ctx }) {
|
||||
ctx.addLine("debugger;");
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-log
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "log",
|
||||
priority: 1,
|
||||
atNodeEncounter({ ctx, value }) {
|
||||
const expr = ctx.formatExpression(value);
|
||||
ctx.addLine(`console.log(${expr})`);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
import { compileExpr, compileExprToArray, QWebVar } from "./expression_parser";
|
||||
|
||||
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class CompilationContext {
|
||||
static nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: QWebVar } = {};
|
||||
escaping: boolean = false;
|
||||
parentNode: number | null | string = null;
|
||||
parentTextNode: number | null = null;
|
||||
rootNode: number | null = null;
|
||||
indentLevel: number = 0;
|
||||
rootContext: CompilationContext;
|
||||
shouldDefineParent: boolean = false;
|
||||
shouldDefineScope: boolean = false;
|
||||
protectedScopeNumber: number = 0;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldDefineRefs: boolean = false;
|
||||
shouldDefineResult: boolean = true;
|
||||
loopNumber: number = 0;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
allowMultipleRoots: boolean = false;
|
||||
hasParentWidget: boolean = false;
|
||||
hasKey0: boolean = false;
|
||||
keyStack: boolean[] = [];
|
||||
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.templateName = name || "noname";
|
||||
this.addLine("let h = this.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
return CompilationContext.nextID++;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method generates a "template key", which is basically a unique key
|
||||
* which depends on the currently set keys, and on the iteration numbers (if
|
||||
* we are in a loop).
|
||||
*
|
||||
* Such a key is necessary when we need to associate an id to some element
|
||||
* generated by a template (for example, a component)
|
||||
*/
|
||||
generateTemplateKey(prefix: string = ""): string {
|
||||
const id = this.generateID();
|
||||
if (this.loopNumber === 0 && !this.hasKey0) {
|
||||
return `'${prefix}__${id}__'`;
|
||||
}
|
||||
let key = `\`${prefix}__${id}__`;
|
||||
let start = this.hasKey0 ? 0 : 1;
|
||||
for (let i = start; i < this.loopNumber + 1; i++) {
|
||||
key += `\${key${i}}__`;
|
||||
}
|
||||
this.addLine(`let k${id} = ${key}\`;`);
|
||||
return `k${id}`;
|
||||
}
|
||||
|
||||
generateCode(): string[] {
|
||||
if (this.shouldDefineResult) {
|
||||
this.code.unshift(" let result;");
|
||||
}
|
||||
|
||||
if (this.shouldDefineScope) {
|
||||
this.code.unshift(" let scope = Object.create(context);");
|
||||
}
|
||||
if (this.shouldDefineRefs) {
|
||||
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
|
||||
}
|
||||
if (this.shouldDefineParent) {
|
||||
if (this.hasParentWidget) {
|
||||
this.code.unshift(" let parent = extra.parent;");
|
||||
} else {
|
||||
this.code.unshift(" let parent = context;");
|
||||
}
|
||||
}
|
||||
if (this.shouldDefineQWeb) {
|
||||
this.code.unshift(" let QWeb = this.constructor;");
|
||||
}
|
||||
if (this.shouldDefineUtils) {
|
||||
this.code.unshift(" let utils = this.constructor.utils;");
|
||||
}
|
||||
return this.code;
|
||||
}
|
||||
|
||||
withParent(node: number): CompilationContext {
|
||||
if (
|
||||
!this.allowMultipleRoots &&
|
||||
this === this.rootContext &&
|
||||
(this.parentNode || this.parentTextNode)
|
||||
) {
|
||||
throw new Error("A template should not have more than one root node");
|
||||
}
|
||||
if (!this.rootContext.rootNode) {
|
||||
this.rootContext.rootNode = node;
|
||||
}
|
||||
if (!this.parentNode && this.rootContext.shouldDefineResult) {
|
||||
this.addLine(`result = vn${node};`);
|
||||
}
|
||||
return this.subContext("parentNode", node);
|
||||
}
|
||||
|
||||
subContext(key: keyof CompilationContext, value: any): CompilationContext {
|
||||
const newContext = Object.create(this);
|
||||
newContext[key] = value;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
indent() {
|
||||
this.rootContext.indentLevel++;
|
||||
}
|
||||
|
||||
dedent() {
|
||||
this.rootContext.indentLevel--;
|
||||
}
|
||||
|
||||
addLine(line: string): number {
|
||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||
this.code.push(prefix + line);
|
||||
return this.code.length - 1;
|
||||
}
|
||||
|
||||
addIf(condition: string) {
|
||||
this.addLine(`if (${condition}) {`);
|
||||
this.indent();
|
||||
}
|
||||
|
||||
addElse() {
|
||||
this.dedent();
|
||||
this.addLine("} else {");
|
||||
this.indent();
|
||||
}
|
||||
|
||||
closeIf() {
|
||||
this.dedent();
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
getValue(val: any): QWebVar | string {
|
||||
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an expression for being consumed at render time. Its main job
|
||||
* is to
|
||||
* - replace unknown variables by a lookup in the context
|
||||
* - replace already defined variables by their internal name
|
||||
*/
|
||||
formatExpression(expr: string): string {
|
||||
this.rootContext.shouldDefineScope = true;
|
||||
return compileExpr(expr, this.variables);
|
||||
}
|
||||
captureExpression(expr: string): string {
|
||||
this.rootContext.shouldDefineScope = true;
|
||||
const argId = this.generateID();
|
||||
const tokens = compileExprToArray(expr, this.variables);
|
||||
const done = new Set();
|
||||
return tokens
|
||||
.map((tok, i) => {
|
||||
// "this" in captured expressions should be the current component
|
||||
if (tok.value === "this") {
|
||||
if (!done.has("this")) {
|
||||
done.add("this");
|
||||
this.addLine(`const this_${argId} = utils.getComponent(context);`);
|
||||
}
|
||||
tok.value = `this_${argId}`;
|
||||
}
|
||||
// Variables that should be looked up in the scope. isLocal is for arrow
|
||||
// function arguments that should stay untouched (eg "ev => ev" should
|
||||
// not become "const ev_1 = scope['ev']; ev_1 => ev_1")
|
||||
if (
|
||||
tok.varName &&
|
||||
!tok.isLocal &&
|
||||
// HACK: for backwards compatibility, we don't capture bare methods
|
||||
// this allows them to be called with the rendering context/scope
|
||||
// as their this value.
|
||||
(!tokens[i + 1] || tokens[i + 1].type !== "LEFT_PAREN")
|
||||
) {
|
||||
if (!done.has(tok.varName)) {
|
||||
done.add(tok.varName);
|
||||
this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
|
||||
}
|
||||
tok.value = `${tok.varName}_${argId}`;
|
||||
}
|
||||
return tok.value;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform string interpolation on the given string. Note that if the whole
|
||||
* string is an expression, it simply returns it (formatted and enclosed in
|
||||
* parentheses).
|
||||
* For instance:
|
||||
* 'Hello {{x}}!' -> `Hello ${x}`
|
||||
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
||||
*/
|
||||
interpolate(s: string): string {
|
||||
let matches = s.match(INTERP_REGEXP);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${this.formatExpression(s.slice(2, -2))})`;
|
||||
}
|
||||
|
||||
let r = s.replace(/\{\{.*?\}\}/g, (s) => "${" + this.formatExpression(s.slice(2, -2)) + "}");
|
||||
return "`" + r + "`";
|
||||
}
|
||||
startProtectScope(codeBlock?: boolean): number {
|
||||
const protectID = this.generateID();
|
||||
this.rootContext.protectedScopeNumber++;
|
||||
this.rootContext.shouldDefineScope = true;
|
||||
const scopeExpr = `Object.create(scope);`;
|
||||
this.addLine(`let _origScope${protectID} = scope;`);
|
||||
this.addLine(`scope = ${scopeExpr}`);
|
||||
if (!codeBlock) {
|
||||
this.addLine(`scope.__access_mode__ = 'ro';`);
|
||||
}
|
||||
return protectID;
|
||||
}
|
||||
stopProtectScope(protectID: number) {
|
||||
this.rootContext.protectedScopeNumber--;
|
||||
this.addLine(`scope = _origScope${protectID};`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Owl QWeb Expression Parser
|
||||
*
|
||||
* Owl needs in various contexts to be able to understand the structure of a
|
||||
* string representing a javascript expression. The usual goal is to be able
|
||||
* to rewrite some variables. For example, if a template has
|
||||
*
|
||||
* ```xml
|
||||
* <t t-if="computeSomething({val: state.val})">...</t>
|
||||
* ```
|
||||
*
|
||||
* this needs to be translated in something like this:
|
||||
*
|
||||
* ```js
|
||||
* if (context["computeSomething"]({val: context["state"].val})) { ... }
|
||||
* ```
|
||||
*
|
||||
* This file contains the implementation of an extremely naive tokenizer/parser
|
||||
* and evaluator for javascript expressions. The supported grammar is basically
|
||||
* only expressive enough to understand the shape of objects, of arrays, and
|
||||
* various operators.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Misc types, constants and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
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(
|
||||
","
|
||||
);
|
||||
|
||||
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
gt: ">",
|
||||
gte: ">=",
|
||||
lt: "<",
|
||||
lte: "<=",
|
||||
});
|
||||
|
||||
export interface QWebVar {
|
||||
id: string; // foo
|
||||
expr: string; // scope.foo (local variables => only foo)
|
||||
value?: string; // 1 + 3
|
||||
hasBody?: boolean;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tokenizer
|
||||
//------------------------------------------------------------------------------
|
||||
type TKind =
|
||||
| "LEFT_BRACE"
|
||||
| "RIGHT_BRACE"
|
||||
| "LEFT_BRACKET"
|
||||
| "RIGHT_BRACKET"
|
||||
| "LEFT_PAREN"
|
||||
| "RIGHT_PAREN"
|
||||
| "COMMA"
|
||||
| "VALUE"
|
||||
| "TEMPLATE_STRING"
|
||||
| "SYMBOL"
|
||||
| "OPERATOR"
|
||||
| "COLON";
|
||||
|
||||
interface Token {
|
||||
type: TKind;
|
||||
value: string;
|
||||
originalValue?: string;
|
||||
size?: number;
|
||||
varName?: string;
|
||||
replace?: Function;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
|
||||
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
|
||||
"{": "LEFT_BRACE",
|
||||
"}": "RIGHT_BRACE",
|
||||
"[": "LEFT_BRACKET",
|
||||
"]": "RIGHT_BRACKET",
|
||||
":": "COLON",
|
||||
",": "COMMA",
|
||||
"(": "LEFT_PAREN",
|
||||
")": "RIGHT_PAREN",
|
||||
});
|
||||
|
||||
// note that the space after typeof is relevant. It makes sure that the formatted
|
||||
// expression has a space after typeof
|
||||
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
|
||||
|
||||
type Tokenizer = (expr: string) => Token | false;
|
||||
|
||||
let tokenizeString: Tokenizer = function (expr) {
|
||||
let s = expr[0];
|
||||
let start = s;
|
||||
if (s !== "'" && s !== '"' && s !== "`") {
|
||||
return false;
|
||||
}
|
||||
let i = 1;
|
||||
let cur;
|
||||
while (expr[i] && expr[i] !== start) {
|
||||
cur = expr[i];
|
||||
s += cur;
|
||||
if (cur === "\\") {
|
||||
i++;
|
||||
cur = expr[i];
|
||||
if (!cur) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += cur;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (expr[i] !== start) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += start;
|
||||
if (start === "`") {
|
||||
return {
|
||||
type: "TEMPLATE_STRING",
|
||||
value: s,
|
||||
replace(replacer) {
|
||||
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
|
||||
return "${" + replacer(group) + "}";
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
return { type: "VALUE", value: s };
|
||||
};
|
||||
|
||||
let tokenizeNumber: Tokenizer = function (expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[0-9]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/[0-9]|\./)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
return { type: "VALUE", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let tokenizeSymbol: Tokenizer = function (expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[a-zA-Z_\$]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/\w/)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
if (s in WORD_REPLACEMENT) {
|
||||
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
|
||||
}
|
||||
return { type: "SYMBOL", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const tokenizeStatic: Tokenizer = function (expr) {
|
||||
const char = expr[0];
|
||||
if (char && char in STATIC_TOKEN_MAP) {
|
||||
return { type: STATIC_TOKEN_MAP[char], value: char };
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const tokenizeOperator: Tokenizer = function (expr) {
|
||||
for (let op of OPERATORS) {
|
||||
if (expr.startsWith(op)) {
|
||||
return { type: "OPERATOR", value: op };
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const TOKENIZERS = [
|
||||
tokenizeString,
|
||||
tokenizeNumber,
|
||||
tokenizeOperator,
|
||||
tokenizeSymbol,
|
||||
tokenizeStatic,
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert a javascript expression (as a string) into a list of tokens. For
|
||||
* example: `tokenize("1 + b")` will return:
|
||||
* ```js
|
||||
* [
|
||||
* {type: "VALUE", value: "1"},
|
||||
* {type: "OPERATOR", value: "+"},
|
||||
* {type: "SYMBOL", value: "b"}
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export function tokenize(expr: string): Token[] {
|
||||
const result: Token[] = [];
|
||||
let token: boolean | Token = true;
|
||||
|
||||
while (token) {
|
||||
expr = expr.trim();
|
||||
if (expr) {
|
||||
for (let tokenizer of TOKENIZERS) {
|
||||
token = tokenizer(expr);
|
||||
if (token) {
|
||||
result.push(token);
|
||||
expr = expr.slice(token.size || token.value.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
token = false;
|
||||
}
|
||||
}
|
||||
if (expr.length) {
|
||||
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Expression "evaluator"
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
|
||||
const isRightSeparator = (token) =>
|
||||
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
|
||||
|
||||
/**
|
||||
* This is the main function exported by this file. This is the code that will
|
||||
* process an expression (given as a string) and returns another expression with
|
||||
* proper lookups in the context.
|
||||
*
|
||||
* Usually, this kind of code would be very simple to do if we had an AST (so,
|
||||
* if we had a javascript parser), since then, we would only need to find the
|
||||
* variables and replace them. However, a parser is more complicated, and there
|
||||
* are no standard builtin parser API.
|
||||
*
|
||||
* Since this method is applied to simple javasript expressions, and the work to
|
||||
* be done is actually quite simple, we actually can get away with not using a
|
||||
* parser, which helps with the code size.
|
||||
*
|
||||
* Here is the heuristic used by this method to determine if a token is a
|
||||
* variable:
|
||||
* - by default, all symbols are considered a variable
|
||||
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
|
||||
* - or if the previous token is a left brace or a comma, and the next token is
|
||||
* a colon (in that case, this is an object key: `{a: b}`)
|
||||
*
|
||||
* Some specific code is also required to support arrow functions. If we detect
|
||||
* the arrow operator, then we add the current (or some previous tokens) token to
|
||||
* the list of variables so it does not get replaced by a lookup in the context
|
||||
*/
|
||||
export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
|
||||
const localVars = new Set<string>();
|
||||
scope = Object.create(scope);
|
||||
const tokens = tokenize(expr);
|
||||
|
||||
let i = 0;
|
||||
let stack = []; // to track last opening [ or {
|
||||
|
||||
while (i < tokens.length) {
|
||||
let token = tokens[i];
|
||||
let prevToken = tokens[i - 1];
|
||||
let nextToken = tokens[i + 1];
|
||||
let groupType = stack[stack.length - 1];
|
||||
|
||||
switch (token.type) {
|
||||
case "LEFT_BRACE":
|
||||
case "LEFT_BRACKET":
|
||||
stack.push(token.type);
|
||||
break;
|
||||
case "RIGHT_BRACE":
|
||||
case "RIGHT_BRACKET":
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
||||
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
|
||||
if (prevToken) {
|
||||
// normalize missing tokens: {a} should be equivalent to {a:a}
|
||||
if (
|
||||
groupType === "LEFT_BRACE" &&
|
||||
isLeftSeparator(prevToken) &&
|
||||
isRightSeparator(nextToken)
|
||||
) {
|
||||
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
|
||||
nextToken = tokens[i + 1];
|
||||
}
|
||||
|
||||
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
|
||||
isVar = false;
|
||||
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
|
||||
if (nextToken && nextToken.type === "COLON") {
|
||||
isVar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (token.type === "TEMPLATE_STRING") {
|
||||
token.value = token.replace((expr) => compileExpr(expr, scope));
|
||||
}
|
||||
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
|
||||
if (token.type === "RIGHT_PAREN") {
|
||||
let j = i - 1;
|
||||
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
|
||||
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
|
||||
tokens[j].value = tokens[j].originalValue!;
|
||||
scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value };
|
||||
localVars.add(tokens[j].value);
|
||||
}
|
||||
j--;
|
||||
}
|
||||
} else {
|
||||
scope[token.value] = { id: token.value, expr: token.value };
|
||||
localVars.add(token.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (isVar) {
|
||||
token.varName = token.value;
|
||||
if (token.value in scope && "id" in scope[token.value]) {
|
||||
token.value = scope[token.value].expr!;
|
||||
} else {
|
||||
token.originalValue = token.value;
|
||||
token.value = `scope['${token.value}']`;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
// Mark all variables that have been used locally.
|
||||
// This assumes the expression has only one scope (incorrect but "good enough for now")
|
||||
for (const token of tokens) {
|
||||
if (token.type === "SYMBOL" && localVars.has(token.value)) {
|
||||
token.isLocal = true;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
|
||||
return compileExprToArray(expr, scope)
|
||||
.map((t) => t.value)
|
||||
.join("");
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
import { STATUS } from "../component/component";
|
||||
import { VNode } from "../vdom/index";
|
||||
import { INTERP_REGEXP } from "./compilation_context";
|
||||
import { QWeb } from "./qweb";
|
||||
import { browser } from "../browser";
|
||||
|
||||
/**
|
||||
* Owl QWeb Extensions
|
||||
*
|
||||
* This file contains the implementation of non standard QWeb directives, added
|
||||
* by Owl and that will only work on Owl projects:
|
||||
*
|
||||
* - t-on
|
||||
* - t-ref
|
||||
* - t-transition
|
||||
* - t-mounted
|
||||
* - t-slot
|
||||
* - t-model
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-on
|
||||
//------------------------------------------------------------------------------
|
||||
// these are pieces of code that will be injected into the event handler if
|
||||
// modifiers are specified
|
||||
export const MODS_CODE = {
|
||||
prevent: "e.preventDefault();",
|
||||
self: "if (e.target !== this.elm) {return}",
|
||||
stop: "e.stopPropagation();",
|
||||
};
|
||||
|
||||
interface HandlerInfo {
|
||||
event: string;
|
||||
handler: string;
|
||||
}
|
||||
|
||||
const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i;
|
||||
|
||||
export function makeHandlerCode(
|
||||
ctx,
|
||||
fullName,
|
||||
value,
|
||||
putInCache: boolean,
|
||||
modcodes = MODS_CODE
|
||||
): HandlerInfo {
|
||||
let [event, ...mods] = fullName.slice(5).split(".");
|
||||
if (mods.includes("capture")) {
|
||||
event = "!" + event;
|
||||
}
|
||||
if (!event) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let code: string;
|
||||
// check if it is a method with no args, a method with args or an expression
|
||||
let args: string = "";
|
||||
const name: string = value.replace(/\(.*\)/, function (_args) {
|
||||
args = _args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
const isMethodCall = name.match(FNAMEREGEXP);
|
||||
|
||||
// then generate code
|
||||
if (isMethodCall) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
const comp = `utils.getComponent(context)`;
|
||||
if (args) {
|
||||
const argId = ctx.generateID();
|
||||
ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`);
|
||||
code = `${comp}['${name}'](...args${argId}, e);`;
|
||||
putInCache = false;
|
||||
} else {
|
||||
code = `${comp}['${name}'](e);`;
|
||||
}
|
||||
} else {
|
||||
// if we get here, then it is an expression
|
||||
// we need to capture every variable in it
|
||||
putInCache = false;
|
||||
code = ctx.captureExpression(value);
|
||||
code = `const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }`;
|
||||
}
|
||||
const modCode = mods.map((mod) => modcodes[mod]).join("");
|
||||
let handler = `function (e) {if (context.__owl__.status === ${STATUS.DESTROYED}){return}${modCode}${code}}`;
|
||||
if (putInCache) {
|
||||
const key = ctx.generateTemplateKey(event);
|
||||
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
|
||||
handler = `extra.handlers[${key}]`;
|
||||
}
|
||||
return { event, handler };
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "on",
|
||||
priority: 90,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||
const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
|
||||
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-ref
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineRefs = true;
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
||||
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-transition
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.nextFrame = function (cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||
};
|
||||
|
||||
QWeb.utils.transitionInsert = function (vn: VNode, name: string) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
// remove potential duplicated vnode that is currently being removed, to
|
||||
// prevent from having twice the same node in the DOM during an animation
|
||||
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
||||
if (dup) {
|
||||
dup.remove();
|
||||
}
|
||||
|
||||
elm.classList.add(name + "-enter");
|
||||
elm.classList.add(name + "-enter-active");
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-enter-active");
|
||||
elm.classList.remove(name + "-enter-to");
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-enter");
|
||||
elm.classList.add(name + "-enter-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
QWeb.utils.transitionRemove = function (vn: VNode, name: string, rm: () => void) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
elm.setAttribute("data-owl-key", vn.key!);
|
||||
|
||||
elm.classList.add(name + "-leave");
|
||||
elm.classList.add(name + "-leave-active");
|
||||
const finalize = () => {
|
||||
if (!elm.classList.contains(name + "-leave-active")) {
|
||||
return;
|
||||
}
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
rm();
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-leave");
|
||||
elm.classList.add(name + "-leave-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
||||
/* istanbul ignore next */
|
||||
while (delays.length < durations.length) {
|
||||
delays = delays.concat(delays);
|
||||
}
|
||||
|
||||
return Math.max.apply(
|
||||
null,
|
||||
durations.map((d, i) => {
|
||||
return toMs(d) + toMs(delays[i]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
||||
// in a locale-dependent way, using a comma instead of a dot.
|
||||
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
||||
// as a floor function) causing unexpected behaviors
|
||||
function toMs(s: string): number {
|
||||
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
||||
}
|
||||
|
||||
function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||
if (!elm.parentNode) {
|
||||
// if we get here, this means that the element was removed for some other
|
||||
// reasons, and in that case, we don't want to work on animation since nothing
|
||||
// will be displayed anyway.
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = window.getComputedStyle(elm);
|
||||
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
||||
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
|
||||
const timeout: number = getTimeout(delays, durations);
|
||||
if (timeout > 0) {
|
||||
const transitionEndCB = () => {
|
||||
if (!elm.parentNode) return;
|
||||
cb();
|
||||
browser.clearTimeout(fallbackTimeout);
|
||||
elm.removeEventListener("transitionend", transitionEndCB);
|
||||
};
|
||||
elm.addEventListener("transitionend", transitionEndCB, { once: true });
|
||||
const fallbackTimeout = browser.setTimeout(transitionEndCB, timeout + 1);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "transition",
|
||||
priority: 96,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
if (!QWeb.enableTransitions) {
|
||||
return;
|
||||
}
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let name = value;
|
||||
const hooks = {
|
||||
insert: `utils.transitionInsert(vn, '${name}');`,
|
||||
remove: `utils.transitionRemove(vn, '${name}', rm);`,
|
||||
};
|
||||
for (let hookName in hooks) {
|
||||
addNodeHook(hookName, hooks[hookName]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-slot
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "slot",
|
||||
priority: 80,
|
||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||
const slotKey = ctx.generateID();
|
||||
const valueExpr = value.match(INTERP_REGEXP) ? ctx.interpolate(value) : `'${value}'`;
|
||||
ctx.addLine(
|
||||
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + ${valueExpr}];`
|
||||
);
|
||||
ctx.addIf(`slot${slotKey}`);
|
||||
let parentNode = `c${ctx.parentNode}`;
|
||||
if (!ctx.parentNode) {
|
||||
ctx.rootContext.shouldDefineResult = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
parentNode = `children${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${parentNode}= []`);
|
||||
ctx.addLine(`result = {}`);
|
||||
}
|
||||
ctx.addLine(
|
||||
`slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`
|
||||
);
|
||||
if (!ctx.parentNode) {
|
||||
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
|
||||
}
|
||||
if (node.hasChildNodes()) {
|
||||
ctx.addElse();
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
nodeCopy.removeAttribute("t-slot");
|
||||
qweb._compileNode(nodeCopy, ctx);
|
||||
}
|
||||
ctx.closeIf();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-model
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.toNumber = function (val: string): number | string {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
};
|
||||
|
||||
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
|
||||
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "model",
|
||||
priority: 42,
|
||||
atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
|
||||
const type = node.getAttribute("type");
|
||||
let handler;
|
||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||
|
||||
// First step: we need to understand the structure of the expression, and
|
||||
// from it, extract a base expression (that we can capture, which is
|
||||
// important because it will be used in a handler later) and a formatted
|
||||
// expression (which uses the captured base expression)
|
||||
//
|
||||
// Also, we support 2 kinds of values: some.expr.value or some.expr[value]
|
||||
// For the first one, we have:
|
||||
// - base expression = scope[some].expr
|
||||
// - expression = exprX.value (where exprX is the var that captures the base expr)
|
||||
// and for the expression with brackets:
|
||||
// - base expression = scope[some].expr
|
||||
// - expression = exprX[keyX] (where exprX is the var that captures the base expr
|
||||
// and keyX captures scope[value])
|
||||
let expr: string;
|
||||
let baseExpr: string;
|
||||
|
||||
if (hasDotAtTheEnd.test(value)) {
|
||||
// we manage the case where the expr has a dot: some.expr.value
|
||||
const index = value.lastIndexOf(".");
|
||||
baseExpr = value.slice(0, index);
|
||||
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
|
||||
expr = `expr${nodeID}${value.slice(index)}`;
|
||||
} else if (hasBracketsAtTheEnd.test(value)) {
|
||||
// we manage here the case where the expr ends in a bracket expression:
|
||||
// some.expr[value]
|
||||
const index = value.lastIndexOf("[");
|
||||
baseExpr = value.slice(0, index);
|
||||
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
|
||||
let exprKey = value.trimRight().slice(index + 1, -1);
|
||||
ctx.addLine(`let exprKey${nodeID} = ${ctx.formatExpression(exprKey)};`);
|
||||
expr = `expr${nodeID}[exprKey${nodeID}]`;
|
||||
} else {
|
||||
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
||||
}
|
||||
|
||||
const key = ctx.generateTemplateKey();
|
||||
if (node.tagName === "select") {
|
||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||
addNodeHook("create", `n.elm.value=${expr};`);
|
||||
event = "change";
|
||||
handler = `(ev) => {${expr} = ev.target.value}`;
|
||||
} else if (type === "checkbox") {
|
||||
ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
|
||||
handler = `(ev) => {${expr} = ev.target.checked}`;
|
||||
} else if (type === "radio") {
|
||||
const nodeValue = node.getAttribute("value")!;
|
||||
ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
|
||||
handler = `(ev) => {${expr} = ev.target.value}`;
|
||||
event = "click";
|
||||
} else {
|
||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
||||
let valueCode = `ev.target.value${trimCode}`;
|
||||
if (fullName.includes(".number")) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
valueCode = `utils.toNumber(${valueCode})`;
|
||||
}
|
||||
handler = `(ev) => {${expr} = ${valueCode}}`;
|
||||
}
|
||||
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
|
||||
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-key
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "key",
|
||||
priority: 45,
|
||||
atNodeEncounter({ ctx, value, node }) {
|
||||
if (ctx.loopNumber === 0) {
|
||||
ctx.keyStack.push(ctx.rootContext.hasKey0);
|
||||
ctx.rootContext.hasKey0 = true;
|
||||
}
|
||||
ctx.addLine("{");
|
||||
ctx.indent();
|
||||
ctx.addLine(`let key${ctx.loopNumber} = ${ctx.formatExpression(value)};`);
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.dedent();
|
||||
ctx.addLine("}");
|
||||
if (ctx.loopNumber === 0) {
|
||||
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import "./base_directives";
|
||||
import "./extensions";
|
||||
|
||||
export { CompiledTemplate, QWeb } from "./qweb";
|
||||
@@ -0,0 +1,930 @@
|
||||
import { EventBus } from "../core/event_bus";
|
||||
import { h, patch, VNode } from "../vdom/index";
|
||||
import { CompilationContext } from "./compilation_context";
|
||||
import { shallowEqual, escape } from "../utils";
|
||||
import { addNS } from "../vdom/vdom";
|
||||
|
||||
/**
|
||||
* Owl QWeb Engine
|
||||
*
|
||||
* In this file, you will find a QWeb engine/template compiler. It is the core
|
||||
* of how Owl component works.
|
||||
*
|
||||
* Briefly, Owl QWeb compiles XML templates into functions that output a virtual
|
||||
* DOM representation.
|
||||
*
|
||||
* We have here:
|
||||
* - a CompilationContext class, which is an internal object that contains all
|
||||
* compilation specific information, while a template is being compiled.
|
||||
* - a QWeb class: this is the code of the QWeb compiler.
|
||||
*
|
||||
* Note that this file does not contain the implementation of the QWeb
|
||||
* directives (see qweb_directives.ts and qweb_extensions.ts).
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
export type EvalContext = { [key: string]: any };
|
||||
export type CompiledTemplate = (context: EvalContext, extra: any) => VNode;
|
||||
|
||||
interface Template {
|
||||
elem: Element;
|
||||
fn: CompiledTemplate;
|
||||
}
|
||||
|
||||
interface CompilationInfo {
|
||||
node: Element;
|
||||
qweb: QWeb;
|
||||
ctx: CompilationContext;
|
||||
fullName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface NodeCreationCompilationInfo extends CompilationInfo {
|
||||
nodeID: number;
|
||||
addNodeHook: Function;
|
||||
}
|
||||
|
||||
export interface Directive {
|
||||
name: string;
|
||||
extraNames?: string[];
|
||||
priority: number;
|
||||
// if return true, then directive is fully applied and there is no need to
|
||||
// keep processing node. Otherwise, we keep going.
|
||||
atNodeEncounter?(info: CompilationInfo): boolean | void;
|
||||
atNodeCreation?(info: NodeCreationCompilationInfo): void;
|
||||
finalize?(info: CompilationInfo): void;
|
||||
}
|
||||
|
||||
interface QWebConfig {
|
||||
templates?: string;
|
||||
translateFn?(text: string): string;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Const/global stuff/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||
|
||||
const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||
|
||||
const NODE_HOOKS_PARAMS = {
|
||||
create: "(_, n)",
|
||||
insert: "vn",
|
||||
remove: "(vn, rm)",
|
||||
destroy: "()",
|
||||
};
|
||||
|
||||
interface Utils {
|
||||
toClassObj(expr: any): Object;
|
||||
shallowEqual(p1: Object, p2: Object): boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function isComponent(obj): boolean {
|
||||
return obj && obj.hasOwnProperty("__owl__");
|
||||
}
|
||||
|
||||
class VDomArray extends Array {
|
||||
toString() {
|
||||
return vDomToString(this);
|
||||
}
|
||||
}
|
||||
|
||||
function vDomToString(vdom: VNode[]): string {
|
||||
return vdom
|
||||
.map((vnode) => {
|
||||
if (vnode.sel) {
|
||||
const node = document.createElement(vnode.sel);
|
||||
const result = patch(node, vnode);
|
||||
return (<HTMLElement>result.elm).outerHTML;
|
||||
} else {
|
||||
return vnode.text;
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
const UTILS: Utils = {
|
||||
zero: Symbol("zero"),
|
||||
toClassObj(expr) {
|
||||
const result = {};
|
||||
if (typeof expr === "string") {
|
||||
// we transform here a list of classes into an object:
|
||||
// 'hey you' becomes {hey: true, you: true}
|
||||
expr = expr.trim();
|
||||
if (!expr) {
|
||||
return {};
|
||||
}
|
||||
let words = expr.split(/\s+/);
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
result[words[i]] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// this is already an object, but we may need to split keys:
|
||||
// {'a b': true, 'a c': false} should become {a: true, b: true, c: false}
|
||||
for (let key in expr) {
|
||||
const value = expr[key];
|
||||
const words = key.split(/\s+/);
|
||||
for (let word of words) {
|
||||
result[word] = result[word] || value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* This method combines the current context with the variables defined in a
|
||||
* scope for use in a slot.
|
||||
*
|
||||
* The implementation is kind of tricky because we want to preserve the
|
||||
* prototype chain structure of the cloned result. So we need to traverse the
|
||||
* prototype chain, cloning each level respectively.
|
||||
*/
|
||||
combine(context, scope) {
|
||||
let clone = context;
|
||||
const scopeStack = [];
|
||||
while (!isComponent(scope)) {
|
||||
scopeStack.push(scope);
|
||||
scope = scope.__proto__;
|
||||
}
|
||||
while (scopeStack.length) {
|
||||
let scope = scopeStack.pop();
|
||||
clone = Object.create(clone);
|
||||
Object.assign(clone, scope);
|
||||
}
|
||||
return clone;
|
||||
},
|
||||
shallowEqual,
|
||||
addNameSpace(vnode) {
|
||||
addNS(vnode.data, vnode.children, vnode.sel);
|
||||
},
|
||||
VDomArray,
|
||||
vDomToString,
|
||||
getComponent(obj) {
|
||||
while (obj && !isComponent(obj)) {
|
||||
obj = obj.__proto__;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
getScope(obj, property: string) {
|
||||
const obj0 = obj;
|
||||
while (
|
||||
obj &&
|
||||
!obj.hasOwnProperty(property) &&
|
||||
!(obj.hasOwnProperty("__access_mode__") && obj.__access_mode__ === "ro")
|
||||
) {
|
||||
const newObj = obj.__proto__;
|
||||
if (!newObj || isComponent(newObj)) {
|
||||
return obj0;
|
||||
}
|
||||
obj = newObj;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
};
|
||||
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
function escapeQuotes(str: string): string {
|
||||
return str.replace(/\'/g, "\\'");
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// QWeb rendering engine
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class QWeb extends EventBus {
|
||||
templates: { [name: string]: Template };
|
||||
static utils = UTILS;
|
||||
static components = Object.create(null);
|
||||
|
||||
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
|
||||
name: 1,
|
||||
att: 1,
|
||||
attf: 1,
|
||||
translation: 1,
|
||||
tag: 1,
|
||||
};
|
||||
static DIRECTIVES: Directive[] = [];
|
||||
|
||||
static TEMPLATES: { [name: string]: Template } = {};
|
||||
|
||||
static nextId: number = 1;
|
||||
|
||||
h = h;
|
||||
// dev mode enables better error messages or more costly validations
|
||||
static dev: boolean = false;
|
||||
static enableTransitions: boolean = true;
|
||||
|
||||
// slots contains sub templates defined with t-set inside t-component nodes, and
|
||||
// are meant to be used by the t-slot directive.
|
||||
static slots = {};
|
||||
static nextSlotId = 1;
|
||||
|
||||
// subTemplates are stored in two objects: a (local) mapping from a name to an
|
||||
// id, and a (global) mapping from an id to the compiled function. This is
|
||||
// necessary to ensure that global templates can be called with more than one
|
||||
// QWeb instance.
|
||||
subTemplates: { [key: string]: number } = {};
|
||||
static subTemplates: { [id: number]: Function } = {};
|
||||
|
||||
isUpdating: boolean = false;
|
||||
translateFn?: QWebConfig["translateFn"];
|
||||
|
||||
constructor(config: QWebConfig = {}) {
|
||||
super();
|
||||
this.templates = Object.create(QWeb.TEMPLATES);
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
}
|
||||
if (config.translateFn) {
|
||||
this.translateFn = config.translateFn;
|
||||
}
|
||||
}
|
||||
|
||||
static addDirective(directive: Directive) {
|
||||
if (directive.name in QWeb.DIRECTIVE_NAMES) {
|
||||
throw new Error(`Directive "${directive.name} already registered`);
|
||||
}
|
||||
QWeb.DIRECTIVES.push(directive);
|
||||
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
|
||||
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
|
||||
if (directive.extraNames) {
|
||||
directive.extraNames.forEach((n) => (QWeb.DIRECTIVE_NAMES[n] = 1));
|
||||
}
|
||||
}
|
||||
|
||||
static registerComponent(name: string, Component: any) {
|
||||
if (QWeb.components[name]) {
|
||||
throw new Error(`Component '${name}' has already been registered`);
|
||||
}
|
||||
QWeb.components[name] = Component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register globally a template. All QWeb instances will obtain their
|
||||
* templates from their own template map, and then, from the global static
|
||||
* TEMPLATES property.
|
||||
*/
|
||||
static registerTemplate(name: string, template: string) {
|
||||
if (QWeb.TEMPLATES[name]) {
|
||||
throw new Error(`Template '${name}' has already been registered`);
|
||||
}
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate(name, template);
|
||||
QWeb.TEMPLATES[name] = qweb.templates[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template to the internal template map. Note that it is not
|
||||
* immediately compiled.
|
||||
*/
|
||||
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
|
||||
if (allowDuplicate && name in this.templates) {
|
||||
return;
|
||||
}
|
||||
const doc = parseXML(xmlString);
|
||||
if (!doc.firstChild) {
|
||||
throw new Error("Invalid template (should not be empty)");
|
||||
}
|
||||
this._addTemplate(name, <Element>doc.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load templates from a xml (as a string or xml document). This will look up
|
||||
* for the first <templates> tag, and will consider each child of this as a
|
||||
* template, with the name given by the t-name attribute.
|
||||
*/
|
||||
addTemplates(xmlstr: string | Document) {
|
||||
if (!xmlstr) {
|
||||
return;
|
||||
}
|
||||
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
|
||||
const templates = doc.getElementsByTagName("templates")[0];
|
||||
if (!templates) {
|
||||
return;
|
||||
}
|
||||
for (let elem of <any>templates.children) {
|
||||
const name = elem.getAttribute("t-name");
|
||||
this._addTemplate(name, elem);
|
||||
}
|
||||
}
|
||||
|
||||
_addTemplate(name: string, elem: Element) {
|
||||
if (name in this.templates) {
|
||||
throw new Error(`Template ${name} already defined`);
|
||||
}
|
||||
this._processTemplate(elem);
|
||||
const template = {
|
||||
elem,
|
||||
fn: function (this: QWeb, context, extra) {
|
||||
const compiledFunction = this._compile(name);
|
||||
template.fn = compiledFunction;
|
||||
return compiledFunction.call(this, context, extra);
|
||||
},
|
||||
};
|
||||
this.templates[name] = template;
|
||||
}
|
||||
|
||||
_processTemplate(elem: Element) {
|
||||
let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
|
||||
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
||||
let node = tbranch[i];
|
||||
let prevElem = node.previousElementSibling!;
|
||||
let pattr = function (name) {
|
||||
return prevElem.getAttribute(name);
|
||||
};
|
||||
let nattr = function (name) {
|
||||
return +!!node.getAttribute(name);
|
||||
};
|
||||
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
||||
if (pattr("t-foreach")) {
|
||||
throw new Error(
|
||||
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
|
||||
);
|
||||
}
|
||||
if (
|
||||
["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
|
||||
return a + b;
|
||||
}) > 1
|
||||
) {
|
||||
throw new Error("Only one conditional branching directive is allowed per node");
|
||||
}
|
||||
// All text (with only spaces) and comment nodes (nodeType 8) between
|
||||
// branch nodes are removed
|
||||
let textNode;
|
||||
while ((textNode = node.previousSibling) !== prevElem) {
|
||||
if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
|
||||
throw new Error("text is not allowed between branching directives");
|
||||
}
|
||||
textNode.remove();
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render a template
|
||||
*
|
||||
* @param {string} name the template should already have been added
|
||||
*/
|
||||
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
|
||||
const template = this.templates[name];
|
||||
if (!template) {
|
||||
throw new Error(`Template ${name} does not exist`);
|
||||
}
|
||||
return template.fn.call(this, context, extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template to a html string.
|
||||
*
|
||||
* Note that this is more limited than the `render` method: it is not suitable
|
||||
* to render a full component tree, since this is an asynchronous operation.
|
||||
* This method can only render templates without components.
|
||||
*/
|
||||
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
|
||||
const vnode = this.render(name, context, extra);
|
||||
if (vnode.sel === undefined) {
|
||||
return vnode.text!;
|
||||
}
|
||||
const node = document.createElement(vnode.sel);
|
||||
const elem = patch(node, vnode).elm as HTMLElement;
|
||||
function escapeTextNodes(node) {
|
||||
if (node.nodeType === 3) {
|
||||
node.textContent = escape(node.textContent);
|
||||
}
|
||||
for (let n of node.childNodes) {
|
||||
escapeTextNodes(n);
|
||||
}
|
||||
}
|
||||
escapeTextNodes(elem);
|
||||
return elem.outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force all widgets connected to this QWeb instance to rerender themselves.
|
||||
*
|
||||
* This method is mostly useful for external code that want to modify the
|
||||
* application in some cases. For example, a router plugin.
|
||||
*/
|
||||
forceUpdate() {
|
||||
this.isUpdating = true;
|
||||
Promise.resolve().then(() => {
|
||||
if (this.isUpdating) {
|
||||
this.isUpdating = false;
|
||||
this.trigger("update");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_compile(
|
||||
name: string,
|
||||
options: {
|
||||
elem?: Element;
|
||||
hasParent?: boolean;
|
||||
defineKey?: boolean;
|
||||
} = {}
|
||||
): CompiledTemplate {
|
||||
const elem = options.elem || this.templates[name].elem;
|
||||
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
||||
const ctx = new CompilationContext(name);
|
||||
if (elem.tagName !== "t") {
|
||||
ctx.shouldDefineResult = false;
|
||||
}
|
||||
if (options.hasParent) {
|
||||
ctx.variables = Object.create(null);
|
||||
ctx.parentNode = ctx.generateID();
|
||||
ctx.allowMultipleRoots = true;
|
||||
ctx.shouldDefineParent = true;
|
||||
ctx.hasParentWidget = true;
|
||||
ctx.shouldDefineResult = false;
|
||||
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
|
||||
if (options.defineKey) {
|
||||
ctx.addLine(`let key0 = extra.key || "";`);
|
||||
ctx.hasKey0 = true;
|
||||
}
|
||||
}
|
||||
this._compileNode(elem, ctx);
|
||||
|
||||
if (!options.hasParent) {
|
||||
if (ctx.shouldDefineResult) {
|
||||
ctx.addLine(`return result;`);
|
||||
} else {
|
||||
if (!ctx.rootNode) {
|
||||
throw new Error(`A template should have one root node (${ctx.templateName})`);
|
||||
}
|
||||
ctx.addLine(`return vn${ctx.rootNode};`);
|
||||
}
|
||||
}
|
||||
|
||||
let code = ctx.generateCode();
|
||||
const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
|
||||
code.unshift(` // Template name: "${templateName}"`);
|
||||
|
||||
let template;
|
||||
try {
|
||||
template = new Function("context, extra", code.join("\n")) as CompiledTemplate;
|
||||
} catch (e) {
|
||||
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
|
||||
console.warn(code.join("\n"));
|
||||
console.groupEnd();
|
||||
throw new Error(
|
||||
`Invalid generated code while compiling template '${templateName}': ${e.message}`
|
||||
);
|
||||
}
|
||||
if (isDebug) {
|
||||
const tpl = this.templates[name];
|
||||
if (tpl) {
|
||||
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code from an xml node
|
||||
*
|
||||
*/
|
||||
_compileNode(node: ChildNode, ctx: CompilationContext) {
|
||||
if (!(node instanceof Element)) {
|
||||
// this is a text node, there are no directive to apply
|
||||
let text = node.textContent!;
|
||||
if (!ctx.inPreTag) {
|
||||
if (lineBreakRE.test(text) && !text.trim()) {
|
||||
return;
|
||||
}
|
||||
text = text.replace(whitespaceRE, " ");
|
||||
}
|
||||
if (this.translateFn) {
|
||||
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
|
||||
const match = translationRE.exec(text);
|
||||
text = match[1] + this.translateFn(match[2]) + match[3];
|
||||
}
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
if (node.nodeType === 3) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
||||
} else if (node.nodeType === 8) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(h('!', \`${text}\`));`);
|
||||
}
|
||||
} else if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
||||
} else {
|
||||
// this is an unusual situation: this text node is the result of the
|
||||
// template rendering.
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.addLine(`result = vn${nodeID};`);
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
|
||||
const tCallNode = document.implementation.createDocument(
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"t",
|
||||
null
|
||||
).documentElement;
|
||||
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
|
||||
node.removeAttribute("t-call");
|
||||
node.prepend(tCallNode);
|
||||
}
|
||||
|
||||
const firstLetter = node.tagName[0];
|
||||
if (firstLetter === firstLetter.toUpperCase()) {
|
||||
// this is a component, we modify in place the xml document to change
|
||||
// <SomeComponent ... /> to <SomeComponent t-component="SomeComponent" ... />
|
||||
node.setAttribute("t-component", node.tagName);
|
||||
} else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
|
||||
throw new Error(
|
||||
`Directive 't-component' can only be used on <t> nodes (used on a <${node.tagName}>)`
|
||||
);
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
|
||||
const validDirectives: {
|
||||
directive: Directive;
|
||||
value: string;
|
||||
fullName: string;
|
||||
}[] = [];
|
||||
|
||||
const finalizers: typeof validDirectives = [];
|
||||
|
||||
// maybe this is not optimal: we iterate on all attributes here, and again
|
||||
// just after for each directive.
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let attrName = attributes[i].name;
|
||||
if (attrName.startsWith("t-")) {
|
||||
let dName = attrName.slice(2).split(/-|\./)[0];
|
||||
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
||||
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
||||
}
|
||||
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
|
||||
const tNode = document.implementation.createDocument(
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"t",
|
||||
null
|
||||
).documentElement;
|
||||
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
|
||||
for (let child of Array.from(node.childNodes)) {
|
||||
tNode.appendChild(child);
|
||||
}
|
||||
node.appendChild(tNode);
|
||||
node.removeAttribute(attrName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DIR_N = QWeb.DIRECTIVES.length;
|
||||
const ATTR_N = attributes.length;
|
||||
let withHandlers = false;
|
||||
for (let i = 0; i < DIR_N; i++) {
|
||||
let directive = QWeb.DIRECTIVES[i];
|
||||
let fullName;
|
||||
let value;
|
||||
for (let j = 0; j < ATTR_N; j++) {
|
||||
const name = attributes[j].name;
|
||||
if (
|
||||
name === "t-" + directive.name ||
|
||||
name.startsWith("t-" + directive.name + "-") ||
|
||||
name.startsWith("t-" + directive.name + ".")
|
||||
) {
|
||||
fullName = name;
|
||||
value = attributes[j].textContent;
|
||||
validDirectives.push({ directive, value, fullName });
|
||||
if (directive.name === "on" || directive.name === "model") {
|
||||
withHandlers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.finalize) {
|
||||
finalizers.push({ directive, value, fullName });
|
||||
}
|
||||
if (directive.atNodeEncounter) {
|
||||
const isDone = directive.atNodeEncounter({
|
||||
node,
|
||||
qweb: this,
|
||||
ctx,
|
||||
fullName,
|
||||
value,
|
||||
});
|
||||
if (isDone) {
|
||||
for (let { directive, value, fullName } of finalizers) {
|
||||
directive.finalize!({ node, qweb: this, ctx, fullName, value });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.nodeName !== "t" || node.hasAttribute("t-tag")) {
|
||||
let nodeHooks = {};
|
||||
let addNodeHook = function (hook, handler) {
|
||||
nodeHooks[hook] = nodeHooks[hook] || [];
|
||||
nodeHooks[hook].push(handler);
|
||||
};
|
||||
if (node.tagName === "select" && node.hasAttribute("t-att-value")) {
|
||||
const value = node.getAttribute("t-att-value");
|
||||
let exprId = ctx.generateID();
|
||||
ctx.addLine(`let expr${exprId} = ${ctx.formatExpression(value)};`);
|
||||
let expr = `expr${exprId}`;
|
||||
node.setAttribute("t-att-value", expr);
|
||||
addNodeHook("create", `n.elm.value=${expr};`);
|
||||
}
|
||||
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.atNodeCreation) {
|
||||
directive.atNodeCreation({
|
||||
node,
|
||||
qweb: this,
|
||||
ctx,
|
||||
fullName,
|
||||
value,
|
||||
nodeID,
|
||||
addNodeHook,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(nodeHooks).length) {
|
||||
ctx.addLine(`p${nodeID}.hook = {`);
|
||||
for (let hook in nodeHooks) {
|
||||
ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
|
||||
for (let handler of nodeHooks[hook]) {
|
||||
ctx.addLine(` ${handler}`);
|
||||
}
|
||||
ctx.addLine(` },`);
|
||||
}
|
||||
ctx.addLine(`};`);
|
||||
}
|
||||
}
|
||||
if (node.nodeName === "pre") {
|
||||
ctx = ctx.subContext("inPreTag", true);
|
||||
}
|
||||
|
||||
this._compileChildren(node, ctx);
|
||||
// svg support
|
||||
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
|
||||
// the root node. This is the easiest way to support svg sub components:
|
||||
// they need to have a g tag as root. Otherwise, we would need a complete
|
||||
// list of allowed svg tags.
|
||||
const shouldAddNS =
|
||||
node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
|
||||
if (shouldAddNS) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
|
||||
}
|
||||
|
||||
for (let { directive, value, fullName } of finalizers) {
|
||||
directive.finalize!({ node, qweb: this, ctx, fullName, value });
|
||||
}
|
||||
}
|
||||
|
||||
_compileGenericNode(
|
||||
node: ChildNode,
|
||||
ctx: CompilationContext,
|
||||
withHandlers: boolean = true
|
||||
): number {
|
||||
// nodeType 1 is generic tag
|
||||
if (node.nodeType !== 1) {
|
||||
throw new Error("unsupported node type");
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
const attrs: string[] = [];
|
||||
const props: string[] = [];
|
||||
const tattrs: number[] = [];
|
||||
|
||||
function handleProperties(key, val) {
|
||||
let isProp = false;
|
||||
switch (node.nodeName) {
|
||||
case "input":
|
||||
let type = (<Element>node).getAttribute("type");
|
||||
if (type === "checkbox" || type === "radio") {
|
||||
if (key === "checked" || key === "indeterminate") {
|
||||
isProp = true;
|
||||
}
|
||||
}
|
||||
if (key === "value" || key === "readonly" || key === "disabled") {
|
||||
isProp = true;
|
||||
}
|
||||
break;
|
||||
case "option":
|
||||
isProp = key === "selected" || key === "disabled";
|
||||
break;
|
||||
case "textarea":
|
||||
isProp = key === "readonly" || key === "disabled" || key === "value";
|
||||
break;
|
||||
case "select":
|
||||
isProp = key === "disabled" || key === "value";
|
||||
break;
|
||||
case "button":
|
||||
case "optgroup":
|
||||
isProp = key === "disabled";
|
||||
break;
|
||||
}
|
||||
if (isProp) {
|
||||
props.push(`${key}: ${val}`);
|
||||
}
|
||||
}
|
||||
let classObj = "";
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let name = attributes[i].name;
|
||||
let value = attributes[i].textContent!;
|
||||
|
||||
if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
|
||||
value = this.translateFn(value);
|
||||
}
|
||||
|
||||
// regular attributes
|
||||
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
|
||||
const attID = ctx.generateID();
|
||||
if (name === "class") {
|
||||
if ((value = value.trim())) {
|
||||
let classDef = value
|
||||
.split(/\s+/)
|
||||
.map((a) => `'${escapeQuotes(a)}':true`)
|
||||
.join(",");
|
||||
if (classObj) {
|
||||
ctx.addLine(`Object.assign(${classObj}, {${classDef}})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`);
|
||||
if (!name.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
name = '"' + name + '"';
|
||||
}
|
||||
attrs.push(`${name}: _${attID}`);
|
||||
handleProperties(name, `_${attID}`);
|
||||
}
|
||||
}
|
||||
|
||||
// dynamic attributes
|
||||
if (name.startsWith("t-att-")) {
|
||||
let attName = name.slice(6);
|
||||
const v = ctx.getValue(value);
|
||||
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`;
|
||||
|
||||
if (attName === "class") {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
formattedValue = `utils.toClassObj(${formattedValue})`;
|
||||
if (classObj) {
|
||||
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = ${formattedValue};`);
|
||||
}
|
||||
} else {
|
||||
const attID = ctx.generateID();
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
// we need to combine dynamic with non dynamic attributes:
|
||||
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
|
||||
const attValue = (<Element>node).getAttribute(attName);
|
||||
if (attValue) {
|
||||
const attValueID = ctx.generateID();
|
||||
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
|
||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||
const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
|
||||
attrs.splice(attrIndex, 1);
|
||||
}
|
||||
if (node.nodeName === "select" && attName === "value") {
|
||||
attrs.push(`${attName}: ${v}`);
|
||||
handleProperties(attName, v);
|
||||
} else {
|
||||
ctx.addLine(`let _${attID} = ${formattedValue};`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
handleProperties(attName, "_" + attID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (name.startsWith("t-attf-")) {
|
||||
let attName = name.slice(7);
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
const formattedExpr = ctx.interpolate(value);
|
||||
const attID = ctx.generateID();
|
||||
let staticVal = (<Element>node).getAttribute(attName);
|
||||
if (staticVal) {
|
||||
ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`);
|
||||
} else {
|
||||
ctx.addLine(`let _${attID} = ${formattedExpr};`);
|
||||
}
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
}
|
||||
|
||||
// t-att= attributes
|
||||
if (name === "t-att") {
|
||||
let id = ctx.generateID();
|
||||
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
|
||||
tattrs.push(id);
|
||||
}
|
||||
}
|
||||
let nodeID = ctx.generateID();
|
||||
let key = ctx.loopNumber || ctx.hasKey0 ? `\`\${key${ctx.loopNumber}}_${nodeID}\`` : nodeID;
|
||||
const parts = [`key:${key}`];
|
||||
if (attrs.length + tattrs.length > 0) {
|
||||
parts.push(`attrs:{${attrs.join(",")}}`);
|
||||
}
|
||||
if (props.length > 0) {
|
||||
parts.push(`props:{${props.join(",")}}`);
|
||||
}
|
||||
if (classObj) {
|
||||
parts.push(`class:${classObj}`);
|
||||
}
|
||||
if (withHandlers) {
|
||||
parts.push(`on:{}`);
|
||||
}
|
||||
|
||||
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
|
||||
for (let id of tattrs) {
|
||||
ctx.addIf(`_${id} instanceof Array`);
|
||||
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
|
||||
ctx.addElse();
|
||||
ctx.addLine(`for (let key in _${id}) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
ctx.closeIf();
|
||||
}
|
||||
let nodeName = `'${node.nodeName}'`;
|
||||
if ((<Element>node).hasAttribute("t-tag")) {
|
||||
const tagExpr = (<Element>node).getAttribute("t-tag");
|
||||
(<Element>node).removeAttribute("t-tag");
|
||||
nodeName = `tag${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${nodeName} = ${ctx.formatExpression(tagExpr)};`);
|
||||
}
|
||||
ctx.addLine(`let vn${nodeID} = h(${nodeName}, p${nodeID}, c${nodeID});`);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
||||
} else if (ctx.loopNumber || ctx.hasKey0) {
|
||||
ctx.rootContext.shouldDefineResult = true;
|
||||
ctx.addLine(`result = vn${nodeID};`);
|
||||
}
|
||||
|
||||
return nodeID;
|
||||
}
|
||||
|
||||
_compileChildren(node: ChildNode, ctx: CompilationContext) {
|
||||
if (node.childNodes.length > 0) {
|
||||
for (let child of Array.from(node.childNodes)) {
|
||||
this._compileNode(child, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Component } from "../component/component";
|
||||
import { xml } from "../tags";
|
||||
import { Destination, RouterEnv } from "./router";
|
||||
|
||||
type Props = Destination;
|
||||
|
||||
export class Link<Env extends RouterEnv> extends Component<Props, Env> {
|
||||
static template = xml`
|
||||
<a t-att-class="{'router-link-active': isActive }"
|
||||
t-att-href="href"
|
||||
t-on-click="navigate">
|
||||
<t t-slot="default"/>
|
||||
</a>
|
||||
`;
|
||||
|
||||
href: string = this.env.router.destToPath(this.props);
|
||||
|
||||
async willUpdateProps(nextProps) {
|
||||
this.href = this.env.router.destToPath(nextProps);
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
if (this.env.router.mode === "hash") {
|
||||
return (<any>document.location).hash === this.href;
|
||||
}
|
||||
return (<any>document.location).pathname === this.href;
|
||||
}
|
||||
|
||||
navigate(ev) {
|
||||
// don't redirect with control keys
|
||||
if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
|
||||
return;
|
||||
}
|
||||
// don't redirect on right click
|
||||
if (ev.button !== undefined && ev.button !== 0) {
|
||||
return;
|
||||
}
|
||||
// don't redirect if `target="_blank"`
|
||||
if (ev.currentTarget && ev.currentTarget.getAttribute) {
|
||||
const target = ev.currentTarget.getAttribute("target");
|
||||
if (/\b_blank\b/i.test(target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ev.preventDefault();
|
||||
this.env.router.navigate(this.props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component } from "../component/component";
|
||||
import { xml } from "../tags";
|
||||
import { EnvWithRouter } from "./router";
|
||||
|
||||
export class RouteComponent extends Component<{}, EnvWithRouter> {
|
||||
static template = xml`
|
||||
<t>
|
||||
<t
|
||||
t-if="routeComponent"
|
||||
t-component="routeComponent"
|
||||
t-key="env.router.currentRouteName"
|
||||
t-props="env.router.currentParams" />
|
||||
</t>
|
||||
`;
|
||||
|
||||
get routeComponent(): any {
|
||||
return this.env.router.currentRoute && this.env.router.currentRoute.component;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { Env } from "../component/component";
|
||||
import { QWeb } from "../qweb/index";
|
||||
import { shallowEqual } from "../utils";
|
||||
|
||||
type NavigationGuard = (info: {
|
||||
env: Env;
|
||||
to: Route | null;
|
||||
from: Route | null;
|
||||
}) => boolean | Destination;
|
||||
|
||||
export interface Route {
|
||||
name: string;
|
||||
path: string;
|
||||
extractionRegExp: RegExp;
|
||||
component?: any;
|
||||
redirect?: Destination;
|
||||
params: string[];
|
||||
beforeRouteEnter?: NavigationGuard;
|
||||
}
|
||||
|
||||
export type RouteParams = { [key: string]: string | number };
|
||||
|
||||
export interface RouterEnv extends Env {
|
||||
router: Router;
|
||||
}
|
||||
|
||||
export interface Destination {
|
||||
path?: string;
|
||||
to?: string;
|
||||
params?: RouteParams;
|
||||
}
|
||||
|
||||
interface PositiveMatchResult {
|
||||
type: "match";
|
||||
route: Route;
|
||||
params: RouteParams;
|
||||
}
|
||||
|
||||
interface NegativeMatchResult {
|
||||
type: "nomatch";
|
||||
}
|
||||
|
||||
interface CancelledMatch {
|
||||
type: "cancelled";
|
||||
}
|
||||
|
||||
type MatchResult = PositiveMatchResult | NegativeMatchResult | CancelledMatch;
|
||||
|
||||
interface Options {
|
||||
mode: Router["mode"];
|
||||
}
|
||||
|
||||
export interface EnvWithRouter extends Env {
|
||||
router: Router;
|
||||
}
|
||||
|
||||
const paramRegexp = /\{\{(.*?)\}\}/;
|
||||
const globalParamRegexp = new RegExp(paramRegexp.source, "g");
|
||||
|
||||
export class Router {
|
||||
currentRoute: Route | null = null;
|
||||
currentParams: RouteParams | null = null;
|
||||
mode: "history" | "hash";
|
||||
|
||||
routes: { [id: string]: Route };
|
||||
routeIds: string[];
|
||||
env: RouterEnv;
|
||||
|
||||
constructor(
|
||||
env: Partial<EnvWithRouter>,
|
||||
routes: Partial<Route>[],
|
||||
options: Options = { mode: "history" }
|
||||
) {
|
||||
env.router = this;
|
||||
this.mode = options.mode;
|
||||
this.env = env as RouterEnv;
|
||||
|
||||
this.routes = {};
|
||||
this.routeIds = [];
|
||||
let nextId = 1;
|
||||
for (let partialRoute of routes) {
|
||||
if (!partialRoute.name) {
|
||||
partialRoute.name = "__route__" + nextId++;
|
||||
}
|
||||
if (partialRoute.component) {
|
||||
QWeb.registerComponent("__component__" + partialRoute.name, partialRoute.component);
|
||||
}
|
||||
if (partialRoute.redirect) {
|
||||
this.validateDestination(partialRoute.redirect);
|
||||
}
|
||||
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
|
||||
partialRoute.extractionRegExp = makeExtractionRegExp(partialRoute.path);
|
||||
this.routes[partialRoute.name] = partialRoute as Route;
|
||||
this.routeIds.push(partialRoute.name);
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public API
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async start() {
|
||||
(this as any)._listener = (ev) => this._navigate(this.currentPath(), ev);
|
||||
window.addEventListener("popstate", (this as any)._listener);
|
||||
if (this.mode === "hash") {
|
||||
window.addEventListener("hashchange", (this as any)._listener);
|
||||
}
|
||||
const result = await this.matchAndApplyRules(this.currentPath());
|
||||
if (result.type === "match") {
|
||||
this.currentRoute = result.route;
|
||||
this.currentParams = result.params;
|
||||
const currentPath = this.routeToPath(result.route, result.params);
|
||||
if (currentPath !== this.currentPath()) {
|
||||
this.setUrlFromPath(currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async navigate(to: Destination): Promise<boolean> {
|
||||
const path = this.destToPath(to);
|
||||
return this._navigate(path);
|
||||
}
|
||||
async _navigate(path: string, ev?: any): Promise<boolean> {
|
||||
const initialName = this.currentRouteName;
|
||||
const initialParams = this.currentParams;
|
||||
const result = await this.matchAndApplyRules(path);
|
||||
if (result.type === "match") {
|
||||
let finalPath = this.routeToPath(result.route, result.params);
|
||||
if (path.indexOf("?") > -1) {
|
||||
finalPath += "?" + path.split("?")[1];
|
||||
}
|
||||
const isPopStateEvent = ev && ev instanceof PopStateEvent;
|
||||
if (!isPopStateEvent) {
|
||||
this.setUrlFromPath(finalPath);
|
||||
}
|
||||
this.currentRoute = result.route;
|
||||
this.currentParams = result.params;
|
||||
} else if (result.type === "nomatch") {
|
||||
this.currentRoute = null;
|
||||
this.currentParams = null;
|
||||
}
|
||||
const didChange =
|
||||
this.currentRouteName !== initialName || !shallowEqual(this.currentParams, initialParams);
|
||||
if (didChange) {
|
||||
this.env.qweb.forceUpdate();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
destToPath(dest: Destination): string {
|
||||
this.validateDestination(dest);
|
||||
return dest.path || this.routeToPath(this.routes[dest.to!], dest.params!);
|
||||
}
|
||||
|
||||
get currentRouteName(): string | null {
|
||||
return this.currentRoute && this.currentRoute.name;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
private setUrlFromPath(path: string) {
|
||||
const separator = this.mode === "hash" ? location.pathname : "";
|
||||
const url = location.origin + separator + path;
|
||||
if (url !== window.location.href) {
|
||||
window.history.pushState({}, path, url);
|
||||
}
|
||||
}
|
||||
|
||||
private validateDestination(dest: Destination) {
|
||||
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
|
||||
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private routeToPath(route: Route, params: RouteParams): string {
|
||||
const prefix = this.mode === "hash" ? "#" : "";
|
||||
return (
|
||||
prefix +
|
||||
route.path.replace(globalParamRegexp, (match, param) => {
|
||||
const [key] = param.split(".");
|
||||
return <string>params[key];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private currentPath(): string {
|
||||
let result = this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
|
||||
return result || "/";
|
||||
}
|
||||
|
||||
private match(path: string): MatchResult {
|
||||
for (let routeId of this.routeIds) {
|
||||
let route = this.routes[routeId];
|
||||
let params = this.getRouteParams(route, path);
|
||||
if (params) {
|
||||
return {
|
||||
type: "match",
|
||||
route: route,
|
||||
params: params,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { type: "nomatch" };
|
||||
}
|
||||
|
||||
private async matchAndApplyRules(path: string): Promise<MatchResult> {
|
||||
const result = this.match(path);
|
||||
if (result.type === "match") {
|
||||
return this.applyRules(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async applyRules(matchResult: PositiveMatchResult): Promise<MatchResult> {
|
||||
const route = matchResult.route;
|
||||
if (route.redirect) {
|
||||
const path = this.destToPath(route.redirect);
|
||||
return this.matchAndApplyRules(path);
|
||||
}
|
||||
if (route.beforeRouteEnter) {
|
||||
const result = await route.beforeRouteEnter({
|
||||
env: this.env,
|
||||
from: this.currentRoute,
|
||||
to: route,
|
||||
});
|
||||
if (result === false) {
|
||||
return { type: "cancelled" };
|
||||
} else if (result !== true) {
|
||||
// we want to navigate to another destination
|
||||
const path = this.destToPath(result);
|
||||
return this.matchAndApplyRules(path);
|
||||
}
|
||||
}
|
||||
|
||||
return matchResult;
|
||||
}
|
||||
|
||||
private getRouteParams(route: Route, path: string): RouteParams | false {
|
||||
if (route.path === "*") {
|
||||
return {};
|
||||
}
|
||||
if (path.indexOf("?") > -1) {
|
||||
path = path.split("?")[0];
|
||||
}
|
||||
if (path.startsWith("#")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
const paramsMatch = path.match(route.extractionRegExp);
|
||||
if (!paramsMatch) {
|
||||
return false;
|
||||
}
|
||||
const result = {};
|
||||
route.params.forEach((param, index) => {
|
||||
const [key, suffix] = param.split(".");
|
||||
const paramValue = paramsMatch[index + 1];
|
||||
if (suffix === "number") {
|
||||
return (result[key] = parseInt(paramValue, 10));
|
||||
}
|
||||
return (result[key] = paramValue);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function findParams(str: string): string[] {
|
||||
const result: string[] = [];
|
||||
let m;
|
||||
do {
|
||||
m = globalParamRegexp.exec(str);
|
||||
if (m) {
|
||||
result.push(m[1]);
|
||||
}
|
||||
} while (m);
|
||||
return result;
|
||||
}
|
||||
|
||||
function escapeRegExp(str: string) {
|
||||
return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
||||
}
|
||||
|
||||
function makeExtractionRegExp(path: string) {
|
||||
// replace param strings with capture groups so that we can build a regex to match over the path
|
||||
const extractionString = path
|
||||
.split(paramRegexp)
|
||||
.map((part, index) => {
|
||||
return index % 2 ? "(.*)" : escapeRegExp(part);
|
||||
})
|
||||
.join("");
|
||||
// Example: /home/{{param1}}/{{param2}} => ^\/home\/(.*)\/(.*)$
|
||||
return new RegExp(`^${extractionString}$`);
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { Component, Env } from "./component/component";
|
||||
import { Context, useContextWithCB } from "./context";
|
||||
import { onWillUpdateProps } from "./hooks";
|
||||
|
||||
/**
|
||||
* Owl Store
|
||||
*
|
||||
* We have here:
|
||||
* - a Store class
|
||||
* - useStore hook
|
||||
* - useDispatch hook
|
||||
* - useGetters hook
|
||||
*
|
||||
* The Owl store is our answer to the problem of managing complex state across
|
||||
* components. The main idea is that the store owns some state, allow external
|
||||
* code to modify it through actions, and for each state changes,
|
||||
* connected component will be notified, and updated if necessary.
|
||||
*
|
||||
* Note that this code is partly inspired by VueX and React/Redux
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Store Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface EnvWithStore extends Env {
|
||||
store: Store;
|
||||
}
|
||||
|
||||
export type Action = ({ state, dispatch, env, getters }, ...payload: any) => any;
|
||||
export type Getter = ({ state: any, getters }, payload?) => any;
|
||||
|
||||
interface StoreConfig {
|
||||
env?: Env;
|
||||
state?: any;
|
||||
actions?: { [name: string]: Action };
|
||||
getters?: { [name: string]: Getter };
|
||||
}
|
||||
|
||||
export class Store extends Context {
|
||||
actions: any;
|
||||
env: any;
|
||||
getters: { [name: string]: (payload?) => any };
|
||||
updateFunctions: { [key: number]: (() => boolean)[] };
|
||||
|
||||
constructor(config: StoreConfig) {
|
||||
super(config.state);
|
||||
this.actions = config.actions;
|
||||
this.env = config.env;
|
||||
this.getters = {};
|
||||
this.updateFunctions = [];
|
||||
if (config.getters) {
|
||||
const firstArg = {
|
||||
state: this.state,
|
||||
getters: this.getters,
|
||||
};
|
||||
for (let g in config.getters) {
|
||||
this.getters[g] = config.getters[g].bind(this, firstArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(action: string, ...payload: any): Promise<void> | void {
|
||||
if (!this.actions[action]) {
|
||||
throw new Error(`[Error] action ${action} is undefined`);
|
||||
}
|
||||
const result = this.actions[action](
|
||||
{
|
||||
dispatch: this.dispatch.bind(this),
|
||||
env: this.env,
|
||||
state: this.state,
|
||||
getters: this.getters,
|
||||
},
|
||||
...payload
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
__notifyComponents(): Promise<void> {
|
||||
this.trigger("before-update");
|
||||
return super.__notifyComponents();
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectorOptions {
|
||||
store?: Store;
|
||||
isEqual?: (a: any, b: any) => boolean;
|
||||
onUpdate?: (result: any) => any;
|
||||
}
|
||||
|
||||
const isStrictEqual = (a, b) => a === b;
|
||||
|
||||
export function useStore(selector, options: SelectorOptions = {}): any {
|
||||
const component = Component.current as Component<any, EnvWithStore>;
|
||||
const componentId = component.__owl__.id;
|
||||
const store = options.store || (component.env.store as Store);
|
||||
if (!(store instanceof Store)) {
|
||||
throw new Error(`No store found when connecting '${component.constructor.name}'`);
|
||||
}
|
||||
let result = selector(store.state, component.props);
|
||||
const hashFn = store.observer.revNumber.bind(store.observer);
|
||||
let revNumber = hashFn(result);
|
||||
const isEqual = options.isEqual || isStrictEqual;
|
||||
if (!store.updateFunctions[componentId]) {
|
||||
store.updateFunctions[componentId] = [];
|
||||
}
|
||||
function selectCompareUpdate(state, props): boolean {
|
||||
const oldResult = result;
|
||||
result = selector(state, props);
|
||||
const newRevNumber = hashFn(result);
|
||||
if ((newRevNumber > 0 && revNumber !== newRevNumber) || !isEqual(oldResult, result)) {
|
||||
revNumber = newRevNumber;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (options.onUpdate) {
|
||||
store.on("before-update", component, () => {
|
||||
const newValue = selector(store!.state, component.props!);
|
||||
options.onUpdate(newValue);
|
||||
});
|
||||
}
|
||||
store.updateFunctions[componentId].push(function (): boolean {
|
||||
return selectCompareUpdate(store!.state, component.props);
|
||||
});
|
||||
|
||||
useContextWithCB(store, component, function (): Promise<void> | void {
|
||||
let shouldRender = false;
|
||||
for (let fn of store.updateFunctions[componentId]) {
|
||||
shouldRender = fn() || shouldRender;
|
||||
}
|
||||
if (shouldRender) {
|
||||
return component.render();
|
||||
}
|
||||
});
|
||||
onWillUpdateProps((props) => {
|
||||
selectCompareUpdate(store.state, props);
|
||||
});
|
||||
|
||||
const __destroy = component.__destroy;
|
||||
component.__destroy = (parent) => {
|
||||
delete store.updateFunctions[componentId];
|
||||
if (options.onUpdate) {
|
||||
store.off("before-update", component);
|
||||
}
|
||||
__destroy.call(component, parent);
|
||||
};
|
||||
|
||||
if (typeof result !== "object" || result === null) {
|
||||
return result;
|
||||
}
|
||||
return new Proxy(result, {
|
||||
get(target, k) {
|
||||
return result[k];
|
||||
},
|
||||
set(target, k, v) {
|
||||
throw new Error("Store state should only be modified through actions");
|
||||
},
|
||||
has(target, k) {
|
||||
return k in result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDispatch(store?: Store): Store["dispatch"] {
|
||||
store = store || (Component.current!.env as EnvWithStore).store;
|
||||
return store.dispatch.bind(store);
|
||||
}
|
||||
|
||||
export function useGetters(store?: Store): Store["getters"] {
|
||||
store = store || (Component.current!.env as EnvWithStore).store;
|
||||
return store.getters;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { QWeb } from "./qweb/index";
|
||||
import { registerSheet } from "./component/styles";
|
||||
|
||||
/**
|
||||
* Owl Tags
|
||||
*
|
||||
* We have here a (very) small collection of tag functions:
|
||||
*
|
||||
* - xml
|
||||
*
|
||||
* The plan is to add a few other tags such as css, globalcss.
|
||||
*/
|
||||
|
||||
/**
|
||||
* XML tag helper for defining templates. With this, one can simply define
|
||||
* an inline template with just the template xml:
|
||||
* ```js
|
||||
* class A extends Component {
|
||||
* static template = xml`<div>some template</div>`;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function xml(strings, ...args) {
|
||||
const name = `__template__${QWeb.nextId++}`;
|
||||
const value = String.raw(strings, ...args);
|
||||
QWeb.registerTemplate(name, value);
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
||||
* an inline stylesheet with just the following code:
|
||||
* ```js
|
||||
* class A extends Component {
|
||||
* static style = css`.component-a { color: red; }`;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function css(strings, ...args) {
|
||||
const name = `__sheet__${QWeb.nextId++}`;
|
||||
const value = String.raw(strings, ...args);
|
||||
registerSheet(name, value);
|
||||
return name;
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Owl Utils
|
||||
*
|
||||
* We have here a small collection of utility functions:
|
||||
*
|
||||
* - whenReady
|
||||
* - loadJS
|
||||
* - loadFile
|
||||
* - escape
|
||||
* - debounce
|
||||
*/
|
||||
|
||||
import { browser } from "./browser";
|
||||
|
||||
export function whenReady(fn?: any) {
|
||||
return new Promise(function (resolve) {
|
||||
if (document.readyState !== "loading") {
|
||||
resolve();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", resolve, false);
|
||||
}
|
||||
}).then(fn || function () {});
|
||||
}
|
||||
|
||||
const loadedScripts: { [key: string]: Promise<void> } = {};
|
||||
|
||||
export function loadJS(url: string): Promise<void> {
|
||||
if (url in loadedScripts) {
|
||||
return loadedScripts[url];
|
||||
}
|
||||
const promise: Promise<void> = new Promise(function (resolve, reject) {
|
||||
const script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = url;
|
||||
script.onload = function () {
|
||||
resolve();
|
||||
};
|
||||
script.onerror = function () {
|
||||
reject(`Error loading file '${url}'`);
|
||||
};
|
||||
const head = document.head || document.getElementsByTagName("head")[0];
|
||||
head.appendChild(script);
|
||||
});
|
||||
loadedScripts[url] = promise;
|
||||
return promise;
|
||||
}
|
||||
|
||||
export async function loadFile(url: string): Promise<string> {
|
||||
const result = await browser.fetch(url);
|
||||
if (!result.ok) {
|
||||
throw new Error("Error while fetching xml templates");
|
||||
}
|
||||
return await result.text();
|
||||
}
|
||||
|
||||
export function escape(str: string | number | undefined): string {
|
||||
if (str === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof str === "number") {
|
||||
return String(str);
|
||||
}
|
||||
const p = document.createElement("p");
|
||||
p.textContent = str;
|
||||
return p.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function, that, as long as it continues to be invoked, will not
|
||||
* be triggered. The function will be called after it stops being called for
|
||||
* N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
* leading edge, instead of the trailing.
|
||||
*
|
||||
* Inspired by https://davidwalsh.name/javascript-debounce-function
|
||||
*/
|
||||
export function debounce(func: Function, wait: number, immediate?: boolean): Function {
|
||||
let timeout;
|
||||
return function (this: any) {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
function later() {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
}
|
||||
const callNow = immediate && !timeout;
|
||||
browser.clearTimeout(timeout);
|
||||
timeout = browser.setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function shallowEqual(p1, p2): boolean {
|
||||
for (let k in p1) {
|
||||
if (p1[k] !== p2[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { VNode, h, addNS } from "./vdom";
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
export function htmlToVDOM(html: string): VNode[] {
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
const result: VNode[] = [];
|
||||
for (let child of doc.body.childNodes) {
|
||||
result.push(htmlToVNode(child));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function htmlToVNode(node: ChildNode): VNode {
|
||||
if (!(node instanceof Element)) {
|
||||
if (node instanceof Comment) {
|
||||
return h("!", node.textContent);
|
||||
}
|
||||
return { text: node.textContent! } as VNode;
|
||||
}
|
||||
const attrs = {};
|
||||
for (let attr of node.attributes) {
|
||||
attrs[attr.name] = attr.textContent;
|
||||
}
|
||||
const children: VNode[] = [];
|
||||
for (let c of node.childNodes) {
|
||||
children.push(htmlToVNode(c));
|
||||
}
|
||||
const vnode = h((node as Element).tagName, { attrs }, children);
|
||||
if (vnode.sel === "svg") {
|
||||
addNS(vnode.data, (vnode as any).children, vnode.sel);
|
||||
}
|
||||
return vnode;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { attrsModule, classModule, eventListenersModule, propsModule } from "./modules";
|
||||
import { init } from "./vdom";
|
||||
//------------------------------------------------------------------------------
|
||||
// patch
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export { h, VNode } from "./vdom";
|
||||
|
||||
export const patch = init([eventListenersModule, attrsModule, propsModule, classModule]);
|
||||
@@ -0,0 +1,247 @@
|
||||
import { Module, VNode, VNodeData } from "./vdom";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/props.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
cur: any,
|
||||
old: any,
|
||||
elm = vnode.elm,
|
||||
oldProps = (oldVnode.data as VNodeData).props,
|
||||
props = (vnode.data as VNodeData).props;
|
||||
|
||||
if (!oldProps && !props) return;
|
||||
if (oldProps === props) return;
|
||||
oldProps = oldProps || {};
|
||||
props = props || {};
|
||||
|
||||
for (key in oldProps) {
|
||||
if (!props[key]) {
|
||||
delete (elm as any)[key];
|
||||
}
|
||||
}
|
||||
for (key in props) {
|
||||
cur = props[key];
|
||||
old = oldProps[key];
|
||||
if (old !== cur && (key !== "value" || (elm as any)[key] !== cur)) {
|
||||
(elm as any)[key] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const propsModule = {
|
||||
create: updateProps,
|
||||
update: updateProps,
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// module/eventlisteners.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function invokeHandler(handler: any, vnode?: VNode, event?: Event): void {
|
||||
if (typeof handler === "function") {
|
||||
// call function handler
|
||||
handler.call(vnode, event, vnode);
|
||||
} else if (typeof handler === "object") {
|
||||
// call handler with arguments
|
||||
if (typeof handler[0] === "function") {
|
||||
// special case for single argument for performance
|
||||
if (handler.length === 2) {
|
||||
handler[0].call(vnode, handler[1], event, vnode);
|
||||
} else {
|
||||
var args = handler.slice(1);
|
||||
args.push(event);
|
||||
args.push(vnode);
|
||||
handler[0].apply(vnode, args);
|
||||
}
|
||||
} else {
|
||||
// call multiple handlers
|
||||
for (let i = 0, iLen = handler.length; i < iLen; i++) {
|
||||
invokeHandler(handler[i], vnode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(event: Event, vnode: VNode) {
|
||||
var name = event.type,
|
||||
on = (vnode.data as VNodeData).on;
|
||||
|
||||
// call event handler(s) if exists
|
||||
if (on) {
|
||||
if (on[name]) {
|
||||
invokeHandler(on[name], vnode, event);
|
||||
} else if (on["!" + name]) {
|
||||
invokeHandler(on["!" + name], vnode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createListener() {
|
||||
return function handler(event: Event) {
|
||||
handleEvent(event, (handler as any).vnode);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
|
||||
var oldOn = (oldVnode.data as VNodeData).on,
|
||||
oldListener = (oldVnode as any).listener,
|
||||
oldElm: Element = oldVnode.elm as Element,
|
||||
on = vnode && (vnode.data as VNodeData).on,
|
||||
elm: Element = (vnode && vnode.elm) as Element,
|
||||
name: string;
|
||||
|
||||
// optimization for reused immutable handlers
|
||||
if (oldOn === on) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing listeners which no longer used
|
||||
if (oldOn && oldListener) {
|
||||
// if element changed or deleted we remove all existing listeners unconditionally
|
||||
if (!on) {
|
||||
for (name in oldOn) {
|
||||
// remove listener if element was changed or existing listeners removed
|
||||
const capture = name.charAt(0) === "!";
|
||||
name = capture ? name.slice(1) : name;
|
||||
oldElm.removeEventListener(name, oldListener, capture);
|
||||
}
|
||||
} else {
|
||||
for (name in oldOn) {
|
||||
// remove listener if existing listener removed
|
||||
if (!on[name]) {
|
||||
const capture = name.charAt(0) === "!";
|
||||
name = capture ? name.slice(1) : name;
|
||||
oldElm.removeEventListener(name, oldListener, capture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add new listeners which has not already attached
|
||||
if (on) {
|
||||
// reuse existing listener or create new
|
||||
var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener());
|
||||
// update vnode for listener
|
||||
listener.vnode = vnode;
|
||||
|
||||
// if element changed or added we add all needed listeners unconditionally
|
||||
if (!oldOn) {
|
||||
for (name in on) {
|
||||
// add listener if element was changed or new listeners added
|
||||
const capture = name.charAt(0) === "!";
|
||||
name = capture ? name.slice(1) : name;
|
||||
elm.addEventListener(name, listener, capture);
|
||||
}
|
||||
} else {
|
||||
for (name in on) {
|
||||
// add listener if new listener added
|
||||
if (!oldOn[name]) {
|
||||
const capture = name.charAt(0) === "!";
|
||||
name = capture ? name.slice(1) : name;
|
||||
elm.addEventListener(name, listener, capture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventListenersModule = {
|
||||
create: updateEventListeners,
|
||||
update: updateEventListeners,
|
||||
destroy: updateEventListeners,
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// attributes.ts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const xlinkNS = "http://www.w3.org/1999/xlink";
|
||||
const xmlNS = "http://www.w3.org/XML/1998/namespace";
|
||||
const colonChar = 58;
|
||||
const xChar = 120;
|
||||
|
||||
function updateAttrs(oldVnode: VNode, vnode: VNode): void {
|
||||
var key: string,
|
||||
elm: Element = vnode.elm as Element,
|
||||
oldAttrs = (oldVnode.data as VNodeData).attrs,
|
||||
attrs = (vnode.data as VNodeData).attrs;
|
||||
|
||||
if (!oldAttrs && !attrs) return;
|
||||
if (oldAttrs === attrs) return;
|
||||
oldAttrs = oldAttrs || {};
|
||||
attrs = attrs || {};
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
const cur = attrs[key];
|
||||
const old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
if (cur === true) {
|
||||
elm.setAttribute(key, "");
|
||||
} else if (cur === false) {
|
||||
elm.removeAttribute(key);
|
||||
} else {
|
||||
if (key.charCodeAt(0) !== xChar) {
|
||||
elm.setAttribute(key, cur);
|
||||
} else if (key.charCodeAt(3) === colonChar) {
|
||||
// Assume xml namespace
|
||||
elm.setAttributeNS(xmlNS, key, cur);
|
||||
} else if (key.charCodeAt(5) === colonChar) {
|
||||
// Assume xlink namespace
|
||||
elm.setAttributeNS(xlinkNS, key, cur);
|
||||
} else {
|
||||
elm.setAttribute(key, cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const attrsModule = {
|
||||
create: updateAttrs,
|
||||
update: updateAttrs,
|
||||
} as Module;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// class.ts
|
||||
//------------------------------------------------------------------------------
|
||||
function updateClass(oldVnode: VNode, vnode: VNode): void {
|
||||
var cur: any,
|
||||
name: string,
|
||||
elm: Element,
|
||||
oldClass = (oldVnode.data as VNodeData).class,
|
||||
klass = (vnode.data as VNodeData).class;
|
||||
|
||||
if (!oldClass && !klass) return;
|
||||
if (oldClass === klass) return;
|
||||
oldClass = oldClass || {};
|
||||
klass = klass || {};
|
||||
|
||||
elm = vnode.elm as Element;
|
||||
|
||||
for (name in oldClass) {
|
||||
if (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) {
|
||||
// was `true` and now not provided
|
||||
elm.classList.remove(name);
|
||||
}
|
||||
}
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
(elm.classList as any)[cur ? "add" : "remove"](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const classModule = { create: updateClass, update: updateClass } as Module;
|
||||
@@ -0,0 +1,616 @@
|
||||
/**
|
||||
* Owl VDOM
|
||||
*
|
||||
* This file contains an implementation of a virtual DOM, which is a system that
|
||||
* can generate in-memory representations of a DOM tree, compare them, and
|
||||
* eventually change a concrete DOM tree to match its representation, in an
|
||||
* hopefully efficient way.
|
||||
*
|
||||
* Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our
|
||||
* needs (see https://github.com/snabbdom/snabbdom).
|
||||
*
|
||||
* The main exported values are:
|
||||
* - interface VNode
|
||||
* - h function (a helper function to generate a vnode)
|
||||
* - patch function (to apply a vnode to an actual DOM node)
|
||||
*/
|
||||
|
||||
// because those in TypeScript are too restrictive: https://github.com/Microsoft/TSJS-lib-generator/pull/237
|
||||
declare global {
|
||||
interface Element {
|
||||
setAttribute(name: string, value: string | number | boolean): void;
|
||||
setAttributeNS(
|
||||
namespaceURI: string,
|
||||
qualifiedName: string,
|
||||
value: string | number | boolean
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
type Props = Record<string, any>;
|
||||
type Attrs = Record<string, string | number | boolean>;
|
||||
type On = { [N in keyof HTMLElementEventMap]?: (ev: HTMLElementEventMap[N]) => void } & {
|
||||
[event: string]: EventListener;
|
||||
};
|
||||
|
||||
export interface Module {
|
||||
pre: PreHook;
|
||||
create: CreateHook;
|
||||
update: UpdateHook;
|
||||
destroy: DestroyHook;
|
||||
remove: RemoveHook;
|
||||
post: PostHook;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// vnode.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type Key = string | number;
|
||||
|
||||
export interface VNode {
|
||||
sel: string | undefined;
|
||||
data: VNodeData | undefined;
|
||||
children: Array<VNode | string> | undefined;
|
||||
elm: Node | undefined;
|
||||
text: string | undefined;
|
||||
key: Key | undefined;
|
||||
}
|
||||
|
||||
export interface VNodeData {
|
||||
props?: Props;
|
||||
attrs?: Attrs;
|
||||
on?: On;
|
||||
hook?: Hooks;
|
||||
key?: Key;
|
||||
ns?: string; // for SVGs
|
||||
[key: string]: any; // for any other 3rd party module
|
||||
}
|
||||
|
||||
function vnode(
|
||||
sel: string | undefined,
|
||||
data: any | undefined,
|
||||
children: Array<VNode | string> | undefined,
|
||||
text: string | undefined,
|
||||
elm: Element | Text | undefined
|
||||
): VNode {
|
||||
let key = data === undefined ? undefined : data.key;
|
||||
return { sel, data, children, text, elm, key };
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// snabbdom.ts
|
||||
//------------------------------------------------------------------------------
|
||||
function isUndef(s: any): boolean {
|
||||
return s === undefined;
|
||||
}
|
||||
function isDef(s: any): boolean {
|
||||
return s !== undefined;
|
||||
}
|
||||
|
||||
type VNodeQueue = Array<VNode>;
|
||||
|
||||
const emptyNode = vnode("", {}, [], undefined, undefined);
|
||||
|
||||
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
|
||||
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
|
||||
}
|
||||
|
||||
function isVnode(vnode: any): vnode is VNode {
|
||||
return vnode.sel !== undefined;
|
||||
}
|
||||
|
||||
type KeyToIndexMap = { [key: string]: number };
|
||||
|
||||
type ArraysOf<T> = { [K in keyof T]: T[K][] };
|
||||
|
||||
type ModuleHooks = ArraysOf<Module>;
|
||||
|
||||
function createKeyToOldIdx(
|
||||
children: Array<VNode>,
|
||||
beginIdx: number,
|
||||
endIdx: number
|
||||
): KeyToIndexMap {
|
||||
let i: number,
|
||||
map: KeyToIndexMap = {},
|
||||
key: Key | undefined,
|
||||
ch;
|
||||
for (i = beginIdx; i <= endIdx; ++i) {
|
||||
ch = children[i];
|
||||
if (ch != null) {
|
||||
key = ch.key;
|
||||
if (key !== undefined) map[key] = i;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const hooks: (keyof Module)[] = ["create", "update", "remove", "destroy", "pre", "post"];
|
||||
|
||||
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
|
||||
let i: number,
|
||||
j: number,
|
||||
cbs = {} as ModuleHooks;
|
||||
|
||||
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
|
||||
|
||||
for (i = 0; i < hooks.length; ++i) {
|
||||
cbs[hooks[i]] = [];
|
||||
for (j = 0; j < modules.length; ++j) {
|
||||
const hook = modules[j][hooks[i]];
|
||||
if (hook !== undefined) {
|
||||
(cbs[hooks[i]] as Array<any>).push(hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emptyNodeAt(elm: Element) {
|
||||
const id = elm.id ? "#" + elm.id : "";
|
||||
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
|
||||
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
|
||||
}
|
||||
|
||||
function createRmCb(childElm: Node, listeners: number) {
|
||||
return function rmCb() {
|
||||
if (--listeners === 0) {
|
||||
const parent = api.parentNode(childElm);
|
||||
api.removeChild(parent, childElm);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
if (isDef((i = data.hook)) && isDef((i = i.init))) {
|
||||
i(vnode);
|
||||
data = vnode.data;
|
||||
}
|
||||
}
|
||||
let children = vnode.children,
|
||||
sel = vnode.sel;
|
||||
if (sel === "!") {
|
||||
if (isUndef(vnode.text)) {
|
||||
vnode.text = "";
|
||||
}
|
||||
vnode.elm = api.createComment(vnode.text as string);
|
||||
} else if (sel !== undefined) {
|
||||
const elm =
|
||||
vnode.elm ||
|
||||
(vnode.elm =
|
||||
isDef(data) && isDef((i = (data as VNodeData).ns))
|
||||
? api.createElementNS(i, sel)
|
||||
: api.createElement(sel));
|
||||
for (i = 0, iLen = cbs.create.length; i < iLen; ++i) cbs.create[i](emptyNode, vnode);
|
||||
if (array(children)) {
|
||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
const ch = children[i];
|
||||
if (ch != null) {
|
||||
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
|
||||
}
|
||||
}
|
||||
} else if (primitive(vnode.text)) {
|
||||
api.appendChild(elm, api.createTextNode(vnode.text));
|
||||
}
|
||||
i = (vnode.data as VNodeData).hook; // Reuse variable
|
||||
if (isDef(i)) {
|
||||
if (i.create) i.create(emptyNode, vnode);
|
||||
if (i.insert) insertedVnodeQueue.push(vnode);
|
||||
}
|
||||
} else {
|
||||
vnode.elm = api.createTextNode(vnode.text as string);
|
||||
}
|
||||
return vnode.elm;
|
||||
}
|
||||
|
||||
function addVnodes(
|
||||
parentElm: Node,
|
||||
before: Node | null,
|
||||
vnodes: Array<VNode>,
|
||||
startIdx: number,
|
||||
endIdx: number,
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
const ch = vnodes[startIdx];
|
||||
if (ch != null) {
|
||||
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function invokeDestroyHook(vnode: VNode) {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
j: number,
|
||||
jLen: number,
|
||||
data = vnode.data;
|
||||
if (data !== undefined) {
|
||||
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
|
||||
for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) cbs.destroy[i](vnode);
|
||||
if (vnode.children !== undefined) {
|
||||
for (j = 0, jLen = vnode.children.length; j < jLen; ++j) {
|
||||
i = vnode.children[j];
|
||||
if (i != null && typeof i !== "string") {
|
||||
invokeDestroyHook(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVnodes(
|
||||
parentElm: Node,
|
||||
vnodes: Array<VNode>,
|
||||
startIdx: number,
|
||||
endIdx: number
|
||||
): void {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
let i: any,
|
||||
iLen: number,
|
||||
listeners: number,
|
||||
rm: () => void,
|
||||
ch = vnodes[startIdx];
|
||||
if (ch != null) {
|
||||
if (isDef(ch.sel)) {
|
||||
invokeDestroyHook(ch);
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(ch.elm as Node, listeners);
|
||||
for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) cbs.remove[i](ch, rm);
|
||||
if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) {
|
||||
i(ch, rm);
|
||||
} else {
|
||||
rm();
|
||||
}
|
||||
} else {
|
||||
// Text node
|
||||
api.removeChild(parentElm, ch.elm as Node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateChildren(
|
||||
parentElm: Node,
|
||||
oldCh: Array<VNode>,
|
||||
newCh: Array<VNode>,
|
||||
insertedVnodeQueue: VNodeQueue
|
||||
) {
|
||||
let oldStartIdx = 0,
|
||||
newStartIdx = 0;
|
||||
let oldEndIdx = oldCh.length - 1;
|
||||
let oldStartVnode = oldCh[0];
|
||||
let oldEndVnode = oldCh[oldEndIdx];
|
||||
let newEndIdx = newCh.length - 1;
|
||||
let newStartVnode = newCh[0];
|
||||
let newEndVnode = newCh[newEndIdx];
|
||||
let oldKeyToIdx: any;
|
||||
let idxInOld: number;
|
||||
let elmToMove: VNode;
|
||||
let before: any;
|
||||
|
||||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||||
if (oldStartVnode == null) {
|
||||
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
|
||||
} else if (oldEndVnode == null) {
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
} else if (newStartVnode == null) {
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else if (newEndVnode == null) {
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newStartVnode)) {
|
||||
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else if (sameVnode(oldEndVnode, newEndVnode)) {
|
||||
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newEndVnode)) {
|
||||
// Vnode moved right
|
||||
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
oldStartVnode.elm as Node,
|
||||
api.nextSibling(oldEndVnode.elm as Node)
|
||||
);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
||||
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
if (oldKeyToIdx === undefined) {
|
||||
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
idxInOld = oldKeyToIdx[newStartVnode.key as string];
|
||||
if (isUndef(idxInOld)) {
|
||||
// New element
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
createElm(newStartVnode, insertedVnodeQueue),
|
||||
oldStartVnode.elm as Node
|
||||
);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
elmToMove = oldCh[idxInOld];
|
||||
if (elmToMove.sel !== newStartVnode.sel) {
|
||||
api.insertBefore(
|
||||
parentElm,
|
||||
createElm(newStartVnode, insertedVnodeQueue),
|
||||
oldStartVnode.elm as Node
|
||||
);
|
||||
} else {
|
||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
||||
oldCh[idxInOld] = undefined as any;
|
||||
api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
|
||||
}
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
|
||||
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
|
||||
} else {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
|
||||
let i: any, iLen: number, hook: any;
|
||||
if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
const elm = (vnode.elm = oldVnode.elm as Node);
|
||||
let oldCh = oldVnode.children;
|
||||
let ch = vnode.children;
|
||||
if (oldVnode === vnode) return;
|
||||
if (vnode.data !== undefined) {
|
||||
for (i = 0, iLen = cbs.update.length; i < iLen; ++i) cbs.update[i](oldVnode, vnode);
|
||||
i = vnode.data.hook;
|
||||
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch)
|
||||
updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
|
||||
} else if (isDef(ch)) {
|
||||
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
|
||||
addVnodes(
|
||||
elm,
|
||||
null,
|
||||
ch as Array<VNode>,
|
||||
0,
|
||||
(ch as Array<VNode>).length - 1,
|
||||
insertedVnodeQueue
|
||||
);
|
||||
} else if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
||||
} else if (isDef(oldVnode.text)) {
|
||||
api.setTextContent(elm, "");
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
|
||||
}
|
||||
api.setTextContent(elm, vnode.text as string);
|
||||
}
|
||||
if (isDef(hook) && isDef((i = hook.postpatch))) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
}
|
||||
|
||||
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
|
||||
let i: number, iLen: number, elm: Node, parent: Node;
|
||||
const insertedVnodeQueue: VNodeQueue = [];
|
||||
for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) cbs.pre[i]();
|
||||
|
||||
if (!isVnode(oldVnode)) {
|
||||
oldVnode = emptyNodeAt(oldVnode);
|
||||
}
|
||||
|
||||
if (sameVnode(oldVnode, vnode)) {
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
||||
} else {
|
||||
elm = oldVnode.elm as Node;
|
||||
parent = api.parentNode(elm);
|
||||
|
||||
createElm(vnode, insertedVnodeQueue);
|
||||
|
||||
if (parent !== null) {
|
||||
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
|
||||
removeVnodes(parent, [oldVnode], 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) {
|
||||
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(
|
||||
insertedVnodeQueue[i]
|
||||
);
|
||||
}
|
||||
for (i = 0, iLen = cbs.post.length; i < iLen; ++i) cbs.post[i]();
|
||||
return vnode;
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// is.ts
|
||||
//------------------------------------------------------------------------------
|
||||
const array = Array.isArray;
|
||||
function primitive(s: any): s is string | number {
|
||||
return typeof s === "string" || typeof s === "number";
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// htmldomapi.ts
|
||||
//------------------------------------------------------------------------------
|
||||
interface DOMAPI {
|
||||
createElement: (tagName: any) => HTMLElement;
|
||||
createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
|
||||
createTextNode: (text: string) => Text;
|
||||
createComment: (text: string) => Comment;
|
||||
insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void;
|
||||
removeChild: (node: Node, child: Node) => void;
|
||||
appendChild: (node: Node, child: Node) => void;
|
||||
parentNode: (node: Node) => Node;
|
||||
nextSibling: (node: Node) => Node;
|
||||
tagName: (elm: Element) => string;
|
||||
setTextContent: (node: Node, text: string | null) => void;
|
||||
}
|
||||
|
||||
function createElement(tagName: any): HTMLElement {
|
||||
return document.createElement(tagName);
|
||||
}
|
||||
|
||||
function createElementNS(namespaceURI: string, qualifiedName: string): Element {
|
||||
return document.createElementNS(namespaceURI, qualifiedName);
|
||||
}
|
||||
|
||||
function createTextNode(text: string): Text {
|
||||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
function createComment(text: string): Comment {
|
||||
return document.createComment(text);
|
||||
}
|
||||
|
||||
function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void {
|
||||
parentNode.insertBefore(newNode, referenceNode);
|
||||
}
|
||||
|
||||
function removeChild(node: Node, child: Node): void {
|
||||
node.removeChild(child);
|
||||
}
|
||||
|
||||
function appendChild(node: Node, child: Node): void {
|
||||
node.appendChild(child);
|
||||
}
|
||||
|
||||
function parentNode(node: Node): Node | null {
|
||||
return node.parentNode;
|
||||
}
|
||||
|
||||
function nextSibling(node: Node): Node | null {
|
||||
return node.nextSibling;
|
||||
}
|
||||
|
||||
function tagName(elm: Element): string {
|
||||
return elm.tagName;
|
||||
}
|
||||
|
||||
function setTextContent(node: Node, text: string | null): void {
|
||||
node.textContent = text;
|
||||
}
|
||||
|
||||
const htmlDomApi = {
|
||||
createElement,
|
||||
createElementNS,
|
||||
createTextNode,
|
||||
createComment,
|
||||
insertBefore,
|
||||
removeChild,
|
||||
appendChild,
|
||||
parentNode,
|
||||
nextSibling,
|
||||
tagName,
|
||||
setTextContent,
|
||||
} as DOMAPI;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// hooks.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type PreHook = () => any;
|
||||
type InitHook = (vNode: VNode) => any;
|
||||
type CreateHook = (emptyVNode: VNode, vNode: VNode) => any;
|
||||
type InsertHook = (vNode: VNode) => any;
|
||||
type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any;
|
||||
type UpdateHook = (oldVNode: VNode, vNode: VNode) => any;
|
||||
type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any;
|
||||
type DestroyHook = (vNode: VNode) => any;
|
||||
type RemoveHook = (vNode: VNode, removeCallback: () => void) => any;
|
||||
type PostHook = () => any;
|
||||
|
||||
interface Hooks {
|
||||
pre?: PreHook;
|
||||
init?: InitHook;
|
||||
create?: CreateHook;
|
||||
insert?: InsertHook;
|
||||
prepatch?: PrePatchHook;
|
||||
update?: UpdateHook;
|
||||
postpatch?: PostPatchHook;
|
||||
destroy?: DestroyHook;
|
||||
remove?: RemoveHook;
|
||||
post?: PostHook;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// h.ts
|
||||
//------------------------------------------------------------------------------
|
||||
type VNodes = Array<VNode>;
|
||||
type VNodeChildElement = VNode | string | number | undefined | null;
|
||||
type ArrayOrElement<T> = T | T[];
|
||||
type VNodeChildren = ArrayOrElement<VNodeChildElement>;
|
||||
|
||||
export function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
|
||||
if (sel === "dummy") {
|
||||
// we do not need to add the namespace on dummy elements, they come from a
|
||||
// subcomponent, which will handle the namespace itself
|
||||
return;
|
||||
}
|
||||
data.ns = "http://www.w3.org/2000/svg";
|
||||
if (sel !== "foreignObject" && children !== undefined) {
|
||||
for (let i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
const child = children[i];
|
||||
let childData = child.data;
|
||||
if (childData !== undefined) {
|
||||
addNS(childData, (child as VNode).children as VNodes, child.sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function h(sel: string): VNode;
|
||||
export function h(sel: string, data: VNodeData): VNode;
|
||||
export function h(sel: string, children: VNodeChildren): VNode;
|
||||
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
|
||||
export function h(sel: any, b?: any, c?: any): VNode {
|
||||
var data: VNodeData = {},
|
||||
children: any,
|
||||
text: any,
|
||||
i: number,
|
||||
iLen: number;
|
||||
if (c !== undefined) {
|
||||
data = b;
|
||||
if (array(c)) {
|
||||
children = c;
|
||||
} else if (primitive(c)) {
|
||||
text = c;
|
||||
} else if (c && c.sel) {
|
||||
children = [c];
|
||||
}
|
||||
} else if (b !== undefined) {
|
||||
if (array(b)) {
|
||||
children = b;
|
||||
} else if (primitive(b)) {
|
||||
text = b;
|
||||
} else if (b && b.sel) {
|
||||
children = [b];
|
||||
} else {
|
||||
data = b;
|
||||
}
|
||||
}
|
||||
if (children !== undefined) {
|
||||
for (i = 0, iLen = children.length; i < iLen; ++i) {
|
||||
if (primitive(children[i]))
|
||||
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
|
||||
}
|
||||
}
|
||||
return vnode(sel, data, children, text, undefined);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`animations t-transition combined with component 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"Parent\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
// Component 'Child'
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, undefined);
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Child\`;
|
||||
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
const __patch2 = w2.__patch;
|
||||
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
|
||||
w2.destroy();
|
||||
};
|
||||
delete w2.__owl__.transitionInserted;
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition combined with t-component and t-if 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"Parent\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
if (scope['state'].display) {
|
||||
// Component 'Child'
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, undefined);
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Child\`;
|
||||
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
const __patch2 = w2.__patch;
|
||||
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
|
||||
w2.destroy();
|
||||
};
|
||||
delete w2.__owl__.transitionInserted;
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition combined with t-component, remove and re-add before transitionend 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__2\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
if (scope['state'].flag) {
|
||||
// Component 'Child'
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, undefined);
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Child\`;
|
||||
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
const __patch2 = w2.__patch;
|
||||
w2.__patch = (t, vn) => {__patch2.call(w2, t, vn); if(!w2.__owl__.transitionInserted){w2.__owl__.transitionInserted = true;utils.transitionInsert(w2.__owl__.vnode, 'chimay');}};
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {let finalize = () => {
|
||||
w2.destroy();
|
||||
};
|
||||
delete w2.__owl__.transitionInserted;
|
||||
utils.transitionRemove(vn, 'chimay', finalize);}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
}
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition with no delay/duration 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"test\\"
|
||||
let utils = this.constructor.utils;
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
utils.transitionInsert(vn, 'jupiler');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
utils.transitionRemove(vn, 'jupiler', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition, on a simple node (insert) 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"test\\"
|
||||
let utils = this.constructor.utils;
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
utils.transitionInsert(vn, 'chimay');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
utils.transitionRemove(vn, 'chimay', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`animations t-transition, on a simple node, not in the DOM 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"test\\"
|
||||
let utils = this.constructor.utils;
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('span', p1, c1);
|
||||
p1.hook = {
|
||||
insert: vn => {
|
||||
utils.transitionInsert(vn, 'chimay');
|
||||
},
|
||||
remove: (vn, rm) => {
|
||||
utils.transitionRemove(vn, 'chimay', rm);
|
||||
},
|
||||
};
|
||||
c1.push({text: \`blue\`});
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`various scenarios scenarios with async store updates and some components events 1`] = `"<div><button>Do stuff</button><div><span>Attachment 100</span><span>Name: text.txt</span></div></div>"`;
|
||||
|
||||
exports[`various scenarios scenarios with async store updates and some components events 2`] = `"<div><button>Do stuff</button></div>"`;
|
||||
@@ -0,0 +1,452 @@
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { useRef, useState } from "../src/hooks";
|
||||
import { QWeb } from "../src/qweb/index";
|
||||
import { xml } from "../src/tags";
|
||||
import {
|
||||
makeDeferred,
|
||||
makeTestEnv,
|
||||
makeTestFixture,
|
||||
nextFrame,
|
||||
patchNextFrame,
|
||||
renderToDOM,
|
||||
unpatchNextFrame,
|
||||
} from "./helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// We create before each test:
|
||||
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||
// manipulations. Note that it is removed after each test.
|
||||
// - qweb: a new QWeb instance
|
||||
// - env: a WEnv, necessary to create new components
|
||||
// - cssEl: a stylesheet injected into the dom
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let qweb: QWeb;
|
||||
let env: Env;
|
||||
let cssEl: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
Component.env = env;
|
||||
qweb = new QWeb();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
class Widget extends Component<any, any> {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
describe("animations", () => {
|
||||
beforeEach(() => {
|
||||
cssEl = document.createElement("style");
|
||||
let css = `
|
||||
.chimay-enter-active, .chimay-leave-active {
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
.chimay-enter, .chimay-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
cssEl.textContent = css.trim();
|
||||
document.head.appendChild(cssEl);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.head.removeChild(cssEl);
|
||||
unpatchNextFrame();
|
||||
});
|
||||
|
||||
test("t-transition, on a simple node (insert)", async () => {
|
||||
expect.assertions(5);
|
||||
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
|
||||
|
||||
let def = makeDeferred();
|
||||
patchNextFrame((cb) => {
|
||||
expect(node.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
|
||||
fixture.appendChild(node);
|
||||
|
||||
expect(node.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
node.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(node.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition, on a simple node, not in the DOM", async () => {
|
||||
expect.assertions(5);
|
||||
qweb.addTemplate("test", `<span t-transition="chimay">blue</span>`);
|
||||
|
||||
let def = makeDeferred();
|
||||
patchNextFrame((cb) => {
|
||||
expect(node.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
|
||||
|
||||
expect(node.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
node.dispatchEvent(new Event("transitionend"));
|
||||
// we check here that the css classes have not been removed, since the
|
||||
// element is not in the dom, we actually do not want to do anything.
|
||||
expect(node.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
});
|
||||
|
||||
test("t-transition with no delay/duration", async () => {
|
||||
expect.assertions(4);
|
||||
qweb.addTemplate("test", `<span t-transition="jupiler">blue</span>`);
|
||||
|
||||
let def = makeDeferred();
|
||||
patchNextFrame((cb) => {
|
||||
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
|
||||
cb();
|
||||
expect(node.className).toBe("");
|
||||
def.resolve();
|
||||
});
|
||||
let node: HTMLElement = <HTMLElement>renderToDOM(qweb, "test");
|
||||
fixture.appendChild(node);
|
||||
expect(node.className).toBe("jupiler-enter jupiler-enter-active");
|
||||
await def;
|
||||
});
|
||||
|
||||
test("t-transition on a conditional node", async () => {
|
||||
expect.assertions(7);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-if="!state.hide" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = useState({ hide: false });
|
||||
}
|
||||
const widget = new TestWidget();
|
||||
|
||||
// insert widget into the DOM
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame((cb) => {
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
|
||||
// remove span from the DOM
|
||||
def = makeDeferred();
|
||||
widget.state.hide = true;
|
||||
patchNextFrame((cb) => {
|
||||
expect(spanNode.className).toBe("chimay-leave chimay-leave-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-leave-active chimay-leave-to");
|
||||
def.resolve();
|
||||
});
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition combined with t-ref", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"TestWidget",
|
||||
`<div><span t-ref="span" t-transition="chimay">blue</span></div>`
|
||||
);
|
||||
class TestWidget extends Widget {
|
||||
state = useState({ hide: false });
|
||||
span = useRef("span");
|
||||
}
|
||||
const widget = new TestWidget();
|
||||
|
||||
// insert widget into the DOM
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame((cb) => {
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
cb();
|
||||
expect(spanNode.className).toBe("chimay-enter-active chimay-enter-to");
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
expect(widget.span.el).toBe(spanNode);
|
||||
expect(spanNode.className).toBe("chimay-enter chimay-enter-active");
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(spanNode.className).toBe("");
|
||||
});
|
||||
|
||||
test("t-transition combined with component", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
env.qweb.addTemplate("Parent", `<div><Child t-transition="chimay"/></div>`);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child: Child };
|
||||
}
|
||||
const widget = new Parent();
|
||||
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame((cb) => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
});
|
||||
|
||||
test("t-transition combined with t-component and t-if", async () => {
|
||||
expect.assertions(8);
|
||||
|
||||
env.qweb.addTemplate(
|
||||
"Parent",
|
||||
`<div><t t-if="state.display" t-component="Child" t-transition="chimay"/></div>`
|
||||
);
|
||||
env.qweb.addTemplate("Child", `<span>blue</span>`);
|
||||
class Child extends Widget {}
|
||||
class Parent extends Widget {
|
||||
static components = { Child: Child };
|
||||
state = useState({ display: true });
|
||||
}
|
||||
const widget = new Parent();
|
||||
|
||||
let def = makeDeferred();
|
||||
var spanNode;
|
||||
patchNextFrame((cb) => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter-active chimay-enter-to">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await widget.mount(fixture);
|
||||
spanNode = widget.el!.children[0];
|
||||
|
||||
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter chimay-enter-active">blue</span></div>'
|
||||
);
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
|
||||
// remove span from the DOM
|
||||
def = makeDeferred();
|
||||
widget.state.display = false;
|
||||
patchNextFrame((cb) => {
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave chimay-leave-active" data-owl-key="__3__">blue</span></div>'
|
||||
);
|
||||
cb();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
|
||||
);
|
||||
def.resolve();
|
||||
});
|
||||
await def; // wait for the mocked repaint to be done
|
||||
spanNode.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
});
|
||||
|
||||
test("t-transition, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
env.qweb.addTemplates(
|
||||
`<templates>
|
||||
<div t-name="Parent">
|
||||
<button t-on-click="toggle">Toggle</button>
|
||||
<span t-if="state.flag" t-transition="chimay">blue</span>
|
||||
</div>
|
||||
</templates>`
|
||||
);
|
||||
class Parent extends Widget {
|
||||
state = useState({ flag: false });
|
||||
|
||||
toggle() {
|
||||
this.state.flag = !this.state.flag;
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new Parent();
|
||||
await widget.mount(fixture);
|
||||
let button = widget.el!.querySelector("button");
|
||||
|
||||
let def = makeDeferred();
|
||||
let phase = "enter";
|
||||
patchNextFrame((cb) => {
|
||||
let spans = fixture.querySelectorAll("span");
|
||||
expect(spans.length).toBe(1);
|
||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||
cb();
|
||||
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
// click display the span
|
||||
button!.click();
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||
|
||||
// click to remove the span, and click again to re-add it before transitionend
|
||||
def = makeDeferred();
|
||||
phase = "leave";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
def = makeDeferred();
|
||||
phase = "enter";
|
||||
button!.click();
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><button>Toggle</button><span class="">blue</span></div>');
|
||||
});
|
||||
|
||||
test("t-transition combined with t-component, remove and re-add before transitionend", async () => {
|
||||
expect.assertions(12);
|
||||
|
||||
class Child extends Widget {
|
||||
static template = xml`<span>blue</span>`;
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static template = xml`
|
||||
<div t-name="Parent">
|
||||
<t t-if="state.flag" t-component="Child" t-transition="chimay"/>
|
||||
</div>`;
|
||||
static components = { Child };
|
||||
state = useState({ flag: false });
|
||||
}
|
||||
|
||||
const widget = new Parent();
|
||||
await widget.mount(fixture);
|
||||
expect(env.qweb.templates[Parent.template].fn.toString()).toMatchSnapshot();
|
||||
|
||||
let def = makeDeferred();
|
||||
let phase = "enter";
|
||||
patchNextFrame((cb) => {
|
||||
let spans = fixture.querySelectorAll("span");
|
||||
expect(spans.length).toBe(1);
|
||||
expect(spans[0].className).toBe(`chimay-${phase} chimay-${phase}-active`);
|
||||
cb();
|
||||
expect(spans[0].className).toBe(`chimay-${phase}-active chimay-${phase}-to`);
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
// display the span
|
||||
widget.state.flag = true;
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
|
||||
// click to remove the span, and click again to re-add it before transitionend
|
||||
def = makeDeferred();
|
||||
phase = "leave";
|
||||
|
||||
widget.state.flag = false;
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
def = makeDeferred();
|
||||
phase = "enter";
|
||||
widget.state.flag = true;
|
||||
|
||||
await def; // wait for the mocked repaint to be done
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend")); // mock end of css transition
|
||||
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
|
||||
});
|
||||
|
||||
test("transitionInsert is called the correct amount of times", async () => {
|
||||
const oldTransitionInsert = QWeb.utils.transitionInsert;
|
||||
QWeb.utils.transitionInsert = jest.fn(oldTransitionInsert);
|
||||
|
||||
class Child extends Widget {
|
||||
static template = xml`<span>blue</span>`;
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static template = xml`
|
||||
<div t-name="Parent">
|
||||
<Child t-if="state.flag" t-transition="chimay"/>
|
||||
</div>`;
|
||||
static components = { Child };
|
||||
state = useState({ flag: false });
|
||||
}
|
||||
|
||||
patchNextFrame((cb) => cb());
|
||||
|
||||
const widget = new Parent();
|
||||
await widget.mount(fixture);
|
||||
|
||||
widget.state.flag = true;
|
||||
|
||||
await nextFrame();
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
|
||||
expect(fixture.innerHTML).toBe('<div><span class="">blue</span></div>');
|
||||
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
|
||||
|
||||
widget.state.flag = false;
|
||||
await nextFrame();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-leave-active chimay-leave-to" data-owl-key="__3__">blue</span></div>'
|
||||
);
|
||||
expect(QWeb.utils.transitionInsert).toBeCalledTimes(1);
|
||||
|
||||
widget.state.flag = true;
|
||||
await nextFrame();
|
||||
expect(fixture.innerHTML).toBe(
|
||||
'<div><span class="chimay-enter-active chimay-enter-to" data-owl-key="__3__">blue</span></div>'
|
||||
);
|
||||
expect(QWeb.utils.transitionInsert).toBeCalledTimes(2);
|
||||
|
||||
widget.state.flag = false;
|
||||
await nextFrame();
|
||||
widget.state.flag = true;
|
||||
await nextFrame();
|
||||
|
||||
expect(QWeb.utils.transitionInsert).toBeCalledTimes(3);
|
||||
widget.el!.querySelector("span")!.dispatchEvent(new Event("transitionend"));
|
||||
expect(fixture.innerHTML).toBe('<div><span class="" data-owl-key="__3__">blue</span></div>');
|
||||
QWeb.utils.transitionInsert = oldTransitionInsert;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`class and style attributes with t-component dynamic t-att-style is properly added and updated on widget root el 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"ParentWidget\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
// Component 'child'
|
||||
const _4 = scope['state'].style;
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, undefined).then(()=>{if (w2.__owl__.status === 5) {return};w2.el.style=_4;});;
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`child\`;
|
||||
let W2 = scope['child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.style = _4;}});});
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"ParentWidget\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
context.__owl__.refs = context.__owl__.refs || {};
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
// Component 'Child'
|
||||
const ref4 = \`child\`;
|
||||
let _5 = {'a':true};
|
||||
Object.assign(_5, {b:scope['state'].b})
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, undefined);
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Child\`;
|
||||
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){}});});
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.classObj=_5;
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v2) 2`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"Child\\"
|
||||
let utils = this.constructor.utils;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let _7 = {'c':true};
|
||||
Object.assign(_7, utils.toClassObj({d:scope['state'].d}))
|
||||
let c8 = [], p8 = {key:8,class:_7};
|
||||
let vn8 = h('span', p8, c8);
|
||||
return vn8;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"ParentWidget\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
context.__owl__.refs = context.__owl__.refs || {};
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
// Component 'Child'
|
||||
const ref4 = \`child\`;
|
||||
let _5 = {'a':true};
|
||||
Object.assign(_5, utils.toClassObj(scope['state'].b?'b':''))
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, undefined);
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Child\`;
|
||||
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){}});});
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {insert(vn) {context.__owl__.refs[ref4] = w2;},remove() {},destroy(vn) {w2.destroy();delete context.__owl__.refs[ref4];}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.classObj=_5;
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`class and style attributes with t-component t-att-class is properly added/removed on widget root el (v3) 2`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"Child\\"
|
||||
let utils = this.constructor.utils;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let _7 = {'c':true};
|
||||
Object.assign(_7, utils.toClassObj(scope['state'].d?'d':''))
|
||||
let c8 = [], p8 = {key:8,class:_7};
|
||||
let vn8 = h('span', p8, c8);
|
||||
return vn8;
|
||||
}"
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"App\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
// Component 'Child'
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {message:1};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, undefined);
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Child\`;
|
||||
let W2 = scope['Child'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
@@ -0,0 +1,758 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`t-slot directive can define and call slots 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"Parent\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
// Component 'Dialog'
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Dialog\`;
|
||||
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
w2.__owl__.slotId = 1;
|
||||
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive can define and call slots 2`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"Dialog\\"
|
||||
let h = this.h;
|
||||
let c8 = [], p8 = {key:8};
|
||||
let vn8 = h('div', p8, c8);
|
||||
let c9 = [], p9 = {key:9};
|
||||
let vn9 = h('div', p9, c9);
|
||||
c8.push(vn9);
|
||||
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
|
||||
if (slot10) {
|
||||
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
|
||||
}
|
||||
let c11 = [], p11 = {key:11};
|
||||
let vn11 = h('div', p11, c11);
|
||||
c8.push(vn11);
|
||||
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
|
||||
if (slot12) {
|
||||
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
|
||||
}
|
||||
return vn8;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive can define and call slots 3`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_header_template\\"
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c4 = extra.parentNode;
|
||||
let c5 = [], p5 = {key:5};
|
||||
let vn5 = h('span', p5, c5);
|
||||
c4.push(vn5);
|
||||
c5.push({text: \`header\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive can define and call slots 4`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_footer_template\\"
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c6 = extra.parentNode;
|
||||
let c7 = [], p7 = {key:7};
|
||||
let vn7 = h('span', p7, c7);
|
||||
c6.push(vn7);
|
||||
c7.push({text: \`footer\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive can define and call slots using old t-set keyword 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__2\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
// Component 'Dialog'
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Dialog\`;
|
||||
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
w2.__owl__.slotId = 1;
|
||||
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive can define and call slots using old t-set keyword 2`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__1\\"
|
||||
let h = this.h;
|
||||
let c8 = [], p8 = {key:8};
|
||||
let vn8 = h('div', p8, c8);
|
||||
let c9 = [], p9 = {key:9};
|
||||
let vn9 = h('div', p9, c9);
|
||||
c8.push(vn9);
|
||||
const slot10 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
|
||||
if (slot10) {
|
||||
slot10.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
|
||||
}
|
||||
let c11 = [], p11 = {key:11};
|
||||
let vn11 = h('div', p11, c11);
|
||||
c8.push(vn11);
|
||||
const slot12 = this.constructor.slots[context.__owl__.slotId + '_' + 'footer'];
|
||||
if (slot12) {
|
||||
slot12.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c11, parent: extra.parent || context}));
|
||||
}
|
||||
return vn8;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive can define and call slots using old t-set keyword 3`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_header_template\\"
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c4 = extra.parentNode;
|
||||
let c5 = [], p5 = {key:5};
|
||||
let vn5 = h('span', p5, c5);
|
||||
c4.push(vn5);
|
||||
c5.push({text: \`header\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive can define and call slots using old t-set keyword 4`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_footer_template\\"
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c6 = extra.parentNode;
|
||||
let c7 = [], p7 = {key:7};
|
||||
let vn7 = h('span', p7, c7);
|
||||
c6.push(vn7);
|
||||
c7.push({text: \`footer\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive content is the default slot 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_default_template\\"
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c4 = extra.parentNode;
|
||||
let c5 = [], p5 = {key:5};
|
||||
let vn5 = h('span', p5, c5);
|
||||
c4.push(vn5);
|
||||
c5.push({text: \`sts rocks\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive dafault slots can define a default content 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__1\\"
|
||||
let h = this.h;
|
||||
let c4 = [], p4 = {key:4};
|
||||
let vn4 = h('span', p4, c4);
|
||||
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||
if (slot5) {
|
||||
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
|
||||
} else {
|
||||
c4.push({text: \`default content\`});
|
||||
}
|
||||
return vn4;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive default slot next to named slot, with default content 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__2\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
// Component 'Dialog'
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Dialog\`;
|
||||
let W2 = scope['Dialog'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
w2.__owl__.slotId = 1;
|
||||
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive default slot work with text nodes 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_default_template\\"
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c4 = extra.parentNode;
|
||||
c4.push({text: \`sts rocks\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive dynamic t-slot call 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__1\\"
|
||||
let utils = this.constructor.utils;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c9 = [], p9 = {key:9,on:{}};
|
||||
let vn9 = h('button', p9, c9);
|
||||
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['toggle'](e);};
|
||||
p9.on['click'] = extra.handlers['click__10__'];
|
||||
const slot11 = this.constructor.slots[context.__owl__.slotId + '_' + (scope['current'].slot)];
|
||||
if (slot11) {
|
||||
slot11.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c9, parent: extra.parent || context}));
|
||||
}
|
||||
return vn9;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive multiple roots are allowed in a default slot 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_default_template\\"
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c4 = extra.parentNode;
|
||||
let c5 = [], p5 = {key:5};
|
||||
let vn5 = h('span', p5, c5);
|
||||
c4.push(vn5);
|
||||
c5.push({text: \`sts\`});
|
||||
let c6 = [], p6 = {key:6};
|
||||
let vn6 = h('span', p6, c6);
|
||||
c4.push(vn6);
|
||||
c6.push({text: \`rocks\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive multiple roots are allowed in a named slot 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_content_template\\"
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c4 = extra.parentNode;
|
||||
let c5 = [], p5 = {key:5};
|
||||
let vn5 = h('span', p5, c5);
|
||||
c4.push(vn5);
|
||||
c5.push({text: \`sts\`});
|
||||
let c6 = [], p6 = {key:6};
|
||||
let vn6 = h('span', p6, c6);
|
||||
c4.push(vn6);
|
||||
c6.push({text: \`rocks\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive named slots can define a default content 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__1\\"
|
||||
let h = this.h;
|
||||
let c4 = [], p4 = {key:4};
|
||||
let vn4 = h('span', p4, c4);
|
||||
const slot5 = this.constructor.slots[context.__owl__.slotId + '_' + 'header'];
|
||||
if (slot5) {
|
||||
slot5.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c4, parent: extra.parent || context}));
|
||||
} else {
|
||||
c4.push({text: \`default content\`});
|
||||
}
|
||||
return vn4;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive refs are properly bound in slots 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_footer_template\\"
|
||||
let utils = this.constructor.utils;
|
||||
let parent = extra.parent;
|
||||
context.__owl__.refs = context.__owl__.refs || {};
|
||||
let h = this.h;
|
||||
let c8 = extra.parentNode;
|
||||
let c9 = [], p9 = {key:9,on:{}};
|
||||
let vn9 = h('button', p9, c9);
|
||||
c8.push(vn9);
|
||||
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);};
|
||||
p9.on['click'] = extra.handlers['click__10__'];
|
||||
const ref11 = \`myButton\`;
|
||||
p9.hook = {
|
||||
create: (_, n) => {
|
||||
context.__owl__.refs[ref11] = n.elm;
|
||||
},
|
||||
destroy: () => {
|
||||
delete context.__owl__.refs[ref11];
|
||||
},
|
||||
};
|
||||
c9.push({text: \`do something\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_footer_template\\"
|
||||
let utils = this.constructor.utils;
|
||||
let parent = extra.parent;
|
||||
let h = this.h;
|
||||
let c8 = extra.parentNode;
|
||||
let c9 = [], p9 = {key:9,on:{}};
|
||||
let vn9 = h('button', p9, c9);
|
||||
c8.push(vn9);
|
||||
extra.handlers['click__10__'] = extra.handlers['click__10__'] || function (e) {if (context.__owl__.status === 5){return}utils.getComponent(context)['doSomething'](e);};
|
||||
p9.on['click'] = extra.handlers['click__10__'];
|
||||
c9.push({text: \`do something\`});
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context, part 2 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"Link\\"
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let _12 = scope['props'].to;
|
||||
let c13 = [], p13 = {key:13,attrs:{href: _12}};
|
||||
let vn13 = h('a', p13, c13);
|
||||
const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||
if (slot14) {
|
||||
slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
|
||||
}
|
||||
return vn13;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context, part 2 2`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"App\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2};
|
||||
let vn2 = h('u', p2, c2);
|
||||
c1.push(vn2);
|
||||
let _3 = scope['state'].users;
|
||||
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
let _4 = _3;
|
||||
let _5 = _3;
|
||||
if (!(_3 instanceof Array)) {
|
||||
_4 = Object.keys(_3);
|
||||
_5 = Object.values(_3);
|
||||
}
|
||||
let _length4 = _4.length;
|
||||
let _origScope6 = scope;
|
||||
scope = Object.create(scope);
|
||||
for (let i1 = 0; i1 < _length4; i1++) {
|
||||
scope.user_first = i1 === 0
|
||||
scope.user_last = i1 === _length4 - 1
|
||||
scope.user_index = i1
|
||||
scope.user = _4[i1]
|
||||
scope.user_value = _5[i1]
|
||||
let key1 = scope['user'].id;
|
||||
let c7 = [], p7 = {key:\`\${key1}_7\`};
|
||||
let vn7 = h('li', p7, c7);
|
||||
c2.push(vn7);
|
||||
// Component 'Link'
|
||||
let k9 = \`__9__\${key1}__\`;
|
||||
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
|
||||
let props8 = {to:'/user/'+scope['user'].id};
|
||||
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
|
||||
w8.destroy();
|
||||
w8 = false;
|
||||
}
|
||||
if (w8) {
|
||||
w8.__updateProps(props8, extra.fiber, utils.combine(context, scope));
|
||||
let pvnode = w8.__owl__.pvnode;
|
||||
c7.push(pvnode);
|
||||
} else {
|
||||
let componentKey8 = \`Link\`;
|
||||
let W8 = scope['Link'] || context.constructor.components[componentKey8] || QWeb.components[componentKey8];
|
||||
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
|
||||
w8 = new W8(parent, props8);
|
||||
parent.__owl__.cmap[k9] = w8.__owl__.id;
|
||||
w8.__owl__.slotId = 1;
|
||||
let fiber = w8.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
|
||||
c7.push(pvnode);
|
||||
w8.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w8.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
}
|
||||
scope = _origScope6;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context, part 2 3`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_default_template\\"
|
||||
let parent = extra.parent;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c10 = extra.parentNode;
|
||||
c10.push({text: \`User \`});
|
||||
let _11 = scope['user'].name;
|
||||
if (_11 != null) {
|
||||
c10.push({text: _11});
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context, part 3 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"Link\\"
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let _12 = scope['props'].to;
|
||||
let c13 = [], p13 = {key:13,attrs:{href: _12}};
|
||||
let vn13 = h('a', p13, c13);
|
||||
const slot14 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||
if (slot14) {
|
||||
slot14.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c13, parent: extra.parent || context}));
|
||||
}
|
||||
return vn13;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context, part 3 2`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"App\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
let c2 = [], p2 = {key:2};
|
||||
let vn2 = h('u', p2, c2);
|
||||
c1.push(vn2);
|
||||
let _3 = scope['state'].users;
|
||||
if (!_3) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
let _4 = _3;
|
||||
let _5 = _3;
|
||||
if (!(_3 instanceof Array)) {
|
||||
_4 = Object.keys(_3);
|
||||
_5 = Object.values(_3);
|
||||
}
|
||||
let _length4 = _4.length;
|
||||
let _origScope6 = scope;
|
||||
scope = Object.create(scope);
|
||||
for (let i1 = 0; i1 < _length4; i1++) {
|
||||
scope.user_first = i1 === 0
|
||||
scope.user_last = i1 === _length4 - 1
|
||||
scope.user_index = i1
|
||||
scope.user = _4[i1]
|
||||
scope.user_value = _5[i1]
|
||||
let key1 = scope['user'].id;
|
||||
let c7 = [], p7 = {key:\`\${key1}_7\`};
|
||||
let vn7 = h('li', p7, c7);
|
||||
c2.push(vn7);
|
||||
utils.getScope(scope, 'userdescr').userdescr = 'User '+scope['user'].name;
|
||||
// Component 'Link'
|
||||
let k9 = \`__9__\${key1}__\`;
|
||||
let w8 = k9 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k9]] : false;
|
||||
let props8 = {to:'/user/'+scope['user'].id};
|
||||
if (w8 && w8.__owl__.currentFiber && !w8.__owl__.vnode) {
|
||||
w8.destroy();
|
||||
w8 = false;
|
||||
}
|
||||
if (w8) {
|
||||
w8.__updateProps(props8, extra.fiber, utils.combine(context, scope));
|
||||
let pvnode = w8.__owl__.pvnode;
|
||||
c7.push(pvnode);
|
||||
} else {
|
||||
let componentKey8 = \`Link\`;
|
||||
let W8 = scope['Link'] || context.constructor.components[componentKey8] || QWeb.components[componentKey8];
|
||||
if (!W8) {throw new Error('Cannot find the definition of component \\"' + componentKey8 + '\\"')}
|
||||
w8 = new W8(parent, props8);
|
||||
parent.__owl__.cmap[k9] = w8.__owl__.id;
|
||||
w8.__owl__.slotId = 1;
|
||||
let fiber = w8.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: k9, hook: {remove() {},destroy(vn) {w8.destroy();}}});
|
||||
c7.push(pvnode);
|
||||
w8.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w8.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
}
|
||||
scope = _origScope6;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context, part 3 3`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_default_template\\"
|
||||
let parent = extra.parent;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c10 = extra.parentNode;
|
||||
let _11 = scope['userdescr'];
|
||||
if (_11 != null) {
|
||||
c10.push({text: _11});
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context, part 4 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"App\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
scope.userdescr = 'User '+scope['state'].user.name;
|
||||
// Component 'Link'
|
||||
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
|
||||
let props2 = {to:'/user/'+scope['state'].user.id};
|
||||
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
|
||||
w2.destroy();
|
||||
w2 = false;
|
||||
}
|
||||
if (w2) {
|
||||
w2.__updateProps(props2, extra.fiber, utils.combine(context, scope));
|
||||
let pvnode = w2.__owl__.pvnode;
|
||||
c1.push(pvnode);
|
||||
} else {
|
||||
let componentKey2 = \`Link\`;
|
||||
let W2 = scope['Link'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2];
|
||||
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
|
||||
w2 = new W2(parent, props2);
|
||||
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
|
||||
w2.__owl__.slotId = 1;
|
||||
let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
|
||||
c1.push(pvnode);
|
||||
w2.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w2.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots are rendered with proper context, part 4 2`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"slot_default_template\\"
|
||||
let parent = extra.parent;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c4 = extra.parentNode;
|
||||
let _5 = scope['userdescr'];
|
||||
if (_5 != null) {
|
||||
c4.push({text: _5});
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive slots in t-foreach in t-foreach 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__2\\"
|
||||
let utils = this.constructor.utils;
|
||||
let QWeb = this.constructor;
|
||||
let parent = context;
|
||||
let scope = Object.create(context);
|
||||
let h = this.h;
|
||||
let c1 = [], p1 = {key:1};
|
||||
let vn1 = h('div', p1, c1);
|
||||
let _2 = scope['tree'];
|
||||
if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
let _3 = _2;
|
||||
let _4 = _2;
|
||||
if (!(_2 instanceof Array)) {
|
||||
_3 = Object.keys(_2);
|
||||
_4 = Object.values(_2);
|
||||
}
|
||||
let _length3 = _3.length;
|
||||
let _origScope5 = scope;
|
||||
scope = Object.create(scope);
|
||||
for (let i1 = 0; i1 < _length3; i1++) {
|
||||
scope.node1_first = i1 === 0
|
||||
scope.node1_last = i1 === _length3 - 1
|
||||
scope.node1_index = i1
|
||||
scope.node1 = _3[i1]
|
||||
scope.node1_value = _4[i1]
|
||||
let key1 = scope['node1'].key;
|
||||
let c6 = [], p6 = {key:\`\${key1}_6\`};
|
||||
let vn6 = h('div', p6, c6);
|
||||
c1.push(vn6);
|
||||
let _7 = scope['node1'].value;
|
||||
if (_7 != null) {
|
||||
c6.push({text: _7});
|
||||
}
|
||||
let c8 = [], p8 = {key:\`\${key1}_8\`};
|
||||
let vn8 = h('ul', p8, c8);
|
||||
c1.push(vn8);
|
||||
let _9 = scope['node1'].nodes;
|
||||
if (!_9) { throw new Error('QWeb error: Invalid loop expression')}
|
||||
let _10 = _9;
|
||||
let _11 = _9;
|
||||
if (!(_9 instanceof Array)) {
|
||||
_10 = Object.keys(_9);
|
||||
_11 = Object.values(_9);
|
||||
}
|
||||
let _length10 = _10.length;
|
||||
let _origScope12 = scope;
|
||||
scope = Object.create(scope);
|
||||
for (let i2 = 0; i2 < _length10; i2++) {
|
||||
scope.node2_first = i2 === 0
|
||||
scope.node2_last = i2 === _length10 - 1
|
||||
scope.node2_index = i2
|
||||
scope.node2 = _10[i2]
|
||||
scope.node2_value = _11[i2]
|
||||
let key2 = scope['node2'].key;
|
||||
// Component 'Child'
|
||||
let k14 = \`__14__\${key1}__\${key2}__\`;
|
||||
let w13 = k14 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k14]] : false;
|
||||
let props13 = {};
|
||||
if (w13 && w13.__owl__.currentFiber && !w13.__owl__.vnode) {
|
||||
w13.destroy();
|
||||
w13 = false;
|
||||
}
|
||||
if (w13) {
|
||||
w13.__updateProps(props13, extra.fiber, utils.combine(context, scope));
|
||||
let pvnode = w13.__owl__.pvnode;
|
||||
c8.push(pvnode);
|
||||
} else {
|
||||
let componentKey13 = \`Child\`;
|
||||
let W13 = scope['Child'] || context.constructor.components[componentKey13] || QWeb.components[componentKey13];
|
||||
if (!W13) {throw new Error('Cannot find the definition of component \\"' + componentKey13 + '\\"')}
|
||||
w13 = new W13(parent, props13);
|
||||
parent.__owl__.cmap[k14] = w13.__owl__.id;
|
||||
w13.__owl__.slotId = 1;
|
||||
let fiber = w13.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; });
|
||||
let pvnode = h('dummy', {key: k14, hook: {remove() {},destroy(vn) {w13.destroy();}}});
|
||||
c8.push(pvnode);
|
||||
w13.__owl__.pvnode = pvnode;
|
||||
}
|
||||
w13.__owl__.parentLastFiberId = extra.fiber.id;
|
||||
}
|
||||
scope = _origScope12;
|
||||
}
|
||||
scope = _origScope5;
|
||||
return vn1;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive t-set t-value in a slot 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__1\\"
|
||||
let h = this.h;
|
||||
let c5 = [], p5 = {key:5};
|
||||
let vn5 = h('span', p5, c5);
|
||||
const slot6 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||
if (slot6) {
|
||||
slot6.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: c5, parent: extra.parent || context}));
|
||||
}
|
||||
return vn5;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-slot directive template can just return a slot 1`] = `
|
||||
"function anonymous(context, extra
|
||||
) {
|
||||
// Template name: \\"__template__2\\"
|
||||
let utils = this.constructor.utils;
|
||||
let result;
|
||||
let h = this.h;
|
||||
const slot7 = this.constructor.slots[context.__owl__.slotId + '_' + 'default'];
|
||||
if (slot7) {
|
||||
let children8= []
|
||||
result = {}
|
||||
slot7.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children8, parent: extra.parent || context}));
|
||||
utils.defineProxy(result, children8[0]);
|
||||
}
|
||||
return result;
|
||||
}"
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,273 @@
|
||||
import { Component, Env } from "../../src/component/component";
|
||||
import { QWeb } from "../../src/qweb/qweb";
|
||||
import { xml } from "../../src/tags";
|
||||
import { useState, useRef } from "../../src/hooks";
|
||||
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// We create before each test:
|
||||
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||
// manipulations. Note that it is removed after each test.
|
||||
// - env: a WEnv, necessary to create new components
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
Component.env = env;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
describe("class and style attributes with t-component", () => {
|
||||
test("class is properly added on widget root el", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<div class="c"/>`;
|
||||
}
|
||||
class ParentWidget extends Component {
|
||||
static template = xml`<div><Child class="a b"/></div>`;
|
||||
static components = { Child };
|
||||
}
|
||||
const widget = new ParentWidget();
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div><div class="c a b"></div></div>`);
|
||||
});
|
||||
|
||||
test("empty class attribute is not added on widget root el", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<span/>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><Child class=""/></div>`;
|
||||
static components = { Child };
|
||||
}
|
||||
const widget = new Parent();
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
|
||||
});
|
||||
|
||||
test("t-att-class is properly added/removed on widget root el", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<div class="c"/>`;
|
||||
}
|
||||
class ParentWidget extends Component {
|
||||
static template = xml`<div><Child t-att-class="{a:state.a, b:state.b}"/></div>`;
|
||||
static components = { Child };
|
||||
state = useState({ a: true, b: false });
|
||||
}
|
||||
const widget = new ParentWidget();
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div><div class="c a"></div></div>`);
|
||||
expect(QWeb.TEMPLATES[ParentWidget.template].fn.toString());
|
||||
|
||||
widget.state.a = false;
|
||||
widget.state.b = true;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe(`<div><div class="c b"></div></div>`);
|
||||
});
|
||||
|
||||
test("class with extra whitespaces", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`<div>
|
||||
<Child class="a b c d"/>
|
||||
</div>`
|
||||
);
|
||||
class Child extends Component {}
|
||||
class ParentWidget extends Component {
|
||||
static components = { Child };
|
||||
}
|
||||
env.qweb.addTemplate("Child", `<div/>`);
|
||||
const widget = new ParentWidget();
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div><div class="a b c d"></div></div>`);
|
||||
});
|
||||
|
||||
test("t-att-class is properly added/removed on widget root el (v2)", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget">
|
||||
<Child class="a" t-att-class="{ b: state.b }" t-ref="child"/>
|
||||
</div>
|
||||
<span t-name="Child" class="c" t-att-class="{ d: state.d }"/>
|
||||
</templates>`);
|
||||
|
||||
class Child extends Component {
|
||||
state = useState({ d: true });
|
||||
}
|
||||
class ParentWidget extends Component {
|
||||
static components = { Child };
|
||||
state = useState({ b: true });
|
||||
child = useRef("child");
|
||||
}
|
||||
const widget = new ParentWidget();
|
||||
await widget.mount(fixture);
|
||||
|
||||
const span = fixture.querySelector("span")!;
|
||||
expect(span.className).toBe("c d a b");
|
||||
|
||||
widget.state.b = false;
|
||||
await nextTick();
|
||||
expect(span.className).toBe("c d a");
|
||||
|
||||
(widget.child.comp as Child).state.d = false;
|
||||
await nextTick();
|
||||
expect(span.className).toBe("c a");
|
||||
|
||||
widget.state.b = true;
|
||||
await nextTick();
|
||||
expect(span.className).toBe("c a b");
|
||||
|
||||
(widget.child.comp as Child).state.d = true;
|
||||
await nextTick();
|
||||
expect(span.className).toBe("c a b d");
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("t-att-class is properly added/removed on widget root el (v3)", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ParentWidget">
|
||||
<Child class="a" t-att-class="state.b ? 'b' : ''" t-ref="child"/>
|
||||
</div>
|
||||
<span t-name="Child" class="c" t-att-class="state.d ? 'd' : ''"/>
|
||||
</templates>`);
|
||||
|
||||
class Child extends Component {
|
||||
state = useState({ d: true });
|
||||
}
|
||||
class ParentWidget extends Component {
|
||||
static components = { Child };
|
||||
state = useState({ b: true });
|
||||
child = useRef("child");
|
||||
}
|
||||
const widget = new ParentWidget();
|
||||
await widget.mount(fixture);
|
||||
|
||||
const span = fixture.querySelector("span")!;
|
||||
expect(span.className).toBe("c d a b");
|
||||
|
||||
widget.state.b = false;
|
||||
await nextTick();
|
||||
expect(span.className).toBe("c d a");
|
||||
|
||||
(widget.child.comp as Child).state.d = false;
|
||||
await nextTick();
|
||||
expect(span.className).toBe("c a");
|
||||
|
||||
widget.state.b = true;
|
||||
await nextTick();
|
||||
expect(span.className).toBe("c a b");
|
||||
|
||||
(widget.child.comp as Child).state.d = true;
|
||||
await nextTick();
|
||||
expect(span.className).toBe("c a b d");
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("class on components do not interfere with user defined classes", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App" t-att-class="{ c: state.c }" />
|
||||
</templates>`);
|
||||
|
||||
class App extends Component {
|
||||
state = useState({ c: true });
|
||||
mounted() {
|
||||
this.el!.classList.add("user");
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new App();
|
||||
await widget.mount(fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe('<div class="c user"></div>');
|
||||
|
||||
widget.state.c = false;
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe('<div class="user"></div>');
|
||||
});
|
||||
|
||||
test("style is properly added on widget root el", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`
|
||||
<div>
|
||||
<t t-component="child" style="font-weight: bold;"/>
|
||||
</div>`
|
||||
);
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div/>`;
|
||||
}
|
||||
|
||||
class ParentWidget extends Component {
|
||||
static components = { child: SomeComponent };
|
||||
}
|
||||
const widget = new ParentWidget();
|
||||
await widget.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div><div style="font-weight: bold;"></div></div>`);
|
||||
});
|
||||
|
||||
test("dynamic t-att-style is properly added and updated on widget root el", async () => {
|
||||
env.qweb.addTemplate(
|
||||
"ParentWidget",
|
||||
`
|
||||
<div>
|
||||
<t t-component="child" t-att-style="state.style"/>
|
||||
</div>`
|
||||
);
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div/>`;
|
||||
}
|
||||
|
||||
class ParentWidget extends Component {
|
||||
static components = { child: SomeComponent };
|
||||
state = useState({ style: "font-size: 20px" });
|
||||
}
|
||||
const widget = new ParentWidget();
|
||||
await widget.mount(fixture);
|
||||
|
||||
expect(env.qweb.templates.ParentWidget.fn.toString()).toMatchSnapshot();
|
||||
|
||||
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 20px;"></div></div>`);
|
||||
|
||||
widget.state.style = "font-size: 30px";
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe(`<div><div style="font-size: 30px;"></div></div>`);
|
||||
});
|
||||
|
||||
test("error in subcomponent with class", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<div t-esc="this.will.crash"/>`;
|
||||
}
|
||||
class ParentWidget extends Component {
|
||||
static template = xml`<div><Child class="a"/></div>`;
|
||||
static components = { Child };
|
||||
}
|
||||
const widget = new ParentWidget();
|
||||
let error;
|
||||
try {
|
||||
await widget.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
const regexp =
|
||||
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||
expect(error.message).toMatch(regexp);
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,620 @@
|
||||
import { Component, Env, mount, STATUS } from "../../src/component/component";
|
||||
import { useState } from "../../src/hooks";
|
||||
import { xml } from "../../src/tags";
|
||||
import { makeTestEnv, makeTestFixture, nextTick } from "../helpers";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// We create before each test:
|
||||
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
||||
// manipulations. Note that it is removed after each test.
|
||||
// - env: a WEnv, necessary to create new components
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
Component.env = env;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
describe("component error handling (catchError)", () => {
|
||||
/**
|
||||
* This test suite requires often to wait for 3 ticks. Here is why:
|
||||
* - First tick is to let the app render and crash.
|
||||
* - When we crash, we call the catchError handler in a setTimeout (because we
|
||||
* need to wait for the previous rendering to be completely stopped). So, we
|
||||
* need to wait for the second tick.
|
||||
* - Then, when the handler changes the state, we need to wait for the interface
|
||||
* to be rerendered.
|
||||
* */
|
||||
|
||||
test("can catch an error in a component render function", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
class ErrorComponent extends Component {
|
||||
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
|
||||
}
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else=""><t t-slot="default" /></t>
|
||||
</div>`;
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
|
||||
</div>`;
|
||||
state = useState({ flag: false });
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div><div>heyfalse</div></div></div>");
|
||||
app.state.flag = true;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("no component catching error lead to full app destruction", async () => {
|
||||
expect.assertions(6);
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
|
||||
class ErrorComponent extends Component {
|
||||
static template = xml`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
static template = xml`<div><ErrorComponent flag="state.flag"/></div>`;
|
||||
static components = { ErrorComponent };
|
||||
state = useState({ flag: false });
|
||||
async render() {
|
||||
try {
|
||||
await super.render();
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
/Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>heyfalse</div></div>");
|
||||
app.state.flag = true;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
expect(app.__owl__.status).toBe(STATUS.DESTROYED);
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
class ErrorComponent extends Component {
|
||||
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
|
||||
}
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else=""><t t-slot="default" /></t>
|
||||
</div>`;
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
||||
</div>`;
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("can catch an error in the initial call of a component render function (parent updated)", async () => {
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
class ErrorComponent extends Component {
|
||||
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
|
||||
}
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else=""><t t-slot="default" /></t>
|
||||
</div>`;
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<ErrorBoundary t-if="state.flag"><ErrorComponent /></ErrorBoundary>
|
||||
</div>`;
|
||||
state = useState({ flag: false });
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
app.state.flag = true;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("can catch an error in the constructor call of a component render function", async () => {
|
||||
const handler = jest.fn();
|
||||
env.qweb.on("error", null, handler);
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else=""><t t-slot="default" /></t>
|
||||
</div>
|
||||
<div t-name="ErrorComponent">Some text</div>
|
||||
<div t-name="App">
|
||||
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
||||
</div>
|
||||
</templates>`);
|
||||
class ErrorComponent extends Component {
|
||||
constructor(parent) {
|
||||
super(parent);
|
||||
throw new Error("NOOOOO");
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Component {
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Component {
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("can catch an error in the willStart call", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
class ErrorComponent extends Component {
|
||||
static template = xml`<div t-name="ErrorComponent">Some text</div>`;
|
||||
async willStart() {
|
||||
// we wait a little bit to be in a different stack frame
|
||||
await nextTick();
|
||||
throw new Error("NOOOOO");
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else=""><t t-slot="default" /></t>
|
||||
</div>`;
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Component {
|
||||
static template = xml`<div><ErrorBoundary><ErrorComponent /></ErrorBoundary></div>`;
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test.skip("can catch an error in the mounted call", async () => {
|
||||
// we do not catch error in mounted anymore
|
||||
console.error = jest.fn();
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="ErrorBoundary">
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else=""><t t-slot="default" /></t>
|
||||
</div>
|
||||
<div t-name="ErrorComponent">Some text</div>
|
||||
<div t-name="App">
|
||||
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
|
||||
</div>
|
||||
</templates>`);
|
||||
class ErrorComponent extends Component {
|
||||
mounted() {
|
||||
throw new Error("NOOOOO");
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Component {
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Component {
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
|
||||
});
|
||||
|
||||
test.skip("can catch an error in the willPatch call", async () => {
|
||||
// we do not catch error in willPatch anymore
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
class ErrorComponent extends Component {
|
||||
static template = xml`<div><t t-esc="props.message"/></div>`;
|
||||
willPatch() {
|
||||
throw new Error("NOOOOO");
|
||||
}
|
||||
}
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="state.error">Error handled</t>
|
||||
<t t-else=""><t t-slot="default" /></t>
|
||||
</div>`;
|
||||
state = useState({ error: false });
|
||||
|
||||
catchError() {
|
||||
this.state.error = true;
|
||||
}
|
||||
}
|
||||
class App extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span><t t-esc="state.message"/></span>
|
||||
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
|
||||
</div>`;
|
||||
state = useState({ message: "abc" });
|
||||
static components = { ErrorBoundary, ErrorComponent };
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>abc</span><div><div>abc</div></div></div>");
|
||||
app.state.message = "def";
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("a rendering error will reject the mount promise", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn(() => {});
|
||||
// we do not catch error in willPatch anymore
|
||||
class App extends Component {
|
||||
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
let error;
|
||||
try {
|
||||
await app.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
const regexp =
|
||||
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||
expect(error.message).toMatch(regexp);
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("an error in mounted call will reject the mount promise", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn(() => {});
|
||||
|
||||
class App extends Component {
|
||||
static template = xml`<div>abc</div>`;
|
||||
mounted() {
|
||||
throw new Error("boom");
|
||||
}
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
let error;
|
||||
try {
|
||||
await app.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("boom");
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("an error in willPatch call will reject the render promise", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn(() => {});
|
||||
|
||||
class App extends Component {
|
||||
static template = xml`<div><t t-esc="val"/></div>`;
|
||||
val = 3;
|
||||
willPatch() {
|
||||
throw new Error("boom");
|
||||
}
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
app.val = 4;
|
||||
let error;
|
||||
try {
|
||||
await app.render();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("boom");
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("an error in patched call will reject the render promise", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn(() => {});
|
||||
|
||||
class App extends Component {
|
||||
static template = xml`<div><t t-esc="val"/></div>`;
|
||||
val = 3;
|
||||
patched() {
|
||||
throw new Error("boom");
|
||||
}
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
app.val = 4;
|
||||
let error;
|
||||
try {
|
||||
await app.render();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("boom");
|
||||
expect(fixture.innerHTML).toBe("");
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("a rendering error in a sub component will reject the mount promise", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn(() => {});
|
||||
// we do not catch error in willPatch anymore
|
||||
class Child extends Component {
|
||||
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
|
||||
}
|
||||
class App extends Component {
|
||||
static template = xml`<div><Child/></div>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
let error;
|
||||
try {
|
||||
await app.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
const regexp =
|
||||
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||
expect(error.message).toMatch(regexp);
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("a rendering error will reject the render promise", async () => {
|
||||
const consoleError = console.error;
|
||||
console.error = jest.fn(() => {});
|
||||
// we do not catch error in willPatch anymore
|
||||
class App extends Component {
|
||||
static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
|
||||
flag = false;
|
||||
}
|
||||
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div></div>");
|
||||
app.flag = true;
|
||||
let error;
|
||||
try {
|
||||
await app.render();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
const regexp =
|
||||
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
|
||||
expect(error.message).toMatch(regexp);
|
||||
|
||||
expect(console.error).toBeCalledTimes(0);
|
||||
console.error = consoleError;
|
||||
});
|
||||
|
||||
test("a rendering error will reject the render promise (with sub components)", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<span></span>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><Child/><t t-esc="x.y"/></div>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
let error;
|
||||
try {
|
||||
const parent = new Parent();
|
||||
await parent.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
const regexp =
|
||||
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
|
||||
expect(error.message).toMatch(regexp);
|
||||
});
|
||||
|
||||
test("simple catchError", async () => {
|
||||
class Boom extends Component {
|
||||
static template = xml`<div t-esc="a.b.c"/>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="error">Error</t>
|
||||
<t t-else="">
|
||||
<Boom />
|
||||
</t>
|
||||
</div>`;
|
||||
static components = { Boom };
|
||||
|
||||
error = false;
|
||||
|
||||
catchError(error) {
|
||||
this.error = error;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Parent, { target: fixture });
|
||||
expect(fixture.innerHTML).toBe("<div>Error</div>");
|
||||
});
|
||||
|
||||
test("catchError in catchError", async () => {
|
||||
class Boom extends Component {
|
||||
static template = xml`<div t-esc="a.b.c"/>`;
|
||||
}
|
||||
|
||||
class Child extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Boom />
|
||||
</div>`;
|
||||
static components = { Boom };
|
||||
|
||||
catchError(error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="error">Error</t>
|
||||
<t t-else="">
|
||||
<Child />
|
||||
</t>
|
||||
</div>`;
|
||||
static components = { Child };
|
||||
|
||||
error = false;
|
||||
|
||||
catchError(error) {
|
||||
this.error = error;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Parent, { target: fixture });
|
||||
expect(fixture.innerHTML).toBe("<div>Error</div>");
|
||||
});
|
||||
|
||||
test("errors in mounted and in willUnmount", async () => {
|
||||
expect.assertions(1);
|
||||
class Example extends Component {
|
||||
static template = xml`<div/>`;
|
||||
val;
|
||||
mounted() {
|
||||
throw new Error("Error in mounted");
|
||||
this.val = { foo: "bar" };
|
||||
}
|
||||
willUnmount() {
|
||||
console.log(this.val.foo);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await mount(Example, { target: fixture });
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Error in mounted");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,868 @@
|
||||
import { Component, Env } from "../../src/component/component";
|
||||
import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
|
||||
import { useState } from "../../src/hooks";
|
||||
import { QWeb } from "../../src/qweb";
|
||||
import { xml } from "../../src/tags";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
let dev: boolean = false;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
env = makeTestEnv();
|
||||
Component.env = env;
|
||||
dev = QWeb.dev;
|
||||
QWeb.dev = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
QWeb.dev = dev;
|
||||
});
|
||||
|
||||
class Widget extends Component {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
describe("props validation", () => {
|
||||
test("validation is only done in dev mode", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
static template = xml`<div>hey</div>`;
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static components = { TestWidget };
|
||||
static template = xml`<div><TestWidget /></div>`;
|
||||
}
|
||||
|
||||
let error;
|
||||
QWeb.dev = true;
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
|
||||
|
||||
error = undefined;
|
||||
|
||||
QWeb.dev = false;
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
});
|
||||
|
||||
test("props: list of strings", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
static template = xml`<div>hey</div>`;
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static components = { TestWidget };
|
||||
static template = xml`<div><TestWidget /></div>`;
|
||||
}
|
||||
|
||||
let error;
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
|
||||
});
|
||||
|
||||
test("validate simple types", async () => {
|
||||
const Tests = [
|
||||
{ type: Number, ok: 1, ko: "1" },
|
||||
{ type: Boolean, ok: true, ko: "1" },
|
||||
{ type: String, ok: "1", ko: 1 },
|
||||
{ type: Object, ok: {}, ko: "1" },
|
||||
{ type: Date, ok: new Date(), ko: "1" },
|
||||
{ type: Function, ok: () => {}, ko: "1" },
|
||||
];
|
||||
|
||||
let props;
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||
get p() {
|
||||
return props.p;
|
||||
}
|
||||
}
|
||||
for (let test of Tests) {
|
||||
let TestWidget = class extends Widget {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: test.type };
|
||||
};
|
||||
Parent.components = { TestWidget };
|
||||
|
||||
let error;
|
||||
props = {};
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
|
||||
|
||||
error = undefined;
|
||||
props = { p: test.ok };
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
props = { p: test.ko };
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
|
||||
}
|
||||
});
|
||||
|
||||
test("validate simple types, alternate form", async () => {
|
||||
const Tests = [
|
||||
{ type: Number, ok: 1, ko: "1" },
|
||||
{ type: Boolean, ok: true, ko: "1" },
|
||||
{ type: String, ok: "1", ko: 1 },
|
||||
{ type: Object, ok: {}, ko: "1" },
|
||||
{ type: Date, ok: new Date(), ko: "1" },
|
||||
{ type: Function, ok: () => {}, ko: "1" },
|
||||
];
|
||||
|
||||
let props;
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||
get p() {
|
||||
return props.p;
|
||||
}
|
||||
}
|
||||
for (let test of Tests) {
|
||||
let TestWidget = class extends Component {
|
||||
static props = { p: { type: test.type } };
|
||||
static template = xml`<div>hey</div>`;
|
||||
};
|
||||
Parent.components = { TestWidget };
|
||||
|
||||
let error;
|
||||
props = {};
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe(`Missing props 'p' (component '_a')`);
|
||||
|
||||
error = undefined;
|
||||
props = { p: test.ok };
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
props = { p: test.ko };
|
||||
try {
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
|
||||
}
|
||||
});
|
||||
|
||||
test("can validate a prop with multiple types", async () => {
|
||||
class TestWidget extends Component {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: [String, Boolean] };
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
get p() {
|
||||
return props.p;
|
||||
}
|
||||
}
|
||||
|
||||
let error;
|
||||
let props;
|
||||
try {
|
||||
props = { p: "string" };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: true };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: 1 };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||
});
|
||||
|
||||
test("can validate an optional props", async () => {
|
||||
class TestWidget extends Component {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: { type: String, optional: true } };
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
get p() {
|
||||
return props.p;
|
||||
}
|
||||
}
|
||||
|
||||
let error;
|
||||
let props;
|
||||
try {
|
||||
props = { p: "key" };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = {};
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: 1 };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||
});
|
||||
|
||||
test("can validate an array with given primitive type", async () => {
|
||||
class TestWidget extends Component {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: { type: Array, element: String } };
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
get p() {
|
||||
return props.p;
|
||||
}
|
||||
}
|
||||
|
||||
let error;
|
||||
let props;
|
||||
try {
|
||||
props = { p: [] };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: ["string"] };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: [1] };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
|
||||
error = undefined;
|
||||
try {
|
||||
props = { p: ["string", 1] };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
});
|
||||
|
||||
test("can validate an array with multiple sub element types", async () => {
|
||||
class TestWidget extends Component {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = { p: { type: Array, element: [String, Boolean] } };
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
get p() {
|
||||
return props.p;
|
||||
}
|
||||
}
|
||||
|
||||
let error;
|
||||
let props;
|
||||
try {
|
||||
props = { p: [] };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: ["string"] };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: [false, true, "string"] };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: [true, 1] };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||
});
|
||||
|
||||
test("can validate an object with simple shape", async () => {
|
||||
class TestWidget extends Component {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = {
|
||||
p: { type: Object, shape: { id: Number, url: String } },
|
||||
};
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
get p() {
|
||||
return props.p;
|
||||
}
|
||||
}
|
||||
|
||||
let error;
|
||||
let props;
|
||||
try {
|
||||
props = { p: { id: 1, url: "url" } };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: { id: 1, url: "url", extra: true } };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid prop 'p' in component TestWidget (unknown prop 'extra')");
|
||||
|
||||
try {
|
||||
props = { p: { id: "1", url: "url" } };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||
|
||||
error = undefined;
|
||||
try {
|
||||
props = { p: { id: 1 } };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||
});
|
||||
|
||||
test("can validate recursively complicated prop def", async () => {
|
||||
class TestWidget extends Component {
|
||||
static template = xml`<div>hey</div>`;
|
||||
static props = {
|
||||
p: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
url: [Boolean, { type: Array, element: Number }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
get p() {
|
||||
return props.p;
|
||||
}
|
||||
}
|
||||
|
||||
let error;
|
||||
let props;
|
||||
try {
|
||||
props = { p: { id: 1, url: true } };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: { id: 1, url: [12] } };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
props = { p: { id: 1, url: [12, true] } };
|
||||
const p = new Parent();
|
||||
await p.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
|
||||
});
|
||||
|
||||
test("can validate optional attributes in nested sub props", () => {
|
||||
class TestComponent extends Component {
|
||||
static props = {
|
||||
myprop: {
|
||||
type: Array,
|
||||
element: {
|
||||
type: Object,
|
||||
shape: {
|
||||
num: { type: Number, optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
let error;
|
||||
try {
|
||||
QWeb.utils.validateProps(TestComponent, { myprop: [{}] });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
QWeb.utils.validateProps(TestComponent, { myprop: [{ a: 1 }] });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe(
|
||||
"Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
|
||||
);
|
||||
});
|
||||
|
||||
test("can validate with a custom validator", () => {
|
||||
class TestComponent extends Component {
|
||||
static props = {
|
||||
size: {
|
||||
validate: (e) => ["small", "medium", "large"].includes(e),
|
||||
},
|
||||
};
|
||||
}
|
||||
let error;
|
||||
try {
|
||||
QWeb.utils.validateProps(TestComponent, { size: "small" });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
|
||||
try {
|
||||
QWeb.utils.validateProps(TestComponent, { size: "abcdef" });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'");
|
||||
});
|
||||
|
||||
test("can validate with a custom validator, and a type", () => {
|
||||
const validator = jest.fn((n) => 0 <= n && n <= 10);
|
||||
class TestComponent extends Component {
|
||||
static props = {
|
||||
n: {
|
||||
type: Number,
|
||||
validate: validator,
|
||||
},
|
||||
};
|
||||
}
|
||||
let error;
|
||||
try {
|
||||
QWeb.utils.validateProps(TestComponent, { n: 3 });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeUndefined();
|
||||
expect(validator).toBeCalledTimes(1);
|
||||
|
||||
try {
|
||||
QWeb.utils.validateProps(TestComponent, { n: "str" });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
|
||||
expect(validator).toBeCalledTimes(1);
|
||||
|
||||
error = null;
|
||||
try {
|
||||
QWeb.utils.validateProps(TestComponent, { n: 100 });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
|
||||
expect(validator).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
test("props are validated in dev mode (code snapshot)", async () => {
|
||||
env.qweb.addTemplates(`
|
||||
<templates>
|
||||
<div t-name="App">
|
||||
<Child message="1"/>
|
||||
</div>
|
||||
<div t-name="Child"><t t-esc="props.message"/></div>
|
||||
</templates>`);
|
||||
|
||||
class Child extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
class App extends Widget {
|
||||
static components = { Child };
|
||||
}
|
||||
const app = new App();
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||
// need to make sure there are 2 call to update props. one at component
|
||||
// creation, and one at update time.
|
||||
expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("props: list of strings with optional props", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message", "someProp?"];
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { someProp: 1 });
|
||||
}).toThrow();
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: 1 });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("props: can be defined with a boolean", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = { message: true };
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, {});
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("props with type array, and no element", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = { myprop: { type: Array } };
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { myprop: [1] });
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { myprop: 1 });
|
||||
}).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`);
|
||||
});
|
||||
|
||||
test("props with type object, and no shape", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = { myprop: { type: Object } };
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { myprop: { a: 3 } });
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { myprop: false });
|
||||
}).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`);
|
||||
});
|
||||
|
||||
test("props: extra props cause an error", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message"];
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("props: extra props cause an error, part 2", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = { message: true };
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("props: optional prop do not cause an error", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["message?"];
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: 1 });
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("optional prop do not cause an error if value is undefined", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = { message: { type: String, optional: true } };
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: undefined });
|
||||
}).not.toThrow();
|
||||
expect(() => {
|
||||
QWeb.utils.validateProps(TestWidget, { message: null });
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("missing required boolean prop causes an error", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["p"];
|
||||
static template = xml`<span><t t-if="props.p">hey</t></span>`;
|
||||
}
|
||||
|
||||
class App extends Widget {
|
||||
static template = xml`<div><TestWidget/></div>`;
|
||||
static components = { TestWidget };
|
||||
}
|
||||
|
||||
const w = new App(undefined, {});
|
||||
let error;
|
||||
try {
|
||||
await w.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
|
||||
});
|
||||
|
||||
test("props are validated whenever component is updated", async () => {
|
||||
let error;
|
||||
class TestWidget extends Component {
|
||||
static props = { p: { type: Number } };
|
||||
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||
|
||||
async __updateProps() {
|
||||
try {
|
||||
await Component.prototype.__updateProps.apply(this, arguments);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="state.p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
state: any = useState({ p: 1 });
|
||||
}
|
||||
|
||||
const w = new Parent();
|
||||
await w.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||
|
||||
w.state.p = undefined;
|
||||
await nextTick();
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
|
||||
});
|
||||
|
||||
test("default values are applied before validating props at update", async () => {
|
||||
class TestWidget extends Component {
|
||||
static props = { p: { type: Number } };
|
||||
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||
static defaultProps = { p: 4 };
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget p="state.p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
state: any = useState({ p: 1 });
|
||||
}
|
||||
|
||||
const w = new Parent();
|
||||
await w.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||
|
||||
w.state.p = undefined;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
|
||||
});
|
||||
|
||||
test("mix of optional and mandatory", async () => {
|
||||
class Child extends Component {
|
||||
static props = {
|
||||
optional: { type: String, optional: true },
|
||||
mandatory: Number,
|
||||
};
|
||||
static template = xml` <div><t t-esc="props.mandatory"/></div>`;
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
static components = { Child };
|
||||
static template = xml`<div><Child/></div>`;
|
||||
}
|
||||
|
||||
const w = new App(undefined, {});
|
||||
let error;
|
||||
try {
|
||||
await w.mount(fixture);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toBe("Missing props 'mandatory' (component 'Child')");
|
||||
});
|
||||
});
|
||||
|
||||
describe("default props", () => {
|
||||
test("can set default values", async () => {
|
||||
class TestWidget extends Component {
|
||||
static defaultProps = { p: 4 };
|
||||
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><TestWidget /></div>`;
|
||||
static components = { TestWidget };
|
||||
}
|
||||
|
||||
const w = new Parent();
|
||||
await w.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
|
||||
});
|
||||
|
||||
test("default values are also set whenever component is updated", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static template = xml`<div><t t-esc="props.p"/></div>`;
|
||||
static defaultProps = { p: 4 };
|
||||
}
|
||||
class Parent extends Widget {
|
||||
static template = xml`<div><TestWidget p="state.p"/></div>`;
|
||||
static components = { TestWidget };
|
||||
state: any = useState({ p: 1 });
|
||||
}
|
||||
|
||||
const w = new Parent();
|
||||
await w.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
|
||||
|
||||
w.state.p = undefined;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
|
||||
});
|
||||
|
||||
test("can set default required boolean values", async () => {
|
||||
class TestWidget extends Widget {
|
||||
static props = ["p", "q"];
|
||||
static defaultProps = { p: true, q: false };
|
||||
static template = xml`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`;
|
||||
}
|
||||
|
||||
class App extends Widget {
|
||||
static template = xml`<div><TestWidget/></div>`;
|
||||
static components = { TestWidget };
|
||||
}
|
||||
|
||||
const w = new App(undefined, {});
|
||||
await w.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user