mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1018 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f40ffc59d8 | |||
| 3fc50462e6 | |||
| 4b1a94d990 | |||
| 0f49048616 | |||
| ca3431e2f9 | |||
| 7f580a4e1d | |||
| c7459ef87b | |||
| 5772b4e9e4 | |||
| e57e2ee378 | |||
| 2e03332acd | |||
| c16d7d52b1 | |||
| 9c4c3e3b83 | |||
| 55ac43c1db | |||
| d046913a01 | |||
| 0e6059467f | |||
| 51538c2fea | |||
| 83d4471048 | |||
| 8a967e5b2d | |||
| 385e118e58 | |||
| a3111eb9ca | |||
| 1fc88f626f | |||
| 5d5a530505 | |||
| c7bd0ab85c | |||
| 31b57cbb40 | |||
| 31fce0926c | |||
| b56a9c24cf | |||
| 6dcdb77eab | |||
| a7daef380a | |||
| 22a79cdedd | |||
| e4b810c027 | |||
| b10a700381 | |||
| 2b4d8874c7 | |||
| 98b58b505b | |||
| 586033fd95 | |||
| 1fe0bf08b1 | |||
| 4779707923 | |||
| d917af4614 | |||
| 4c77132ae2 | |||
| 114f21586e | |||
| 32d8b23b9d | |||
| a83731007a | |||
| 5c71744e19 | |||
| d09771b04b | |||
| 7137130c38 | |||
| 0cd66c8518 | |||
| 5d9cff0331 | |||
| 41f1262eb7 | |||
| 41344ef4ec | |||
| 7d14db7d31 | |||
| 1179e84971 | |||
| 24b1ea7604 | |||
| 3e4ebb6378 | |||
| 859748aed9 | |||
| fd13277e1d | |||
| 7fb166bd50 | |||
| decf42c742 | |||
| 989b0d6709 | |||
| 9b93521da4 | |||
| de240b1ebc | |||
| 55dbc01a1b | |||
| e3b1566943 | |||
| 828be28653 | |||
| d80fad760c | |||
| 7611ea6033 | |||
| c7d515a6b3 | |||
| d277039b14 | |||
| 77ff5ee895 | |||
| 47c6d6cc3c | |||
| 0625b5883a | |||
| 53ab54b1ec | |||
| 4770b91faa | |||
| c356351de2 | |||
| f04423da23 | |||
| b3062d29f1 | |||
| 56086242bb | |||
| 998ecbb337 | |||
| 50355e6a3d | |||
| 67f86a4ab8 | |||
| 14d2328c88 | |||
| e4fdd32f22 | |||
| 8d1d0a2244 | |||
| 0457e5d4ed | |||
| 8920b4b93a | |||
| 6908102a72 | |||
| d6348b8310 | |||
| 79738e00c7 | |||
| 2a1b99be2d | |||
| 8a472231cf | |||
| bb373e6a7a | |||
| d735213758 | |||
| 076b0d774e | |||
| f405fe9323 | |||
| 1ae9d514b9 | |||
| 592d9a458e | |||
| 0dbd2bd463 | |||
| 8ec7a6f9bf | |||
| 12b8ce963e | |||
| bb9d65e95b | |||
| 73c339fff1 | |||
| 406be446a5 | |||
| bd2aa8a72f | |||
| 3536f41f00 | |||
| 804ad3c35e | |||
| 4a922ed82d | |||
| a6f0985d43 | |||
| 6aee1355c8 | |||
| 921ced7c90 | |||
| 1da930cb25 | |||
| cc8e11c9c9 | |||
| 3196b585fd | |||
| ea2ccc5a03 | |||
| 960808aeb2 | |||
| 1fb1d37e32 | |||
| 24ce8613c5 | |||
| 2c1226d737 | |||
| 140818b5f9 | |||
| 83de53d283 | |||
| 50aac42bdc | |||
| bd98d4d0d0 | |||
| 4d68dac24d | |||
| 722abd6d5f | |||
| a7305a5cdb | |||
| ff734c706c | |||
| add5fdd737 | |||
| a221411938 | |||
| bb6479f44f | |||
| aa95149997 | |||
| e4b4ee471f | |||
| 3af5e57825 | |||
| 979712f84e | |||
| 93f2c1d766 | |||
| 0728c8333d | |||
| 6639d361c3 | |||
| 72962f1dd1 | |||
| a4d9aae9a7 | |||
| 1e8576ad40 | |||
| 753d82149e | |||
| 700030cc7d | |||
| d828f39a2d | |||
| 0a73154985 | |||
| d88eb34d4f | |||
| a8d88d4009 | |||
| 6d9ed0d62f | |||
| b33471e819 | |||
| 3fb65b3a89 | |||
| 09d192999a | |||
| 466cf50b73 | |||
| 96620d3e8e | |||
| 176c89b278 | |||
| 38941bc26f | |||
| ce8ddd1cbf | |||
| 81f44ee5d3 | |||
| 99b5e9ec55 | |||
| 4f35f03986 | |||
| eab0caa6cb | |||
| 5a2c769eab | |||
| 89d63ff29a | |||
| a2e8abc243 | |||
| dfd0dcedb8 | |||
| ad743c205c | |||
| 7c78442e43 | |||
| 0d13c362d3 | |||
| 374dbb2fd9 | |||
| 2a5f37cf4b | |||
| cfb6b9f958 | |||
| 82f6923a21 | |||
| cccb379377 | |||
| 82c7c24438 | |||
| 41ad5db2e3 | |||
| 42a140a8e3 | |||
| 7711733a23 | |||
| 281b32965e | |||
| cec451fd15 | |||
| 6a7703ea82 | |||
| 4a03a60084 | |||
| a6bdca082a | |||
| 90167c5436 | |||
| c221721d7f | |||
| 38f39b6755 | |||
| 1775467434 | |||
| 52d0526ddd | |||
| 4b170b9b45 | |||
| 3e1fe07ba7 | |||
| 5bf47500d5 | |||
| 75ad0835e9 | |||
| 3d6a5eb828 | |||
| aceaeef8cc | |||
| 06fc3a2c77 | |||
| 92cc4375f8 | |||
| 9f2e2bcc66 | |||
| c7af885f43 | |||
| bd5637c0a3 | |||
| 3c98ef8cb1 | |||
| aad6b806ba | |||
| 772c275bd4 | |||
| 416deeb865 | |||
| 7e40fa300a | |||
| cc1eea0945 | |||
| bf9cceb56f | |||
| 7eaecac0b5 | |||
| ddc358f48a | |||
| e2819323ee | |||
| 983b9f996d | |||
| fd295b3be3 | |||
| e675f7ff5b | |||
| 702fb3b253 | |||
| 63fbcf99fd | |||
| 14a6289f60 | |||
| 894deed13b | |||
| 6f435c36d8 | |||
| 5dddf8f9a3 | |||
| 463eb4bb86 | |||
| b66d5231d3 | |||
| 3c12519277 | |||
| c1a973a4d8 | |||
| e91e50a812 | |||
| 2e176f135d | |||
| 49c7585998 | |||
| f32b1deb2c | |||
| 1da3ecdbee | |||
| a1c619f094 | |||
| eceb3e6280 | |||
| 8a1ac13975 | |||
| 779003e715 | |||
| 8c600fa539 | |||
| bcc4fe2a27 | |||
| bb4948f3dc | |||
| cc4480e001 | |||
| 7143c2e39b | |||
| a8d8310b8e | |||
| 0bbea351a6 | |||
| 2601a176c4 | |||
| 05a57d6da5 | |||
| e6e6c31632 | |||
| 3f66d9fe6c | |||
| 5d8141a67c | |||
| 6459d8d289 | |||
| 2943ca3921 | |||
| 4866ed8e8a | |||
| 93b88cad8d | |||
| b90180a9e0 | |||
| 8239a5d2cd | |||
| a073568667 | |||
| 7143dd3ff5 | |||
| c0cf2c9e3d | |||
| db9658c140 | |||
| eb2c41aa91 | |||
| a45ca98dac | |||
| ced777f0be | |||
| 7df0a4e93f | |||
| f3555cfae0 | |||
| 629b379ea9 | |||
| a400fc5e69 | |||
| 5d4a38ad0f | |||
| 093218a067 | |||
| ed3e6dcbb6 | |||
| cb107cef7d | |||
| aecc320c29 | |||
| e580ec00fe | |||
| ee5f6c7569 | |||
| c627b0add8 | |||
| b902edc1be | |||
| 1761af9c24 | |||
| db93ef08ff | |||
| 03787cfb39 | |||
| 10745c52d0 | |||
| 2b90e3a688 | |||
| b2ea241270 | |||
| 81e5b24f2f | |||
| 717fd3b6ab | |||
| 3fa1bb62f6 | |||
| 201f06c187 | |||
| ae30d9db7d | |||
| af80cefa76 | |||
| 1f6e84d141 | |||
| 1658d15b87 | |||
| c1439814bf | |||
| d5fbaff9f7 | |||
| 8c16790471 | |||
| 9b8c582b32 | |||
| 1700a6fba3 | |||
| 7513b1e507 | |||
| e0c0306acd | |||
| bca6afeb90 | |||
| 756d32daa0 | |||
| 8169f05edc | |||
| 153f4379f4 | |||
| 7ffb9afbd9 | |||
| c03042b44d | |||
| df2d6b6a0e | |||
| 7c04cc425e | |||
| 16f1e2c237 | |||
| 8a84b5be56 | |||
| 219923d752 | |||
| 348b505e5f | |||
| d3745e4e5f | |||
| 80cb6b7a91 | |||
| 4cceb239dd | |||
| 2ae0149adb | |||
| 3eb63452e7 | |||
| 1296964ae2 | |||
| c71db28bc6 | |||
| a0b2551e4a | |||
| ebd2e4324f | |||
| aa3148eddf | |||
| 42811344da | |||
| a0e1af83ac | |||
| c16d8ed6de | |||
| 15e4c856da | |||
| ced5d0f69f | |||
| b6eb4d009e | |||
| 8c71d99e5f | |||
| d7f3f4defe | |||
| 0f2192604c | |||
| 10df0b5f4a | |||
| d569ea1c28 | |||
| 52fa81c510 | |||
| d6668e3439 | |||
| ee1ef20ce1 | |||
| 9d5ffe11c7 | |||
| 900a3ee501 | |||
| 0a544bd7e8 | |||
| 4415cc8932 | |||
| efa147fdab | |||
| e746574a1d | |||
| c06049076a | |||
| 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 |
@@ -0,0 +1,28 @@
|
||||
# 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
|
||||
- run: npm run build
|
||||
+14
-1
@@ -1,5 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
|
||||
npm-debug.log
|
||||
|
||||
# misc
|
||||
@@ -14,8 +15,20 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
#ide's
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
|
||||
# Extras temp file
|
||||
/extras/owl.js
|
||||
/tools/owl.js
|
||||
|
||||
release-notes.md
|
||||
|
||||
.rpt2_cache
|
||||
|
||||
# useful in some cases
|
||||
/temp
|
||||
+766
@@ -0,0 +1,766 @@
|
||||
# Changelog
|
||||
|
||||
This document contains an overview of all changes between Owl 1.x and
|
||||
Owl 2.x, with some pointers on how to update the code.
|
||||
|
||||
Note that some of these changes can be magically implemented (for example, by
|
||||
patching the `setup` method of `Component` to auto register all the lifecycle
|
||||
methods as hooks). This will be done for the transition period, but will be
|
||||
removed after.
|
||||
|
||||
## From Owl 1.x to Owl 2.0
|
||||
|
||||
All changes are documented here in no particular order.
|
||||
|
||||
**Components**
|
||||
|
||||
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
|
||||
- breaking: component.el is removed ([details](#9-componentel-is-removed))
|
||||
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||
- new `onWillDestroy`, `onWillRender` and `onRendered` hooks ([doc](doc/reference/component.md#lifecycle))
|
||||
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
|
||||
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
|
||||
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
|
||||
- breaking: components can no longer be instantiated and mounted by hand ([details](#5-components-can-no-longer-be-instantiated-and-mounted-by-hand))
|
||||
- breaking: components can no longer be unmounted/remounted ([details](#6-components-can-no-longer-be-unmountedremounted))
|
||||
- breaking: template name is no longer inferred from the class name ([details](#7-template-name-is-no-longer-inferred-from-the-class-name))
|
||||
- breaking: components no longer have a `shouldUpdate` method ([details](#8-components-no-longer-have-a-shouldupdate-method))
|
||||
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
|
||||
- breaking: `render` method does not return a promise anymore ([details](#35-render-method-does-not-return-a-promise-anymore))
|
||||
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
|
||||
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
|
||||
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
|
||||
- breaking: prop validation system does not allow default prop on a mandatory (not optional) prop ([doc](doc/reference/props.md#props-validation))
|
||||
- breaking: rendering a component does not necessarily render child components ([details](#40-rendering-a-component-does-not-necessarily-render-child-components))
|
||||
|
||||
|
||||
|
||||
**Templates**
|
||||
|
||||
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
|
||||
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
|
||||
- breaking: `t-raw` directive has been removed (replaced by `t-out`) ([details](#38-t-raw-directive-has-been-removed-replaced-by-t-out))
|
||||
- new: add support for synthetic events ([doc](doc/reference/event_handling.md#synthetic-events))
|
||||
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
|
||||
- new: components can use the `.bind` suffix to bind function props ([doc](doc/reference/props.md#binding-function-props))
|
||||
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
|
||||
- new: an error is thrown if an handler defined in a `t-on-` directive is not a function (failed silently previously in some cases)
|
||||
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
|
||||
- new: the `this` variable in template expressions is now bound to the component
|
||||
|
||||
|
||||
**Reactivity**
|
||||
|
||||
- finer grained reactivity: owl 2 tracks change per key/component
|
||||
- finer grained reactivity: sub components can reobserve state ([doc](doc/reference/reactivity.md))
|
||||
- new: `reactive` function: create reactive state (without being linked to a component) ([doc](doc/reference/reactivity.md#reactive))
|
||||
- new: `markRaw` function: mark an object or array so that it is ignored by the reactivity system ([doc](doc/reference/reactivity.md#markraw))
|
||||
- new: `toRaw` function: given a reactive objet, return the raw (non reactive) underlying object ([doc](doc/reference/reactivity.md#toraw))
|
||||
|
||||
|
||||
**Slots**
|
||||
|
||||
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
|
||||
- slots capabilities have been improved ([doc](doc/reference/slots.md))
|
||||
- params can be give to slot content (to pass information from slot owner to slot user)
|
||||
- slots are given as a `prop` (and can be manipulated/propagated to sub components )
|
||||
- slots can define scopes (to pass information from slot user to slot owner)
|
||||
|
||||
|
||||
**Portal**
|
||||
|
||||
- Portal are now defined with `t-portal` ([details](#33-portal-are-now-defined-with-t-portal))
|
||||
- portals can now have arbitrary content (no longer restricted to one single child)
|
||||
- breaking: does no longer transfer dom events ([details](#13-portal-does-no-longer-transfer-dom-events))
|
||||
- breaking: does render as an empty text node instead of `<portal/>` ([details](#14-portal-does-render-as-an-empty-text-node-instead-of-portal))
|
||||
|
||||
|
||||
**Miscellaneous**
|
||||
|
||||
- improved performance
|
||||
- much simpler code
|
||||
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
|
||||
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||
- breaking: `Context` is removed ([details](#15-context-is-removed))
|
||||
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
|
||||
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
|
||||
- new hook: `useChildSubEnv` (only applies to child components) ([details](#27-usechildsubenv-only-applies-to-child-components))
|
||||
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
|
||||
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
|
||||
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
|
||||
- breaking: `Store` is removed ([details](#22-store-is-removed))
|
||||
- breaking: `Router` is removed ([details](#23-router-is-removed))
|
||||
- breaking: transition system is removed ([details](#24-transition-system-is-removed))
|
||||
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
|
||||
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
|
||||
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
|
||||
- breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed))
|
||||
- breaking: `browser` object has been removed ([details](#39-browser-object-has-been-removed))
|
||||
|
||||
## Details/Rationale/Migration
|
||||
|
||||
All changes are listed in no particular order.
|
||||
|
||||
### 1. component lifecycle methods are removed
|
||||
|
||||
There was two ways to define hooks: the component methods (`willStart`, `mounted`, ...) and the hooks (`onWillStart`, `onMounted`, ...). In Owl 2, the component methods have been removed.
|
||||
|
||||
Rationale: it makes the implementation simpler and slightly faster. Hooks are more composable
|
||||
than component methods. It enforces a single entry point to check all the useful lifecycle
|
||||
calls (instead of it being scattered in the component definition). It feels more "modern".
|
||||
|
||||
Migration: lifecycle methods should be defined in the `setup`:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
mounted() {
|
||||
// do something
|
||||
}
|
||||
}
|
||||
```
|
||||
should become:
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Documentation: [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||
|
||||
### 2. components can no longer be mounted in a detached dom element
|
||||
|
||||
Nor document fragment.
|
||||
|
||||
Rationale: it is actually very difficult to do it: this implies that a component
|
||||
can be mounted more than once, that we need to check every time different status,
|
||||
that some elements is in the dom, and was a cause for bugs. Also, we don't use it
|
||||
in practice. Removing this means that we have a much simpler mental model of what
|
||||
happens.
|
||||
|
||||
Migration: well, not really easy. The code needs to be refactored in a different way.
|
||||
|
||||
|
||||
### 3. **`t-set` will no longer work to define a slot**
|
||||
|
||||
The `t-set` directive cannot define a slot anymore. Only the `t-set-slot` directive
|
||||
can do it.
|
||||
|
||||
Rationale: it was left for compatibility reason, but was deprecated anyway.
|
||||
|
||||
Migration: `t-set` should be changed to `t-set-slot` (when defining a slot)
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
<SideBar><t t-set="content">content</t></SideBar>
|
||||
```
|
||||
should become:
|
||||
```xml
|
||||
<SideBar><t t-set-slot="content">content</t></SideBar>
|
||||
```
|
||||
|
||||
### 4. `mount` method API is simpler
|
||||
|
||||
Before, the `mount` method was used like this:
|
||||
|
||||
```js
|
||||
await mount(Root, { target: document.body });
|
||||
```
|
||||
|
||||
It is now simpler and takes the root component and a target argument:
|
||||
|
||||
```js
|
||||
await mount(Root, document.body);
|
||||
```
|
||||
|
||||
Rationale: the `mount` method is only useful anyway for small toy examples,
|
||||
because real applications will need to configure the templates, the translations,
|
||||
and other stuff. All complex usecases need to go through the new `App` class,
|
||||
that encapsulates the root of an owl application.
|
||||
|
||||
Documentation: [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
|
||||
### 5. components can no longer be instantiated and mounted by hand
|
||||
|
||||
In Owl 1, it was possible to instantiate a component by hand:
|
||||
|
||||
```js
|
||||
const root = new Root();
|
||||
await root.mount(document.body);
|
||||
```
|
||||
|
||||
Now, it is no longer possible. All component instantiations should be done by
|
||||
the owl framework itself.
|
||||
|
||||
Rationale: the `mount` method does not make sense for all non root components.
|
||||
Also, the fact that it was possible for a component to be sometimes root,
|
||||
sometimes a child made for a weird constructor signature. This changes makes it
|
||||
simpler.
|
||||
|
||||
Migration: all code doing that should use either the `mount` method (if the use
|
||||
case is simple enough, or the `App` class):
|
||||
|
||||
```js
|
||||
const app = new App(Root);
|
||||
app.configure({ templates: ..., ...});
|
||||
await app.mount(document.body);
|
||||
```
|
||||
|
||||
### 6. components can no longer be unmounted/remounted
|
||||
|
||||
Rationale: this is a very difficult feature to implement (it adds a lot of possible
|
||||
state transitions), compared to its benefit.
|
||||
|
||||
Migration: all code using it should find a way to export and reimport the state
|
||||
|
||||
### 7. template name is no longer inferred from the class name
|
||||
|
||||
Before, it was possible to define a component without specifying its template:
|
||||
|
||||
```js
|
||||
class Blabla extends Component {
|
||||
// no static template here!
|
||||
}
|
||||
```
|
||||
with the `Blabla` template. It also worked with subclasses. But then, this means
|
||||
that the code had to look up all the super classes names to find the correct
|
||||
template.
|
||||
|
||||
Rationale: in practice, it is not really useful, since all templates are usually
|
||||
namespaced: `web.SomeComponent` anyway. All the trouble to do that was just not
|
||||
worth it.
|
||||
|
||||
Migration: simply explicitely defines the template key everytime:
|
||||
|
||||
```js
|
||||
class Blabla extends Component {}
|
||||
Blabla.template = "Blabla";
|
||||
```
|
||||
|
||||
### 8. components no longer have a `shouldUpdate` method
|
||||
|
||||
Rationale: `shouldUpdate` is a dangerous method to use, that may cause a lot of
|
||||
issues. Vue does not have such a mechanism (see https://github.com/vuejs/vue/issues/4255),
|
||||
because the reactivity system in Vue is smart enough to only rerender the minimal
|
||||
subset of components that is subscribed to a piece of state. Now, Owl 2 features
|
||||
a much more powerful reactivity system, so the same rationale applies: in a way,
|
||||
it's like each Owl 2 component has a `shouldUpdate` method that precisely tracks
|
||||
every value used by the component.
|
||||
|
||||
Migration code: remove the `shouldUpdate` methods, and it should work as well
|
||||
as before.
|
||||
|
||||
### 9. component.el is removed
|
||||
|
||||
This comes from the fact that Owl 2 supports fragments (arbitrary content).
|
||||
|
||||
Migration: if one need a reference to the root htmlelement of a template, it is
|
||||
suggested to simply add a `ref` on it, and access the reference as needed.
|
||||
|
||||
Documentation: [Refs](doc/reference/refs.md)
|
||||
|
||||
### 10. style/class on components are now regular props
|
||||
|
||||
Before, it was possible to do this in a template:
|
||||
|
||||
```xml
|
||||
<Child style="..." class="..."/>
|
||||
```
|
||||
(or with `t-att-style` and `t-att-class`). This does no longer work, as they are
|
||||
now considered normal props.
|
||||
|
||||
Rationale: with the move to fragments, the semantics of where the style/class
|
||||
attribute should be set is unclear. Also, it is actually very hard to implement
|
||||
properly, in particular with higher order components. And another issue is that
|
||||
it (slightly) breaks the encapsulation of behaviour from the `Child` component
|
||||
perspective.
|
||||
|
||||
Migration: each component that wishes to be customized should explicitely add
|
||||
the `class` and `style` attributes in its template. Also, the parent component
|
||||
should be aware that since we are talking about props, it should be a javascript expression:
|
||||
|
||||
In parent:
|
||||
|
||||
```xml
|
||||
<Child class="'o_my_god'"/>
|
||||
```
|
||||
and in child:
|
||||
|
||||
```xml
|
||||
<div t-att-class="props.class">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### 11. components can no longer be mounted with position=self
|
||||
|
||||
Rationale: this is due to the implementation of owl 2 virtual dom. The hack
|
||||
necessary to support position=self does not work. This position also is not
|
||||
compatible with the fact that a component can have a root `<div>` then later,
|
||||
change it to something else, or even a text node.
|
||||
|
||||
Migration: no real way to do the same. Owl application needs to be appended or
|
||||
prepended in something, maybe a `div`. Remember that you the root component
|
||||
can have multiple roots
|
||||
|
||||
Documentation:
|
||||
- [Fragments](doc/reference/templates.md#fragments)
|
||||
- [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
|
||||
|
||||
### 13. Portal does no longer transfer DOM events
|
||||
|
||||
In Owl 1, a Portal component would listen to events emitted on its portalled
|
||||
child, and redispatch them on itself. It no longer works.
|
||||
|
||||
Rationale: Portal now supports an arbitrary content (so, more than one child,
|
||||
and potentially no html element), so it is already unclear what it should listen
|
||||
to. Also, redispatching events was an hack. And this changes allows the portal
|
||||
to render itself as a text node, which is nice. This is also in line with the
|
||||
fact that modern Owl moves toward using callback instead of `t-on` for communication.
|
||||
|
||||
Migration: use callback if possible to communicate. Otherwise, use a sub env.
|
||||
|
||||
### 14. Portal does render as an empty text node instead of `<portal/>`
|
||||
|
||||
That is pretty nice. No real migration needed.
|
||||
|
||||
### 15. Context is removed
|
||||
|
||||
Context was an abstraction in Owl that was used to define some reactive state
|
||||
and to let some components subscribe to it, then only them would be rerendered
|
||||
if the context was updated. This has been removed.
|
||||
|
||||
Rationale: first, the Context api and code was kind of awkward, which is a sign
|
||||
that the abstraction is not well thought. But the good news is that it is actually
|
||||
completely replaced by the new reactivity system, which is even more powerful,
|
||||
since it can tracks changes key by key.
|
||||
|
||||
Migration: replace all uses of Context with the new reactivity system.
|
||||
|
||||
```js
|
||||
// somewhere, maybe in a service, or in the global env
|
||||
const context = observe({some: "state"})
|
||||
|
||||
// in a component that would previously get a reference to the context:
|
||||
|
||||
setup() {
|
||||
this.context = useState(context);
|
||||
// now the component is subscribed to the context and will react to any
|
||||
// change for any key read by the component, and only those changes
|
||||
}
|
||||
```
|
||||
|
||||
### 16. `env` is now totally empty
|
||||
|
||||
In Owl 1, the `env` object had to contain a QWeb instance. This was the way
|
||||
components would get a reference to their template function. It no longer works
|
||||
that way: the `env` object is now totally empty (from the perspective of Owl).
|
||||
It is now a user space concept, useful for the application.
|
||||
|
||||
Rationale: first, there is no longer a QWeb class. Also, this changes simplifies
|
||||
the way components works internally.
|
||||
|
||||
Migration: there is no proper way to get an equivalent. The closest is to get
|
||||
a reference to the root App using `this.__owl__.app`. If you need to do this,
|
||||
let us know. If this is a legitimate usecase, we may add a `useApp` hook.
|
||||
|
||||
Documentation: [Environment](doc/reference/environment.md)
|
||||
|
||||
### 17. `t-component` no longer accepts strings
|
||||
|
||||
In owl 1, we could write this:
|
||||
|
||||
```xml
|
||||
<t t-component="Coucou"/>
|
||||
```
|
||||
|
||||
This meant that Owl would look for the component class like this: `components["Coucou"]`,
|
||||
so, essentially equivalent to `<Coucou/>`. In Owl 2, the `t-component` directive
|
||||
is assumed to be an expression evaluating to a component class:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
static template = xml`<t t-component="Child"/>`;
|
||||
Child = Child;
|
||||
}
|
||||
```
|
||||
|
||||
Rationale: it simply seems more consistent with the way directive works. Also,
|
||||
the implementation is slightly simpler.
|
||||
|
||||
Migration: simply using `constructor.components.Coucou` instead of `Coucou` will
|
||||
do the trick.
|
||||
|
||||
Documentation: [Component](doc/reference/component.md#dynamic-sub-components)
|
||||
|
||||
### 18. most exports are exported at top level
|
||||
|
||||
Most exports are flattened: for ex, `onMounted` is in owl, not in `owl.hooks`.
|
||||
|
||||
Rationale: this makes it easier to work with, instead of importing stuff from
|
||||
`owl`, then `owl.hooks` and `owl.tags` for example.
|
||||
|
||||
Migration: all import code simply need to be slightly adapted.
|
||||
|
||||
### 19. Properties are no longer set as attributes
|
||||
|
||||
Formerly, html properties `<input type="checkbox" t-att-checked="blah"/>` were
|
||||
set as property and as attribute, so, they would be visible in the DOM:
|
||||
`<input type="checkbox" checked="blah"/>`. Now, they are treated as property only:
|
||||
`<input type="checkbox"/>`.
|
||||
|
||||
Rationale: this is actually simple to do, is faster, and makes more sense to me.
|
||||
|
||||
### 20. `t-foreach` should always have a corresponding `t-key`
|
||||
|
||||
It was possible in Owl 1 to write a `t-foreach` without a `t-key`. In that case,
|
||||
the index was used as key. Since it was clearly a possible bug, Owl 1 had a
|
||||
warning in some cases, when it could detect that there was definitely not a `t-key`.
|
||||
However, this was imperfect, and in some cases no warning was displayed. In Owl 2,
|
||||
the tag with a `t-foreach` has to have a corresponding `t-key`.
|
||||
|
||||
Rationale: this makes it easier to avoid bugs.
|
||||
|
||||
Migration: simply move the `t-key` to the tag with the `t-foreach`. If this is
|
||||
a situation where there is really not a need for a `t-key`, you can still add
|
||||
it with the `_index` suffix:
|
||||
|
||||
```xml
|
||||
<div t-foreach="items" t-as="item" t-key="item_index">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### 21. `EventBus` api changed: it is now an `EventTarget`
|
||||
|
||||
In Owl 1, the `EventBus` class was done manually, with a custom API. In Owl 2,
|
||||
it simply extends `EventTarget` (the native Dom class), so its implementation
|
||||
is basically only 5 lines long. This means that it has now the usual DOM interface:
|
||||
|
||||
```js
|
||||
bus.addEventListener('event-name', callback);
|
||||
```
|
||||
|
||||
Rationale: it makes it easier to have just one interface to remember, it makes
|
||||
the code simpler
|
||||
|
||||
Migration: most bus methods need to be adapted. So, `bus.on("event-type", owner, (info) => {...})` has to be
|
||||
rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {...}).bind(owner))`.
|
||||
|
||||
Do not forget to similarly replace `bus.off(...)` by `bus.removeEventListener(...)`
|
||||
|
||||
Documentation: [EventBus](doc/reference/utils.md#eventbus)
|
||||
|
||||
### 22. `Store` is removed
|
||||
|
||||
The Store system had been abandoned in owl 2.
|
||||
|
||||
Rationale: first, it was complicated to maintain. Second, it was not really
|
||||
used in Odoo. Finally, the new reactivity system seems to be a pretty good basis
|
||||
to write a store, and it should not take much work. Also, this can be done in
|
||||
user space (so, not necessarily at the framework level). Another point is that
|
||||
the store API was invented before the hooks, then was still a little awkward.
|
||||
|
||||
Migration:
|
||||
- rewrite the code not to use a store
|
||||
- probably use the reactivity system instead and build a store class and a few
|
||||
hooks on top of it.
|
||||
|
||||
### 23. `Router` is removed
|
||||
|
||||
Rationale: Router was not used that much, and it felt like it did not fit in Owl 2.
|
||||
Its API needs to be reworked, and we are not confident that it is a good
|
||||
experience to use it. Also, it can be done in userspace (it does not need specific
|
||||
integration at the framework level)
|
||||
|
||||
Migration: reimport all missing piece from the code in Owl 1.
|
||||
|
||||
### 24. transition system is removed
|
||||
|
||||
Rationale: this was a high ratio cost/value, with a lot of potential for bugs.
|
||||
We feel like there should be a way to reimplement in userspace the simple cases.
|
||||
|
||||
Maybe something like: add a `t-ref` in the template, and define a hook `useFadeOut`
|
||||
that takes the ref, and add a fadeout class at initial render, then in mounted,
|
||||
wait for a micro tick and remove it.
|
||||
|
||||
Migration: try to reimplement it manually.
|
||||
|
||||
### 25. no more global components or templates
|
||||
|
||||
It was possible in Owl 1 to register globally a component or a template. This is
|
||||
no longer the case in Owl 2.
|
||||
|
||||
Rationale: first, this was a tradeoff: ease of use was gained, but at the cost
|
||||
of a higher complexity. Users had to know that there was a magic mechanism. Also,
|
||||
it was not used much in practice, and the cost of having to import manually components
|
||||
is low. Finally, this can be mostly done in user space (for example, by subclassing
|
||||
`Component`).
|
||||
|
||||
Migration: import manually all required global components, or find a way to organize
|
||||
the code to do it.
|
||||
|
||||
### 26. `AsyncRoot` utility component is removed
|
||||
|
||||
Rationale: it was difficult to understand, never used, and not really useful.
|
||||
It seems better to control the asynchrony of an application by simply controlling
|
||||
how/when the state is updated, and how each component is loading/updating itself.
|
||||
|
||||
Migration: remove the `AsyncRoot` component, then possibly, reorganize the code
|
||||
to fetch data in a higher order component, and using a `t-if/t-else` to display
|
||||
either a fallback when the data is not ready, or the actual component with data
|
||||
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
|
||||
us so we can study this usecase.
|
||||
|
||||
### 27. `useChildSubEnv` (only applies to child components)
|
||||
|
||||
In Owl, a call to `useSubEnv` would define a new environment for the children
|
||||
AND the component. It is very useful, but in some cases, one only need to update
|
||||
the children component environment. This can now be done with a new hook:
|
||||
[`useChildSubEnv`](doc/reference/hooks.md#usesubenv-and-usechildsubenv)
|
||||
|
||||
### 28. `env` is now frozen
|
||||
|
||||
In Owl 2, the `env` object is frozen. It can no longer be modified (structurally)
|
||||
arbitrarily.
|
||||
|
||||
Rationale: it seems like the `env` object purpose is to have a global channel of
|
||||
communication between components. It is however scary if anyone can add something
|
||||
to it. The usual use case is to add something to the environment for some child
|
||||
components. This use case still works with `useSubEnv`.
|
||||
|
||||
Migration: use `useSubEnv` instead of writing directly to the env. Also, note
|
||||
that the environment given to the App can initially contain anything.
|
||||
|
||||
Documentation: [Environment](doc/reference/environment.md)
|
||||
|
||||
### 29. `t-ref` does not work on component
|
||||
|
||||
Before, `t-ref` could be used to get a reference to a child component. It no
|
||||
longer works.
|
||||
|
||||
Rationale: the possibility of having a ref to a child component breaks the
|
||||
encapsulation provided by Owl components: a child component now has a private
|
||||
and a public interface. Another issue is that it may be unclear when the ref
|
||||
should be set: is the component active on setup, or on mounted? Also, it is
|
||||
kind of awkward to implement.
|
||||
|
||||
Migration: the `env` and `props` should provide a communication channel wide enough:
|
||||
the sub component can expose its public API by calling a callback at the proper
|
||||
timing, or by triggering an event.
|
||||
|
||||
### 30. `t-on` does not accept expressions, only functions
|
||||
|
||||
In Owl 1, it was possible to define simple expressions inline, in a template:
|
||||
|
||||
```xml
|
||||
<button t-on-click="state.value = state.value + 1">blabla</button>
|
||||
<button t-on-click="someFunction(someVar)">blabla</button>
|
||||
```
|
||||
|
||||
This does not work anymore. Now, the `t-on` directive assumes that what it get is
|
||||
a function.
|
||||
|
||||
Rationale: the fact that owl 1 had to support expressions meant that it was not
|
||||
possible to properly inject the event in general. With this restriction, Owl 2
|
||||
can support more general use cases. Also, the examples above can simply be
|
||||
wrapped in a lambda function.
|
||||
|
||||
Migration: use lambda functions. For example, the two examples above can be
|
||||
adapted like this:
|
||||
|
||||
```xml
|
||||
<button t-on-click="() => state.value = state.value + 1">blabla</button>
|
||||
<button t-on-click="() => this.someFunction(someVar)">blabla</button>
|
||||
```
|
||||
|
||||
Documentation: [Event Handling](doc/reference/event_handling.md)
|
||||
|
||||
### 31. components can now have arbitrary content
|
||||
|
||||
Before Owl 2, components had to limit themselves to one single htmlelement as
|
||||
root. Now, the content is arbitrary: it can be empty, or multiple html elements.
|
||||
So, the following template works for components:
|
||||
|
||||
```xml
|
||||
<div>1</div>
|
||||
<div>2</div>
|
||||
hello
|
||||
```
|
||||
|
||||
Documentation: [Fragments](doc/reference/templates.md#fragments)
|
||||
|
||||
### 32. `renderToString` on QWeb has been removed
|
||||
|
||||
Rationale: the `renderToString` function was a qweb method, which made sense because
|
||||
the qweb instance knew all templates. But now, the closest analogy is the `App`
|
||||
class, but it is not as convenient, since the `app` instance is no longer visible
|
||||
to components (while before, `qweb` was in the environment).
|
||||
|
||||
Also, this can easily be done in userspace, by mounting a component in a div. For example:
|
||||
|
||||
```js
|
||||
export async function renderToString(template, context) {
|
||||
class C extends Component {
|
||||
static template = template;
|
||||
setup () {
|
||||
Object.assign(this, context);
|
||||
}
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
const app = new App(C);
|
||||
await app.mount(div);
|
||||
const result = div.innerHTML;
|
||||
app.destroy();
|
||||
div.remove();
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
The function above works for most cases, but is asynchronous. An alternative
|
||||
function could look like this:
|
||||
|
||||
```js
|
||||
const { App, blockDom } = owl;
|
||||
const app = new App(Component); // act as a template repository
|
||||
|
||||
function renderToString(template, context = {}) {
|
||||
app.addTemplate(template, template, { allowDuplicate: true });
|
||||
const templateFn = app.getTemplate(template);
|
||||
const bdom = templateFn(context, {});
|
||||
const div = document.createElement('div')
|
||||
blockDom.mount(bdom, div);
|
||||
return div.innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
This is a synchronous function, so it will not work with components, but it should
|
||||
be useful for most simple templates.
|
||||
|
||||
Also note that these two examples do not translate their templates. To do that,
|
||||
they need to be modified to pass the proper translate function to the `App`
|
||||
configuration.
|
||||
|
||||
### 33. Portal are now defined with `t-portal`
|
||||
|
||||
Before Owl 2, one could use the `Portal` component by importing it and using it.
|
||||
Now, it is no longer available. Instead, we can simply use the `t-portal` directive:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
some content
|
||||
<span t-portal="'body'">
|
||||
portalled content
|
||||
</span>
|
||||
<div>
|
||||
```
|
||||
|
||||
Rationale: it makes it slightly simpler to use (just need the directive, instead
|
||||
of having to import and use a sub component), it makes the implementation slightly
|
||||
simpler as well. Also, it prevents subclassing the Portal component, which could
|
||||
be dangerous, since it is really doing weird stuff under the hood, and could
|
||||
easily be broken inadvertendly.
|
||||
|
||||
### 34. `debounce` utility function has been removed
|
||||
|
||||
Rationale: it did not really help that much, is available as utility function
|
||||
elsewhere, so, we decided to have a smaller footprint by focusing Owl on what
|
||||
it does best.
|
||||
|
||||
### 35. `render` method does not return a promise anymore
|
||||
|
||||
Rationale: using the `render` method directly and waiting for it to complete
|
||||
was slightly un-declarative. Also, it can be done using the lifecycle hooks
|
||||
any way.
|
||||
|
||||
Migration: if necessary, one can use the lifecycle hooks to execute code after
|
||||
the next mounted/patched operation.
|
||||
|
||||
### 36. `catchError` method is replaced by `onError` hook
|
||||
|
||||
The `catchError` method was used to provide a way to components to handle errors
|
||||
occurring during the component lifecycle. This has been replaced by a `onError`
|
||||
hook, with a similar API.
|
||||
|
||||
Rationale: `catchError` felt a little big awkward, when most of the way we
|
||||
interact with componentss is via hooks. Using hooks felt more natural and
|
||||
consistent.
|
||||
|
||||
Migration: mostly replace all `catchError` methods by `onError` hooks in the
|
||||
`setup` method.
|
||||
|
||||
Documentation: [Error Handling](doc/reference/error_handling.md)
|
||||
|
||||
|
||||
## 37. Support for inline css (`css` tag and static `style`) has been removed
|
||||
|
||||
Rationale: Owl tries to focus on what it does best, and supporting inline css
|
||||
was not a priority. It used to support some simplified scss language, but it
|
||||
was feared that it would cause more trouble than it was worth. Also, it seems
|
||||
like it can be done in userspace.
|
||||
|
||||
Migration: it seems possible to implement an equivalent solution using hooks. A
|
||||
simple implementation could look like this:
|
||||
|
||||
```js
|
||||
let cache = {};
|
||||
|
||||
function useStyle(css) {
|
||||
if (!css in cache) {
|
||||
const sheet = document.createElement("style");
|
||||
sheet.innerHTML = css;
|
||||
cache[css] = sheet;
|
||||
document.head.appendChild(sheet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 38. `t-raw` directive has been removed (replaced by `t-out`)
|
||||
|
||||
To match the Odoo qweb server implementation, Owl does no longer implement `t-raw`.
|
||||
It is replaced by the `t-out` directive, which is safer: it requires the data
|
||||
to be marked explicitely as markup if it is to be inserted without escaping.
|
||||
Otherwise, it will be escaped (just like `t-esc`).
|
||||
|
||||
Migration: replace all `t-raw` uses by `t-out`, and uses the `markup` function
|
||||
to mark all the js values.
|
||||
|
||||
Documentation: [Outputting data](doc/reference/templates.md#outputting-data)
|
||||
|
||||
## 39. `browser` object has been removed
|
||||
|
||||
Rationale: the `browser` object caused more trouble than it was worth. Also, it
|
||||
seems like this should be done in user space, not at the framework level.
|
||||
|
||||
Migration: code should just be adapted to either use another browser object,
|
||||
or to use native browser function (and then, just mock them directly).
|
||||
|
||||
## 40. Rendering a component does not necessarily render child components
|
||||
|
||||
Before, if one had the following component tree:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
```
|
||||
|
||||
when `A` would render, it would also render `B` and `C`. Now, in Owl 2, it will
|
||||
(shallow) compare the before and after props, and `B` or `C` will only be rerendered
|
||||
if their props have changed.
|
||||
|
||||
Now, the question is what happens if the props have changed, but in a deeper way?
|
||||
In that case, Owl will know, because each props are now reactive. So, if some
|
||||
inner value read by `B` was changed, then only `B` will be updated.
|
||||
|
||||
Rationale: This was just not possible in Owl 1, but it now possible. This is
|
||||
due to the rewriteof the underlying rendering engine and the reactivity
|
||||
system. The goal is to have a big performance boost in large screen with many
|
||||
components: now Owl only rerender what is strictly useful.
|
||||
|
||||
@@ -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.
|
||||
@@ -1,12 +1,9 @@
|
||||
|
||||
For copyright information, please see the COPYRIGHT file.
|
||||
|
||||
Odoo is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
|
||||
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 Odoo may be published
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,86 +1,128 @@
|
||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Odoo Web Library</a> 🦉</h1>
|
||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Owl Framework</a> 🦉</h1>
|
||||
|
||||
_A web framework for structured, dynamic and maintainable applications_
|
||||
[](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_
|
||||
|
||||
**Try it online!** you can experiment with the Owl framework in an online [playground](https://odoo.github.io/owl/playground).
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Odoo Web Library (OWL) is a small
|
||||
UI framework intended to be the basis for the [Odoo](https://www.odoo.com/) Web Client, and hopefully many
|
||||
other Odoo related projects. OWL's main feature is a _declarative component system_, with QWeb as a template engine, asynchronous rendering, and an underlying virtual dom.
|
||||
The Odoo Web Library (Owl) is a smallish (~<20kb gzipped) UI framework built by
|
||||
[Odoo](https://www.odoo.com/) for its products. 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:
|
||||
|
||||
**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.
|
||||
- a declarative component system,
|
||||
- a fine grained reactivity system similar to Vue,
|
||||
- hooks
|
||||
- fragments
|
||||
- asynchronous rendering
|
||||
|
||||
## OWL's design principles
|
||||
Owl components are defined with ES6 classes and xml templates, uses an
|
||||
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
|
||||
asynchronous.
|
||||
|
||||
OWL is designed to be used in highly dynamic applications where changing
|
||||
requirements are common, code needs to be maintained by large teams.
|
||||
Quick links:
|
||||
|
||||
- **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`.
|
||||
- [documentation](#documentation),
|
||||
- [changelog](CHANGELOG.md) (from Owl 1.x to 2.x),
|
||||
- [playground](https://odoo.github.io/owl/playground)
|
||||
|
||||
Owl is not designed to be fast or small (even though it is quite good on those
|
||||
two topics). If you are interested in a comparison with React or Vue, you will
|
||||
find some more information [here](doc/comparison.md).
|
||||
## Example
|
||||
|
||||
# Example
|
||||
|
||||
Here is a short example to illustrate interactive widgets:
|
||||
Here is a short example to illustrate interactive components:
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
inlineTemplate = `
|
||||
<button t-on-click="increment">
|
||||
const { Component, useState, mount, xml } = owl;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="() => state.value = state.value + props.increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = { value: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
const qweb = new owl.QWeb();
|
||||
const counter = new ClickCounter({ qweb });
|
||||
counter.mount(document.body);
|
||||
class Root extends Component {
|
||||
static template = xml`
|
||||
<span>Hello Owl</span>
|
||||
<Counter increment="2"/>`;
|
||||
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
More interesting examples can be found on the [playground](https://odoo.github.io/owl/playground) application.
|
||||
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/templates.md#inline-templates) to define inline templates.
|
||||
But this is not mandatory, many applications will load templates separately.
|
||||
|
||||
## Installing/Building
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl-0.9.0.js](https://github.com/odoo/owl/releases/download/v0.9.0/owl-0.9.0.js)
|
||||
- [owl-0.9.0.min.js](https://github.com/odoo/owl/releases/download/v0.9.0/owl-0.9.0.min.js)
|
||||
|
||||
Some npm scripts are available:
|
||||
|
||||
| Command | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------- |
|
||||
| `npm install` | install every dependency required for this project |
|
||||
| `npm run build` | build a bundle of _owl_ in the _/dist/_ folder |
|
||||
| `npm run minify` | minify the prebuilt owl.js file |
|
||||
| `npm run test` | run all tests |
|
||||
| `npm run test:watch` | run all tests, and keep a watcher |
|
||||
| `npm run extras` | build extras applications, start a static server (see [here](extras/readme.md)) |
|
||||
More interesting examples can be found on the
|
||||
[playground](https://odoo.github.io/owl/playground) application.
|
||||
|
||||
## Documentation
|
||||
|
||||
The complete documentation can be found [here](doc/readme.md). The most important sections are:
|
||||
### Learning Owl
|
||||
|
||||
- [Quick Start](doc/quick_start.md)
|
||||
- [Component](doc/component.md)
|
||||
- [QWeb](doc/qweb.md)
|
||||
Are you new to Owl? This is the place to start!
|
||||
|
||||
## License
|
||||
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
|
||||
- [How to start an Owl project](doc/learning/quick_start.md)
|
||||
- [How to test Components](doc/learning/how_to_test.md)
|
||||
|
||||
### Reference
|
||||
|
||||
- [Overview](doc/readme.md)
|
||||
- [App](doc/reference/app.md)
|
||||
- [Component](doc/reference/component.md)
|
||||
- [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||
- [Concurrency Model](doc/reference/concurrency_model.md)
|
||||
- [Dev mode](doc/reference/app.md#dev-mode)
|
||||
- [Dynamic sub components](doc/reference/component.md#dynamic-sub-components)
|
||||
- [Environment](doc/reference/environment.md)
|
||||
- [Error Handling](doc/reference/error_handling.md)
|
||||
- [Event Handling](doc/reference/event_handling.md)
|
||||
- [Form Input Bindings](doc/reference/input_bindings.md)
|
||||
- [Fragments](doc/reference/templates.md#fragments)
|
||||
- [Hooks](doc/reference/hooks.md)
|
||||
- [Loading Templates](doc/reference/app.md#loading-templates)
|
||||
- [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
- [Portal](doc/reference/portal.md)
|
||||
- [Precompiling templates](doc/reference/precompiling_templates.md)
|
||||
- [Props](doc/reference/props.md)
|
||||
- [Props Validation](doc/reference/props.md#props-validation)
|
||||
- [Reactivity](doc/reference/reactivity.md)
|
||||
- [Rendering SVG](doc/reference/templates.md#rendering-svg)
|
||||
- [Refs](doc/reference/refs.md)
|
||||
- [Slots](doc/reference/slots.md)
|
||||
- [Sub components](doc/reference/component.md#sub-components)
|
||||
- [Sub templates](doc/reference/templates.md#sub-templates)
|
||||
- [Templates (Qweb)](doc/reference/templates.md)
|
||||
- [Translations](doc/reference/translations.md)
|
||||
- [Utils](doc/reference/utils.md)
|
||||
|
||||
### Other Topics
|
||||
|
||||
- [Notes On Owl Architecture](doc/miscellaneous/architecture.md)
|
||||
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||
- [Notes on compiled templates](doc/miscellaneous/compiled_template.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)
|
||||
|
||||
OWL is [GPL licensed](./LICENSE).
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# 🦉 Animations 🦉
|
||||
|
||||
|
||||
Animation is a complex topic. There are many different use cases, and many
|
||||
solutions and technologies.
|
||||
|
||||
## 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
|
||||
<button class="flash" t-on-click="doSomething">Click</button>
|
||||
```
|
||||
|
||||
with the following CSS:
|
||||
|
||||
```css
|
||||
.flash {
|
||||
transition: background 0.5s;
|
||||
}
|
||||
|
||||
.flash:active {
|
||||
background-color: #41454a;
|
||||
transition: background 0s;
|
||||
}
|
||||
```
|
||||
|
||||
will produce a nice flash effect whenever the user click (or activate with the
|
||||
keyboard) the button.
|
||||
|
||||
## CSS Transitions (single element)
|
||||
|
||||
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 (see [QWeb documentation](doc/qweb.md#t-transition-directive)).
|
||||
@@ -1,194 +0,0 @@
|
||||
# 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, some
|
||||
effort was done 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.
|
||||
|
||||
- [Size](#size)
|
||||
- [Tooling/Build Step](#toolingbuild-step)
|
||||
- [Templating](#templating)
|
||||
- [Asynchronous rendering](#asynchronous-rendering)
|
||||
- [Reactiveness](#reactiveness)
|
||||
- [State Management](#state-management)
|
||||
|
||||
### 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) | Size (minified, gzipped) |
|
||||
| ------------------------ | --------------- | ------------------------ |
|
||||
| OWL | 32kb | 11kb |
|
||||
| Vue + VueX | | 30kb |
|
||||
| React + ReactDOM + Redux | | 40kb |
|
||||
| jQuery | 86kb | 30kb |
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
```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!
|
||||
|
||||
### 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 widget 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 widget may want to fetch an external
|
||||
library (a calendar widget may need a specialized calendar rendering library),
|
||||
in its willStart hook.
|
||||
|
||||
```javascript
|
||||
class MyCalendarWidget 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.
|
||||
|
||||
### 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 a counter whenever it changes (and a _deep_ counter for each of
|
||||
its parents). This assumes that the state is actually a tree.
|
||||
|
||||
### 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 mutations and
|
||||
actions, like VueX, it keeps track of the state changes, but it does not notify
|
||||
a component when the state changes. Instead, components need to connect to the
|
||||
store like in redux, with a function that will listen to the relevant state.
|
||||
@@ -1,316 +0,0 @@
|
||||
# 🦉 OWL Component 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Templates](#templates)
|
||||
- [Reference](#reference)
|
||||
- [Properties](#properties)
|
||||
- [Methods](#methods)
|
||||
- [Lifecycle](#lifecycle)
|
||||
|
||||
## Overview
|
||||
|
||||
OWL components are the building blocks for user interface. They are designed to be:
|
||||
|
||||
1. **declarative:** the user interface should be described in term of the state
|
||||
of the application, not as a sequence of imperative steps.
|
||||
|
||||
2. **composable:** each widget can seamlessly be created in a parent widget by
|
||||
a simple directive in its template.
|
||||
|
||||
3. **asynchronous rendering:** the framework will transparently wait for each
|
||||
subwidgets 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.md) template (either defined inline or preloaded in QWeb).
|
||||
The rendering is done by QWeb, which will generate a virtual dom representation
|
||||
of the widget, then patch the DOM to apply the changes in an efficient way.
|
||||
|
||||
OWL components observe their states, and rerender themselves whenever it is
|
||||
changed. This is done by an [observer](observer.md).
|
||||
|
||||
## Example
|
||||
|
||||
Let us have a look at a simple component:
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
template = "click-counter";
|
||||
state = { value: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<button t-name="click-counter" 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 show how a component should be defined: it simply subclasses the
|
||||
Component class. It needs to define a template (`click-counter` here). Also,
|
||||
a state object is defined. It is not mandatory to use the state object, but it
|
||||
is certainly encouraged. The state object is [observed](observer.md), and any
|
||||
change to it will cause a rerendering.
|
||||
|
||||
## Templates
|
||||
|
||||
The example above shows a QWeb template with a `t-on-click` directive. Widget
|
||||
templates are standard [QWeb](qweb.md) templates, but with an extra directive:
|
||||
`t-widget`. With the `t-widget` directive, widget templates can declare sub
|
||||
widgets:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<span>some text</span>
|
||||
<t t-widget="MyWidget" t-props="{info: 13}">
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the template create a widget MyWidget just after the span. See
|
||||
the [QWeb](qweb.md) documentation for more information on the `t-widget` directive.
|
||||
|
||||
Note that the rendering context for the template is the widget itself. This means
|
||||
that the template can access `state`, `props`, `env`, or any methods defined in the widget.
|
||||
|
||||
## Reference
|
||||
|
||||
An Owl component is a small class which represent a widget or some UI element.
|
||||
It exists in the context of an environment (`env`), which is propagated from a
|
||||
parent to its children. The environment needs to have a QWeb instance, which
|
||||
will be used to render the component template.
|
||||
|
||||
### 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, which contains a QWeb instance.
|
||||
|
||||
- **`template`** (string): a string, which is the name of the QWeb template that will render
|
||||
the component.
|
||||
|
||||
- **`inlineTemplate`** (string, optional): a string that represents a xml template. If set,
|
||||
this will be loaded into QWeb and used instead of the `template` property.
|
||||
|
||||
- **`state`** (Object): this is the location of the component's state, if there is
|
||||
any. After the willStart method, the `state` property is observed, and each
|
||||
change will cause the widget to rerender itself.
|
||||
|
||||
- **`props`** (Object): this is an object given (in the constructor) by the parent
|
||||
to configure the component. It can be dynamically changed later by the parent,
|
||||
in some case. Note that `props` are owned by the parent, not by the component.
|
||||
As such, it should not ever be modified by the component.
|
||||
|
||||
- **`refs`** (Object): the `refs` object contains all references to sub DOM nodes
|
||||
or sub widgets defined by a `t-ref` directive in the component's template.
|
||||
|
||||
### Methods
|
||||
|
||||
- **`mount(target)`** (async): this is the main way a component's hierarchy is added to the
|
||||
DOM: the root component is mounted to a target HTMLElement. 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.
|
||||
|
||||
- **`unmount()`**: in case a component need 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 widget 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. 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.
|
||||
|
||||
- **`updateEnv(nextEnv)`**: update the environment of a component and all its
|
||||
children. This forces a complete rerender. For example, this could be useful
|
||||
if we have a `isMobile` key in the environment, to decide if we want a mobile
|
||||
interface or a destkop one.
|
||||
|
||||
- **`set(target, key, value)`**. This method is necessary in some cases when we
|
||||
need to modify the state of the component in a way that is not visible to the
|
||||
observer (see [observer's technical limitations](observer.md#technical-limitations)).
|
||||
For example, if we need to add a key to the state.
|
||||
|
||||
- **`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.
|
||||
|
||||
### 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 |
|
||||
| ------------------------------------------------ | --------------------------------------- |
|
||||
| **[constructor](#constructor)** | constructor |
|
||||
| **[willStart](#willStart)** | async, before first rendering |
|
||||
| **[mounted](#mounted)** | when component is render and in 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 |
|
||||
|
||||
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 = {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 {
|
||||
template = "click-counter";
|
||||
state = { value: 0 };
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### `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 widget 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.
|
||||
|
||||
The widget rendering will take place after `willStart` is completed.
|
||||
|
||||
#### `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.
|
||||
|
||||
Note that the state is now observed. It is however 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 some asynchronous task
|
||||
performed, 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 some
|
||||
information from the DOM. For example, the current position of the
|
||||
scrollbar.
|
||||
|
||||
Note that modifying the state object 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 widget is not in the DOM (this can
|
||||
happen with widgets with `t-keepalive`).
|
||||
|
||||
The return value of this method will be given as the first argument of the
|
||||
corresponding `patched` call.
|
||||
|
||||
#### `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 widget is
|
||||
not in the DOM (this can happen with widgets with `t-keepalive`).
|
||||
|
||||
The `snapshot` parameter is the result of the previous `willPatch` call.
|
||||
|
||||
Updating the widget 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 patched. 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 some 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`. The `willUnmount` method will be
|
||||
called in reverse order: first the children, then the parents.
|
||||
@@ -0,0 +1,95 @@
|
||||
# 🦉 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
|
||||
let lastFixture = null;
|
||||
|
||||
export function makeTestFixture() {
|
||||
let fixture = document.createElement("div");
|
||||
document.body.appendChild(fixture);
|
||||
if (lastFixture) {
|
||||
lastFixture.remove();
|
||||
}
|
||||
lastFixture = fixture;
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export async function nextTick() {
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
```
|
||||
|
||||
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 } from '../helpers';
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup
|
||||
//------------------------------------------------------------------------------
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
describe("SomeComponent", () => {
|
||||
test("component behaves as expected", async () => {
|
||||
const props = {...}; // depends on the component
|
||||
const comp = await mount(SomeComponent, 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.
|
||||
@@ -0,0 +1,399 @@
|
||||
# 🦉 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.
|
||||
Note that there are multiple files, and in this case, we need one of the two
|
||||
files suffixed with `.iife`: they are built to be directly used in a browser.
|
||||
|
||||
Now, `index.html` should contain the following:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
<script src="owl.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
And `app.js` should look like this:
|
||||
|
||||
```js
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
// Owl Components
|
||||
class Root extends Component {
|
||||
static template = xml`<div>Hello Owl</div>`;
|
||||
}
|
||||
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
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/
|
||||
index.html
|
||||
main.js
|
||||
owl.js
|
||||
root.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).
|
||||
Note that there are multiple files, and in this case, we need one of the two
|
||||
files suffixed with `.iife`: they are built to be directly used in a browser.
|
||||
|
||||
Now, `index.html` should contain the following:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
<script src="owl.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.js" type="module"></script>
|
||||
</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 `root.js` and `main.js`:
|
||||
|
||||
```js
|
||||
// root.js ----------------------------------------------------------------------
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
export class Root extends Component {
|
||||
static template = xml`<div>Hello Owl</div>`;
|
||||
}
|
||||
|
||||
// main.js ---------------------------------------------------------------------
|
||||
import { Root } from "./root.js";
|
||||
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
The `main.js` file imports the `root.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/
|
||||
Root.js
|
||||
main.js
|
||||
tests/
|
||||
components/
|
||||
Root.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/Root.js -------------------------------------------------------
|
||||
import { Component, xml, useState } from "@odoo/owl";
|
||||
|
||||
export class Root 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 { Root } from "./components/Root";
|
||||
|
||||
mount(Root, document.body);
|
||||
|
||||
// tests/components/Root.test.js ------------------------------------------------
|
||||
import { Root } from "../../src/components/Root";
|
||||
import { makeTestFixture, nextTick, click } from "../helpers";
|
||||
import { mount } from "@odoo/owl";
|
||||
|
||||
let fixture;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
describe("Root", () => {
|
||||
test("Works as expected...", async () => {
|
||||
await mount(Root, 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() {
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
await new Promise((resolve) => 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
|
||||
```
|
||||
@@ -0,0 +1,992 @@
|
||||
# 🦉 OWL Tutorial: TodoApp 🦉
|
||||
|
||||
For this tutorial, we will build a very simple Todo list application. The app
|
||||
should satisfy the following requirements:
|
||||
|
||||
- let the user create and remove tasks
|
||||
- tasks can be marked as completed
|
||||
- tasks can be filtered to display active/completed tasks
|
||||
|
||||
This project will be an opportunity to discover and learn some important Owl
|
||||
concepts, such as components, store, and how to organize an application.
|
||||
|
||||
## Content
|
||||
|
||||
1. [Setting up the project](#1-setting-up-the-project)
|
||||
2. [Adding a first component](#2-adding-a-first-component)
|
||||
3. [Displaying a list of tasks](#3-displaying-a-list-of-tasks)
|
||||
4. [Layout: some basic css](#4-layout-some-basic-css)
|
||||
5. [Extracting Task as a subcomponent](#5-extracting-task-as-a-subcomponent)
|
||||
6. [Adding tasks (part 1)](#6-adding-tasks-part-1)
|
||||
7. [Adding tasks (part 2)](#7-adding-tasks-part-2)
|
||||
8. [Toggling tasks](#8-toggling-tasks)
|
||||
9. [Deleting tasks](#9-deleting-tasks)
|
||||
10. [Using a store](#10-using-a-store)
|
||||
11. [Saving tasks in local storage](#11-saving-tasks-in-local-storage)
|
||||
12. [Filtering tasks](#12-filtering-tasks)
|
||||
13. [The Final Touch](#13-the-final-touch)
|
||||
14. [Final Code](#final-code)
|
||||
|
||||
## 1. Setting up the project
|
||||
|
||||
For this tutorial, we will do a very simple project, with static files and
|
||||
no additional tooling. The first step is to create the following file structure:
|
||||
|
||||
```
|
||||
todoapp/
|
||||
index.html
|
||||
app.css
|
||||
app.js
|
||||
owl.js
|
||||
```
|
||||
|
||||
The entry point for this application is the file `index.html`, which should have
|
||||
the following content:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OWL Todo App</title>
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="owl.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Then, `app.css` can be left empty for now. It will be useful later on to style
|
||||
our application. `app.js` is where we will write all our code. For now, let's
|
||||
just put the following code:
|
||||
|
||||
```js
|
||||
(function () {
|
||||
console.log("hello owl", owl.__info__.version);
|
||||
})();
|
||||
```
|
||||
|
||||
Note that we put everything inside an immediately executed function to avoid leaking
|
||||
anything to the global scope.
|
||||
|
||||
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
|
||||
are built to run directly on the browser, and rename it `owl.js` (other files such as `owl.cjs.js` are
|
||||
built to be bundled by other tools).
|
||||
|
||||
Now, the project should be ready. Loading the `index.html` file into a browser
|
||||
should show an empty page, with the title `Owl Todo App`, and it should log a
|
||||
message such as `hello owl 2.x.y` in the console.
|
||||
|
||||
## 2. Adding a first component
|
||||
|
||||
An Owl application is made out of [components](../reference/component.md), with
|
||||
a single root component. Let us start by defining a `Root` component. Replace the
|
||||
content of the function in `app.js` by the following code:
|
||||
|
||||
```js
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
// Owl Components
|
||||
class Root extends Component {
|
||||
static template = xml`<div>todo app</div>`;
|
||||
}
|
||||
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
Now, reloading the page in a browser should display a message.
|
||||
|
||||
The code is pretty simple: we define a component with an inline template, then
|
||||
mount it in the document body.
|
||||
|
||||
Note 1: in a larger project, we would split the code in multiple files, with
|
||||
components in a sub folder, and a main file that would initialize the application.
|
||||
However, this is a very small project, and we want to keep it as simple as possible.
|
||||
|
||||
Note 2: this tutorial uses the static class field syntax. This is not yet
|
||||
supported by all browsers. Most real projects will transpile their code, so this
|
||||
is not a problem, but for this tutorial, if you need the code to work on every
|
||||
browser, you will need to translate each `static` keyword to an assignation to
|
||||
the class:
|
||||
|
||||
```js
|
||||
class App extends Component {}
|
||||
App.template = xml`<div>todo app</div>`;
|
||||
```
|
||||
|
||||
Note 3: writing inline templates with the [`xml` helper](../reference/templates.md#inline-templates)
|
||||
is nice, but there is no syntax highlighting, and this makes it very easy to
|
||||
have malformed xml. Some editors support syntax highlighting for this situation.
|
||||
For example, VS Code has an addon `Comment tagged template`, which, if installed,
|
||||
will properly display tagged templates:
|
||||
|
||||
```js
|
||||
static template = xml /* xml */`<div>todo app</div>`;
|
||||
```
|
||||
|
||||
Note 4: Large applications will probably want to be able to translate templates.
|
||||
Using inline templates makes it slightly harder, since we need additional tooling
|
||||
to extract the xml from the code, and to replace it with the translated values.
|
||||
|
||||
## 3. Displaying a list of tasks
|
||||
|
||||
Now that the basics are done, it is time to start thinking about tasks. To
|
||||
accomplish what we need, we will keep track of the tasks as an array of objects
|
||||
with the following keys:
|
||||
|
||||
- `id`: a number. It is extremely useful to have a way to uniquely identify
|
||||
tasks. Since the title is something created/edited by the user, it offers
|
||||
no guarantee that it is unique. So, we will generate a unique `id` number for
|
||||
each task.
|
||||
- `text`: a string, to explain what the task is about.
|
||||
- `isCompleted`: a boolean, to keep track of the status of the task
|
||||
|
||||
Now that we decided on the internal format of the state, let us add some demo
|
||||
data and a template to the `App` component:
|
||||
|
||||
```js
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<div class="task">
|
||||
<input type="checkbox" t-att-checked="task.isCompleted"/>
|
||||
<span><t t-esc="task.text"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</div>`;
|
||||
|
||||
tasks = [
|
||||
{
|
||||
id: 1,
|
||||
text: "buy milk",
|
||||
isCompleted: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: "clean house",
|
||||
isCompleted: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate
|
||||
through the tasks. It can find the `tasks` list from the component, since the
|
||||
component is the rendering context. Note that we use the `id` of each task as a
|
||||
`t-key`, which is very common. There are two css classes: `task-list` and `task`,
|
||||
that we will use in the next section.
|
||||
|
||||
Finally, notice the use of the `t-att-checked` attribute:
|
||||
prefixing an attribute by [`t-att`](../reference/templates.md#dynamic-attributes) makes
|
||||
it dynamic. Owl will evaluate the expression and set it as the value of the
|
||||
attribute.
|
||||
|
||||
## 4. Layout: some basic css
|
||||
|
||||
So far, our task list looks quite bad. Let us add the following to `app.css`:
|
||||
|
||||
```css
|
||||
.task-list {
|
||||
width: 300px;
|
||||
margin: 50px auto;
|
||||
background: aliceblue;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.task {
|
||||
font-size: 18px;
|
||||
color: #111111;
|
||||
}
|
||||
```
|
||||
|
||||
This is better. Now, let us add an extra feature: completed tasks should be
|
||||
styled a little differently, to make it clearer that they are not as important.
|
||||
To do that, we will add a dynamic css class on each task:
|
||||
|
||||
```xml
|
||||
<div class="task" t-att-class="task.isCompleted ? 'done' : ''">
|
||||
```
|
||||
|
||||
```css
|
||||
.task.done {
|
||||
opacity: 0.7;
|
||||
}
|
||||
```
|
||||
|
||||
Notice that we have here another use of a dynamic attribute.
|
||||
|
||||
## 5. Extracting Task as a subcomponent
|
||||
|
||||
It is now clear that there should be a `Task` component to encapsulate the look
|
||||
and behavior of a task.
|
||||
|
||||
This `Task` component will display a task, but it cannot _own_ the state of the
|
||||
task: a piece of data should only have one owner. Doing otherwise is asking for
|
||||
trouble. So, the `Task` component will get its data as a `prop`. This means that
|
||||
the data is still owned by the `App` component, but can be used by the `Task`
|
||||
component (without modifying it).
|
||||
|
||||
Since we are moving code around, it is a good opportunity to refactor the code
|
||||
a little bit:
|
||||
|
||||
```js
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Task extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
</div>`;
|
||||
static props = ["task"];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Root extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>`;
|
||||
static components = { Task };
|
||||
|
||||
tasks = [
|
||||
...
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
mount(Root, document.body, {dev: true});
|
||||
```
|
||||
|
||||
A lot of stuff happened here:
|
||||
|
||||
- first, we have now a sub component `Task`, defined on top of the file,
|
||||
- whenever we define a sub component, it needs to be added to the static
|
||||
[`components`](../reference/component.md#static-properties)
|
||||
key of its parent, so Owl can get a reference to it,
|
||||
- the `Task` component has a `props` key: this is only useful for validation
|
||||
purpose. It says that each `Task` should be given exactly one prop, named
|
||||
`task`. If this is not the case, Owl will throw an
|
||||
[error](../reference/props.md#props-validation). This is extremely
|
||||
useful when refactoring components
|
||||
- finally, to activate the props validation, we need to set Owl's
|
||||
[mode](../reference/app.md#configuration) to `dev`. This is done in the last argument
|
||||
of the `mount` function. Note that this should be removed when an app is used in a real
|
||||
production environment, since `dev` mode is slightly slower, due to extra
|
||||
checks and validations.
|
||||
|
||||
## 6. Adding tasks (part 1)
|
||||
|
||||
We still use a list of hardcoded tasks. It's really time to give the user a way
|
||||
to add tasks himself. The first step is to add an input to the `Root` component.
|
||||
But this input will be outside of the task list, so we need to adapt `Root`
|
||||
template, js, and css:
|
||||
|
||||
```xml
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
const text = ev.target.value.trim();
|
||||
ev.target.value = "";
|
||||
console.log('adding task', text);
|
||||
// todo
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
.todo-app {
|
||||
width: 300px;
|
||||
margin: 50px auto;
|
||||
background: aliceblue;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.todo-app > input {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
We now have a working input, which log to the console whenever the user adds a
|
||||
task. Notice that when you load the page, the input is not focused. But adding
|
||||
tasks is a core feature of a task list, so let us make it as fast as possible by
|
||||
focusing the input.
|
||||
|
||||
We need to execute code when the `Root` component is ready (mounted). Let's do
|
||||
that using the `onMounted` hook. We will also need to get a reference to the
|
||||
input, by using the `t-ref` directive with the [`useRef`](../reference/hooks.md#useref) hook:
|
||||
|
||||
```xml
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
```
|
||||
|
||||
```js
|
||||
// on top of file:
|
||||
const { Component, mount, xml, useRef, onMounted } = owl;
|
||||
```
|
||||
|
||||
```js
|
||||
// in App
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
}
|
||||
```
|
||||
|
||||
This is a very common situation: whenever we need to perform some actions depending
|
||||
on the lifecycle of a component, we need to do it in the `setup` method, by using
|
||||
one of the lifecycle hook. Here, we first get a reference to the `inputRef`,
|
||||
then in the `onMounted` hook, we simply focus the html element.
|
||||
|
||||
## 7. Adding tasks (part 2)
|
||||
|
||||
In the previous section, we did everything except implement the code that actually
|
||||
create tasks! So, let us do that now.
|
||||
|
||||
We need a way to generate unique `id` numbers. To do that, we will simply add a
|
||||
`nextId` number in `App`. At the same time, let us remove the demo tasks in `App`:
|
||||
|
||||
```js
|
||||
nextId = 1;
|
||||
tasks = [];
|
||||
```
|
||||
|
||||
Now, the `addTask` method can be implemented:
|
||||
|
||||
```js
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
const text = ev.target.value.trim();
|
||||
ev.target.value = "";
|
||||
if (text) {
|
||||
const newTask = {
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(newTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This almost works, but if you test it, you will notice that no new task is ever
|
||||
displayed when the user press `Enter`. But if you add a `debugger` or a
|
||||
`console.log` statement, you will see that the code is actually running as
|
||||
expected. The problem is that Owl has no way of knowing that it needs to rerender
|
||||
the user interface. We can fix the issue by making `tasks` reactive, with the
|
||||
[`useState`](../reference/hooks.md#usestate) hook:
|
||||
|
||||
```js
|
||||
// on top of the file
|
||||
const { Component, mount, xml, useRef, onMounted, useState } = owl;
|
||||
|
||||
// replace the task definition in App with the following:
|
||||
tasks = useState([]);
|
||||
```
|
||||
|
||||
It now works as expected!
|
||||
|
||||
## 8. Toggling tasks
|
||||
|
||||
If you tried to mark a task as completed, you may have noticed that the text
|
||||
did not change in opacity. This is because there is no code to modify the
|
||||
`isCompleted` flag.
|
||||
|
||||
Now, this is an interesting situation: the task is displayed by the `Task`
|
||||
component, but it is not the owner of its state, so ideally, it should not modify it.
|
||||
However, for now, that's what we will do (this will be improved in a later step).
|
||||
In `Task`, change the `input` to:
|
||||
|
||||
```xml
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
|
||||
```
|
||||
|
||||
and add the `toggleTask` method:
|
||||
|
||||
```js
|
||||
toggleTask() {
|
||||
this.props.task.isCompleted = !this.props.task.isCompleted;
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Deleting tasks
|
||||
|
||||
Let us now add the possibility do delete tasks. This is different from the previous
|
||||
feature: deleting task has to be done on the task itself, but the actual operation
|
||||
need to be done on the task list. So, we need to communicate the request to the
|
||||
`Root` component. This is usually done by providing a callback in a prop.
|
||||
|
||||
First, let us update the `Task` template, css and js:
|
||||
|
||||
```xml
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
<span class="delete" t-on-click="deleteTask">🗑</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.task {
|
||||
font-size: 18px;
|
||||
color: #111111;
|
||||
display: grid;
|
||||
grid-template-columns: 30px auto 30px;
|
||||
}
|
||||
|
||||
.task > input {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.delete {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task:hover .delete {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
static props = ["task", "onDelete"];
|
||||
|
||||
deleteTask() {
|
||||
this.props.onDelete(this.props.task);
|
||||
}
|
||||
```
|
||||
|
||||
And now, we need to provide the `onDelete` callback to each tasks in the `Root`
|
||||
component:
|
||||
|
||||
```xml
|
||||
<Task task="task" onDelete.bind="deleteTask"/>
|
||||
```
|
||||
|
||||
```js
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex(t => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
|
||||
suffix that makes sure the function callback is bound to the component.
|
||||
|
||||
## 10. Using a store
|
||||
|
||||
Looking at the code, it is apparent that all the code handling tasks is scattered
|
||||
all around the application. Also, it mixes UI code and business logic
|
||||
code. Owl does not provide any high level abstraction to manage business logic,
|
||||
but it is easy to do it with the basic reactivity primitives (`useState` and `reactive`).
|
||||
|
||||
Let us use it in our application to implement a central store. This is a pretty
|
||||
large refactoring (for our application), since it involves extracting all task
|
||||
related code out of the components. Here is the new content of the `app.js` file:
|
||||
|
||||
```js
|
||||
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Store
|
||||
// -------------------------------------------------------------------------
|
||||
function useStore() {
|
||||
const env = useEnv();
|
||||
return useState(env.store);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TaskList
|
||||
// -------------------------------------------------------------------------
|
||||
class TaskList {
|
||||
nextId = 1;
|
||||
tasks = [];
|
||||
|
||||
addTask(text) {
|
||||
text = text.trim();
|
||||
if (text) {
|
||||
const task = {
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTask(task) {
|
||||
task.isCompleted = !task.isCompleted;
|
||||
}
|
||||
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex((t) => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
return reactive(new TaskList());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Task extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="() => store.toggleTask(props.task)"/>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
|
||||
</div>`;
|
||||
|
||||
static props = ["task"];
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="store.tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>`;
|
||||
static components = { Task };
|
||||
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
this.store.addTask(ev.target.value);
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
const env = {
|
||||
store: createTaskStore(),
|
||||
};
|
||||
mount(Root, document.body, { dev: true, env });
|
||||
```
|
||||
|
||||
## 11. Saving tasks in local storage
|
||||
|
||||
Now, our TodoApp works great, except if the user closes or refresh the browser!
|
||||
It is really inconvenient to only keep the state of the application in memory.
|
||||
To fix this, we will save the tasks in the local storage. With our current
|
||||
codebase, it is a simple change: we need to save tasks to local storage and
|
||||
listen to any change.
|
||||
|
||||
```js
|
||||
class TaskList {
|
||||
constructor(tasks) {
|
||||
this.tasks = tasks || [];
|
||||
const taskIds = this.tasks.map((t) => t.id);
|
||||
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||
saveTasks();
|
||||
return taskStore;
|
||||
}
|
||||
```
|
||||
|
||||
The key point is that the `reactive` function takes a callback that will be called
|
||||
every time an observed value is changed. Note that we need to call the `saveTasks`
|
||||
method initially to make sure we observe all current values.
|
||||
|
||||
## 12. Filtering tasks
|
||||
|
||||
We are almost done, we can add/update/delete tasks. The only missing feature is
|
||||
the possibility to display the task according to their completed status. We will
|
||||
need to keep track of the state of the filter in `Root`, then filter the visible
|
||||
tasks according to its value.
|
||||
|
||||
```js
|
||||
class Root extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="task-panel" t-if="store.tasks.length">
|
||||
<div class="task-counter">
|
||||
<t t-esc="displayedTasks.length"/>
|
||||
<t t-if="displayedTasks.length lt store.tasks.length">
|
||||
/ <t t-esc="store.tasks.length"/>
|
||||
</t>
|
||||
task(s)
|
||||
</div>
|
||||
<div>
|
||||
<span t-foreach="['all', 'active', 'completed']"
|
||||
t-as="f" t-key="f"
|
||||
t-att-class="{active: filter.value===f}"
|
||||
t-on-click="() => this.setFilter(f)"
|
||||
t-esc="f"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
...
|
||||
this.filter = useState({ value: "all" });
|
||||
}
|
||||
|
||||
get displayedTasks() {
|
||||
const tasks = this.store.tasks;
|
||||
switch (this.filter.value) {
|
||||
case "active": return tasks.filter(t => !t.isCompleted);
|
||||
case "completed": return tasks.filter(t => t.isCompleted);
|
||||
case "all": return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.filter.value = filter;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
.task-panel {
|
||||
color: #0088ff;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.task-panel .task-counter {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.task-panel span {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-panel span.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
Notice here that we set dynamically the css class of the filter with the object
|
||||
syntax.
|
||||
|
||||
## 13. The Final Touch
|
||||
|
||||
Our list is feature complete. We can still add a few extra details to improve
|
||||
the user experience.
|
||||
|
||||
1. Add a visual feedback when the user mouse is over a task:
|
||||
|
||||
```css
|
||||
.task:hover {
|
||||
background-color: #def0ff;
|
||||
}
|
||||
```
|
||||
|
||||
2. Make the text of a task clickable, to toggle its checkbox:
|
||||
|
||||
```xml
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"
|
||||
t-att-id="props.task.id"
|
||||
t-on-click="() => store.toggleTask(props.task)"/>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
||||
```
|
||||
|
||||
3. Strike the text of completed task:
|
||||
|
||||
```css
|
||||
.task.done label {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
```
|
||||
|
||||
## Final code
|
||||
|
||||
Our application is now complete. It works, the UI code is well separated from
|
||||
the business logic code, it is testable, all under 150 lines of code (template
|
||||
included!).
|
||||
|
||||
For reference, here is the final code:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OWL Todo App</title>
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<script src="owl.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```js
|
||||
(function () {
|
||||
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Store
|
||||
// -------------------------------------------------------------------------
|
||||
function useStore() {
|
||||
const env = useEnv();
|
||||
return useState(env.store);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TaskList
|
||||
// -------------------------------------------------------------------------
|
||||
class TaskList {
|
||||
constructor(tasks) {
|
||||
this.tasks = tasks || [];
|
||||
const taskIds = this.tasks.map((t) => t.id);
|
||||
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||
}
|
||||
|
||||
addTask(text) {
|
||||
text = text.trim();
|
||||
if (text) {
|
||||
const task = {
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTask(task) {
|
||||
task.isCompleted = !task.isCompleted;
|
||||
}
|
||||
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex((t) => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||
saveTasks();
|
||||
return taskStore;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Task extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox"
|
||||
t-att-id="props.task.id"
|
||||
t-att-checked="props.task.isCompleted"
|
||||
t-on-click="() => store.toggleTask(props.task)"/>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
||||
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
|
||||
</div>`;
|
||||
|
||||
static props = ["task"];
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="task-panel" t-if="store.tasks.length">
|
||||
<div class="task-counter">
|
||||
<t t-esc="displayedTasks.length"/>
|
||||
<t t-if="displayedTasks.length lt store.tasks.length">
|
||||
/ <t t-esc="store.tasks.length"/>
|
||||
</t>
|
||||
task(s)
|
||||
</div>
|
||||
<div>
|
||||
<span t-foreach="['all', 'active', 'completed']"
|
||||
t-as="f" t-key="f"
|
||||
t-att-class="{active: filter.value===f}"
|
||||
t-on-click="() => this.setFilter(f)"
|
||||
t-esc="f"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
static components = { Task };
|
||||
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
this.store = useStore();
|
||||
this.filter = useState({ value: "all" });
|
||||
}
|
||||
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
this.store.addTask(ev.target.value);
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
get displayedTasks() {
|
||||
const tasks = this.store.tasks;
|
||||
switch (this.filter.value) {
|
||||
case "active":
|
||||
return tasks.filter((t) => !t.isCompleted);
|
||||
case "completed":
|
||||
return tasks.filter((t) => t.isCompleted);
|
||||
case "all":
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.filter.value = filter;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
const env = { store: createTaskStore() };
|
||||
mount(Root, document.body, { dev: true, env });
|
||||
})();
|
||||
```
|
||||
|
||||
```css
|
||||
.todo-app {
|
||||
width: 300px;
|
||||
margin: 50px auto;
|
||||
background: aliceblue;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.todo-app > input {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.task {
|
||||
font-size: 18px;
|
||||
color: #111111;
|
||||
display: grid;
|
||||
grid-template-columns: 30px auto 30px;
|
||||
}
|
||||
|
||||
.task:hover {
|
||||
background-color: #def0ff;
|
||||
}
|
||||
|
||||
.task > input {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.delete {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task:hover .delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.task.done {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.task.done label {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.task-panel {
|
||||
color: #0088ff;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.task-panel .task-counter {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.task-panel span {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-panel span.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,68 @@
|
||||
# 🦉 Notes On Owl Architecture 🦉
|
||||
|
||||
We explain here how Owl is designed
|
||||
|
||||
Warning: these notes are technical by nature, and intended for people working
|
||||
on Owl (or interested in understanding its design).
|
||||
|
||||
## Overview
|
||||
|
||||
Roughly speaking, Owl has 5 main parts:
|
||||
|
||||
- a virtual dom system (in `src/blockdom`)
|
||||
- a component system (in `src/component`)
|
||||
- a template compiler (located in the `src/compiler` folder)
|
||||
- a small runtime code to tie them together (in `src/app`)
|
||||
- a reactivity system (in `src/reactivity.ts`)
|
||||
|
||||
There are some other files, but the core of Owl can be understood with these
|
||||
five main parts.
|
||||
|
||||
The virtual dom is an optimized virtual dom based on blocks, which supports
|
||||
multi blocks (for fragments). Everything that owl renders is internally
|
||||
represented by a virtual node. The job of the virtual dom is to efficiently
|
||||
represent the current state of the application, and to build an actual DOM
|
||||
representation when needed, or update the DOM whenever it is needed.
|
||||
|
||||
- some other helpers/smaller scale stuff
|
||||
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).
|
||||
|
||||
# 🦉 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,255 @@
|
||||
# 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)
|
||||
- [Reactivity](#reactivity)
|
||||
- [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/templates.md#inline-templates) 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/templates.md#inline-templates) 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)).
|
||||
|
||||
## Reactivity
|
||||
|
||||
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).
|
||||
|
||||
## 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` abstraction.
|
||||
@@ -0,0 +1,98 @@
|
||||
# 🦉 Notes On Owl Compiled Templates 🦉
|
||||
|
||||
This page will explain what an Owl compiled template look like. This is a
|
||||
technical document intended for developers interested in understanding how Owl
|
||||
works internally.
|
||||
|
||||
Broadly speaking, Owl compiles templates into a javascript function (a closure)
|
||||
that returns a function (the "render" function). The point of the closure is to
|
||||
have a place to store all values specific to the template (in particular, "blocks").
|
||||
Once a template is compiled, its closure function is called once to get the
|
||||
render function, and from then on, only the render function is used.
|
||||
|
||||
The render function takes some context (and some additional information) and
|
||||
return a virtual dom representation of the rendered template, as a block tree.
|
||||
A block tree is a very light weight representation that only contains the dynamic
|
||||
part of the template, and its structure. It is actually independant of the
|
||||
static part of the templates (which are contained in the blocks captured by the
|
||||
closure). This means that the work performed at render time is only to collect
|
||||
dynamic data, and to describe the block structure of the result.
|
||||
|
||||
It looks like this, in pseudo code:
|
||||
|
||||
```js
|
||||
function closure(bdom, helpers) {
|
||||
// here is some place to put stuff specific to the template, such as
|
||||
// blocks
|
||||
...
|
||||
|
||||
return function render(context, node, key) {
|
||||
// only build here all dynamic parts of the template
|
||||
// build a block tree
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, let us see an example. Consider the following template:
|
||||
|
||||
```xml
|
||||
<div class="some-class">
|
||||
<div class="blabla">
|
||||
<span><t t-esc="state.value"/></span>
|
||||
</div>
|
||||
<t t-if="state.info">
|
||||
<p class="info" t-att-class="someAttribute">
|
||||
<t t-esc="state.info"/>
|
||||
</p>
|
||||
</t>
|
||||
<SomeComponent value="value"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
If you look carefully, there are 5 dynamic things:
|
||||
|
||||
- a text value (the first `t-esc`),
|
||||
- a sub block (the `t-if`),
|
||||
- a dynamic attribute (the `t-att-class` attribute),
|
||||
- another text value (the second `t-esc`),
|
||||
- and finally, a sub component
|
||||
|
||||
Here is the compiled code for this template:
|
||||
|
||||
```js
|
||||
function closure(bdom, helpers) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(
|
||||
`<div class="some-class"><div class="blabla"><span><block-text-0/></span></div><block-child-0/><block-child-1/></div>`
|
||||
);
|
||||
let block2 = createBlock(`<p class="info" block-attribute-0="class"><block-text-1/></p>`);
|
||||
|
||||
return function render(ctx, node, key = "") {
|
||||
let b2, b3;
|
||||
let txt1 = ctx["state"].value;
|
||||
if (ctx["state"].info) {
|
||||
let attr1 = ctx["someAttribute"];
|
||||
let txt2 = ctx["state"].info;
|
||||
b2 = block2([attr1, txt2]);
|
||||
}
|
||||
b3 = component(`SomeComponent`, { value: ctx["value"] }, key + `__1`, node, ctx);
|
||||
return block1([txt1], [b2, b3]);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The values captured in the closure capture the static part of the template: we
|
||||
define here two blocks (which contains a template node, that can be deep cloned
|
||||
whenever a block is mounted). Then the render function only describes the block
|
||||
tree structure of the result, depending on the context. This means that we
|
||||
minimize the amount of work done at render time.
|
||||
|
||||
Then, when we want to patch the dom, Owl will uses the `patch` function from
|
||||
blockdom, which then will diff the block tree, and deep clone new blocks whenever
|
||||
a new block is inserted, keep track of dynamic parts of each block, and update
|
||||
them accordingly.
|
||||
|
||||
With this design, the cost of rendering a template is proportional to the number
|
||||
of dynamic values, and not to the size of the template.
|
||||
@@ -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 🦉.
|
||||
@@ -1,54 +0,0 @@
|
||||
# 🦉 Observer 🦉
|
||||
|
||||
Owl need to be able to react to state changes. For example, whenever the state
|
||||
of a component is changed, we need to rerender it. To help with that, we have
|
||||
an Observer class. Its job is to observe some object state, and react to any
|
||||
change. To do that, it recursively replace all keys of the observed state by
|
||||
getters and setters.
|
||||
|
||||
For example, this code will display `update` in the console:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
observer.notifyCB = () => console.log("update");
|
||||
observer.observe(obj);
|
||||
|
||||
const obj = { a: { b: 1 } };
|
||||
obj.a.b = 2;
|
||||
```
|
||||
|
||||
## Technical Limitations
|
||||
|
||||
Since the observer uses getters and setters, it is actually unable to react to
|
||||
changes in two situations:
|
||||
|
||||
- adding a key to an object:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const obj = { a: 1 };
|
||||
observer.observe(obj);
|
||||
obj.b = 2; // will do nothing
|
||||
```
|
||||
|
||||
In that case, we need a way to tell the observer that something happened.
|
||||
This can be done by using the `set` method:
|
||||
|
||||
```javascript
|
||||
observer.set(obj, "b", 2);
|
||||
```
|
||||
|
||||
- modifying an array by setting a new value at a given index:
|
||||
|
||||
```javascript
|
||||
const observer = new owl.Observer();
|
||||
const obj = { todos: [{ id: 1, text: "todo" }] };
|
||||
observer.observe(obj);
|
||||
obj[0] = { id: 2, text: "othertodo" }; // will do nothing, and obj[0] is not observed
|
||||
```
|
||||
|
||||
In that case, the solution is the same, we can simply use the `set` method:
|
||||
|
||||
```javascript
|
||||
observer.set(obj, 0, { id: 2, text: "othertodo" });
|
||||
```
|
||||
@@ -1,101 +0,0 @@
|
||||
# 🦉 Quick Start 🦉
|
||||
|
||||
## Static server
|
||||
|
||||
Let us assume that we have a static server running somewhere. We could then
|
||||
simply add an html page with a few extra files.
|
||||
|
||||
### HTML and CSS
|
||||
|
||||
In a file `index.html`:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>My OWL App</title>
|
||||
<link href="app.css" rel="stylesheet" />
|
||||
<script src="owl-X.Y.Z.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script src="app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
In `app.css`:
|
||||
|
||||
```css
|
||||
button {
|
||||
color: darkred;
|
||||
font-size: 30px;
|
||||
width: 220px;
|
||||
}
|
||||
```
|
||||
|
||||
Also, let's not forget to add a release of OWL (`owl-X.Y.Z.js`)
|
||||
|
||||
### XML
|
||||
|
||||
In `templates.xml`:
|
||||
|
||||
```xml
|
||||
<templates>
|
||||
<button t-name="clickcounter" t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>
|
||||
</templates>
|
||||
```
|
||||
|
||||
### JS
|
||||
|
||||
To build an application (or a sub-part of an application), we need two things:
|
||||
|
||||
- an environment: it is the global context in which we are working. It needs to
|
||||
contain a QWeb instance (preloaded with templates), and anything else that we
|
||||
need. In practice, it could context some user session information, some
|
||||
configuration keys (for example, isMobile = true/false if we are in mobile mode).
|
||||
|
||||
- a description of the user interface: there should be a root widget, which can
|
||||
have sub widgets
|
||||
|
||||
Here are a few steps that we may take to get started:
|
||||
|
||||
- get the templates
|
||||
- create a qweb engine, with the templates
|
||||
- create an environment
|
||||
- create an instance of the root widget
|
||||
- mount the root widget to a DOM element
|
||||
|
||||
Let us now add the javascript to make it work, in `app.js`:
|
||||
|
||||
```javascript
|
||||
class ClickCounter extends owl.Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.template = "clickcounter";
|
||||
this.state = { value: 0 };
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const counter = new ClickCounter(env);
|
||||
const target = document.getElementById("main");
|
||||
await counter.mount(target);
|
||||
}
|
||||
|
||||
start();
|
||||
```
|
||||
-361
@@ -1,361 +0,0 @@
|
||||
# 🦉 QWeb 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [QWeb Specification](#qweb-specification)
|
||||
- [Static html nodes](#static-html-nodes)
|
||||
- [`t-esc` directive](#t-esc-directive)
|
||||
- [`t-raw` directive](#t-raw-directive)
|
||||
- [`t-set` directive](#t-set-directive)
|
||||
- [`t-if` directive](#t-if-directive)
|
||||
- [Expression evaluation](#expression-evaluation)
|
||||
- [`t-att` directive (dynamic attributes)](#t-att-directive-dynamic-attributes)
|
||||
- [JS/OWL Specific Extensions](#jsowl-specific-extensions)
|
||||
- [`t-on` directive](#t-on-directive)
|
||||
- [Component: `t-widget`, `t-props`](#component-t-widget-t-props)
|
||||
- [`t-ref` directive](#t-ref-directive)
|
||||
- [`t-key` directive](#t-key-directive)
|
||||
- [`t-transition` directive](#t-transition)
|
||||
- [Debugging (`t-debug` and `t-log`)](#debugging-t-debug-and-t-log)
|
||||
- [White spaces](#white-spaces)
|
||||
- [Root nodes](#root-nodes)
|
||||
|
||||
## Overview
|
||||
|
||||
[QWeb](https://www.odoo.com/documentation/12.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.
|
||||
|
||||
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.
|
||||
|
||||
The QWeb implementation in the OWL project is slightly different. It compiles
|
||||
templates into functions that output a virtual DOM instead of a string. This is
|
||||
necessary for the component system. In addition, it has a few extra directives
|
||||
(see [OWL Specific Extensions](#owlspecificextensions))
|
||||
|
||||
|
||||
## QWeb Specification
|
||||
|
||||
|
||||
### Static html nodes
|
||||
|
||||
Normal, regular html nodes are rendered into themselves:
|
||||
|
||||
```xml
|
||||
<div>hello</div> <!–– rendered as itself ––>
|
||||
```
|
||||
|
||||
### `t-esc` directive
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
### `t-raw` directive
|
||||
|
||||
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-esc="value"/></p>
|
||||
```
|
||||
|
||||
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
|
||||
|
||||
```html
|
||||
<p><span>foo</span></p>
|
||||
```
|
||||
|
||||
### `t-set` directive
|
||||
|
||||
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, ...
|
||||
|
||||
### `t-if` directive
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
### Expression evaluation
|
||||
|
||||
It is useful to explain the various rules that applies on QWeb expressions. These
|
||||
expressions are strings that will be converted to a javascript expression at
|
||||
compile time.
|
||||
|
||||
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:
|
||||
|
||||
```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 | will be replaced by |
|
||||
| ----- | ------------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
```xml
|
||||
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||
```
|
||||
|
||||
### `t-att` directive (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> -->
|
||||
```
|
||||
|
||||
## JS/OWL Specific Extensions
|
||||
|
||||
### `t-on` directive
|
||||
|
||||
### Component: `t-widget`, `t-props`
|
||||
|
||||
### `t-ref` directive
|
||||
|
||||
### `t-key` directive
|
||||
|
||||
Even though Owl tries to be as declarative as possible, some DOM state is still
|
||||
locked inside the DOM: for example, the scrolling state, the current user selection,
|
||||
the focused element or the state of an input. This is why we use a virtual dom
|
||||
algorithm to keep the actual DOM node as much as possible. However, this is
|
||||
sometimes not enough, and we need to help Owl decide if an element is actually
|
||||
the same, or is different. The `t-key` directive is used to give an identity to an element.
|
||||
|
||||
There are three main use cases:
|
||||
|
||||
- *elements in a list*:
|
||||
```xml
|
||||
<span t-foreach="todos" t-as="todo" t-key="todo.id">
|
||||
<t t-esc="todo.text"/>
|
||||
</span>
|
||||
```
|
||||
|
||||
- *`t-if`/`t-else`*
|
||||
|
||||
- *animations*: give a different identity to a component. Ex: thread id with
|
||||
animations on add/remove message.
|
||||
|
||||
### `t-transition` directive
|
||||
|
||||
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
|
||||
will happen:
|
||||
|
||||
At node creation:
|
||||
|
||||
- the css classes `name-enter` and `name-enter-active` will be added before the
|
||||
node is added to 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),
|
||||
- the css class `name-enter-active` will be removed whenever a css transition
|
||||
ends.
|
||||
|
||||
At node destruction:
|
||||
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||
node is removed to the DOM,
|
||||
- the css class `name-leave` will be removed on the next animation frame (so it
|
||||
can be used to trigger css transition effects),
|
||||
- the css class `name-leave-active` will be removed whenever a css transition
|
||||
ends. Only then will the element be removed from the DOM.
|
||||
|
||||
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 .5s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
```
|
||||
|
||||
Note: you will find more information on animations in the [animation](doc/animations.md)
|
||||
documentation.
|
||||
|
||||
### Debugging (`t-debug` and `t-log`)
|
||||
|
||||
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
|
||||
|
||||
|
||||
### White spaces
|
||||
|
||||
White spaces in a templates 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="1">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.
|
||||
+45
-11
@@ -1,15 +1,49 @@
|
||||
# 🦉 OWL Documentation 🦉
|
||||
# 🦉 Owl overview 🦉
|
||||
|
||||
Here is a list of everything exported by the Owl library:
|
||||
|
||||
## Reference
|
||||
Main entities:
|
||||
|
||||
- [Component](component.md)
|
||||
- [QWeb](qweb.md)
|
||||
- [Store](store.md)
|
||||
- [Observer](observer.md)
|
||||
- [Virtual DOM](vdom.md)
|
||||
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
|
||||
- [`Component`](reference/component.md): the main class to define a concrete Owl component
|
||||
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
|
||||
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
|
||||
|
||||
## Miscellaneous
|
||||
- [Quick Start](quick_start.md)
|
||||
- [Animations](animations.md)
|
||||
- [Comparison with React/Vue](comparison.md)
|
||||
Reactivity
|
||||
|
||||
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
|
||||
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
|
||||
- [`markRaw`](reference/reactivity.md#markraw): mark an object or array so that it is ignored by the reactivity system
|
||||
- [`toRaw`](reference/reactivity.md#toraw): given a reactive objet, return the raw (non reactive) underlying object
|
||||
|
||||
Lifecycle hooks:
|
||||
|
||||
- [`onWillStart`](reference/component.md#willstart): hook to define asynchronous code that should be executed before component is rendered
|
||||
- [`onMounted`](reference/component.md#mounted): hook to define code that should be executed when component is mounted
|
||||
- [`onWillPatch`](reference/component.md#willpatch): hook to define code that should be executed before component is patched
|
||||
- [`onWillUpdateProps`](reference/component.md#willupdateprops): hook to define code that should be executed before component is updated
|
||||
- [`onPatched`](reference/component.md#patched): hook to define code that should be executed when component is patched
|
||||
- [`onWillRender`](reference/component.md#willrender): hook to define code that should be executed before component is rendered
|
||||
- [`onRendered`](reference/component.md#rendered): hook to define code that should be executed after component is rendered
|
||||
- [`onWillUnmount`](reference/component.md#willunmount): hook to define code that should be executed before component is unmounted
|
||||
- [`onWillDestroy`](reference/component.md#willdestroy): hook to define code that should be executed before component is destroyed
|
||||
- [`onError`](reference/component.md#onerror): hook to define a Owl error handler
|
||||
|
||||
Other hooks:
|
||||
|
||||
- [`useComponent`](reference/hooks.md#usecomponent): return a reference to the current component (useful to create derived hooks)
|
||||
- [`useEffect`](reference/hooks.md#useeffect): define an effect with its dependencies
|
||||
- [`useEnv`](reference/hooks.md#useenv): return a reference to the current env
|
||||
- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM
|
||||
- [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`)
|
||||
- [`useChildSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for child components)
|
||||
- [`useSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for current component and child components)
|
||||
|
||||
Utility/helpers:
|
||||
|
||||
- [`EventBus`](reference/utils.md#eventbus): a simple event bus
|
||||
- [`loadFile`](reference/utils.md#loadfile): an helper to load a file from the server
|
||||
- [`markup`](reference/templates.md#outputting-data): utility function to define strings that represent html (should not be escaped)
|
||||
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
||||
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
||||
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# 🦉 App 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [API](#api)
|
||||
- [Configuration](#configuration)
|
||||
- [`mount` helper](#mount-helper)
|
||||
- [Loading templates](#loading-templates)
|
||||
|
||||
## Overview
|
||||
|
||||
Every Owl application has a root element, a set of templates, an environment and
|
||||
possibly a few other settings. The `App` class is a simple class that represents
|
||||
all of these elements. Here is an example:
|
||||
|
||||
```js
|
||||
const {Component, App } = owl;
|
||||
|
||||
class MyComponent extends Component { ... }
|
||||
|
||||
const app = new App(MyComponent, { props: {...}, templates: "..."});
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
The basic workflow is: create an `App` instance configured with the root
|
||||
component, the templates, and possibly other settings. Then, we mount that
|
||||
instance somewhere in the DOM.
|
||||
|
||||
## API
|
||||
|
||||
- **`constructor(Root[, config])`**: first argument should be a component class (not
|
||||
an instance), and the optional second argument is a configuration object (see below).
|
||||
|
||||
- **`mount(target, options)`**: first argument is an html element, and the optional
|
||||
second argument is an object with mounting options (see below). Mount the app
|
||||
to a target in the DOM. Note that this is an asynchronous operation: the `mount`
|
||||
method returns a promise that resolves to the component instance whenever it
|
||||
is complete.
|
||||
|
||||
The `option` object is an object with the following keys:
|
||||
|
||||
- **`position (string)`**: either `first-child` or `last-child`. This option determines
|
||||
the position of the application in the target: either first or last child.
|
||||
|
||||
- **`destroy()`**: destroys the application
|
||||
|
||||
## Configuration
|
||||
|
||||
The `config` object is an object with some of the following keys:
|
||||
|
||||
- **`env (object)`**: if given, this will be the shared `env` given to each component
|
||||
- **`props (object)`**: the props given to the root component
|
||||
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in
|
||||
[`dev` mode](#dev-mode);
|
||||
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
|
||||
that Owl will not log a message to warn that Owl is in `dev` mode.
|
||||
- **`translatableAttributes (string[])`**: a list of additional attributes that should
|
||||
be translated (see [translations](translations.md))
|
||||
- **`translateFn (function)`**: a function that will be called by owl to translate
|
||||
templates (see [translations](translations.md))
|
||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||
the components created by the application.
|
||||
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
||||
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
||||
|
||||
## `mount` helper
|
||||
|
||||
Note that there is a `mount` helper to do that in just a line:
|
||||
|
||||
```js
|
||||
const { mount, Component } = owl;
|
||||
|
||||
class MyComponent extends Component {
|
||||
...
|
||||
}
|
||||
|
||||
mount(MyComponent, document.body, { props: {...}, templates: "..."});
|
||||
```
|
||||
|
||||
Here is the `mount` function signature:
|
||||
|
||||
**`mount(Component, target, config)`** with the following arguments:
|
||||
|
||||
- **`Component`**: a component class (Root component of the app)
|
||||
- **`target`**: an html element, where the component will be mounted as last child
|
||||
- **`config (optional)`**: a config object (the same as the App config object)
|
||||
|
||||
Most of the time, the `mount` helper is more convenient, but whenever one needs
|
||||
a reference to the actual Owl App, then using the `App` class directly is
|
||||
possible.
|
||||
|
||||
## Loading templates
|
||||
|
||||
Most applications will need to load templates whenever they start. Here is
|
||||
what it could look like in practice:
|
||||
|
||||
```js
|
||||
// in the main js file:
|
||||
const { loadFile, mount } = owl;
|
||||
|
||||
// async, so we can use async/await
|
||||
(async function setup() {
|
||||
const templates = await loadFile(`/some/endpoint/that/return/templates`);
|
||||
const env = {
|
||||
_t: someTranslateFn,
|
||||
templates,
|
||||
// possibly other stuff
|
||||
};
|
||||
|
||||
mount(Root, document.body, { env });
|
||||
})();
|
||||
```
|
||||
|
||||
## Dev mode
|
||||
|
||||
Dev mode activates some additional checks and developer amenities:
|
||||
|
||||
- [Props validation](./props.md#props-validation) is performed
|
||||
- [t-foreach](./templates.md#loops) loops check for key unicity
|
||||
- Lifecycle hooks are wrapped to report their errors in a more developer-friendly way
|
||||
- onWillStart and onWillUpdateProps will emit a warning in the console when they
|
||||
take longer than 3 seconds in an effort to ease debugging the presence of deadlocks
|
||||
@@ -0,0 +1,455 @@
|
||||
# 🦉 Owl Component 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Properties and methods](#properties-and-methods)
|
||||
- [Static Properties](#static-properties)
|
||||
- [Lifecycle](#lifecycle)
|
||||
- [`setup`](#setup)
|
||||
- [`willStart`](#willstart)
|
||||
- [`willRender`](#willrender)
|
||||
- [`rendered`](#rendered)
|
||||
- [`mounted`](#mounted)
|
||||
- [`willUpdateProps`](#willupdateprops)
|
||||
- [`willPatch`](#willpatch)
|
||||
- [`patched`](#patched)
|
||||
- [`willUnmount`](#willunmount)
|
||||
- [`willDestroy`](#willdestroy)
|
||||
- [`onError`](#onerror)
|
||||
- [Sub components](#sub-components)
|
||||
- [Dynamic Sub components](#dynamic-sub-components)
|
||||
- [`status` helper](#status-helper)
|
||||
|
||||
## Overview
|
||||
|
||||
An Owl component is a small class which represents some part of the user interface.
|
||||
It is part of a component tree, and has an [environment](environment.md) (`env`),
|
||||
which is propagated from a parent to its children.
|
||||
|
||||
OWL components are defined by subclassing the `Component` class. For example,
|
||||
here is how a `Counter` component could be implemented:
|
||||
|
||||
```javascript
|
||||
const { Component, xml, useState } = owl;
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, we use the `xml` helper to define inline templates, and the
|
||||
`useState` hook, which returns a reactive version of its argument (see the page
|
||||
on reactivity).
|
||||
|
||||
## Properties and methods
|
||||
|
||||
The `Component` class has a very small API.
|
||||
|
||||
- **`env (object)`**: the component [environment](environment.md)
|
||||
|
||||
- **`props (object)`**: this is an object containing all the [props](props.md) given by
|
||||
the parent to a child component
|
||||
|
||||
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`.
|
||||
|
||||
* **`render(deep[=false])`**: calling this method directly will cause a rerender. Note
|
||||
that with the reactivity system, this should be rare to have to do it manually.
|
||||
Also, the rendering operation is asynchronous, so the DOM will only be updated
|
||||
slightly later (at the next animation frame, if no component delays the
|
||||
rendering)
|
||||
|
||||
By default, the render initiated by this method will stop at each child
|
||||
component if their props are (shallow) equal. To force a render to update
|
||||
all child components, one can use the optional `deep` argument. Note that the
|
||||
value of the `deep` argument needs to be a boolean, not a truthy value.
|
||||
|
||||
## Static Properties
|
||||
|
||||
- **`template (string)`**: this is the name of the 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.
|
||||
|
||||
```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.md#props-validation) 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. See
|
||||
[default props](props.md#default-props) for more information
|
||||
|
||||
```js
|
||||
class Counter extends owl.Component {
|
||||
static defaultProps = {
|
||||
initialValue: 0,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
A solid and robust component system needs a complete lifecycle system to help
|
||||
developers write components. Here is a complete description of the lifecycle of
|
||||
a Owl component:
|
||||
|
||||
| Method | Hook | Description |
|
||||
| --------------------------------------- | ------------------- | ---------------------------------------------------------------------- |
|
||||
| **[setup](#setup)** | none | setup |
|
||||
| **[willStart](#willstart)** | `onWillStart` | async, before first rendering |
|
||||
| **[willRender](#willrender)** | `onWillRender` | just before component is rendered |
|
||||
| **[rendered](#rendered)** | `onRendered` | just after component is rendered |
|
||||
| **[mounted](#mounted)** | `onMounted` | just after component is rendered and added to the DOM |
|
||||
| **[willUpdateProps](#willupdateprops)** | `onWillUpdateProps` | async, before props update |
|
||||
| **[willPatch](#willpatch)** | `onWillPatch` | just before the DOM is patched |
|
||||
| **[patched](#patched)** | `onPatched` | just after the DOM is patched |
|
||||
| **[willUnmount](#willunmount)** | `onWillUnmount` | just before removing component from DOM |
|
||||
| **[willDestroy](#willdestroy)** | `onWillDestroy` | just before component is destroyed |
|
||||
| **[error](#onerror)** | `onError` | catch and handle errors (see [error handling page](error_handling.md)) |
|
||||
|
||||
### `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 the proper place 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 (most of the time asynchronous) 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.
|
||||
|
||||
The `onWillStart` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
this.data = await this.loadData()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
At this point, the component is not yet rendered. Note that slow `willStart`
|
||||
code will slow down the rendering of the user interface. Therefore, some care
|
||||
should be made to make this method as fast as possible.
|
||||
|
||||
Note that if there are more than one `onWillStart` registered callback, then they
|
||||
will all be run in parallel.
|
||||
|
||||
### `willRender`
|
||||
|
||||
It is uncommon but it may happen that one need to execute code just before a
|
||||
component is rendered (more precisely, when its compiled template function is executed).
|
||||
To do that, one can use the `onWillRender` hook:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillRender(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`willRender` hooks are called just before rendering templates, parent first,
|
||||
then children.
|
||||
|
||||
### `rendered`
|
||||
|
||||
It is uncommon but it may happen that one need to execute code just after a
|
||||
component is rendered (more precisely, when its compiled template function is executed).
|
||||
To do that, one can use the `onRendered` hook:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onRendered(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`rendered` hooks are called just after rendering templates, parent first,
|
||||
then children. Note that at this moment, the actual DOM may not exist yet (if
|
||||
it is the first rendering), or is not updated yet. This will be dom in the next
|
||||
animation frame as soon as all the components are ready.
|
||||
|
||||
### `mounted`
|
||||
|
||||
The `mounted` hook is called each time a component is attached to the
|
||||
DOM, after the initial rendering. 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,
|
||||
children, then parents.
|
||||
|
||||
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.
|
||||
|
||||
The `onMounted` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
// do something here
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### `willUpdateProps`
|
||||
|
||||
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).
|
||||
|
||||
The `onWillUpdateProps` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillUpdateProps(nextProps => {
|
||||
return this.loadData({id: nextProps.id});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Notice that it receives the next props for the component.
|
||||
|
||||
This hook is not called during the first render (but `willStart` is called
|
||||
and performs a similar job). Also, as most of the hooks, it is called in the
|
||||
usual order: parents first, then children.
|
||||
|
||||
### `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.
|
||||
|
||||
The `onWillPatch` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillPatch(() => {
|
||||
this.scrollState = this.getScrollSTate();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `willPatch` is called in the usual parent->children order.
|
||||
|
||||
### `patched`
|
||||
|
||||
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.
|
||||
|
||||
The `onPatched` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onPatched(() => {
|
||||
this.scrollState = this.getScrollSTate();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Like `mounted`, the `patched` hook is called in the order: children first, then
|
||||
parent.
|
||||
|
||||
### `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.
|
||||
|
||||
The `onWillUnmount` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
// add some listener
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
// remove listener
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
This is the opposite method of `mounted`. Note that if a component is destroyed
|
||||
before being mounted, the `willUnmount` method may not be called.
|
||||
|
||||
Parent `willUnmount` hooks will be called before children.
|
||||
|
||||
### `willDestroy`
|
||||
|
||||
Sometimes, components need to do some action in the `setup` and clean it up when
|
||||
they are inactive. However, the `willUnmount` hook is not appropriate for the
|
||||
cleaning operation, since the component may be destroyed before it has even been
|
||||
mounted. The `willDestroy` hook is useful in that situation, since it is always
|
||||
called.
|
||||
|
||||
The `onWillUnmount` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillDestroy(() => {
|
||||
// do some cleanup
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `willDestroy` hooks are first called on children, then on parents.
|
||||
|
||||
### `onError`
|
||||
|
||||
Sadly, it may happen that components crashes at runtime. This is an unfortunate
|
||||
reality, and this is why Owl needs to provide a way to handle these errors.
|
||||
|
||||
The `onError` hook is useful when we need to intercept and properly react
|
||||
to errors that occur in some sub components. See the page on
|
||||
[error handling](error_handling.md) for more detail.
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onError(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Sub components
|
||||
|
||||
It is convenient to define a component using other (sub) components. This is
|
||||
called composition, and is very powerful in practice. To do that in Owl, one
|
||||
can just use a tag starting with a capital letter in its template, and register
|
||||
the sub component class in its static `components` object:
|
||||
|
||||
```js
|
||||
class Child extends Component {
|
||||
static template = xml`<div>child component <t t-esc="props.value"/></div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child value="1"/>
|
||||
<Child value="2"/>
|
||||
</div>`;
|
||||
|
||||
static components = { Child };
|
||||
}
|
||||
```
|
||||
|
||||
This example also shows how one can pass information from the parent component
|
||||
to the child component, as props. See the [props section](props.md)
|
||||
for more information.
|
||||
|
||||
## 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. This should
|
||||
be an expression that evaluates to a component class. For example:
|
||||
|
||||
```js
|
||||
class A extends Component {
|
||||
static template = xml`<div>child a</div>`;
|
||||
}
|
||||
class B extends Component {
|
||||
static template = xml`<span>child b</span>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<t t-component="myComponent"/>`;
|
||||
|
||||
state = useState({ child: "a" });
|
||||
|
||||
get myComponent() {
|
||||
return this.state.child === "a" ? A : B;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `status` helper
|
||||
|
||||
It is sometimes convenient to have a way to find out in which state a component
|
||||
is currently. To do that, one can use the `status` helper:
|
||||
|
||||
```js
|
||||
const { status } = owl;
|
||||
// assume component is an instance of a Component
|
||||
|
||||
console.log(status(component));
|
||||
// logs either:
|
||||
// - 'new', if the component is new and has not been mounted yet
|
||||
// - 'mounted', if the component is currently mounted
|
||||
// - 'destroyed' if the component is currently destroyed
|
||||
```
|
||||
@@ -0,0 +1,163 @@
|
||||
# 🦉 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
|
||||
asynchronous hooks, 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 creates
|
||||
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: updating 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`.
|
||||
|
||||
### 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. 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.
|
||||
@@ -0,0 +1,76 @@
|
||||
# 🦉 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)
|
||||
|
||||
## Overview
|
||||
|
||||
An environment is a shared object given to all components in a tree. It is not
|
||||
used by Owl itself, but it is useful for application developers to provide a
|
||||
simple communication channel between components (in addition to the props).
|
||||
|
||||
The `env` given to the [`App`](app.md) is assigned to the `env` component
|
||||
property.
|
||||
|
||||
```
|
||||
Root
|
||||
/ \
|
||||
A B
|
||||
```
|
||||
|
||||
Also, the `env` object is frozen when the application is started. This is done
|
||||
to ensure a simpler mental model of what's happening in runtime. Note that it
|
||||
is only shallowly frozen, so sub objects can be modified.
|
||||
|
||||
## Setting an environment
|
||||
|
||||
The correct way to customize an environment is to simply give it to the `App`,
|
||||
whenever it is created.
|
||||
|
||||
```js
|
||||
const env = {
|
||||
_t: myTranslateFunction,
|
||||
user: {...},
|
||||
services: {
|
||||
...
|
||||
},
|
||||
};
|
||||
|
||||
new App(Root, { env }).mount(document.body);
|
||||
|
||||
// or alternatively
|
||||
mount(App, document.body, { env });
|
||||
```
|
||||
|
||||
## 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 are two hooks for this situation: [`useSubEnv` and `useChildSubEnv`](hooks.md#usesubenv-and-usechildsubenv).
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
setup() {
|
||||
useSubEnv({ myKey: someValue }); // myKey is now available for all child components
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Content of an Environment
|
||||
|
||||
The `env` object content is totally up to the application developer. However,
|
||||
some good use cases for additional keys in the environment are:
|
||||
|
||||
- some configuration keys,
|
||||
- session information,
|
||||
- generic services (such as doing rpcs).
|
||||
- other utility functions that one want to inject, such as a translation function.
|
||||
|
||||
Doing it this way means that components are easily testable: we can simply
|
||||
create a test environment with mock services.
|
||||
@@ -0,0 +1,75 @@
|
||||
# 🦉 Error Handling 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Managing Errors](#managing-errors)
|
||||
- [Example](#example)
|
||||
|
||||
## 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 feedback.
|
||||
|
||||
Clearly, it is usually a little bit extreme to destroy the application. This
|
||||
is why we need a mechanism to handle rendering errors (and errors coming
|
||||
from lifecycle hooks): the `onError` hook.
|
||||
|
||||
The main idea is that the `onError` hook register a function that will be called
|
||||
with the error. This function need to handle the situation, most of the time by
|
||||
updating some state and rerendering itself, so the application can return to a
|
||||
normal state.
|
||||
|
||||
## Managing Errors
|
||||
|
||||
Whenever the `onError` lifecycle hook is used, all errors coming from
|
||||
sub components rendering and/or lifecycle method calls will be caught and given
|
||||
to the `onError` 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 `onError` or any other
|
||||
owl mechanism. This is up to the application developer to properly recover
|
||||
from an error
|
||||
|
||||
- if an error handler is unable to properly handle an error, it can just rethrow
|
||||
an error, and Owl will try looking for another error handler up the component
|
||||
tree.
|
||||
|
||||
## Example
|
||||
|
||||
For example, here is how we could implement a generic component `ErrorBoundary`
|
||||
that render its content, and a fallback if an error happened.
|
||||
|
||||
```js
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<t t-if="error" t-slot="fallback">An error occurred</t>
|
||||
<t t-else="" t-slot="content"`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ error: false });
|
||||
onError(() => (this.state.error = true));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using the `ErrorBoundary` is then simple simple:
|
||||
|
||||
```xml
|
||||
<ErrorBoundary>
|
||||
<SomeOtherComponent/>
|
||||
<t t-set-slot="fallback">Some specific error message</t>
|
||||
</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).
|
||||
@@ -0,0 +1,119 @@
|
||||
# 🦉 Event Handling 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Event Handling](#event-handling)
|
||||
- [Modifiers](#modifiers)
|
||||
- [Synthetic Events](#synthetic-events)
|
||||
- [On Components](#on-components)
|
||||
|
||||
## 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_. This
|
||||
is done with the `t-on` directive. For example:
|
||||
|
||||
```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. The value of the `t-on` expression should be a valid javascript expression
|
||||
that evaluates to a function in the context of the current component. So, one
|
||||
can get a reference to the event, or pass some additional arguments. For example,
|
||||
all the following expressions are valid:
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
<button t-on-click="() => this.increment(3)">Add 3</button>
|
||||
<button t-on-click="ev => this.doStuff(ev, 'value')">Do something</button>
|
||||
```
|
||||
|
||||
Notice the use of the `this` keyword in the lambda function: this is the
|
||||
correct way to call a method on the component in a lambda function.
|
||||
|
||||
One could use the following expression:
|
||||
|
||||
```xml
|
||||
<button t-on-click="() => increment(3)">Add 3</button>
|
||||
```
|
||||
|
||||
But then, the increment function may be unbound (unless the component binds it
|
||||
in its setup function, for example).
|
||||
|
||||
## 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. |
|
||||
| `.synthetic` | define a synthetic event handler (see below) |
|
||||
|
||||
```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.
|
||||
|
||||
## Synthetic Events
|
||||
|
||||
In some cases, attaching an event handler for each element of large lists has
|
||||
a non trivial cost. Owl provides a way to efficiently improve the performance:
|
||||
with synthetic event, it actually adds only one handler on the document body,
|
||||
and will properly call the handler, just as expected.
|
||||
|
||||
The only difference with regular events is that the event is caught at the document
|
||||
body, so it cannot be stopped before it actually gets there. Since it may be
|
||||
surprising in some cases, it is not enabled by default.
|
||||
|
||||
To enable it, one can just use the `.synthetic` suffix:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<t t-foreach="largeList" t-as="elem" t-key="elem.id">
|
||||
<button t-on-click.synthetic="doSomething" ...>
|
||||
<!-- some content -->
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
## On Components
|
||||
|
||||
The `t-on` directive also works on a child component:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
in some template
|
||||
<Child t-on-click="dosomething"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
This will catch all click events on any html element contained in the `Child`
|
||||
sub component. Note that if the child component is reduced to one (or more) text
|
||||
nodes, then clicking on it will not call the handler, since the event will be
|
||||
dispatched by the browser on the parent element (a `div` in this case).
|
||||
@@ -0,0 +1,324 @@
|
||||
# 🦉 Hooks 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [The Hook Rule](#the-hook-rule)
|
||||
- [Lifecycle hooks](#lifecycle-hooks)
|
||||
- [Other hooks](#other-hooks)
|
||||
- [`useState`](#usestate)
|
||||
- [`useRef`](#useref)
|
||||
- [`useSubEnv` and `useChildSubEnv`](#usesubenv-and-usechildsubenv)
|
||||
- [`useExternalListener`](#useexternallistener)
|
||||
- [`useComponent`](#usecomponent)
|
||||
- [`useEnv`](#useenv)
|
||||
- [`useEffect`](#useeffect)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
|
||||
## 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.
|
||||
|
||||
## The Hook Rule
|
||||
|
||||
There is only one rule: every hook for a component has to be called in the _setup_ method, or in class fields:
|
||||
|
||||
```js
|
||||
// ok
|
||||
class SomeComponent extends Component {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
All lifecycle hooks are documented in detail in their specific [section](component.md#lifecycle).
|
||||
|
||||
| Hook | Description |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| **[onWillStart](component.md#willstart)** | async, before first rendering |
|
||||
| **[onWillRender](component.md#willrender)** | just before component is rendered |
|
||||
| **[onRendered](component.md#rendered)** | just after component is rendered |
|
||||
| **[onMounted](component.md#mounted)** | just after component is rendered and added to the DOM |
|
||||
| **[onWillUpdateProps](component.md#willupdateprops)** | async, before props update |
|
||||
| **[onWillPatch](component.md#willpatch)** | just before the DOM is patched |
|
||||
| **[onPatched](component.md#patched)** | just after the DOM is patched |
|
||||
| **[onWillUnmount](component.md#willunmount)** | just before removing component from DOM |
|
||||
| **[onWillDestroy](component.md#willdestroy)** | just before component is destroyed |
|
||||
| **[onError](component.md#onerror)** | catch and handle errors (see [error handling page](error_handling.md)) |
|
||||
|
||||
## Other Hooks
|
||||
|
||||
### `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, Component } = owl;
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### `useRef`
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
of a component, rendered by Owl. It only work on a html element tagged by the
|
||||
`t-ref` directive:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-ref="someDiv"/>
|
||||
<span>hello</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component will be able to access the `div` and the component
|
||||
`SubComponent` with the `useRef` hook:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
inputRef = useRef("someComponent");
|
||||
|
||||
someMethod() {
|
||||
// here, if component is mounted, refs are active:
|
||||
// - this.inputRef.el is the input HTMLElement
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As shown by the example above, the actual HTMLElement instance is accessed with
|
||||
the `el` key.
|
||||
|
||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](templates.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` and `useChildSubEnv`
|
||||
|
||||
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 `useChildSubEnv` hook may be useful: it lets a component add some
|
||||
information to the environment in a way that only its children
|
||||
can access it:
|
||||
|
||||
```js
|
||||
class FormComponent extends Component {
|
||||
setup() {
|
||||
const model = makeModel();
|
||||
// model will be available on this.env for this component and all children
|
||||
useSubEnv({ model });
|
||||
// someKey will be available on this.env for all children
|
||||
useChildSubEnv({ someKey: "value" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `useSubEnv` and `useChildSubEnv` hooks take one argument: an object which
|
||||
contains some key/value that will be added to the current environment. These hooks
|
||||
will create a new env object with the new information:
|
||||
|
||||
- `useSubEnv` will assign this new `env` to itself and to all children components
|
||||
- `useChildSubEnv` will only assign this new `env` to all children components.
|
||||
|
||||
As usual in Owl, [environments](environment.md) created with these two hooks are
|
||||
frozen, to prevent unwanted modifications.
|
||||
|
||||
Note that both these hooks can be called an arbitrary number of times. The `env`
|
||||
will then be updated accordingly.
|
||||
|
||||
### `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);
|
||||
```
|
||||
|
||||
### `useComponent`
|
||||
|
||||
The `useComponent` hook is useful as a building block for some customized hooks,
|
||||
that may need a reference to the component calling them.
|
||||
|
||||
```js
|
||||
function useSomething() {
|
||||
const component = useComponent();
|
||||
// now, component is bound to the instance of the current component
|
||||
}
|
||||
```
|
||||
|
||||
### `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.
|
||||
|
||||
```js
|
||||
function useSomething() {
|
||||
const env = useEnv();
|
||||
// now, env is bound to the env of the current component
|
||||
}
|
||||
```
|
||||
|
||||
### `useEffect`
|
||||
|
||||
This hook will run a callback when a component is mounted and patched, and
|
||||
will run a cleanup function before patching and before unmounting the
|
||||
the component (only if some dependencies have changed).
|
||||
|
||||
It has almost the same API as the React `useEffect` hook, except that the dependencies
|
||||
are defined by a function instead of just the dependencies.
|
||||
|
||||
The `useEffect` hook takes two function: the effect function and the dependency
|
||||
function. The effect function perform some task and return (optionally) a cleanup
|
||||
function. The dependency function returns a list of dependencies. If any of these
|
||||
dependencies changes, then the current effect will be cleaned up and reexecuted.
|
||||
|
||||
Here is an example without any dependencies:
|
||||
|
||||
```js
|
||||
useEffect(
|
||||
() => {
|
||||
window.addEventListener("mousemove", someHandler);
|
||||
return () => window.removeEventListener("mousemove", someHandler);
|
||||
},
|
||||
() => []
|
||||
);
|
||||
```
|
||||
|
||||
In the example above, the dependency list is empty, so the effect is only cleaned
|
||||
up when the component is unmounted.
|
||||
|
||||
If the dependency function is skipped, then the effect will be cleaned up and
|
||||
rerun at every patch.
|
||||
|
||||
Here is another example, of how one could implement a `useAutofocus` hook with
|
||||
the `useEffect` hook:
|
||||
|
||||
```js
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
useEffect(
|
||||
(el) => el && el.focus(),
|
||||
() => [ref.el]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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>`;
|
||||
|
||||
setup() {
|
||||
useAutofocus("myinput");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: mouse position
|
||||
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
```js
|
||||
const { useState, onWillDestroy, Component } = owl;
|
||||
|
||||
// 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;
|
||||
}
|
||||
window.addEventListener("mousemove", update);
|
||||
onWillDestroy(() => {
|
||||
window.removeEventListener("mousemove", update);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static template = xml`<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></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.
|
||||
@@ -0,0 +1,92 @@
|
||||
# 🦉 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.
|
||||
@@ -0,0 +1,17 @@
|
||||
# 🦉 Portal 🦉
|
||||
|
||||
It is sometimes useful to be able to render some content outside the boundaries
|
||||
of a component. To do that, Owl provides a special directive: `t-portal`:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>this is inside the component</div>
|
||||
<div t-portal="'body'">and this is outside</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-portal` directive takes a valid css selector as argument. The content of
|
||||
the portalled template will be mounted at the corresponding location. Note that
|
||||
Owl need to insert an empty text node at the location of the portalled content.
|
||||
@@ -0,0 +1,30 @@
|
||||
# 🦉 Precompiling templates 🦉
|
||||
|
||||
Owl is designed to be used by the Odoo javascript framework. Since Odoo handles
|
||||
its assets in its own non standard way, it was decided/assumed that Owl would
|
||||
compile templates at runtime.
|
||||
|
||||
However, in some cases, it is not optimal, or even worse, not possible to do that.
|
||||
For example, browser extensions do not allow javascript code to create a new
|
||||
function (using the `new Function(...)` syntax).
|
||||
|
||||
Therefore, in these cases, it is required to compile templates ahead of time. It
|
||||
is possible to do that in Owl, but the tooling is still rough. For now, the
|
||||
process is the following:
|
||||
|
||||
1. write your templates in xml files (with a `t-name` directive to declare the name
|
||||
of the template)
|
||||
2. Compile them in a `templates.js` file
|
||||
3. get the `owl.iife.runtime.js` file (which is a owl build without the compiler)
|
||||
4. bundle `owl.iife.runtime.js` and `template.js` with your assets (owl needs to
|
||||
be positioned before the templates)
|
||||
|
||||
Here is a more detailed explanation on how to compile xml files into a js file:
|
||||
|
||||
1. clone the owl repository locally
|
||||
2. `npm install` to install all the required tooling
|
||||
3. `npm run build:runtime` to build the `owl.iife.runtime.js` file
|
||||
4. `npm run build:compiler` to build the template compiler
|
||||
5. `npm run compile_templates -- path/to/your/templates` will scan your target
|
||||
folder, find all xml files, get all templates, compile them, and generate a
|
||||
`templates.js` file.
|
||||
@@ -0,0 +1,270 @@
|
||||
# 🦉 Props 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Definition](#definition)
|
||||
- [Binding function props](#binding-function-props)
|
||||
- [Dynamic Props](#dynamic-props)
|
||||
- [Default Props](#default-props)
|
||||
- [Props validation](#props-validation)
|
||||
- [Good Practices](#good-practices)
|
||||
|
||||
## 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),
|
||||
|
||||
In the following example:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<ComponentA a="state.a" b="'string'"/>
|
||||
<ComponentB t-if="state.flag" model="model"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
the `props` object contains the following keys:
|
||||
|
||||
- for `ComponentA`: `a` and `b`,
|
||||
- for `ComponentB`: `model`,
|
||||
|
||||
## Binding function props
|
||||
|
||||
It is common to have the need to pass a callback as a prop. Since Owl components
|
||||
are class based, the callback frequently needs to be bound to its owner component.
|
||||
So, one can do this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child callback="doSomething"/>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.doSomething = this.doSomething.bind(this);
|
||||
}
|
||||
|
||||
doSomething() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, this is such a common use case that Owl provides a special suffix to do
|
||||
just that: `.bind`. This looks like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child callback.bind="doSomething"/>
|
||||
</div>`;
|
||||
|
||||
doSomething() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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 } };
|
||||
}
|
||||
```
|
||||
|
||||
## Default Props
|
||||
|
||||
If the static `defaultProps` property is defined, it will be used to complete
|
||||
props received by the parent, if missing.
|
||||
|
||||
```js
|
||||
class Counter extends owl.Component {
|
||||
static defaultProps = {
|
||||
initialValue: 0,
|
||||
};
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, the `initialValue` props is now by default set to 0.
|
||||
|
||||
## 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 [how to configure an app](app.md#configuration))
|
||||
- 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 (unless the special prop `*` is present).
|
||||
- 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, it 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
|
||||
- an object describing a value as type. This is done by using the `value` key. For example, `{value: false}` specifies that the corresponding value should be equal to false.
|
||||
- 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.
|
||||
- `optional`: if true, the prop is not mandatory
|
||||
|
||||
There is a special `*` prop that means that additional prop are allowed. This is
|
||||
sometimes useful for generic components that will propagate some or all their
|
||||
props to their child components.
|
||||
|
||||
Note that default values cannot be defined for a mandatory props. Doing so will
|
||||
result in a prop validation error.
|
||||
|
||||
Examples:
|
||||
|
||||
```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],
|
||||
optionalProp: { type: Number, optional: true }
|
||||
};
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented
|
||||
static props = ['message', 'id', 'date'];
|
||||
```
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented. any other key is allowed.
|
||||
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)
|
||||
},
|
||||
someId: [Number, {value: false}], // either a number or false
|
||||
};
|
||||
```
|
||||
|
||||
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
||||
|
||||
## 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).
|
||||
@@ -0,0 +1,131 @@
|
||||
# 🦉 Reactivity 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [`useState`](#usestate)
|
||||
- [`reactive`](#reactive)
|
||||
- [`markRaw`](#markraw)
|
||||
- [`toRaw`](#toraw)
|
||||
|
||||
## Overview
|
||||
|
||||
Reactivity is a big topic in javascript frameworks. The goal is to provide a
|
||||
simple way to manipulate state, in such a way that the interface automatically
|
||||
update accordingly to state changes. Also, we obviously want this to happen in
|
||||
a performant way.
|
||||
|
||||
To solve this issue, Owl provides two reactivity primitives:
|
||||
|
||||
- `reactive`, which returns a proxy to its first argument, and tracks all read/update
|
||||
operation going through it,
|
||||
- `useState`: a hook, that internally uses `reactive`, and is linked to its
|
||||
owner component: any read operation will be tracked (key by key), and any
|
||||
updates to these tracked values will cause the component to be rerendered.
|
||||
|
||||
Most of the time, the `useState` hook is the best solution.
|
||||
|
||||
Since version 2.0, Owl applies the fine grained reactivity at the component
|
||||
level: reactive objects received as props are automatically subscribed to by the
|
||||
component, so Owl can track which part of these props are consumed by each
|
||||
component, and is therefore able to only rerender the impacted components.
|
||||
|
||||
## `useState`
|
||||
|
||||
Let us start by an example of how `useState` could be used:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="increment">
|
||||
<t t-esc="state.value"/>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If one were to use a simple state object, Owl would not be aware that the value
|
||||
was changed and that the component should be rerendered. With the `useState`
|
||||
hook, `this.state` is now a reactive object, so this component works as expected.
|
||||
|
||||
## `reactive`
|
||||
|
||||
The `reactive` function is the basic reactivity primitive. It takes an object
|
||||
or an array as first argument, and optionally, a function as the second argument.
|
||||
The function will be called whenever any tracked value is updated.
|
||||
|
||||
```js
|
||||
const obj = reactive({ a: 1 }, () => console.log("changed"));
|
||||
|
||||
obj.a = 2; // does not log anything: the 'a' key was not read
|
||||
console.log(obj.a); // log 2, and reads the 'a' key => it is now tracked
|
||||
obj.a = 3; // log 'changed' because we updated a tracked value
|
||||
```
|
||||
|
||||
An important property of reactive objects is that they can be reobserved: this
|
||||
will create an independant proxy that tracks another set of keys:
|
||||
|
||||
```js
|
||||
const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));
|
||||
const obj2 = reactive(obj1, () => console.log("observer 2"));
|
||||
|
||||
console.log(obj1.a); // log 1, and reads the 'a' key => it is now tracked by observer 1
|
||||
console.log(obj1.b); // log 2, and 'b' is now tracked by observer 1
|
||||
console.log(obj2.b); // log 2, and 'b' is now tracked by observer 1
|
||||
obj2.a = 3; // log 'observer1', because observer2 does not track a
|
||||
obj2.b = 3; // log 'observer1' and 'observer2'
|
||||
```
|
||||
|
||||
Obviously, one can use `reactive` on the result of a `useState` if wanted, this
|
||||
is the proper way to watch for some state changes.
|
||||
|
||||
## `markRaw`
|
||||
|
||||
Marks an object so that it is ignored by the reactivity system. This function returns its argument.
|
||||
|
||||
```js
|
||||
const someObject = markRaw(...);
|
||||
const state = useState({
|
||||
a: 1,
|
||||
obj: someObject
|
||||
});
|
||||
// here, state.obj === someObject
|
||||
```
|
||||
|
||||
This is useful in some rare cases. For example, some complex and large object such
|
||||
that going through the reactivity system may cause a non trivial performance slowdown.
|
||||
|
||||
However, use this function with caution: this is an escape hatch from the reactivity
|
||||
system, and as such, using it may cause subtle and unintended issues!
|
||||
|
||||
## `toRaw`
|
||||
|
||||
Given a reactive object, this function returns the underlying, non-reactive,
|
||||
corresponding object.
|
||||
|
||||
```js
|
||||
// in setup
|
||||
const state = useState({ value: 1 });
|
||||
|
||||
// later:
|
||||
const rawState = toRaw(this.state);
|
||||
rawState.value = 3; // will NOT be picked up by the reactivity system!!!
|
||||
```
|
||||
|
||||
Here again, this is useful in some situations where we want to explicitely bypass
|
||||
Owl, but using this function means that the responsability of coordinating
|
||||
state update is given to the user code, instead of Owl. Subtle bugs may arise!
|
||||
|
||||
Also, normal (non-reactive objects) will be directly returned by `toRaw`:
|
||||
|
||||
```js
|
||||
const obj = { a: 1 };
|
||||
console.log(toRaw(obj) === obj); // true
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
# 🦉 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,
|
||||
targeted 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Be aware that the `el` property will only be set when the target of the `t-ref`
|
||||
directive is mounted in the DOM. Otherwise, it will be set to `null`.
|
||||
|
||||
The `useRef` hook cannot be used to get a reference to an instance of a sub
|
||||
component.
|
||||
|
||||
Note that this example uses the suffix `ref` to name the reference. This
|
||||
is not mandatory, but it is a useful convention, so we do not forget that it is
|
||||
a reference object.
|
||||
@@ -0,0 +1,260 @@
|
||||
# 🦉 Slots 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Named slots](#named-slots)
|
||||
- [Rendering Context](#rendering-context)
|
||||
- [Default Slot](#default-slot)
|
||||
- [Default Content](#default-content)
|
||||
- [Dynamic slots](#dynamic-slots)
|
||||
- [Slots and props](#slots-and-props)
|
||||
- [Slot params](#slot-params)
|
||||
- [Slot scopes](#slot-scopes)
|
||||
|
||||
## 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 `Navbar`
|
||||
component, which displays a navbar, but with some customizable content. Since
|
||||
the specific content is only known to the user of the `Navbar`, it would be nice
|
||||
to specify it in the template where `Navbar` is used:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<Navbar>
|
||||
<span>Hello Owl</span>
|
||||
</Navbar>
|
||||
</div>
|
||||
```
|
||||
|
||||
This is exactly the way slots work! In the example above, the user of the `Navbar`
|
||||
component specify some content (here, in the default slot). The `Navbar`
|
||||
component can insert that content in its own template at the appropriate location.
|
||||
An important information to notice is that the content of the slot is rendered in
|
||||
the parent context, not in the navbar. As such, it can access values and methods
|
||||
from the parent component.
|
||||
|
||||
Here is how the `Navbar` component could be defined, with the `t-slot` directive:
|
||||
|
||||
```xml
|
||||
<div class="navbar">
|
||||
<t t-slot="default"/>
|
||||
<ul>
|
||||
<!-- rest of the navbar here -->
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Named slots
|
||||
|
||||
Default slots are very useful, but sometimes, we may need more than one slot.
|
||||
This is what named slots are for! For example, suppose we implement a component
|
||||
`InfoBox` that display a title and some specific content. Its template could look
|
||||
like this:
|
||||
|
||||
```xml
|
||||
<div class="info-box">
|
||||
<div class="info-box-title">
|
||||
<t t-slot="title"/>
|
||||
<span class="info-box-close-button" t-on-click="close">X</span>
|
||||
</div>
|
||||
<div class="info-box-content">
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
And one could use it with the `t-set-slot` directive:
|
||||
|
||||
```xml
|
||||
<InfoBox>
|
||||
<t t-set-slot="title">
|
||||
Specific Title. It could be html also.
|
||||
</t>
|
||||
<t t-set-slot="content">
|
||||
<!-- some template here, with html, events, whatever -->
|
||||
</t>
|
||||
</InfoBox>
|
||||
```
|
||||
|
||||
## 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).
|
||||
|
||||
## Default Slot
|
||||
|
||||
All elements inside the component which are not a named slot will be treated as
|
||||
part of the content of 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>
|
||||
```
|
||||
|
||||
One can mix default slot and named slots:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<Child>
|
||||
default content
|
||||
<t t-set-slot="footer">
|
||||
content for footer slot here
|
||||
</t>
|
||||
</Child>
|
||||
</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> -->
|
||||
```
|
||||
|
||||
## Dynamic Slots
|
||||
|
||||
The `t-slot` directive is actually able to use any expressions, using string
|
||||
interplolation:
|
||||
|
||||
```xml
|
||||
<t t-slot="{{current}}" />
|
||||
```
|
||||
|
||||
This will evaluate the `current` expression, and insert the corresponding slot
|
||||
at the place of the `t-slot` directive.
|
||||
|
||||
## Slots and props
|
||||
|
||||
In a sense, slots are almost the same as a prop: they define some information
|
||||
to pass to the child component. To make it possible to use it, and to pass it
|
||||
down to sub component, Owl actually define a special prop `slots` that contains
|
||||
all slot information given to the component. It looks like this:
|
||||
|
||||
```js
|
||||
{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }
|
||||
```
|
||||
|
||||
So, a component can pass its slots to a subcomponent like this:
|
||||
|
||||
```xml
|
||||
<Child slots="props.slots"/>
|
||||
```
|
||||
|
||||
## Slot params
|
||||
|
||||
For advanced usecases, it may be necessary to pass additional information to a
|
||||
slot. This can be done by providing extra key/value pairs to the `t-set-slot`
|
||||
directive. Then, the generic component can read them in its prop `slots`.
|
||||
|
||||
For example, here is how a Notebook component could be implemented (a component
|
||||
with multiple page, and a tab bar, which only render the current active page,
|
||||
and each page has a title).
|
||||
|
||||
```js
|
||||
class Notebook extends Component {
|
||||
static template = xml`
|
||||
<div class="notebook">
|
||||
<div class="tabs">
|
||||
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
|
||||
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
|
||||
<t t-esc="props.slots[tab].title"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="page">
|
||||
<t t-slot="{{currentSlot}}"/>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ activeTab: 0 });
|
||||
this.tabNames = Object.keys(this.props.slots);
|
||||
}
|
||||
|
||||
get currentSlot() {
|
||||
return this.tabNames[this.state.activeTab];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice how one can read the `title` value for each slots. Here is how one could
|
||||
use this `Notebook` component:
|
||||
|
||||
```xml
|
||||
<Notebook>
|
||||
<t t-set-slot="page1" title="'Page 1'">
|
||||
<div>this is in the page 1</div>
|
||||
</t>
|
||||
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
|
||||
<div>this is in the page 2</div>
|
||||
</t>
|
||||
</Notebook>
|
||||
```
|
||||
|
||||
Slot params works like normal props, so one can use the `.bind` suffix to
|
||||
bind a function if needed.
|
||||
|
||||
## Slot scopes
|
||||
|
||||
For other kinds of advanced use cases, the content of a slot may depends on some
|
||||
information specific to the generic component. This is the opposite of the slot
|
||||
params.
|
||||
|
||||
To solve this kind of problems, one can use the `t-slot-scope` directive along
|
||||
with the `t-set-slot`. This defines the name of a variable that can access
|
||||
everything given by the child component:
|
||||
|
||||
```xml
|
||||
<MyComponent>
|
||||
<t t-set-slot="foo" t-slot-scope="scope">
|
||||
content
|
||||
<t t-esc="scope.bool"/>
|
||||
<t t-esc="scope.num"/>
|
||||
</t>
|
||||
</MyComponent>
|
||||
```
|
||||
|
||||
And the child component that includes the slot can provide values like this:
|
||||
|
||||
```xml
|
||||
<t t-slot="foo" bool="other_var" num="5">
|
||||
```
|
||||
|
||||
or this:
|
||||
|
||||
```xml
|
||||
<t t-slot="foo" t-props="someObject">
|
||||
```
|
||||
|
||||
In the case of the default slot, you may declare the slot scope directly on the
|
||||
component itself:
|
||||
|
||||
```xml
|
||||
<MyComponent t-slot-scope="scope">
|
||||
content
|
||||
<t t-esc="scope.bool"/>
|
||||
<t t-esc="scope.num"/>
|
||||
</MyComponent>
|
||||
```
|
||||
|
||||
Slot values works like normal props, so one can use the `.bind` suffix to
|
||||
bind a function if needed.
|
||||
@@ -0,0 +1,703 @@
|
||||
# 🦉 Templates 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Directives](#directives)
|
||||
- [QWeb Template reference](#qweb-template-reference)
|
||||
- [White Spaces](#white-spaces)
|
||||
- [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)
|
||||
- [Sub Templates](#sub-templates)
|
||||
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
||||
- [Debugging](#debugging)
|
||||
- [Fragments](#fragments)
|
||||
- [Inline templates](#inline-templates)
|
||||
- [Rendering svg](#rendering-svg)
|
||||
- [Restrictions](#restrictions)
|
||||
|
||||
## Overview
|
||||
|
||||
Owl templates are describe using the [QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) specification. 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. Also, since Owl is a live
|
||||
component system, there are additional directives specific to Owl (such as `t-on`).
|
||||
|
||||
```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-out` | [Outputting value, possibly 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](#sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-translation` | [Disabling the translation of a node](translations.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#sub-components) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](refs.md) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
|
||||
| `t-on-*` | [Event handling](event_handling.md) |
|
||||
| `t-portal` | [Portal](portal.md) |
|
||||
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
|
||||
| `t-model` | [Form input bindings](input_bindings.md) |
|
||||
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
||||
|
||||
## QWeb Template 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
|
||||
|
||||
### 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-out` directive is almost the same as `t-esc`, but possibly without the
|
||||
escaping. The difference is that the value received by the `t-out` directive
|
||||
will only be not-escaped if it has been marked as such, using the `markup`
|
||||
utility function:
|
||||
|
||||
For example, in the following component:
|
||||
|
||||
```js
|
||||
const { markup, Component, xml } = owl;
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-out="value1"/>
|
||||
<t t-out="value2"/>`;
|
||||
|
||||
value1 = "<div>some text 1</div>";
|
||||
value2 = markup("<div>some text 2</div>");
|
||||
}
|
||||
```
|
||||
|
||||
The first `t-out` will act as a `t-esc` directive, which means that the content
|
||||
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||
this will be injected as html.
|
||||
|
||||
### 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. The dynamic elements can be
|
||||
specified with either `{{...}}` or `#{...}`:
|
||||
|
||||
```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" t-key="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-key="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
```
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
An important difference should be made with the usual `QWeb` behaviour: Owl
|
||||
requires the presence of a `t-key` directive, to be able to properly reconcile
|
||||
renderings.
|
||||
|
||||
`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-key="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 make sure we keep the actual DOM node instead of replacing it with
|
||||
a new one.
|
||||
|
||||
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-key="item_index"><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.
|
||||
|
||||
### 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 -->
|
||||
```
|
||||
|
||||
Note: by default, the rendering context for a sub template is simply the current
|
||||
rendering context (so, the current component). However, it may be useful to be
|
||||
able to specify a specific object as context. This can be done by using the
|
||||
`t-call-context` directive:
|
||||
|
||||
```xml
|
||||
<t t-call="other-template" t-call-context="obj"/>
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
## Fragments
|
||||
|
||||
Owl 2 supports templates with an arbitrary number of root elements, or even just
|
||||
a text node. So, the following templates are all valid:
|
||||
|
||||
```xml
|
||||
hello owl. This is just a text node!
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
<div>ola</div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<div t-if="someCondition"><SomeChildComponent/></div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<t t-if="someCondition"><SomeChildComponent/></t>
|
||||
```
|
||||
|
||||
## Inline templates
|
||||
|
||||
Most real applications will define their templates in a XML file, to benefit
|
||||
from the XML ecosystem, and to do some additional processing, such as translating
|
||||
them. However, in some cases, it is convenient to be able to define a template
|
||||
inline. To do so, one can use the `xml` helper function:
|
||||
|
||||
```js
|
||||
const { Component, xml } = owl;
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
mount(MyComponent, document.body);
|
||||
```
|
||||
|
||||
This function simply generates an unique string id, and register the template
|
||||
under that id in the internals of Owl, then return the id.
|
||||
|
||||
## Rendering svg
|
||||
|
||||
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.
|
||||
|
||||
**Important note:** Owl needs to properly set the namespace for each svg elements.
|
||||
Since Owl compile each template separately, it is not able to determine easily
|
||||
if a template is supposed to be included in a svg namespace or not. Therefore,
|
||||
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
|
||||
be considered as svg. In practice, this means that each component or each sub
|
||||
templates (included with `t-call`) should have one of these tag as root tag.
|
||||
|
||||
## Restrictions
|
||||
|
||||
Note that Owl templates forbid the use of tag and or attributes starting with
|
||||
the `block-` string. This restriction prevents name collision with the internal
|
||||
code of Owl.
|
||||
|
||||
```xml
|
||||
<div><block-1>this will not be accepted by Owl</block-1></div>
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
# 🦉 Translations 🦉
|
||||
|
||||
If properly setup, Owl 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 app = new App(Root, { templates, tranaslateFn });
|
||||
// ...
|
||||
```
|
||||
|
||||
See the [app configuration page](app.md#configuration) for more info on how to
|
||||
configure an Owl application.
|
||||
|
||||
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.
|
||||
|
||||
In some case, it is useful to be able to extend the list of translatable attributes.
|
||||
For example, one may want to also translate `data-title` attributes. To do that,
|
||||
we can define additional attributes with the `translatableAttributes` option:
|
||||
|
||||
```js
|
||||
const app = new App(Root, { templates, tranaslateFn, translatableAttributes: ["data-title"] });
|
||||
// ...
|
||||
```
|
||||
|
||||
It is also possible to remove an attribute from the default list by prefixing it with `-`:
|
||||
|
||||
```js
|
||||
const app = new App(Root, {
|
||||
templates,
|
||||
tranaslateFn,
|
||||
translatableAttributes: ["data-title", "-title"],
|
||||
});
|
||||
// data-title attribute will be translated, but not title attribute...
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
# 🦉 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
|
||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||
- [`EventBus`](#eventbus): a simple EventBus
|
||||
- [`validate`](#validate): a validation function
|
||||
|
||||
## `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
|
||||
const { whenReady } = owl;
|
||||
|
||||
await whenReady();
|
||||
// do something
|
||||
```
|
||||
|
||||
or alternatively:
|
||||
|
||||
```js
|
||||
whenReady(function () {
|
||||
// do something
|
||||
});
|
||||
```
|
||||
|
||||
## `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
|
||||
const { loadFile } = owl;
|
||||
|
||||
async function makeEnv() {
|
||||
const templates = await loadFile("templates.xml");
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
## `EventBus`
|
||||
|
||||
It is a simple `EventBus`, with the same API as usual DOM elements, and an
|
||||
additional `trigger` method to dispatch events:
|
||||
|
||||
```js
|
||||
const bus = new EventBus();
|
||||
bus.addEventListener("event", () => console.log("something happened"));
|
||||
|
||||
bus.trigger("event"); // 'something happened' is logged
|
||||
```
|
||||
|
||||
## `validate`
|
||||
|
||||
The `validate` function is a function that validates if a given object satisfies a
|
||||
specified schema. It is actually used by Owl itself to perform
|
||||
[props validation](props.md#props-validation). For example:
|
||||
|
||||
```js
|
||||
validate(
|
||||
{ a: "hey" },
|
||||
{
|
||||
id: Number,
|
||||
url: [Boolean, { type: Array, element: Number }],
|
||||
}
|
||||
);
|
||||
|
||||
// throws an error with the following information:
|
||||
// - unknown key 'a',
|
||||
// - 'id' is missing (should be a number),
|
||||
// - 'url' is missing (should be a boolean or list of numbers),
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
# 🦉 Store 🦉
|
||||
|
||||
Managing the state in an application is not an easy task. Many different
|
||||
architectures/designs/systems/... have been created. We propose here to use
|
||||
the idea of a central store.
|
||||
|
||||
- Store
|
||||
- connect
|
||||
@@ -1,3 +0,0 @@
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
@@ -1,218 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
odoo.define("app", function(require) {
|
||||
const Widget = require("web.Widget");
|
||||
var core = require("web.core");
|
||||
var dom = require("web.dom");
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var Counter = Widget.extend({
|
||||
template: "counter",
|
||||
events: {
|
||||
"click .o_increment": "_onIncrement"
|
||||
},
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.value = 0;
|
||||
},
|
||||
start: function() {
|
||||
this.updateCounter();
|
||||
},
|
||||
on_attach_callback: function() {},
|
||||
on_detach_callback: function() {},
|
||||
updateCounter: function() {
|
||||
this.$(".o_increment").html("Value: " + this.value);
|
||||
},
|
||||
_onIncrement: function(ev) {
|
||||
ev.stopPropagation();
|
||||
this.value += 1;
|
||||
this.updateCounter();
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var Message = Widget.extend({
|
||||
template: "message",
|
||||
events: {
|
||||
"click .remove": "_onRemove"
|
||||
},
|
||||
|
||||
init: function(parent, message) {
|
||||
this._super(parent);
|
||||
this.author = message.author;
|
||||
this.msg = message.msg;
|
||||
this.id = message.id;
|
||||
},
|
||||
willStart: function() {
|
||||
this.counter = new Counter(this);
|
||||
return this.counter.appendTo($("<div>"));
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$msg = this.$(".msg");
|
||||
dom.append(this.$el, this.counter.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: this.counter }]
|
||||
});
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
if (this.counter) {
|
||||
this.counter.on_attach_callback();
|
||||
}
|
||||
},
|
||||
on_detach_callback: function() {
|
||||
this.isInDom = true;
|
||||
if (this.counter) {
|
||||
this.counter.on_detach_callback();
|
||||
}
|
||||
},
|
||||
|
||||
_onRemove: function() {
|
||||
this.trigger_up("remove_message", { id: this.id });
|
||||
},
|
||||
|
||||
update: function() {
|
||||
this.msg += "!!!";
|
||||
this.$msg.text(this.msg);
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var App = Widget.extend({
|
||||
template: "root",
|
||||
events: {
|
||||
"click .o_btn_msg.100": function() {
|
||||
this.addMessages(100);
|
||||
},
|
||||
"click .o_btn_msg.1000": function() {
|
||||
this.addMessages(1000);
|
||||
},
|
||||
"click .o_btn_msg.10000": function() {
|
||||
this.addMessages(10000);
|
||||
},
|
||||
"click .o_btn_msg.50000": function() {
|
||||
this.addMessages(50000);
|
||||
},
|
||||
"click .updateSomeMessages": function() {
|
||||
this.updateSomeMessages();
|
||||
},
|
||||
"click .clear": function() {
|
||||
this.clear();
|
||||
}
|
||||
},
|
||||
custom_events: {
|
||||
remove_message: "_onRemoveMessage"
|
||||
},
|
||||
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.widgets = {};
|
||||
this.isInDom = false;
|
||||
this.messageCount = 0;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$content = this.$(".content");
|
||||
this.$msgCount = this.$(".message_count");
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
for (let widget of Object.values(this.widgets)) {
|
||||
if (widget.on_attach_callback) {
|
||||
widget.on_attach_callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
on_detach_callback: function() {
|
||||
this.isInDom = true;
|
||||
for (let widget of Object.values(this.widgets)) {
|
||||
if (widget.on_detach_callback) {
|
||||
widget.on_detach_callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateMessageCount() {
|
||||
this.$msgCount.text("Number of msg: " + this.messageCount);
|
||||
},
|
||||
|
||||
addMessages: function(n) {
|
||||
var self = this;
|
||||
startMeasure("add " + n);
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
$.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
stopMeasure();
|
||||
});
|
||||
},
|
||||
clear: function() {
|
||||
startMeasure("clear");
|
||||
this.$content.empty();
|
||||
for (let key in this.widgets) {
|
||||
this.widgets[key].destroy();
|
||||
delete this.widgets[key];
|
||||
}
|
||||
this.messageCount = 0;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
updateSomeMessages: function() {
|
||||
startMeasure("update every 10th");
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
stopMeasure();
|
||||
},
|
||||
_onRemoveMessage: function(ev) {
|
||||
startMeasure("remove message");
|
||||
ev.target.destroy();
|
||||
delete this.widgets[ev.data.id];
|
||||
this.messageCount--;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
// prepare QWeb
|
||||
const templates = await fetch("templates.xml");
|
||||
let strTemplates = await templates.text();
|
||||
strTemplates = strTemplates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
core.qweb.add_template(strTemplates);
|
||||
|
||||
// prepare app
|
||||
const app = new App();
|
||||
await app.appendTo(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Odoo Widget Benchmark (12.0)</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='web.assets_common.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="message_count">
|
||||
Number of msg: 0
|
||||
</div>
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1000 messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10000 messages</button>
|
||||
<button class="o_btn_msg 50000">Add 50000 messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove" >Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,218 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
odoo.define("app", function(require) {
|
||||
const Widget = require("web.Widget");
|
||||
var core = require("web.core");
|
||||
var dom = require("web.dom");
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var Counter = Widget.extend({
|
||||
template: "counter",
|
||||
events: {
|
||||
"click .o_increment": "_onIncrement"
|
||||
},
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.value = 0;
|
||||
},
|
||||
start: function() {
|
||||
this.updateCounter();
|
||||
},
|
||||
on_attach_callback: function() {},
|
||||
on_detach_callback: function() {},
|
||||
updateCounter: function() {
|
||||
this.$(".o_increment").html("Value: " + this.value);
|
||||
},
|
||||
_onIncrement: function(ev) {
|
||||
ev.stopPropagation();
|
||||
this.value += 1;
|
||||
this.updateCounter();
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var Message = Widget.extend({
|
||||
template: "message",
|
||||
events: {
|
||||
"click .remove": "_onRemove"
|
||||
},
|
||||
|
||||
init: function(parent, message) {
|
||||
this._super(parent);
|
||||
this.author = message.author;
|
||||
this.msg = message.msg;
|
||||
this.id = message.id;
|
||||
},
|
||||
willStart: function() {
|
||||
this.counter = new Counter(this);
|
||||
return this.counter.appendTo($("<div>"));
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$msg = this.$(".msg");
|
||||
dom.append(this.$el, this.counter.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: this.counter }]
|
||||
});
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
if (this.counter) {
|
||||
this.counter.on_attach_callback();
|
||||
}
|
||||
},
|
||||
on_detach_callback: function() {
|
||||
this.isInDom = true;
|
||||
if (this.counter) {
|
||||
this.counter.on_detach_callback();
|
||||
}
|
||||
},
|
||||
|
||||
_onRemove: function() {
|
||||
this.trigger_up("remove_message", { id: this.id });
|
||||
},
|
||||
|
||||
update: function() {
|
||||
this.msg += "!!!";
|
||||
this.$msg.text(this.msg);
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//----------------------------------------------------------------------------
|
||||
var App = Widget.extend({
|
||||
template: "root",
|
||||
events: {
|
||||
"click .o_btn_msg.100": function() {
|
||||
this.addMessages(100);
|
||||
},
|
||||
"click .o_btn_msg.1000": function() {
|
||||
this.addMessages(1000);
|
||||
},
|
||||
"click .o_btn_msg.10000": function() {
|
||||
this.addMessages(10000);
|
||||
},
|
||||
"click .o_btn_msg.50000": function() {
|
||||
this.addMessages(50000);
|
||||
},
|
||||
"click .updateSomeMessages": function() {
|
||||
this.updateSomeMessages();
|
||||
},
|
||||
"click .clear": function() {
|
||||
this.clear();
|
||||
}
|
||||
},
|
||||
custom_events: {
|
||||
remove_message: "_onRemoveMessage"
|
||||
},
|
||||
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.widgets = {};
|
||||
this.isInDom = false;
|
||||
this.messageCount = 0;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.$content = this.$(".content");
|
||||
this.$msgCount = this.$(".message_count");
|
||||
},
|
||||
on_attach_callback: function() {
|
||||
this.isInDom = true;
|
||||
for (let widget of Object.values(this.widgets)) {
|
||||
if (widget.on_attach_callback) {
|
||||
widget.on_attach_callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
on_detach_callback: function() {
|
||||
this.isInDom = true;
|
||||
for (let widget of Object.values(this.widgets)) {
|
||||
if (widget.on_detach_callback) {
|
||||
widget.on_detach_callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateMessageCount() {
|
||||
this.$msgCount.text("Number of msg: " + this.messageCount);
|
||||
},
|
||||
|
||||
addMessages: function(n) {
|
||||
var self = this;
|
||||
startMeasure("add " + n);
|
||||
const defs = [];
|
||||
const messages = buildData(n);
|
||||
for (let message of messages) {
|
||||
const widget = new Message(this, message);
|
||||
this.widgets[message.id] = widget;
|
||||
defs.push(widget.appendTo("<div>"));
|
||||
}
|
||||
$.when
|
||||
.apply($, defs)
|
||||
.then(function() {
|
||||
for (let message of messages) {
|
||||
let widget = self.widgets[message.id];
|
||||
dom.append(self.$content, widget.$el, {
|
||||
in_DOM: this.isInDom,
|
||||
callbacks: [{ widget: widget }]
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
self.messageCount += n;
|
||||
self.updateMessageCount();
|
||||
stopMeasure();
|
||||
});
|
||||
},
|
||||
clear: function() {
|
||||
startMeasure("clear");
|
||||
this.$content.empty();
|
||||
for (let key in this.widgets) {
|
||||
this.widgets[key].destroy();
|
||||
delete this.widgets[key];
|
||||
}
|
||||
this.messageCount = 0;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
},
|
||||
updateSomeMessages: function() {
|
||||
startMeasure("update every 10th");
|
||||
const widgets = Object.values(this.widgets);
|
||||
for (let i = 0; i < widgets.length; i += 10) {
|
||||
widgets[i].update();
|
||||
}
|
||||
stopMeasure();
|
||||
},
|
||||
_onRemoveMessage: function(ev) {
|
||||
startMeasure("remove message");
|
||||
ev.target.destroy();
|
||||
delete this.widgets[ev.data.id];
|
||||
this.messageCount--;
|
||||
this.updateMessageCount();
|
||||
stopMeasure();
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
// prepare QWeb
|
||||
const templates = await fetch("templates.xml");
|
||||
let strTemplates = await templates.text();
|
||||
strTemplates = strTemplates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
core.qweb.add_template(strTemplates);
|
||||
|
||||
// prepare app
|
||||
const app = new App();
|
||||
await app.appendTo(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Odoo Widget Benchmark (12.3)</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='web.assets_common.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
<templates>
|
||||
<div class="main" t-name="root">
|
||||
<div class="left-thing">
|
||||
<div class="message_count">
|
||||
Number of msg: 0
|
||||
</div>
|
||||
<button class="o_btn_msg 100">Add 100 messages</button>
|
||||
<button class="o_btn_msg 1000">Add 1000 messages</button>
|
||||
<button class="o_btn_msg 10000">Add 10000 messages</button>
|
||||
<button class="o_btn_msg 50000">Add 50000 messages</button>
|
||||
<button class="updateSomeMessages">Update every 10th message</button>
|
||||
<button class="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="widget.author"/></span>
|
||||
<span class="msg"><t t-esc="widget.msg"/></span>
|
||||
<button class="remove" >Remove</button>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button class="o_increment"></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,85 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure('remove message');
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.7.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL 0.8.0 Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='owl-0.8.0.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,84 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Likes Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends owl.Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
increment() {
|
||||
this.state.counter++;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends owl.Component {
|
||||
template = "message";
|
||||
widgets = { Counter };
|
||||
|
||||
shouldUpdate(nextProps) {
|
||||
return nextProps !== this.props;
|
||||
}
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
template = "root";
|
||||
widgets = { Message };
|
||||
state = { messages: [] };
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.state.messages.push.apply(this.state.messages, newMessages);
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.state.messages = [];
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < this.state.messages.length; i += 10) {
|
||||
const msg = Object.assign({}, messages[i]);
|
||||
msg.author += '!!!';
|
||||
this.set(messages, i, msg);
|
||||
}
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
async function start() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const env = {
|
||||
qweb: new owl.QWeb(templates)
|
||||
};
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
}
|
||||
|
||||
start();
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL Master Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='../../owl.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,32 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="root" class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: <t t-esc="state.messages.length"/></div>
|
||||
<button t-on-click="addMessages(100)">Add 100 messages</button>
|
||||
<button t-on-click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button t-on-click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button t-on-click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button t-on-click="updateSomeMessages">Update every 10th message</button>
|
||||
<button t-on-click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="message" class="message">
|
||||
<span class="author"><t t-esc="props.author"/></span>
|
||||
<span class="msg"><t t-esc="props.msg"/></span>
|
||||
<button class="remove" t-on-click="removeMessage">Remove</button>
|
||||
<t t-widget="Counter"/>
|
||||
</div>
|
||||
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment">Value: <t t-esc="state.counter"/></button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,221 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Counter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
counter: 0
|
||||
};
|
||||
this.increment = this.increment.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
null,
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: this.increment
|
||||
},
|
||||
"Value: ",
|
||||
this.state.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.setState({
|
||||
counter: this.state.counter + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Message extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "message"
|
||||
},
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "author"
|
||||
},
|
||||
this.props.message.author
|
||||
),
|
||||
React.createElement(
|
||||
"span",
|
||||
{
|
||||
className: "msg"
|
||||
},
|
||||
this.props.message.msg
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
className: "remove",
|
||||
onClick: this.removeMessage
|
||||
},
|
||||
"Remove"
|
||||
),
|
||||
React.createElement(Counter, null)
|
||||
);
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.props.removeCB(this.props.message.id);
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
class Main extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
messages: []
|
||||
};
|
||||
this.removeMessage = this.removeMessage.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const msgList = this.state.messages.map(m =>
|
||||
React.createElement(Message, {
|
||||
key: m.id,
|
||||
message: m,
|
||||
removeCB: this.removeMessage
|
||||
})
|
||||
);
|
||||
return React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "main"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "left-thing"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
null,
|
||||
"Number of msg: ",
|
||||
this.state.messages.length
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(100)
|
||||
},
|
||||
"Add 100 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(1000)
|
||||
},
|
||||
"Add 1000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(10000)
|
||||
},
|
||||
"Add 10000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.addMessages(50000)
|
||||
},
|
||||
"Add 50000 messages"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.updateSomeMessages()
|
||||
},
|
||||
"Update every 10th messags"
|
||||
),
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
onClick: _ => this.clear()
|
||||
},
|
||||
"Clear"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "right-thing"
|
||||
},
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "content"
|
||||
},
|
||||
msgList
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = this.state.messages.concat(buildData(n));
|
||||
this.setState({
|
||||
messages: newMessages
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.setState({
|
||||
messages: []
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.state.messages;
|
||||
for (let i = 0; i < messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
this.forceUpdate();
|
||||
stopMeasure();
|
||||
}
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.state.messages.findIndex(m => m.id === id);
|
||||
const messages = this.state.messages.slice();
|
||||
messages.splice(index, 1);
|
||||
this.setState({
|
||||
messages
|
||||
});
|
||||
stopMeasure();
|
||||
}
|
||||
} //-----------------------------
|
||||
// INIT
|
||||
//-----------------------------
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(Main, null),
|
||||
document.getElementById("main")
|
||||
);
|
||||
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>React Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='react.production.min.js'></script>
|
||||
<script src='react-dom.production.min.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
-220
@@ -1,220 +0,0 @@
|
||||
/** @license React v16.8.6
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
/*
|
||||
Modernizr 3.0.0pre (Custom Build) | MIT
|
||||
*/
|
||||
'use strict';(function(da,pb){"object"===typeof exports&&"undefined"!==typeof module?module.exports=pb(require("react")):"function"===typeof define&&define.amd?define(["react"],pb):da.ReactDOM=pb(da.React)})(this,function(da){function pb(a,b,c,d,e,f,g,h){if(!a){a=void 0;if(void 0===b)a=Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var l=[c,d,e,f,g,h],k=0;a=Error(b.replace(/%s/g,function(){return l[k++]}));
|
||||
a.name="Invariant Violation"}a.framesToPop=1;throw a;}}function n(a){for(var b=arguments.length-1,c="https://reactjs.org/docs/error-decoder.html?invariant="+a,d=0;d<b;d++)c+="&args[]="+encodeURIComponent(arguments[d+1]);pb(!1,"Minified React error #"+a+"; visit %s for the full message or use the non-minified dev environment for full errors and additional helpful warnings. ",c)}function rh(a,b,c,d,e,f,g,h,l){qb=!1;$b=null;sh.apply(th,arguments)}function uh(a,b,c,d,e,f,g,h,l){rh.apply(this,arguments);
|
||||
if(qb){if(qb){var k=$b;qb=!1;$b=null}else n("198"),k=void 0;ac||(ac=!0,Yc=k)}}function Ee(){if(bc)for(var a in Na){var b=Na[a],c=bc.indexOf(a);-1<c?void 0:n("96",a);if(!cc[c]){b.extractEvents?void 0:n("97",a);cc[c]=b;c=b.eventTypes;for(var d in c){var e=void 0;var f=c[d],g=b,h=d;Zc.hasOwnProperty(h)?n("99",h):void 0;Zc[h]=f;var l=f.phasedRegistrationNames;if(l){for(e in l)l.hasOwnProperty(e)&&Fe(l[e],g,h);e=!0}else f.registrationName?(Fe(f.registrationName,g,h),e=!0):e=!1;e?void 0:n("98",d,a)}}}}
|
||||
function Fe(a,b,c){Oa[a]?n("100",a):void 0;Oa[a]=b;$c[a]=b.eventTypes[c].dependencies}function Ge(a,b,c){var d=a.type||"unknown-event";a.currentTarget=He(c);uh(d,b,void 0,a);a.currentTarget=null}function Pa(a,b){null==b?n("30"):void 0;if(null==a)return b;if(Array.isArray(a)){if(Array.isArray(b))return a.push.apply(a,b),a;a.push(b);return a}return Array.isArray(b)?[a].concat(b):[a,b]}function ad(a,b,c){Array.isArray(a)?a.forEach(b,c):a&&b.call(c,a)}function Ie(a,b){var c=a.stateNode;if(!c)return null;
|
||||
var d=bd(c);if(!d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":(d=!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1}if(a)return null;c&&"function"!==typeof c?n("231",b,typeof c):void 0;return c}function cd(a){null!==a&&(rb=Pa(rb,a));a=rb;
|
||||
rb=null;if(a&&(ad(a,vh),rb?n("95"):void 0,ac))throw a=Yc,ac=!1,Yc=null,a;}function dc(a){if(a[ea])return a[ea];for(;!a[ea];)if(a.parentNode)a=a.parentNode;else return null;a=a[ea];return 5===a.tag||6===a.tag?a:null}function Je(a){a=a[ea];return!a||5!==a.tag&&6!==a.tag?null:a}function Da(a){if(5===a.tag||6===a.tag)return a.stateNode;n("33")}function dd(a){return a[ec]||null}function fa(a){do a=a.return;while(a&&5!==a.tag);return a?a:null}function Ke(a,b,c){if(b=Ie(a,c.dispatchConfig.phasedRegistrationNames[b]))c._dispatchListeners=
|
||||
Pa(c._dispatchListeners,b),c._dispatchInstances=Pa(c._dispatchInstances,a)}function wh(a){if(a&&a.dispatchConfig.phasedRegistrationNames){for(var b=a._targetInst,c=[];b;)c.push(b),b=fa(b);for(b=c.length;0<b--;)Ke(c[b],"captured",a);for(b=0;b<c.length;b++)Ke(c[b],"bubbled",a)}}function ed(a,b,c){a&&c&&c.dispatchConfig.registrationName&&(b=Ie(a,c.dispatchConfig.registrationName))&&(c._dispatchListeners=Pa(c._dispatchListeners,b),c._dispatchInstances=Pa(c._dispatchInstances,a))}function xh(a){a&&a.dispatchConfig.registrationName&&
|
||||
ed(a._targetInst,null,a)}function Qa(a){ad(a,wh)}function fc(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}function gc(a){if(fd[a])return fd[a];if(!Ra[a])return a;var b=Ra[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Le)return fd[a]=b[c];return a}function Me(){if(hc)return hc;var a,b=gd,c=b.length,d,e="value"in qa?qa.value:qa.textContent,f=e.length;for(a=0;a<c&&b[a]===e[a];a++);var g=c-a;for(d=1;d<=g&&b[c-d]===e[f-d];d++);return hc=e.slice(a,
|
||||
1<d?1-d:void 0)}function ic(){return!0}function jc(){return!1}function J(a,b,c,d){this.dispatchConfig=a;this._targetInst=b;this.nativeEvent=c;a=this.constructor.Interface;for(var e in a)a.hasOwnProperty(e)&&((b=a[e])?this[e]=b(c):"target"===e?this.target=d:this[e]=c[e]);this.isDefaultPrevented=(null!=c.defaultPrevented?c.defaultPrevented:!1===c.returnValue)?ic:jc;this.isPropagationStopped=jc;return this}function yh(a,b,c,d){if(this.eventPool.length){var e=this.eventPool.pop();this.call(e,a,b,c,d);
|
||||
return e}return new this(a,b,c,d)}function zh(a){a instanceof this?void 0:n("279");a.destructor();10>this.eventPool.length&&this.eventPool.push(a)}function Ne(a){a.eventPool=[];a.getPooled=yh;a.release=zh}function Oe(a,b){switch(a){case "keyup":return-1!==Ah.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "blur":return!0;default:return!1}}function Pe(a){a=a.detail;return"object"===typeof a&&"data"in a?a.data:null}function Bh(a,b){switch(a){case "compositionend":return Pe(b);
|
||||
case "keypress":if(32!==b.which)return null;Qe=!0;return Re;case "textInput":return a=b.data,a===Re&&Qe?null:a;default:return null}}function Ch(a,b){if(Sa)return"compositionend"===a||!hd&&Oe(a,b)?(a=Me(),hc=gd=qa=null,Sa=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1<b.char.length)return b.char;if(b.which)return String.fromCharCode(b.which)}return null;case "compositionend":return Se&&"ko"!==b.locale?null:b.data;
|
||||
default:return null}}function Te(a){if(a=Ue(a)){"function"!==typeof id?n("280"):void 0;var b=bd(a.stateNode);id(a.stateNode,a.type,b)}}function Ve(a){Ta?Ua?Ua.push(a):Ua=[a]:Ta=a}function We(){if(Ta){var a=Ta,b=Ua;Ua=Ta=null;Te(a);if(b)for(a=0;a<b.length;a++)Te(b[a])}}function Xe(a,b){if(jd)return a(b);jd=!0;try{return Ye(a,b)}finally{if(jd=!1,null!==Ta||null!==Ua)Ze(),We()}}function $e(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return"input"===b?!!Dh[a.type]:"textarea"===b?!0:!1}function kd(a){a=
|
||||
a.target||a.srcElement||window;a.correspondingUseElement&&(a=a.correspondingUseElement);return 3===a.nodeType?a.parentNode:a}function af(a){if(!ra)return!1;a="on"+a;var b=a in document;b||(b=document.createElement("div"),b.setAttribute(a,"return;"),b="function"===typeof b[a]);return b}function bf(a){var b=a.type;return(a=a.nodeName)&&"input"===a.toLowerCase()&&("checkbox"===b||"radio"===b)}function Eh(a){var b=bf(a)?"checked":"value",c=Object.getOwnPropertyDescriptor(a.constructor.prototype,b),d=
|
||||
""+a[b];if(!a.hasOwnProperty(b)&&"undefined"!==typeof c&&"function"===typeof c.get&&"function"===typeof c.set){var e=c.get,f=c.set;Object.defineProperty(a,b,{configurable:!0,get:function(){return e.call(this)},set:function(a){d=""+a;f.call(this,a)}});Object.defineProperty(a,b,{enumerable:c.enumerable});return{getValue:function(){return d},setValue:function(a){d=""+a},stopTracking:function(){a._valueTracker=null;delete a[b]}}}}function kc(a){a._valueTracker||(a._valueTracker=Eh(a))}function cf(a){if(!a)return!1;
|
||||
var b=a._valueTracker;if(!b)return!0;var c=b.getValue();var d="";a&&(d=bf(a)?a.checked?"true":"false":a.value);a=d;return a!==c?(b.setValue(a),!0):!1}function sb(a){if(null===a||"object"!==typeof a)return null;a=df&&a[df]||a["@@iterator"];return"function"===typeof a?a:null}function sa(a){if(null==a)return null;if("function"===typeof a)return a.displayName||a.name||null;if("string"===typeof a)return a;switch(a){case ld:return"ConcurrentMode";case ta:return"Fragment";case Va:return"Portal";case lc:return"Profiler";
|
||||
case md:return"StrictMode";case nd:return"Suspense"}if("object"===typeof a)switch(a.$$typeof){case ef:return"Context.Consumer";case ff:return"Context.Provider";case od:var b=a.render;b=b.displayName||b.name||"";return a.displayName||(""!==b?"ForwardRef("+b+")":"ForwardRef");case pd:return sa(a.type);case gf:if(a=1===a._status?a._result:null)return sa(a)}return null}function qd(a){var b="";do{a:switch(a.tag){case 3:case 4:case 6:case 7:case 10:case 9:var c="";break a;default:var d=a._debugOwner,e=
|
||||
a._debugSource,f=sa(a.type);c=null;d&&(c=sa(d.type));d=f;f="";e?f=" (at "+e.fileName.replace(Fh,"")+":"+e.lineNumber+")":c&&(f=" (created by "+c+")");c="\n in "+(d||"Unknown")+f}b+=c;a=a.return}while(a);return b}function Gh(a){if(hf.call(jf,a))return!0;if(hf.call(kf,a))return!1;if(Hh.test(a))return jf[a]=!0;kf[a]=!0;return!1}function Ih(a,b,c,d){if(null!==c&&0===c.type)return!1;switch(typeof b){case "function":case "symbol":return!0;case "boolean":if(d)return!1;if(null!==c)return!c.acceptsBooleans;
|
||||
a=a.toLowerCase().slice(0,5);return"data-"!==a&&"aria-"!==a;default:return!1}}function Jh(a,b,c,d){if(null===b||"undefined"===typeof b||Ih(a,b,c,d))return!0;if(d)return!1;if(null!==c)switch(c.type){case 3:return!b;case 4:return!1===b;case 5:return isNaN(b);case 6:return isNaN(b)||1>b}return!1}function K(a,b,c,d,e){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b}function rd(a,b,c,d){var e=A.hasOwnProperty(b)?
|
||||
A[b]:null;var f=null!==e?0===e.type:d?!1:!(2<b.length)||"o"!==b[0]&&"O"!==b[0]||"n"!==b[1]&&"N"!==b[1]?!1:!0;f||(Jh(b,c,e,d)&&(c=null),d||null===e?Gh(b)&&(null===c?a.removeAttribute(b):a.setAttribute(b,""+c)):e.mustUseProperty?a[e.propertyName]=null===c?3===e.type?!1:"":c:(b=e.attributeName,d=e.attributeNamespace,null===c?a.removeAttribute(b):(e=e.type,c=3===e||4===e&&!0===c?"":""+c,d?a.setAttributeNS(d,b,c):a.setAttribute(b,c))))}function ua(a){switch(typeof a){case "boolean":case "number":case "object":case "string":case "undefined":return a;
|
||||
default:return""}}function sd(a,b){var c=b.checked;return B({},b,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=c?c:a._wrapperState.initialChecked})}function lf(a,b){var c=null==b.defaultValue?"":b.defaultValue,d=null!=b.checked?b.checked:b.defaultChecked;c=ua(null!=b.value?b.value:c);a._wrapperState={initialChecked:d,initialValue:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function mf(a,b){b=b.checked;null!=b&&rd(a,"checked",b,!1)}function td(a,
|
||||
b){mf(a,b);var c=ua(b.value),d=b.type;if(null!=c)if("number"===d){if(0===c&&""===a.value||a.value!=c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else if("submit"===d||"reset"===d){a.removeAttribute("value");return}b.hasOwnProperty("value")?ud(a,b.type,c):b.hasOwnProperty("defaultValue")&&ud(a,b.type,ua(b.defaultValue));null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function nf(a,b,c){if(b.hasOwnProperty("value")||b.hasOwnProperty("defaultValue")){var d=b.type;
|
||||
if(!("submit"!==d&&"reset"!==d||void 0!==b.value&&null!==b.value))return;b=""+a._wrapperState.initialValue;c||b===a.value||(a.value=b);a.defaultValue=b}c=a.name;""!==c&&(a.name="");a.defaultChecked=!a.defaultChecked;a.defaultChecked=!!a._wrapperState.initialChecked;""!==c&&(a.name=c)}function ud(a,b,c){if("number"!==b||a.ownerDocument.activeElement!==a)null==c?a.defaultValue=""+a._wrapperState.initialValue:a.defaultValue!==""+c&&(a.defaultValue=""+c)}function of(a,b,c){a=J.getPooled(pf.change,a,b,
|
||||
c);a.type="change";Ve(c);Qa(a);return a}function Kh(a){cd(a)}function mc(a){var b=Da(a);if(cf(b))return a}function Lh(a,b){if("change"===a)return b}function qf(){tb&&(tb.detachEvent("onpropertychange",rf),ub=tb=null)}function rf(a){"value"===a.propertyName&&mc(ub)&&(a=of(ub,a,kd(a)),Xe(Kh,a))}function Mh(a,b,c){"focus"===a?(qf(),tb=b,ub=c,tb.attachEvent("onpropertychange",rf)):"blur"===a&&qf()}function Nh(a,b){if("selectionchange"===a||"keyup"===a||"keydown"===a)return mc(ub)}function Oh(a,b){if("click"===
|
||||
a)return mc(b)}function Ph(a,b){if("input"===a||"change"===a)return mc(b)}function Qh(a){var b=this.nativeEvent;return b.getModifierState?b.getModifierState(a):(a=Rh[a])?!!b[a]:!1}function vd(a){return Qh}function Ea(a,b){return a===b&&(0!==a||1/a===1/b)||a!==a&&b!==b}function vb(a,b){if(Ea(a,b))return!0;if("object"!==typeof a||null===a||"object"!==typeof b||null===b)return!1;var c=Object.keys(a),d=Object.keys(b);if(c.length!==d.length)return!1;for(d=0;d<c.length;d++)if(!Sh.call(b,c[d])||!Ea(a[c[d]],
|
||||
b[c[d]]))return!1;return!0}function wb(a){var b=a;if(a.alternate)for(;b.return;)b=b.return;else{if(0!==(b.effectTag&2))return 1;for(;b.return;)if(b=b.return,0!==(b.effectTag&2))return 1}return 3===b.tag?2:3}function sf(a){2!==wb(a)?n("188"):void 0}function Th(a){var b=a.alternate;if(!b)return b=wb(a),3===b?n("188"):void 0,1===b?null:a;for(var c=a,d=b;;){var e=c.return,f=e?e.alternate:null;if(!e||!f)break;if(e.child===f.child){for(var g=e.child;g;){if(g===c)return sf(e),a;if(g===d)return sf(e),b;g=
|
||||
g.sibling}n("188")}if(c.return!==d.return)c=e,d=f;else{g=!1;for(var h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling}if(!g){for(h=f.child;h;){if(h===c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling}g?void 0:n("189")}}c.alternate!==d?n("190"):void 0}3!==c.tag?n("188"):void 0;return c.stateNode.current===c?a:b}function tf(a){a=Th(a);if(!a)return null;for(var b=a;;){if(5===b.tag||6===b.tag)return b;if(b.child)b.child.return=b,b=b.child;else{if(b===a)break;
|
||||
for(;!b.sibling;){if(!b.return||b.return===a)return null;b=b.return}b.sibling.return=b.return;b=b.sibling}}return null}function nc(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;10===a&&(a=13);return 32<=a||13===a?a:0}function uf(a,b){var c=a[0];a=a[1];var d="on"+(a[0].toUpperCase()+a.slice(1));b={phasedRegistrationNames:{bubbled:d,captured:d+"Capture"},dependencies:[c],isInteractive:b};vf[a]=b;wd[c]=b}function Uh(a){var b=a.targetInst,c=b;do{if(!c){a.ancestors.push(c);
|
||||
break}var d;for(d=c;d.return;)d=d.return;d=3!==d.tag?null:d.stateNode.containerInfo;if(!d)break;a.ancestors.push(c);c=dc(d)}while(c);for(c=0;c<a.ancestors.length;c++){b=a.ancestors[c];var e=kd(a.nativeEvent);d=a.topLevelType;for(var f=a.nativeEvent,g=null,h=0;h<cc.length;h++){var l=cc[h];l&&(l=l.extractEvents(d,b,f,e))&&(g=Pa(g,l))}cd(g)}}function r(a,b){if(!b)return null;var c=(wf(a)?xf:oc).bind(null,a);b.addEventListener(a,c,!1)}function pc(a,b){if(!b)return null;var c=(wf(a)?xf:oc).bind(null,a);
|
||||
b.addEventListener(a,c,!0)}function xf(a,b){yf(oc,a,b)}function oc(a,b){if(qc){var c=kd(b);c=dc(c);null===c||"number"!==typeof c.tag||2===wb(c)||(c=null);if(rc.length){var d=rc.pop();d.topLevelType=a;d.nativeEvent=b;d.targetInst=c;a=d}else a={topLevelType:a,nativeEvent:b,targetInst:c,ancestors:[]};try{Xe(Uh,a)}finally{a.topLevelType=null,a.nativeEvent=null,a.targetInst=null,a.ancestors.length=0,10>rc.length&&rc.push(a)}}}function zf(a){Object.prototype.hasOwnProperty.call(a,sc)||(a[sc]=Vh++,Af[a[sc]]=
|
||||
{});return Af[a[sc]]}function xd(a){a=a||("undefined"!==typeof document?document:void 0);if("undefined"===typeof a)return null;try{return a.activeElement||a.body}catch(b){return a.body}}function Bf(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function Cf(a,b){var c=Bf(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode}c=void 0}c=Bf(c)}}function Df(a,b){return a&&b?a===
|
||||
b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?Df(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function Ef(){for(var a=window,b=xd();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href}catch(d){c=!1}if(c)a=b.contentWindow;else break;b=xd(a.document)}return b}function yd(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||
|
||||
"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}function Wh(){var a=Ef();if(yd(a)){if("selectionStart"in a)var b={start:a.selectionStart,end:a.selectionEnd};else a:{b=(b=a.ownerDocument)&&b.defaultView||window;var c=b.getSelection&&b.getSelection();if(c&&0!==c.rangeCount){b=c.anchorNode;var d=c.anchorOffset,e=c.focusNode;c=c.focusOffset;try{b.nodeType,e.nodeType}catch(cj){b=null;break a}var f=0,g=-1,h=-1,l=0,k=0,m=a,n=null;b:for(;;){for(var p;;){m!==b||0!==d&&3!==
|
||||
m.nodeType||(g=f+d);m!==e||0!==c&&3!==m.nodeType||(h=f+c);3===m.nodeType&&(f+=m.nodeValue.length);if(null===(p=m.firstChild))break;n=m;m=p}for(;;){if(m===a)break b;n===b&&++l===d&&(g=f);n===e&&++k===c&&(h=f);if(null!==(p=m.nextSibling))break;m=n;n=m.parentNode}m=p}b=-1===g||-1===h?null:{start:g,end:h}}else b=null}b=b||{start:0,end:0}}else b=null;return{focusedElem:a,selectionRange:b}}function Xh(a){var b=Ef(),c=a.focusedElem,d=a.selectionRange;if(b!==c&&c&&c.ownerDocument&&Df(c.ownerDocument.documentElement,
|
||||
c)){if(null!==d&&yd(c))if(b=d.start,a=d.end,void 0===a&&(a=b),"selectionStart"in c)c.selectionStart=b,c.selectionEnd=Math.min(a,c.value.length);else if(a=(b=c.ownerDocument||document)&&b.defaultView||window,a.getSelection){a=a.getSelection();var e=c.textContent.length,f=Math.min(d.start,e);d=void 0===d.end?f:Math.min(d.end,e);!a.extend&&f>d&&(e=d,d=f,f=e);e=Cf(c,f);var g=Cf(c,d);e&&g&&(1!==a.rangeCount||a.anchorNode!==e.node||a.anchorOffset!==e.offset||a.focusNode!==g.node||a.focusOffset!==g.offset)&&
|
||||
(b=b.createRange(),b.setStart(e.node,e.offset),a.removeAllRanges(),f>d?(a.addRange(b),a.extend(g.node,g.offset)):(b.setEnd(g.node,g.offset),a.addRange(b)))}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});"function"===typeof c.focus&&c.focus();for(c=0;c<b.length;c++)a=b[c],a.element.scrollLeft=a.left,a.element.scrollTop=a.top}}function Gf(a,b){var c=b.window===b?b.document:9===b.nodeType?b:b.ownerDocument;if(zd||null==Wa||Wa!==xd(c))return null;c=
|
||||
Wa;"selectionStart"in c&&yd(c)?c={start:c.selectionStart,end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset});return xb&&vb(xb,c)?null:(xb=c,a=J.getPooled(Hf.select,Ad,a,b),a.type="select",a.target=Wa,Qa(a),a)}function Yh(a){var b="";da.Children.forEach(a,function(a){null!=a&&(b+=a)});return b}function Bd(a,b){a=B({children:void 0},b);if(b=Yh(b.children))a.children=
|
||||
b;return a}function Xa(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e<c.length;e++)b["$"+c[e]]=!0;for(c=0;c<a.length;c++)e=b.hasOwnProperty("$"+a[c].value),a[c].selected!==e&&(a[c].selected=e),e&&d&&(a[c].defaultSelected=!0)}else{c=""+ua(c);b=null;for(e=0;e<a.length;e++){if(a[e].value===c){a[e].selected=!0;d&&(a[e].defaultSelected=!0);return}null!==b||a[e].disabled||(b=a[e])}null!==b&&(b.selected=!0)}}function Cd(a,b){null!=b.dangerouslySetInnerHTML?n("91"):void 0;return B({},b,{value:void 0,defaultValue:void 0,
|
||||
children:""+a._wrapperState.initialValue})}function If(a,b){var c=b.value;null==c&&(c=b.defaultValue,b=b.children,null!=b&&(null!=c?n("92"):void 0,Array.isArray(b)&&(1>=b.length?void 0:n("93"),b=b[0]),c=b),null==c&&(c=""));a._wrapperState={initialValue:ua(c)}}function Jf(a,b){var c=ua(b.value),d=ua(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d)}function Kf(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";
|
||||
case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Dd(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?Kf(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a}function Lf(a,b,c){return null==b||"boolean"===typeof b||""===b?"":c||"number"!==typeof b||0===b||yb.hasOwnProperty(a)&&yb[a]?(""+b).trim():b+"px"}function Mf(a,b){a=a.style;for(var c in b)if(b.hasOwnProperty(c)){var d=0===c.indexOf("--"),
|
||||
e=Lf(c,b[c],d);"float"===c&&(c="cssFloat");d?a.setProperty(c,e):a[c]=e}}function Ed(a,b){b&&(Zh[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML?n("137",a,""):void 0),null!=b.dangerouslySetInnerHTML&&(null!=b.children?n("60"):void 0,"object"===typeof b.dangerouslySetInnerHTML&&"__html"in b.dangerouslySetInnerHTML?void 0:n("61")),null!=b.style&&"object"!==typeof b.style?n("62",""):void 0)}function Fd(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;
|
||||
default:return!0}}function ha(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=zf(a);b=$c[b];for(var d=0;d<b.length;d++){var e=b[d];if(!c.hasOwnProperty(e)||!c[e]){switch(e){case "scroll":pc("scroll",a);break;case "focus":case "blur":pc("focus",a);pc("blur",a);c.blur=!0;c.focus=!0;break;case "cancel":case "close":af(e)&&pc(e,a);break;case "invalid":case "submit":case "reset":break;default:-1===zb.indexOf(e)&&r(e,a)}c[e]=!0}}}function tc(){}function Nf(a,b){switch(a){case "button":case "input":case "select":case "textarea":return!!b.autoFocus}return!1}
|
||||
function Gd(a,b){return"textarea"===a||"option"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}function $h(a,b,c,d,e,f){a[ec]=e;"input"===c&&"radio"===e.type&&null!=e.name&&mf(a,e);Fd(c,d);d=Fd(c,e);for(f=0;f<b.length;f+=2){var g=b[f],h=b[f+1];"style"===g?Mf(a,h):"dangerouslySetInnerHTML"===g?Of(a,h):"children"===g?Ab(a,h):rd(a,g,h,d)}switch(c){case "input":td(a,
|
||||
e);break;case "textarea":Jf(a,e);break;case "select":b=a._wrapperState.wasMultiple,a._wrapperState.wasMultiple=!!e.multiple,c=e.value,null!=c?Xa(a,!!e.multiple,c,!1):b!==!!e.multiple&&(null!=e.defaultValue?Xa(a,!!e.multiple,e.defaultValue,!0):Xa(a,!!e.multiple,e.multiple?[]:"",!1))}}function Hd(a){for(a=a.nextSibling;a&&1!==a.nodeType&&3!==a.nodeType;)a=a.nextSibling;return a}function Pf(a){for(a=a.firstChild;a&&1!==a.nodeType&&3!==a.nodeType;)a=a.nextSibling;return a}function D(a,b){0>Ya||(a.current=
|
||||
Id[Ya],Id[Ya]=null,Ya--)}function L(a,b,c){Ya++;Id[Ya]=a.current;a.current=b}function Za(a,b){var c=a.type.contextTypes;if(!c)return va;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function E(a){a=a.childContextTypes;return null!==a&&void 0!==a}function uc(a){D(M,a);
|
||||
D(F,a)}function Jd(a){D(M,a);D(F,a)}function Qf(a,b,c){F.current!==va?n("168"):void 0;L(F,b,a);L(M,c,a)}function Rf(a,b,c){var d=a.stateNode;a=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)e in a?void 0:n("108",sa(b)||"Unknown",e);return B({},c,d)}function vc(a){var b=a.stateNode;b=b&&b.__reactInternalMemoizedMergedChildContext||va;Fa=F.current;L(F,b,a);L(M,M.current,a);return!0}function Sf(a,b,c){var d=a.stateNode;d?void 0:n("169");c?(b=
|
||||
Rf(a,b,Fa),d.__reactInternalMemoizedMergedChildContext=b,D(M,a),D(F,a),L(F,b,a)):D(M,a);L(M,c,a)}function Tf(a){return function(b){try{return a(b)}catch(c){}}}function ai(a){if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1;var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=b.inject(a);Kd=Tf(function(a){return b.onCommitFiberRoot(c,a)});Ld=Tf(function(a){return b.onCommitFiberUnmount(c,a)})}catch(d){}return!0}function bi(a,b,c,d){this.tag=a;this.key=
|
||||
c;this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null;this.index=0;this.ref=null;this.pendingProps=b;this.contextDependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null;this.mode=d;this.effectTag=0;this.lastEffect=this.firstEffect=this.nextEffect=null;this.childExpirationTime=this.expirationTime=0;this.alternate=null}function Md(a){a=a.prototype;return!(!a||!a.isReactComponent)}function ci(a){if("function"===typeof a)return Md(a)?1:0;if(void 0!==a&&
|
||||
null!==a){a=a.$$typeof;if(a===od)return 11;if(a===pd)return 14}return 2}function Ga(a,b,c){c=a.alternate;null===c?(c=S(a.tag,b,a.key,a.mode),c.elementType=a.elementType,c.type=a.type,c.stateNode=a.stateNode,c.alternate=a,a.alternate=c):(c.pendingProps=b,c.effectTag=0,c.nextEffect=null,c.firstEffect=null,c.lastEffect=null);c.childExpirationTime=a.childExpirationTime;c.expirationTime=a.expirationTime;c.child=a.child;c.memoizedProps=a.memoizedProps;c.memoizedState=a.memoizedState;c.updateQueue=a.updateQueue;
|
||||
c.contextDependencies=a.contextDependencies;c.sibling=a.sibling;c.index=a.index;c.ref=a.ref;return c}function wc(a,b,c,d,e,f){var g=2;d=a;if("function"===typeof a)Md(a)&&(g=1);else if("string"===typeof a)g=5;else a:switch(a){case ta:return wa(c.children,e,f,b);case ld:return Uf(c,e|3,f,b);case md:return Uf(c,e|2,f,b);case lc:return a=S(12,c,b,e|4),a.elementType=lc,a.type=lc,a.expirationTime=f,a;case nd:return a=S(13,c,b,e),b=nd,a.elementType=b,a.type=b,a.expirationTime=f,a;default:if("object"===typeof a&&
|
||||
null!==a)switch(a.$$typeof){case ff:g=10;break a;case ef:g=9;break a;case od:g=11;break a;case pd:g=14;break a;case gf:g=16;d=null;break a}n("130",null==a?a:typeof a,"")}b=S(g,c,b,e);b.elementType=a;b.type=d;b.expirationTime=f;return b}function wa(a,b,c,d){a=S(7,a,d,b);a.expirationTime=c;return a}function Uf(a,b,c,d){a=S(8,a,d,b);b=0===(b&1)?md:ld;a.elementType=b;a.type=b;a.expirationTime=c;return a}function Nd(a,b,c){a=S(6,a,null,b);a.expirationTime=c;return a}function Od(a,b,c){b=S(4,null!==a.children?
|
||||
a.children:[],a.key,b);b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function Bb(a,b){a.didError=!1;var c=a.earliestPendingTime;0===c?a.earliestPendingTime=a.latestPendingTime=b:c<b?a.earliestPendingTime=b:a.latestPendingTime>b&&(a.latestPendingTime=b);xc(b,a)}function di(a,b){a.didError=!1;if(0===b)a.earliestPendingTime=0,a.latestPendingTime=0,a.earliestSuspendedTime=0,a.latestSuspendedTime=0,a.latestPingedTime=0;else{b<
|
||||
a.latestPingedTime&&(a.latestPingedTime=0);var c=a.latestPendingTime;0!==c&&(c>b?a.earliestPendingTime=a.latestPendingTime=0:a.earliestPendingTime>b&&(a.earliestPendingTime=a.latestPendingTime));c=a.earliestSuspendedTime;0===c?Bb(a,b):b<a.latestSuspendedTime?(a.earliestSuspendedTime=0,a.latestSuspendedTime=0,a.latestPingedTime=0,Bb(a,b)):b>c&&Bb(a,b)}xc(0,a)}function Vf(a,b){a.didError=!1;a.latestPingedTime>=b&&(a.latestPingedTime=0);var c=a.earliestPendingTime,d=a.latestPendingTime;c===b?a.earliestPendingTime=
|
||||
d===b?a.latestPendingTime=0:d:d===b&&(a.latestPendingTime=c);c=a.earliestSuspendedTime;d=a.latestSuspendedTime;0===c?a.earliestSuspendedTime=a.latestSuspendedTime=b:c<b?a.earliestSuspendedTime=b:d>b&&(a.latestSuspendedTime=b);xc(b,a)}function Wf(a,b){var c=a.earliestPendingTime;a=a.earliestSuspendedTime;c>b&&(b=c);a>b&&(b=a);return b}function xc(a,b){var c=b.earliestSuspendedTime,d=b.latestSuspendedTime,e=b.earliestPendingTime,f=b.latestPingedTime;e=0!==e?e:f;0===e&&(0===a||d<a)&&(e=d);a=e;0!==a&&
|
||||
c>a&&(a=c);b.nextExpirationTimeToWorkOn=e;b.expirationTime=a}function P(a,b){if(a&&a.defaultProps){b=B({},b);a=a.defaultProps;for(var c in a)void 0===b[c]&&(b[c]=a[c])}return b}function ei(a){var b=a._result;switch(a._status){case 1:return b;case 2:throw b;case 0:throw b;default:a._status=0;b=a._ctor;b=b();b.then(function(b){0===a._status&&(b=b.default,a._status=1,a._result=b)},function(b){0===a._status&&(a._status=2,a._result=b)});switch(a._status){case 1:return a._result;case 2:throw a._result;
|
||||
}a._result=b;throw b;}}function yc(a,b,c,d){b=a.memoizedState;c=c(d,b);c=null===c||void 0===c?b:B({},b,c);a.memoizedState=c;d=a.updateQueue;null!==d&&0===a.expirationTime&&(d.baseState=c)}function Xf(a,b,c,d,e,f,g){a=a.stateNode;return"function"===typeof a.shouldComponentUpdate?a.shouldComponentUpdate(d,f,g):b.prototype&&b.prototype.isPureReactComponent?!vb(c,d)||!vb(e,f):!0}function Yf(a,b,c,d){var e=!1;d=va;var f=b.contextType;"object"===typeof f&&null!==f?f=T(f):(d=E(b)?Fa:F.current,e=b.contextTypes,
|
||||
f=(e=null!==e&&void 0!==e)?Za(a,d):va);b=new b(c,f);a.memoizedState=null!==b.state&&void 0!==b.state?b.state:null;b.updater=zc;a.stateNode=b;b._reactInternalFiber=a;e&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=d,a.__reactInternalMemoizedMaskedChildContext=f);return b}function Zf(a,b,c,d){a=b.state;"function"===typeof b.componentWillReceiveProps&&b.componentWillReceiveProps(c,d);"function"===typeof b.UNSAFE_componentWillReceiveProps&&b.UNSAFE_componentWillReceiveProps(c,d);b.state!==
|
||||
a&&zc.enqueueReplaceState(b,b.state,null)}function Pd(a,b,c,d){var e=a.stateNode;e.props=c;e.state=a.memoizedState;e.refs=$f;var f=b.contextType;"object"===typeof f&&null!==f?e.context=T(f):(f=E(b)?Fa:F.current,e.context=Za(a,f));f=a.updateQueue;null!==f&&(Cb(a,f,c,e,d),e.state=a.memoizedState);f=b.getDerivedStateFromProps;"function"===typeof f&&(yc(a,b,f,c),e.state=a.memoizedState);"function"===typeof b.getDerivedStateFromProps||"function"===typeof e.getSnapshotBeforeUpdate||"function"!==typeof e.UNSAFE_componentWillMount&&
|
||||
"function"!==typeof e.componentWillMount||(b=e.state,"function"===typeof e.componentWillMount&&e.componentWillMount(),"function"===typeof e.UNSAFE_componentWillMount&&e.UNSAFE_componentWillMount(),b!==e.state&&zc.enqueueReplaceState(e,e.state,null),f=a.updateQueue,null!==f&&(Cb(a,f,c,e,d),e.state=a.memoizedState));"function"===typeof e.componentDidMount&&(a.effectTag|=4)}function Db(a,b,c){a=c.ref;if(null!==a&&"function"!==typeof a&&"object"!==typeof a){if(c._owner){c=c._owner;var d=void 0;c&&(1!==
|
||||
c.tag?n("309"):void 0,d=c.stateNode);d?void 0:n("147",a);var e=""+a;if(null!==b&&null!==b.ref&&"function"===typeof b.ref&&b.ref._stringRef===e)return b.ref;b=function(a){var b=d.refs;b===$f&&(b=d.refs={});null===a?delete b[e]:b[e]=a};b._stringRef=e;return b}"string"!==typeof a?n("284"):void 0;c._owner?void 0:n("290",a)}return a}function Ac(a,b){"textarea"!==a.type&&n("31","[object Object]"===Object.prototype.toString.call(b)?"object with keys {"+Object.keys(b).join(", ")+"}":b,"")}function ag(a){function b(b,
|
||||
c){if(a){var d=b.lastEffect;null!==d?(d.nextEffect=c,b.lastEffect=c):b.firstEffect=b.lastEffect=c;c.nextEffect=null;c.effectTag=8}}function c(c,d){if(!a)return null;for(;null!==d;)b(c,d),d=d.sibling;return null}function d(a,b){for(a=new Map;null!==b;)null!==b.key?a.set(b.key,b):a.set(b.index,b),b=b.sibling;return a}function e(a,b,c){a=Ga(a,b,c);a.index=0;a.sibling=null;return a}function f(b,c,d){b.index=d;if(!a)return c;d=b.alternate;if(null!==d)return d=d.index,d<c?(b.effectTag=2,c):d;b.effectTag=
|
||||
2;return c}function g(b){a&&null===b.alternate&&(b.effectTag=2);return b}function h(a,b,c,d){if(null===b||6!==b.tag)return b=Nd(c,a.mode,d),b.return=a,b;b=e(b,c,d);b.return=a;return b}function l(a,b,c,d){if(null!==b&&b.elementType===c.type)return d=e(b,c.props,d),d.ref=Db(a,b,c),d.return=a,d;d=wc(c.type,c.key,c.props,null,a.mode,d);d.ref=Db(a,b,c);d.return=a;return d}function k(a,b,c,d){if(null===b||4!==b.tag||b.stateNode.containerInfo!==c.containerInfo||b.stateNode.implementation!==c.implementation)return b=
|
||||
Od(c,a.mode,d),b.return=a,b;b=e(b,c.children||[],d);b.return=a;return b}function m(a,b,c,d,f){if(null===b||7!==b.tag)return b=wa(c,a.mode,d,f),b.return=a,b;b=e(b,c,d);b.return=a;return b}function Ff(a,b,c){if("string"===typeof b||"number"===typeof b)return b=Nd(""+b,a.mode,c),b.return=a,b;if("object"===typeof b&&null!==b){switch(b.$$typeof){case Bc:return c=wc(b.type,b.key,b.props,null,a.mode,c),c.ref=Db(a,null,b),c.return=a,c;case Va:return b=Od(b,a.mode,c),b.return=a,b}if(Cc(b)||sb(b))return b=
|
||||
wa(b,a.mode,c,null),b.return=a,b;Ac(a,b)}return null}function p(a,b,c,d){var e=null!==b?b.key:null;if("string"===typeof c||"number"===typeof c)return null!==e?null:h(a,b,""+c,d);if("object"===typeof c&&null!==c){switch(c.$$typeof){case Bc:return c.key===e?c.type===ta?m(a,b,c.props.children,d,e):l(a,b,c,d):null;case Va:return c.key===e?k(a,b,c,d):null}if(Cc(c)||sb(c))return null!==e?null:m(a,b,c,d,null);Ac(a,c)}return null}function r(a,b,c,d,e){if("string"===typeof d||"number"===typeof d)return a=
|
||||
a.get(c)||null,h(b,a,""+d,e);if("object"===typeof d&&null!==d){switch(d.$$typeof){case Bc:return a=a.get(null===d.key?c:d.key)||null,d.type===ta?m(b,a,d.props.children,e,d.key):l(b,a,d,e);case Va:return a=a.get(null===d.key?c:d.key)||null,k(b,a,d,e)}if(Cc(d)||sb(d))return a=a.get(c)||null,m(b,a,d,e,null);Ac(b,d)}return null}function u(e,g,h,k){for(var l=null,m=null,n=g,q=g=0,v=null;null!==n&&q<h.length;q++){n.index>q?(v=n,n=null):v=n.sibling;var Q=p(e,n,h[q],k);if(null===Q){null===n&&(n=v);break}a&&
|
||||
n&&null===Q.alternate&&b(e,n);g=f(Q,g,q);null===m?l=Q:m.sibling=Q;m=Q;n=v}if(q===h.length)return c(e,n),l;if(null===n){for(;q<h.length;q++)if(n=Ff(e,h[q],k))g=f(n,g,q),null===m?l=n:m.sibling=n,m=n;return l}for(n=d(e,n);q<h.length;q++)if(v=r(n,e,q,h[q],k))a&&null!==v.alternate&&n.delete(null===v.key?q:v.key),g=f(v,g,q),null===m?l=v:m.sibling=v,m=v;a&&n.forEach(function(a){return b(e,a)});return l}function x(e,g,h,k){var l=sb(h);"function"!==typeof l?n("150"):void 0;h=l.call(h);null==h?n("151"):void 0;
|
||||
for(var m=l=null,q=g,v=g=0,Q=null,t=h.next();null!==q&&!t.done;v++,t=h.next()){q.index>v?(Q=q,q=null):Q=q.sibling;var u=p(e,q,t.value,k);if(null===u){q||(q=Q);break}a&&q&&null===u.alternate&&b(e,q);g=f(u,g,v);null===m?l=u:m.sibling=u;m=u;q=Q}if(t.done)return c(e,q),l;if(null===q){for(;!t.done;v++,t=h.next())t=Ff(e,t.value,k),null!==t&&(g=f(t,g,v),null===m?l=t:m.sibling=t,m=t);return l}for(q=d(e,q);!t.done;v++,t=h.next())t=r(q,e,v,t.value,k),null!==t&&(a&&null!==t.alternate&&q.delete(null===t.key?
|
||||
v:t.key),g=f(t,g,v),null===m?l=t:m.sibling=t,m=t);a&&q.forEach(function(a){return b(e,a)});return l}return function(a,d,f,h){var k="object"===typeof f&&null!==f&&f.type===ta&&null===f.key;k&&(f=f.props.children);var l="object"===typeof f&&null!==f;if(l)switch(f.$$typeof){case Bc:a:{l=f.key;for(k=d;null!==k;){if(k.key===l)if(7===k.tag?f.type===ta:k.elementType===f.type){c(a,k.sibling);d=e(k,f.type===ta?f.props.children:f.props,h);d.ref=Db(a,k,f);d.return=a;a=d;break a}else{c(a,k);break}else b(a,k);
|
||||
k=k.sibling}f.type===ta?(d=wa(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=wc(f.type,f.key,f.props,null,a.mode,h),h.ref=Db(a,d,f),h.return=a,a=h)}return g(a);case Va:a:{for(k=f.key;null!==d;){if(d.key===k)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[],h);d.return=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=Od(f,a.mode,h);d.return=a;a=d}return g(a)}if("string"===typeof f||"number"===
|
||||
typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f,h),d.return=a,a=d):(c(a,d),d=Nd(f,a.mode,h),d.return=a,a=d),g(a);if(Cc(f))return u(a,d,f,h);if(sb(f))return x(a,d,f,h);l&&Ac(a,f);if("undefined"===typeof f&&!k)switch(a.tag){case 1:case 0:h=a.type,n("152",h.displayName||h.name||"Component")}return c(a,d)}}function Ha(a){a===Eb?n("174"):void 0;return a}function Qd(a,b){L(Fb,b,a);L(Gb,a,a);L(U,Eb,a);var c=b.nodeType;switch(c){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:Dd(null,
|
||||
"");break;default:c=8===c?b.parentNode:b,b=c.namespaceURI||null,c=c.tagName,b=Dd(b,c)}D(U,a);L(U,b,a)}function $a(a){D(U,a);D(Gb,a);D(Fb,a)}function bg(a){Ha(Fb.current);var b=Ha(U.current);var c=Dd(b,a.type);b!==c&&(L(Gb,a,a),L(U,c,a))}function Rd(a){Gb.current===a&&(D(U,a),D(Gb,a))}function V(){n("321")}function Sd(a,b){if(null===b)return!1;for(var c=0;c<b.length&&c<a.length;c++)if(!Ea(a[c],b[c]))return!1;return!0}function Td(a,b,c,d,e,f){Hb=f;xa=b;W=null!==a?a.memoizedState:null;Dc.current=null===
|
||||
W?fi:cg;b=c(d,e);if(Ib){do Ib=!1,Jb+=1,W=null!==a?a.memoizedState:null,ab=bb,X=G=y=null,Dc.current=cg,b=c(d,e);while(Ib);ia=null;Jb=0}Dc.current=Ud;a=xa;a.memoizedState=bb;a.expirationTime=Kb;a.updateQueue=X;a.effectTag|=Lb;a=null!==y&&null!==y.next;Hb=0;ab=G=bb=W=y=xa=null;Kb=0;X=null;Lb=0;a?n("300"):void 0;return b}function Vd(){Dc.current=Ud;Hb=0;ab=G=bb=W=y=xa=null;Kb=0;X=null;Lb=0;Ib=!1;ia=null;Jb=0}function cb(){var a={memoizedState:null,baseState:null,queue:null,baseUpdate:null,next:null};
|
||||
null===G?bb=G=a:G=G.next=a;return G}function Mb(){if(null!==ab)G=ab,ab=G.next,y=W,W=null!==y?y.next:null;else{null===W?n("310"):void 0;y=W;var a={memoizedState:y.memoizedState,baseState:y.baseState,queue:y.queue,baseUpdate:y.baseUpdate,next:null};G=null===G?bb=a:G.next=a;W=y.next}return G}function dg(a,b){return"function"===typeof b?b(a):b}function eg(a,b,c){b=Mb();c=b.queue;null===c?n("311"):void 0;c.lastRenderedReducer=a;if(0<Jb){var d=c.dispatch;if(null!==ia){var e=ia.get(c);if(void 0!==e){ia.delete(c);
|
||||
var f=b.memoizedState;do f=a(f,e.action),e=e.next;while(null!==e);Ea(f,b.memoizedState)||(ja=!0);b.memoizedState=f;b.baseUpdate===c.last&&(b.baseState=f);c.lastRenderedState=f;return[f,d]}}return[b.memoizedState,d]}d=c.last;var g=b.baseUpdate;f=b.baseState;null!==g?(null!==d&&(d.next=null),d=g.next):d=null!==d?d.next:null;if(null!==d){var h=e=null,l=d,k=!1;do{var m=l.expirationTime;m<Hb?(k||(k=!0,h=g,e=f),m>Kb&&(Kb=m)):f=l.eagerReducer===a?l.eagerState:a(f,l.action);g=l;l=l.next}while(null!==l&&l!==
|
||||
d);k||(h=g,e=f);Ea(f,b.memoizedState)||(ja=!0);b.memoizedState=f;b.baseUpdate=h;b.baseState=e;c.lastRenderedState=f}return[b.memoizedState,c.dispatch]}function Wd(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};null===X?(X={lastEffect:null},X.lastEffect=a.next=a):(b=X.lastEffect,null===b?X.lastEffect=a.next=a:(c=b.next,b.next=a,a.next=c,X.lastEffect=a));return a}function Xd(a,b,c,d){var e=cb();Lb|=a;e.memoizedState=Wd(b,c,void 0,void 0===d?null:d)}function Yd(a,b,c,d){var e=Mb();d=void 0===
|
||||
d?null:d;var f=void 0;if(null!==y){var g=y.memoizedState;f=g.destroy;if(null!==d&&Sd(d,g.deps)){Wd(db,c,f,d);return}}Lb|=a;e.memoizedState=Wd(b,c,f,d)}function fg(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null)};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null}}function gg(a,b){}function hg(a,b,c){25>Jb?void 0:n("301");var d=a.alternate;if(a===xa||null!==d&&d===xa)if(Ib=!0,a={expirationTime:Hb,action:c,eagerReducer:null,eagerState:null,next:null},null===
|
||||
ia&&(ia=new Map),c=ia.get(b),void 0===c)ia.set(b,a);else{for(b=c;null!==b.next;)b=b.next;b.next=a}else{eb();var e=ka();e=fb(e,a);var f={expirationTime:e,action:c,eagerReducer:null,eagerState:null,next:null},g=b.last;if(null===g)f.next=f;else{var h=g.next;null!==h&&(f.next=h);g.next=f}b.last=f;if(0===a.expirationTime&&(null===d||0===d.expirationTime)&&(d=b.lastRenderedReducer,null!==d))try{var l=b.lastRenderedState,k=d(l,c);f.eagerReducer=d;f.eagerState=k;if(Ea(k,l))return}catch(m){}finally{}ya(a,
|
||||
e)}}function ig(a,b){var c=S(5,null,null,0);c.elementType="DELETED";c.type="DELETED";c.stateNode=b;c.return=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function jg(a,b){switch(a.tag){case 5:var c=a.type;b=1!==b.nodeType||c.toLowerCase()!==b.nodeName.toLowerCase()?null:b;return null!==b?(a.stateNode=b,!0):!1;case 6:return b=""===a.pendingProps||3!==b.nodeType?null:b,null!==b?(a.stateNode=b,!0):!1;case 13:return!1;default:return!1}}function kg(a){if(Ia){var b=
|
||||
gb;if(b){var c=b;if(!jg(a,b)){b=Hd(c);if(!b||!jg(a,b)){a.effectTag|=2;Ia=!1;la=a;return}ig(la,c)}la=a;gb=Pf(b)}else a.effectTag|=2,Ia=!1,la=a}}function lg(a){for(a=a.return;null!==a&&5!==a.tag&&3!==a.tag&&18!==a.tag;)a=a.return;la=a}function Zd(a){if(a!==la)return!1;if(!Ia)return lg(a),Ia=!0,!1;var b=a.type;if(5!==a.tag||"head"!==b&&"body"!==b&&!Gd(b,a.memoizedProps))for(b=gb;b;)ig(a,b),b=Hd(b);lg(a);gb=la?Hd(a.stateNode):null;return!0}function $d(){gb=la=null;Ia=!1}function N(a,b,c,d){b.child=null===
|
||||
a?ae(b,null,c,d):hb(b,a.child,c,d)}function mg(a,b,c,d,e){c=c.render;var f=b.ref;ib(b,e);d=Td(a,b,c,d,f,e);if(null!==a&&!ja)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),ma(a,b,e);b.effectTag|=1;N(a,b,d,e);return b.child}function ng(a,b,c,d,e,f){if(null===a){var g=c.type;if("function"===typeof g&&!Md(g)&&void 0===g.defaultProps&&null===c.compare&&void 0===c.defaultProps)return b.tag=15,b.type=g,og(a,b,g,d,e,f);a=wc(c.type,null,d,null,b.mode,f);a.ref=
|
||||
b.ref;a.return=b;return b.child=a}g=a.child;if(e<f&&(e=g.memoizedProps,c=c.compare,c=null!==c?c:vb,c(e,d)&&a.ref===b.ref))return ma(a,b,f);b.effectTag|=1;a=Ga(g,d,f);a.ref=b.ref;a.return=b;return b.child=a}function og(a,b,c,d,e,f){return null!==a&&vb(a.memoizedProps,d)&&a.ref===b.ref&&(ja=!1,e<f)?ma(a,b,f):be(a,b,c,d,f)}function pg(a,b){var c=b.ref;if(null===a&&null!==c||null!==a&&a.ref!==c)b.effectTag|=128}function be(a,b,c,d,e){var f=E(c)?Fa:F.current;f=Za(b,f);ib(b,e);c=Td(a,b,c,d,f,e);if(null!==
|
||||
a&&!ja)return b.updateQueue=a.updateQueue,b.effectTag&=-517,a.expirationTime<=e&&(a.expirationTime=0),ma(a,b,e);b.effectTag|=1;N(a,b,c,e);return b.child}function qg(a,b,c,d,e){if(E(c)){var f=!0;vc(b)}else f=!1;ib(b,e);if(null===b.stateNode)null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),Yf(b,c,d,e),Pd(b,c,d,e),d=!0;else if(null===a){var g=b.stateNode,h=b.memoizedProps;g.props=h;var l=g.context,k=c.contextType;"object"===typeof k&&null!==k?k=T(k):(k=E(c)?Fa:F.current,k=Za(b,k));var m=
|
||||
c.getDerivedStateFromProps,n="function"===typeof m||"function"===typeof g.getSnapshotBeforeUpdate;n||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||l!==k)&&Zf(b,g,d,k);za=!1;var p=b.memoizedState;l=g.state=p;var r=b.updateQueue;null!==r&&(Cb(b,r,d,g,e),l=b.memoizedState);h!==d||p!==l||M.current||za?("function"===typeof m&&(yc(b,c,m,d),l=b.memoizedState),(h=za||Xf(b,c,h,d,p,l,k))?(n||"function"!==typeof g.UNSAFE_componentWillMount&&
|
||||
"function"!==typeof g.componentWillMount||("function"===typeof g.componentWillMount&&g.componentWillMount(),"function"===typeof g.UNSAFE_componentWillMount&&g.UNSAFE_componentWillMount()),"function"===typeof g.componentDidMount&&(b.effectTag|=4)):("function"===typeof g.componentDidMount&&(b.effectTag|=4),b.memoizedProps=d,b.memoizedState=l),g.props=d,g.state=l,g.context=k,d=h):("function"===typeof g.componentDidMount&&(b.effectTag|=4),d=!1)}else g=b.stateNode,h=b.memoizedProps,g.props=b.type===b.elementType?
|
||||
h:P(b.type,h),l=g.context,k=c.contextType,"object"===typeof k&&null!==k?k=T(k):(k=E(c)?Fa:F.current,k=Za(b,k)),m=c.getDerivedStateFromProps,(n="function"===typeof m||"function"===typeof g.getSnapshotBeforeUpdate)||"function"!==typeof g.UNSAFE_componentWillReceiveProps&&"function"!==typeof g.componentWillReceiveProps||(h!==d||l!==k)&&Zf(b,g,d,k),za=!1,l=b.memoizedState,p=g.state=l,r=b.updateQueue,null!==r&&(Cb(b,r,d,g,e),p=b.memoizedState),h!==d||l!==p||M.current||za?("function"===typeof m&&(yc(b,
|
||||
c,m,d),p=b.memoizedState),(m=za||Xf(b,c,h,d,l,p,k))?(n||"function"!==typeof g.UNSAFE_componentWillUpdate&&"function"!==typeof g.componentWillUpdate||("function"===typeof g.componentWillUpdate&&g.componentWillUpdate(d,p,k),"function"===typeof g.UNSAFE_componentWillUpdate&&g.UNSAFE_componentWillUpdate(d,p,k)),"function"===typeof g.componentDidUpdate&&(b.effectTag|=4),"function"===typeof g.getSnapshotBeforeUpdate&&(b.effectTag|=256)):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&l===
|
||||
a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=256),b.memoizedProps=d,b.memoizedState=p),g.props=d,g.state=p,g.context=k,d=m):("function"!==typeof g.componentDidUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=4),"function"!==typeof g.getSnapshotBeforeUpdate||h===a.memoizedProps&&l===a.memoizedState||(b.effectTag|=256),d=!1);return ce(a,b,c,d,f,e)}function ce(a,b,c,d,e,f){pg(a,b);var g=0!==(b.effectTag&
|
||||
64);if(!d&&!g)return e&&Sf(b,c,!1),ma(a,b,f);d=b.stateNode;gi.current=b;var h=g&&"function"!==typeof c.getDerivedStateFromError?null:d.render();b.effectTag|=1;null!==a&&g?(b.child=hb(b,a.child,null,f),b.child=hb(b,null,h,f)):N(a,b,h,f);b.memoizedState=d.state;e&&Sf(b,c,!0);return b.child}function rg(a){var b=a.stateNode;b.pendingContext?Qf(a,b.pendingContext,b.pendingContext!==b.context):b.context&&Qf(a,b.context,!1);Qd(a,b.containerInfo)}function sg(a,b,c){var d=b.mode,e=b.pendingProps,f=b.memoizedState;
|
||||
if(0===(b.effectTag&64)){f=null;var g=!1}else f={timedOutAt:null!==f?f.timedOutAt:0},g=!0,b.effectTag&=-65;if(null===a)if(g){var h=e.fallback;a=wa(null,d,0,null);0===(b.mode&1)&&(a.child=null!==b.memoizedState?b.child.child:b.child);d=wa(h,d,c,null);a.sibling=d;c=a;c.return=d.return=b}else c=d=ae(b,null,e.children,c);else null!==a.memoizedState?(d=a.child,h=d.sibling,g?(c=e.fallback,e=Ga(d,d.pendingProps,0),0===(b.mode&1)&&(g=null!==b.memoizedState?b.child.child:b.child,g!==d.child&&(e.child=g)),
|
||||
d=e.sibling=Ga(h,c,h.expirationTime),c=e,e.childExpirationTime=0,c.return=d.return=b):c=d=hb(b,d.child,e.children,c)):(h=a.child,g?(g=e.fallback,e=wa(null,d,0,null),e.child=h,0===(b.mode&1)&&(e.child=null!==b.memoizedState?b.child.child:b.child),d=e.sibling=wa(g,d,c,null),d.effectTag|=2,c=e,e.childExpirationTime=0,c.return=d.return=b):d=c=hb(b,h,e.children,c)),b.stateNode=a.stateNode;b.memoizedState=f;b.child=c;return d}function ma(a,b,c){null!==a&&(b.contextDependencies=a.contextDependencies);if(b.childExpirationTime<
|
||||
c)return null;null!==a&&b.child!==a.child?n("153"):void 0;if(null!==b.child){a=b.child;c=Ga(a,a.pendingProps,a.expirationTime);b.child=c;for(c.return=b;null!==a.sibling;)a=a.sibling,c=c.sibling=Ga(a,a.pendingProps,a.expirationTime),c.return=b;c.sibling=null}return b.child}function hi(a,b,c){var d=b.expirationTime;if(null!==a)if(a.memoizedProps!==b.pendingProps||M.current)ja=!0;else{if(d<c){ja=!1;switch(b.tag){case 3:rg(b);$d();break;case 5:bg(b);break;case 1:E(b.type)&&vc(b);break;case 4:Qd(b,b.stateNode.containerInfo);
|
||||
break;case 10:tg(b,b.memoizedProps.value);break;case 13:if(null!==b.memoizedState){d=b.child.childExpirationTime;if(0!==d&&d>=c)return sg(a,b,c);b=ma(a,b,c);return null!==b?b.sibling:null}}return ma(a,b,c)}}else ja=!1;b.expirationTime=0;switch(b.tag){case 2:d=b.elementType;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;var e=Za(b,F.current);ib(b,c);e=Td(null,b,d,a,e,c);b.effectTag|=1;if("object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof){b.tag=
|
||||
1;Vd();if(E(d)){var f=!0;vc(b)}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;var g=d.getDerivedStateFromProps;"function"===typeof g&&yc(b,d,g,a);e.updater=zc;b.stateNode=e;e._reactInternalFiber=b;Pd(b,d,a,c);b=ce(null,b,d,!0,f,c)}else b.tag=0,N(null,b,e,c),b=b.child;return b;case 16:e=b.elementType;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);f=b.pendingProps;a=ei(e);b.type=a;e=b.tag=ci(a);f=P(a,f);g=void 0;switch(e){case 0:g=be(null,b,a,f,c);break;case 1:g=
|
||||
qg(null,b,a,f,c);break;case 11:g=mg(null,b,a,f,c);break;case 14:g=ng(null,b,a,P(a.type,f),d,c);break;default:n("306",a,"")}return g;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),be(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),qg(a,b,d,e,c);case 3:rg(b);d=b.updateQueue;null===d?n("282"):void 0;e=b.memoizedState;e=null!==e?e.element:null;Cb(b,d,b.pendingProps,null,c);d=b.memoizedState.element;if(d===e)$d(),b=ma(a,b,c);else{e=b.stateNode;if(e=
|
||||
(null===a||null===a.child)&&e.hydrate)gb=Pf(b.stateNode.containerInfo),la=b,e=Ia=!0;e?(b.effectTag|=2,b.child=ae(b,null,d,c)):(N(a,b,d,c),$d());b=b.child}return b;case 5:return bg(b),null===a&&kg(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps:null,g=e.children,Gd(d,e)?g=null:null!==f&&Gd(d,f)&&(b.effectTag|=16),pg(a,b),1!==c&&b.mode&1&&e.hidden?(b.expirationTime=b.childExpirationTime=1,b=null):(N(a,b,g,c),b=b.child),b;case 6:return null===a&&kg(b),null;case 13:return sg(a,b,c);case 4:return Qd(b,
|
||||
b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=hb(b,null,d,c):N(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),mg(a,b,d,e,c);case 7:return N(a,b,b.pendingProps,c),b.child;case 8:return N(a,b,b.pendingProps.children,c),b.child;case 12:return N(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;g=b.memoizedProps;f=e.value;tg(b,f);if(null!==g){var h=g.value;f=Ea(h,f)?0:("function"===typeof d._calculateChangedBits?
|
||||
d._calculateChangedBits(h,f):1073741823)|0;if(0===f){if(g.children===e.children&&!M.current){b=ma(a,b,c);break a}}else for(h=b.child,null!==h&&(h.return=b);null!==h;){var l=h.contextDependencies;if(null!==l){g=h.child;for(var k=l.first;null!==k;){if(k.context===d&&0!==(k.observedBits&f)){1===h.tag&&(k=Aa(c),k.tag=Ec,na(h,k));h.expirationTime<c&&(h.expirationTime=c);k=h.alternate;null!==k&&k.expirationTime<c&&(k.expirationTime=c);k=c;for(var m=h.return;null!==m;){var p=m.alternate;if(m.childExpirationTime<
|
||||
k)m.childExpirationTime=k,null!==p&&p.childExpirationTime<k&&(p.childExpirationTime=k);else if(null!==p&&p.childExpirationTime<k)p.childExpirationTime=k;else break;m=m.return}l.expirationTime<c&&(l.expirationTime=c);break}k=k.next}}else g=10===h.tag?h.type===b.type?null:h.child:h.child;if(null!==g)g.return=h;else for(g=h;null!==g;){if(g===b){g=null;break}h=g.sibling;if(null!==h){h.return=g.return;g=h;break}g=g.return}h=g}}N(a,b,e.children,c);b=b.child}return b;case 9:return e=b.type,f=b.pendingProps,
|
||||
d=f.children,ib(b,c),e=T(e,f.unstable_observedBits),d=d(e),b.effectTag|=1,N(a,b,d,c),b.child;case 14:return e=b.type,f=P(e,b.pendingProps),f=P(e.type,f),ng(a,b,e,f,d,c);case 15:return og(a,b,b.type,b.pendingProps,d,c);case 17:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:P(d,e),null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2),b.tag=1,E(d)?(a=!0,vc(b)):a=!1,ib(b,c),Yf(b,d,e,c),Pd(b,d,e,c),ce(null,b,d,!0,a,c)}n("156")}function tg(a,b){var c=a.type._context;L(de,c._currentValue,
|
||||
a);c._currentValue=b}function ee(a){var b=de.current;D(de,a);a.type._context._currentValue=b}function ib(a,b){Nb=a;Ob=Ja=null;var c=a.contextDependencies;null!==c&&c.expirationTime>=b&&(ja=!0);a.contextDependencies=null}function T(a,b){if(Ob!==a&&!1!==b&&0!==b){if("number"!==typeof b||1073741823===b)Ob=a,b=1073741823;b={context:a,observedBits:b,next:null};null===Ja?(null===Nb?n("308"):void 0,Ja=b,Nb.contextDependencies={first:b,expirationTime:0}):Ja=Ja.next=b}return a._currentValue}function Fc(a){return{baseState:a,
|
||||
firstUpdate:null,lastUpdate:null,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null}}function fe(a){return{baseState:a.baseState,firstUpdate:a.firstUpdate,lastUpdate:a.lastUpdate,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null}}function Aa(a){return{expirationTime:a,tag:ug,payload:null,callback:null,next:null,nextEffect:null}}function Gc(a,
|
||||
b){null===a.lastUpdate?a.firstUpdate=a.lastUpdate=b:(a.lastUpdate.next=b,a.lastUpdate=b)}function na(a,b){var c=a.alternate;if(null===c){var d=a.updateQueue;var e=null;null===d&&(d=a.updateQueue=Fc(a.memoizedState))}else d=a.updateQueue,e=c.updateQueue,null===d?null===e?(d=a.updateQueue=Fc(a.memoizedState),e=c.updateQueue=Fc(c.memoizedState)):d=a.updateQueue=fe(e):null===e&&(e=c.updateQueue=fe(d));null===e||d===e?Gc(d,b):null===d.lastUpdate||null===e.lastUpdate?(Gc(d,b),Gc(e,b)):(Gc(d,b),e.lastUpdate=
|
||||
b)}function vg(a,b){var c=a.updateQueue;c=null===c?a.updateQueue=Fc(a.memoizedState):wg(a,c);null===c.lastCapturedUpdate?c.firstCapturedUpdate=c.lastCapturedUpdate=b:(c.lastCapturedUpdate.next=b,c.lastCapturedUpdate=b)}function wg(a,b){var c=a.alternate;null!==c&&b===c.updateQueue&&(b=a.updateQueue=fe(b));return b}function xg(a,b,c,d,e,f){switch(c.tag){case yg:return a=c.payload,"function"===typeof a?a.call(f,d,e):a;case ge:a.effectTag=a.effectTag&-2049|64;case ug:a=c.payload;e="function"===typeof a?
|
||||
a.call(f,d,e):a;if(null===e||void 0===e)break;return B({},d,e);case Ec:za=!0}return d}function Cb(a,b,c,d,e){za=!1;b=wg(a,b);for(var f=b.baseState,g=null,h=0,l=b.firstUpdate,k=f;null!==l;){var m=l.expirationTime;m<e?(null===g&&(g=l,f=k),h<m&&(h=m)):(k=xg(a,b,l,k,c,d),null!==l.callback&&(a.effectTag|=32,l.nextEffect=null,null===b.lastEffect?b.firstEffect=b.lastEffect=l:(b.lastEffect.nextEffect=l,b.lastEffect=l)));l=l.next}m=null;for(l=b.firstCapturedUpdate;null!==l;){var n=l.expirationTime;n<e?(null===
|
||||
m&&(m=l,null===g&&(f=k)),h<n&&(h=n)):(k=xg(a,b,l,k,c,d),null!==l.callback&&(a.effectTag|=32,l.nextEffect=null,null===b.lastCapturedEffect?b.firstCapturedEffect=b.lastCapturedEffect=l:(b.lastCapturedEffect.nextEffect=l,b.lastCapturedEffect=l)));l=l.next}null===g&&(b.lastUpdate=null);null===m?b.lastCapturedUpdate=null:a.effectTag|=32;null===g&&null===m&&(f=k);b.baseState=f;b.firstUpdate=g;b.firstCapturedUpdate=m;a.expirationTime=h;a.memoizedState=k}function zg(a,b,c,d){null!==b.firstCapturedUpdate&&
|
||||
(null!==b.lastUpdate&&(b.lastUpdate.next=b.firstCapturedUpdate,b.lastUpdate=b.lastCapturedUpdate),b.firstCapturedUpdate=b.lastCapturedUpdate=null);Ag(b.firstEffect,c);b.firstEffect=b.lastEffect=null;Ag(b.firstCapturedEffect,c);b.firstCapturedEffect=b.lastCapturedEffect=null}function Ag(a,b){for(;null!==a;){var c=a.callback;if(null!==c){a.callback=null;var d=b;"function"!==typeof c?n("191",c):void 0;c.call(d)}a=a.nextEffect}}function Hc(a,b){return{value:a,source:b,stack:qd(b)}}function Pb(a){a.effectTag|=
|
||||
4}function Bg(a,b){var c=b.source,d=b.stack;null===d&&null!==c&&(d=qd(c));null!==c&&sa(c.type);b=b.value;null!==a&&1===a.tag&&sa(a.type);try{console.error(b)}catch(e){setTimeout(function(){throw e;})}}function Cg(a){var b=a.ref;if(null!==b)if("function"===typeof b)try{b(null)}catch(c){Ka(a,c)}else b.current=null}function Qb(a,b,c){c=c.updateQueue;c=null!==c?c.lastEffect:null;if(null!==c){var d=c=c.next;do{if((d.tag&a)!==db){var e=d.destroy;d.destroy=void 0;void 0!==e&&e()}(d.tag&b)!==db&&(e=d.create,
|
||||
d.destroy=e());d=d.next}while(d!==c)}}function ii(a,b){for(var c=a;;){if(5===c.tag){var d=c.stateNode;if(b)d.style.display="none";else{d=c.stateNode;var e=c.memoizedProps.style;e=void 0!==e&&null!==e&&e.hasOwnProperty("display")?e.display:null;d.style.display=Lf("display",e)}}else if(6===c.tag)c.stateNode.nodeValue=b?"":c.memoizedProps;else if(13===c.tag&&null!==c.memoizedState){d=c.child.sibling;d.return=c;c=d;continue}else if(null!==c.child){c.child.return=c;c=c.child;continue}if(c===a)break;for(;null===
|
||||
c.sibling;){if(null===c.return||c.return===a)return;c=c.return}c.sibling.return=c.return;c=c.sibling}}function Dg(a){"function"===typeof Ld&&Ld(a);switch(a.tag){case 0:case 11:case 14:case 15:var b=a.updateQueue;if(null!==b&&(b=b.lastEffect,null!==b)){var c=b=b.next;do{var d=c.destroy;if(void 0!==d){var e=a;try{d()}catch(f){Ka(e,f)}}c=c.next}while(c!==b)}break;case 1:Cg(a);b=a.stateNode;if("function"===typeof b.componentWillUnmount)try{b.props=a.memoizedProps,b.state=a.memoizedState,b.componentWillUnmount()}catch(f){Ka(a,
|
||||
f)}break;case 5:Cg(a);break;case 4:Eg(a)}}function Fg(a){return 5===a.tag||3===a.tag||4===a.tag}function Gg(a){a:{for(var b=a.return;null!==b;){if(Fg(b)){var c=b;break a}b=b.return}n("160");c=void 0}var d=b=void 0;switch(c.tag){case 5:b=c.stateNode;d=!1;break;case 3:b=c.stateNode.containerInfo;d=!0;break;case 4:b=c.stateNode.containerInfo;d=!0;break;default:n("161")}c.effectTag&16&&(Ab(b,""),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c.return||Fg(c.return)){c=null;break a}c=
|
||||
c.return}c.sibling.return=c.return;for(c=c.sibling;5!==c.tag&&6!==c.tag&&18!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;else c.child.return=c,c=c.child}if(!(c.effectTag&2)){c=c.stateNode;break a}}for(var e=a;;){if(5===e.tag||6===e.tag)if(c)if(d){var f=b,g=e.stateNode,h=c;8===f.nodeType?f.parentNode.insertBefore(g,h):f.insertBefore(g,h)}else b.insertBefore(e.stateNode,c);else d?(g=b,h=e.stateNode,8===g.nodeType?(f=g.parentNode,f.insertBefore(h,g)):(f=g,f.appendChild(h)),
|
||||
g=g._reactRootContainer,null!==g&&void 0!==g||null!==f.onclick||(f.onclick=tc)):b.appendChild(e.stateNode);else if(4!==e.tag&&null!==e.child){e.child.return=e;e=e.child;continue}if(e===a)break;for(;null===e.sibling;){if(null===e.return||e.return===a)return;e=e.return}e.sibling.return=e.return;e=e.sibling}}function Eg(a){for(var b=a,c=!1,d=void 0,e=void 0;;){if(!c){c=b.return;a:for(;;){null===c?n("160"):void 0;switch(c.tag){case 5:d=c.stateNode;e=!1;break a;case 3:d=c.stateNode.containerInfo;e=!0;
|
||||
break a;case 4:d=c.stateNode.containerInfo;e=!0;break a}c=c.return}c=!0}if(5===b.tag||6===b.tag){a:for(var f=b,g=f;;)if(Dg(g),null!==g.child&&4!==g.tag)g.child.return=g,g=g.child;else{if(g===f)break;for(;null===g.sibling;){if(null===g.return||g.return===f)break a;g=g.return}g.sibling.return=g.return;g=g.sibling}e?(f=d,g=b.stateNode,8===f.nodeType?f.parentNode.removeChild(g):f.removeChild(g)):d.removeChild(b.stateNode)}else if(4===b.tag){if(null!==b.child){d=b.stateNode.containerInfo;e=!0;b.child.return=
|
||||
b;b=b.child;continue}}else if(Dg(b),null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return;b=b.return;4===b.tag&&(c=!1)}b.sibling.return=b.return;b=b.sibling}}function Hg(a,b){switch(b.tag){case 0:case 11:case 14:case 15:Qb(Rb,ji,b);break;case 1:break;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps;a=null!==a?a.memoizedProps:d;var e=b.type,f=b.updateQueue;b.updateQueue=null;null!==f&&$h(c,f,e,a,d,b)}break;case 6:null===
|
||||
b.stateNode?n("162"):void 0;b.stateNode.nodeValue=b.memoizedProps;break;case 3:break;case 12:break;case 13:c=b.memoizedState;d=void 0;a=b;null===c?d=!1:(d=!0,a=b.child,0===c.timedOutAt&&(c.timedOutAt=ka()));null!==a&&ii(a,d);c=b.updateQueue;if(null!==c){b.updateQueue=null;var g=b.stateNode;null===g&&(g=b.stateNode=new ki);c.forEach(function(a){var c=li.bind(null,b,a);g.has(a)||(g.add(a),a.then(c,c))})}break;case 17:break;default:n("163")}}function he(a,b,c){c=Aa(c);c.tag=ge;c.payload={element:null};
|
||||
var d=b.value;c.callback=function(){ie(d);Bg(a,b)};return c}function Ig(a,b,c){c=Aa(c);c.tag=ge;var d=a.type.getDerivedStateFromError;if("function"===typeof d){var e=b.value;c.payload=function(){return d(e)}}var f=a.stateNode;null!==f&&"function"===typeof f.componentDidCatch&&(c.callback=function(){"function"!==typeof d&&(null===Ba?Ba=new Set([this]):Ba.add(this));var c=b.value,e=b.stack;Bg(a,b);this.componentDidCatch(c,{componentStack:null!==e?e:""})});return c}function mi(a,b){switch(a.tag){case 1:return E(a.type)&&
|
||||
uc(a),b=a.effectTag,b&2048?(a.effectTag=b&-2049|64,a):null;case 3:return $a(a),Jd(a),b=a.effectTag,0!==(b&64)?n("285"):void 0,a.effectTag=b&-2049|64,a;case 5:return Rd(a),null;case 13:return b=a.effectTag,b&2048?(a.effectTag=b&-2049|64,a):null;case 18:return null;case 4:return $a(a),null;case 10:return ee(a),null;default:return null}}function Jg(){if(null!==x)for(var a=x.return;null!==a;){var b=a;switch(b.tag){case 1:var c=b.type.childContextTypes;null!==c&&void 0!==c&&uc(b);break;case 3:$a(b);Jd(b);
|
||||
break;case 5:Rd(b);break;case 4:$a(b);break;case 10:ee(b)}a=a.return}Y=null;H=0;La=-1;je=!1;x=null}function ni(){for(;null!==p;){var a=p.effectTag;a&16&&Ab(p.stateNode,"");if(a&128){var b=p.alternate;null!==b&&(b=b.ref,null!==b&&("function"===typeof b?b(null):b.current=null))}switch(a&14){case 2:Gg(p);p.effectTag&=-3;break;case 6:Gg(p);p.effectTag&=-3;Hg(p.alternate,p);break;case 4:Hg(p.alternate,p);break;case 8:a=p,Eg(a),a.return=null,a.child=null,a.memoizedState=null,a.updateQueue=null,a=a.alternate,
|
||||
null!==a&&(a.return=null,a.child=null,a.memoizedState=null,a.updateQueue=null)}p=p.nextEffect}}function oi(){for(;null!==p;){if(p.effectTag&256)a:{var a=p.alternate,b=p;switch(b.tag){case 0:case 11:case 15:Qb(pi,db,b);break a;case 1:if(b.effectTag&256&&null!==a){var c=a.memoizedProps,d=a.memoizedState;a=b.stateNode;b=a.getSnapshotBeforeUpdate(b.elementType===b.type?c:P(b.type,c),d);a.__reactInternalSnapshotBeforeUpdate=b}break a;case 3:case 5:case 6:case 4:case 17:break a;default:n("163")}}p=p.nextEffect}}
|
||||
function qi(a,b){for(;null!==p;){var c=p.effectTag;if(c&36){var d=p.alternate,e=p,f=b;switch(e.tag){case 0:case 11:case 15:Qb(ri,Sb,e);break;case 1:var g=e.stateNode;if(e.effectTag&4)if(null===d)g.componentDidMount();else{var h=e.elementType===e.type?d.memoizedProps:P(e.type,d.memoizedProps);g.componentDidUpdate(h,d.memoizedState,g.__reactInternalSnapshotBeforeUpdate)}d=e.updateQueue;null!==d&&zg(e,d,g,f);break;case 3:d=e.updateQueue;if(null!==d){g=null;if(null!==e.child)switch(e.child.tag){case 5:g=
|
||||
e.child.stateNode;break;case 1:g=e.child.stateNode}zg(e,d,g,f)}break;case 5:f=e.stateNode;null===d&&e.effectTag&4&&Nf(e.type,e.memoizedProps)&&f.focus();break;case 6:break;case 4:break;case 12:break;case 13:break;case 17:break;default:n("163")}}c&128&&(e=p.ref,null!==e&&(f=p.stateNode,"function"===typeof e?e(f):e.current=f));c&512&&(ke=a);p=p.nextEffect}}function si(a,b){Ic=Jc=ke=null;var c=w;w=!0;do{if(b.effectTag&512){var d=!1,e=void 0;try{var f=b;Qb(le,db,f);Qb(db,me,f)}catch(g){d=!0,e=g}d&&Ka(b,
|
||||
e)}b=b.nextEffect}while(null!==b);w=c;c=a.expirationTime;0!==c&&Kc(a,c);z||w||Z(1073741823,!1)}function eb(){null!==Jc&&ti(Jc);null!==Ic&&Ic()}function ui(a,b){Lc=Ca=!0;a.current===b?n("177"):void 0;var c=a.pendingCommitExpirationTime;0===c?n("261"):void 0;a.pendingCommitExpirationTime=0;var d=b.expirationTime,e=b.childExpirationTime;di(a,e>d?e:d);Kg.current=null;d=void 0;1<b.effectTag?null!==b.lastEffect?(b.lastEffect.nextEffect=b,d=b.firstEffect):d=b:d=b.firstEffect;ne=qc;oe=Wh();qc=!1;for(p=d;null!==
|
||||
p;){e=!1;var f=void 0;try{oi()}catch(h){e=!0,f=h}e&&(null===p?n("178"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}for(p=d;null!==p;){e=!1;f=void 0;try{ni()}catch(h){e=!0,f=h}e&&(null===p?n("178"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}Xh(oe);oe=null;qc=!!ne;ne=null;a.current=b;for(p=d;null!==p;){e=!1;f=void 0;try{qi(a,c)}catch(h){e=!0,f=h}e&&(null===p?n("178"):void 0,Ka(p,f),null!==p&&(p=p.nextEffect))}if(null!==d&&null!==ke){var g=si.bind(null,a,d);Jc=Mc(Lg,function(){return vi(g)});Ic=g}Ca=
|
||||
Lc=!1;"function"===typeof Kd&&Kd(b.stateNode);c=b.expirationTime;b=b.childExpirationTime;b=b>c?b:c;0===b&&(Ba=null);wi(a,b)}function Mg(a){for(;;){var b=a.alternate,c=a.return,d=a.sibling;if(0===(a.effectTag&1024)){x=a;a:{var e=b;b=a;var f=H;var g=b.pendingProps;switch(b.tag){case 2:break;case 16:break;case 15:case 0:break;case 1:E(b.type)&&uc(b);break;case 3:$a(b);Jd(b);g=b.stateNode;g.pendingContext&&(g.context=g.pendingContext,g.pendingContext=null);if(null===e||null===e.child)Zd(b),b.effectTag&=
|
||||
-3;pe(b);break;case 5:Rd(b);var h=Ha(Fb.current);f=b.type;if(null!==e&&null!=b.stateNode)Ng(e,b,f,g,h),e.ref!==b.ref&&(b.effectTag|=128);else if(g){var l=Ha(U.current);if(Zd(b)){g=b;e=g.stateNode;var k=g.type,m=g.memoizedProps,p=h;e[ea]=g;e[ec]=m;f=void 0;h=k;switch(h){case "iframe":case "object":r("load",e);break;case "video":case "audio":for(k=0;k<zb.length;k++)r(zb[k],e);break;case "source":r("error",e);break;case "img":case "image":case "link":r("error",e);r("load",e);break;case "form":r("reset",
|
||||
e);r("submit",e);break;case "details":r("toggle",e);break;case "input":lf(e,m);r("invalid",e);ha(p,"onChange");break;case "select":e._wrapperState={wasMultiple:!!m.multiple};r("invalid",e);ha(p,"onChange");break;case "textarea":If(e,m),r("invalid",e),ha(p,"onChange")}Ed(h,m);k=null;for(f in m)m.hasOwnProperty(f)&&(l=m[f],"children"===f?"string"===typeof l?e.textContent!==l&&(k=["children",l]):"number"===typeof l&&e.textContent!==""+l&&(k=["children",""+l]):Oa.hasOwnProperty(f)&&null!=l&&ha(p,f));
|
||||
switch(h){case "input":kc(e);nf(e,m,!0);break;case "textarea":kc(e);f=e.textContent;f===e._wrapperState.initialValue&&(e.value=f);break;case "select":case "option":break;default:"function"===typeof m.onClick&&(e.onclick=tc)}f=k;g.updateQueue=f;g=null!==f?!0:!1;g&&Pb(b)}else{m=b;p=f;e=g;k=9===h.nodeType?h:h.ownerDocument;"http://www.w3.org/1999/xhtml"===l&&(l=Kf(p));"http://www.w3.org/1999/xhtml"===l?"script"===p?(e=k.createElement("div"),e.innerHTML="<script>\x3c/script>",k=e.removeChild(e.firstChild)):
|
||||
"string"===typeof e.is?k=k.createElement(p,{is:e.is}):(k=k.createElement(p),"select"===p&&(p=k,e.multiple?p.multiple=!0:e.size&&(p.size=e.size))):k=k.createElementNS(l,p);e=k;e[ea]=m;e[ec]=g;Og(e,b,!1,!1);m=e;k=f;p=g;var t=h,y=Fd(k,p);switch(k){case "iframe":case "object":r("load",m);h=p;break;case "video":case "audio":for(h=0;h<zb.length;h++)r(zb[h],m);h=p;break;case "source":r("error",m);h=p;break;case "img":case "image":case "link":r("error",m);r("load",m);h=p;break;case "form":r("reset",m);r("submit",
|
||||
m);h=p;break;case "details":r("toggle",m);h=p;break;case "input":lf(m,p);h=sd(m,p);r("invalid",m);ha(t,"onChange");break;case "option":h=Bd(m,p);break;case "select":m._wrapperState={wasMultiple:!!p.multiple};h=B({},p,{value:void 0});r("invalid",m);ha(t,"onChange");break;case "textarea":If(m,p);h=Cd(m,p);r("invalid",m);ha(t,"onChange");break;default:h=p}Ed(k,h);l=void 0;var u=k,w=m,v=h;for(l in v)if(v.hasOwnProperty(l)){var q=v[l];"style"===l?Mf(w,q):"dangerouslySetInnerHTML"===l?(q=q?q.__html:void 0,
|
||||
null!=q&&Of(w,q)):"children"===l?"string"===typeof q?("textarea"!==u||""!==q)&&Ab(w,q):"number"===typeof q&&Ab(w,""+q):"suppressContentEditableWarning"!==l&&"suppressHydrationWarning"!==l&&"autoFocus"!==l&&(Oa.hasOwnProperty(l)?null!=q&&ha(t,l):null!=q&&rd(w,l,q,y))}switch(k){case "input":kc(m);nf(m,p,!1);break;case "textarea":kc(m);h=m.textContent;h===m._wrapperState.initialValue&&(m.value=h);break;case "option":null!=p.value&&m.setAttribute("value",""+ua(p.value));break;case "select":h=m;m=p;h.multiple=
|
||||
!!m.multiple;p=m.value;null!=p?Xa(h,!!m.multiple,p,!1):null!=m.defaultValue&&Xa(h,!!m.multiple,m.defaultValue,!0);break;default:"function"===typeof h.onClick&&(m.onclick=tc)}(g=Nf(f,g))&&Pb(b);b.stateNode=e}null!==b.ref&&(b.effectTag|=128)}else null===b.stateNode?n("166"):void 0;break;case 6:e&&null!=b.stateNode?Pg(e,b,e.memoizedProps,g):("string"!==typeof g&&(null===b.stateNode?n("166"):void 0),e=Ha(Fb.current),Ha(U.current),Zd(b)?(g=b,f=g.stateNode,e=g.memoizedProps,f[ea]=g,(g=f.nodeValue!==e)&&
|
||||
Pb(b)):(f=b,g=(9===e.nodeType?e:e.ownerDocument).createTextNode(g),g[ea]=b,f.stateNode=g));break;case 11:break;case 13:g=b.memoizedState;if(0!==(b.effectTag&64)){b.expirationTime=f;x=b;break a}g=null!==g;f=null!==e&&null!==e.memoizedState;null!==e&&!g&&f&&(e=e.child.sibling,null!==e&&(h=b.firstEffect,null!==h?(b.firstEffect=e,e.nextEffect=h):(b.firstEffect=b.lastEffect=e,e.nextEffect=null),e.effectTag=8));if(g||f)b.effectTag|=4;break;case 7:break;case 8:break;case 12:break;case 4:$a(b);pe(b);break;
|
||||
case 10:ee(b);break;case 9:break;case 14:break;case 17:E(b.type)&&uc(b);break;case 18:break;default:n("156")}x=null}b=a;if(1===H||1!==b.childExpirationTime){g=0;for(f=b.child;null!==f;)e=f.expirationTime,h=f.childExpirationTime,e>g&&(g=e),h>g&&(g=h),f=f.sibling;b.childExpirationTime=g}if(null!==x)return x;null!==c&&0===(c.effectTag&1024)&&(null===c.firstEffect&&(c.firstEffect=a.firstEffect),null!==a.lastEffect&&(null!==c.lastEffect&&(c.lastEffect.nextEffect=a.firstEffect),c.lastEffect=a.lastEffect),
|
||||
1<a.effectTag&&(null!==c.lastEffect?c.lastEffect.nextEffect=a:c.firstEffect=a,c.lastEffect=a))}else{a=mi(a,H);if(null!==a)return a.effectTag&=1023,a;null!==c&&(c.firstEffect=c.lastEffect=null,c.effectTag|=1024)}if(null!==d)return d;if(null!==c)a=c;else break}return null}function Qg(a){var b=hi(a.alternate,a,H);a.memoizedProps=a.pendingProps;null===b&&(b=Mg(a));Kg.current=null;return b}function Rg(a,b){Ca?n("243"):void 0;eb();Ca=!0;var c=qe.current;qe.current=Ud;var d=a.nextExpirationTimeToWorkOn;
|
||||
if(d!==H||a!==Y||null===x)Jg(),Y=a,H=d,x=Ga(Y.current,null,H),a.pendingCommitExpirationTime=0;var e=!1;do{try{if(b)for(;null!==x&&!Nc();)x=Qg(x);else for(;null!==x;)x=Qg(x)}catch(v){if(Ob=Ja=Nb=null,Vd(),null===x)e=!0,ie(v);else{null===x?n("271"):void 0;var f=x,g=f.return;if(null===g)e=!0,ie(v);else{a:{var h=a,l=g,k=f,m=v;g=H;k.effectTag|=1024;k.firstEffect=k.lastEffect=null;if(null!==m&&"object"===typeof m&&"function"===typeof m.then){var p=m;m=l;var t=-1,r=-1;do{if(13===m.tag){var u=m.alternate;
|
||||
if(null!==u&&(u=u.memoizedState,null!==u)){r=10*(1073741822-u.timedOutAt);break}u=m.pendingProps.maxDuration;if("number"===typeof u)if(0>=u)t=0;else if(-1===t||u<t)t=u}m=m.return}while(null!==m);m=l;do{if(u=13===m.tag)u=void 0===m.memoizedProps.fallback?!1:null===m.memoizedState;if(u){l=m.updateQueue;null===l?(l=new Set,l.add(p),m.updateQueue=l):l.add(p);if(0===(m.mode&1)){m.effectTag|=64;k.effectTag&=-1957;1===k.tag&&(null===k.alternate?k.tag=17:(g=Aa(1073741823),g.tag=Ec,na(k,g)));k.expirationTime=
|
||||
1073741823;break a}k=h;l=g;var w=k.pingCache;null===w?(w=k.pingCache=new xi,u=new Set,w.set(p,u)):(u=w.get(p),void 0===u&&(u=new Set,w.set(p,u)));u.has(l)||(u.add(l),k=yi.bind(null,k,p,l),p.then(k,k));-1===t?h=1073741823:(-1===r&&(r=10*(1073741822-Wf(h,g))-5E3),h=r+t);0<=h&&La<h&&(La=h);m.effectTag|=2048;m.expirationTime=g;break a}m=m.return}while(null!==m);m=Error((sa(k.type)||"A React component")+" suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."+
|
||||
qd(k))}je=!0;m=Hc(m,k);h=l;do{switch(h.tag){case 3:h.effectTag|=2048;h.expirationTime=g;g=he(h,m,g);vg(h,g);break a;case 1:if(t=m,r=h.type,k=h.stateNode,0===(h.effectTag&64)&&("function"===typeof r.getDerivedStateFromError||null!==k&&"function"===typeof k.componentDidCatch&&(null===Ba||!Ba.has(k)))){h.effectTag|=2048;h.expirationTime=g;g=Ig(h,t,g);vg(h,g);break a}}h=h.return}while(null!==h)}x=Mg(f);continue}}}break}while(1);Ca=!1;qe.current=c;Ob=Ja=Nb=null;Vd();if(e)Y=null,a.finishedWork=null;else if(null!==
|
||||
x)a.finishedWork=null;else{c=a.current.alternate;null===c?n("281"):void 0;Y=null;if(je){e=a.latestPendingTime;f=a.latestSuspendedTime;g=a.latestPingedTime;if(0!==e&&e<d||0!==f&&f<d||0!==g&&g<d){Vf(a,d);re(a,c,d,a.expirationTime,-1);return}if(!a.didError&&b){a.didError=!0;d=a.nextExpirationTimeToWorkOn=d;b=a.expirationTime=1073741823;re(a,c,d,b,-1);return}}b&&-1!==La?(Vf(a,d),b=10*(1073741822-Wf(a,d)),b<La&&(La=b),b=10*(1073741822-ka()),b=La-b,re(a,c,d,a.expirationTime,0>b?0:b)):(a.pendingCommitExpirationTime=
|
||||
d,a.finishedWork=c)}}function Ka(a,b){for(var c=a.return;null!==c;){switch(c.tag){case 1:var d=c.stateNode;if("function"===typeof c.type.getDerivedStateFromError||"function"===typeof d.componentDidCatch&&(null===Ba||!Ba.has(d))){a=Hc(b,a);a=Ig(c,a,1073741823);na(c,a);ya(c,1073741823);return}break;case 3:a=Hc(b,a);a=he(c,a,1073741823);na(c,a);ya(c,1073741823);return}c=c.return}3===a.tag&&(c=Hc(b,a),c=he(a,c,1073741823),na(a,c),ya(a,1073741823))}function fb(a,b){var c=zi(),d=void 0;if(0===(b.mode&1))d=
|
||||
1073741823;else if(Ca&&!Lc)d=H;else{switch(c){case se:d=1073741823;break;case te:d=1073741822-10*(((1073741822-a+15)/10|0)+1);break;case Lg:d=1073741822-25*(((1073741822-a+500)/25|0)+1);break;case Ai:case Bi:d=1;break;default:n("313")}null!==Y&&d===H&&--d}c===te&&(0===oa||d<oa)&&(oa=d);return d}function yi(a,b,c){var d=a.pingCache;null!==d&&d.delete(b);if(null!==Y&&H===c)Y=null;else if(b=a.earliestSuspendedTime,d=a.latestSuspendedTime,0!==b&&c<=b&&c>=d){a.didError=!1;b=a.latestPingedTime;if(0===b||
|
||||
b>c)a.latestPingedTime=c;xc(c,a);c=a.expirationTime;0!==c&&Kc(a,c)}}function li(a,b){var c=a.stateNode;null!==c&&c.delete(b);b=ka();b=fb(b,a);a=Sg(a,b);null!==a&&(Bb(a,b),b=a.expirationTime,0!==b&&Kc(a,b))}function Sg(a,b){a.expirationTime<b&&(a.expirationTime=b);var c=a.alternate;null!==c&&c.expirationTime<b&&(c.expirationTime=b);var d=a.return,e=null;if(null===d&&3===a.tag)e=a.stateNode;else for(;null!==d;){c=d.alternate;d.childExpirationTime<b&&(d.childExpirationTime=b);null!==c&&c.childExpirationTime<
|
||||
b&&(c.childExpirationTime=b);if(null===d.return&&3===d.tag){e=d.stateNode;break}d=d.return}return e}function ya(a,b){a=Sg(a,b);null!==a&&(!Ca&&0!==H&&b>H&&Jg(),Bb(a,b),Ca&&!Lc&&Y===a||Kc(a,a.expirationTime),Tb>Ci&&(Tb=0,n("185")))}function Tg(a,b,c,d,e){return Mc(se,function(){return a(b,c,d,e)})}function Ub(){aa=1073741822-((ue()-ve)/10|0)}function Ug(a,b){if(0!==Oc){if(b<Oc)return;null!==Pc&&Vg(Pc)}Oc=b;a=ue()-ve;Pc=Wg(Di,{timeout:10*(1073741822-b)-a})}function re(a,b,c,d,e){a.expirationTime=d;
|
||||
0!==e||Nc()?0<e&&(a.timeoutHandle=Ei(Fi.bind(null,a,b,c),e)):(a.pendingCommitExpirationTime=c,a.finishedWork=b)}function Fi(a,b,c){a.pendingCommitExpirationTime=c;a.finishedWork=b;Ub();jb=aa;Xg(a,c)}function wi(a,b){a.expirationTime=b;a.finishedWork=null}function ka(){if(w)return jb;Qc();if(0===C||1===C)Ub(),jb=aa;return jb}function Kc(a,b){null===a.nextScheduledRoot?(a.expirationTime=b,null===I?(ba=I=a,a.nextScheduledRoot=a):(I=I.nextScheduledRoot=a,I.nextScheduledRoot=ba)):b>a.expirationTime&&(a.expirationTime=
|
||||
b);w||(z?Rc&&(ca=a,C=1073741823,Sc(a,1073741823,!1)):1073741823===b?Z(1073741823,!1):Ug(a,b))}function Qc(){var a=0,b=null;if(null!==I)for(var c=I,d=ba;null!==d;){var e=d.expirationTime;if(0===e){null===c||null===I?n("244"):void 0;if(d===d.nextScheduledRoot){ba=I=d.nextScheduledRoot=null;break}else if(d===ba)ba=e=d.nextScheduledRoot,I.nextScheduledRoot=e,d.nextScheduledRoot=null;else if(d===I){I=c;I.nextScheduledRoot=ba;d.nextScheduledRoot=null;break}else c.nextScheduledRoot=d.nextScheduledRoot,d.nextScheduledRoot=
|
||||
null;d=c.nextScheduledRoot}else{e>a&&(a=e,b=d);if(d===I)break;if(1073741823===a)break;c=d;d=d.nextScheduledRoot}}ca=b;C=a}function Nc(){return Tc?!0:Gi()?Tc=!0:!1}function Di(){try{if(!Nc()&&null!==ba){Ub();var a=ba;do{var b=a.expirationTime;0!==b&&aa<=b&&(a.nextExpirationTimeToWorkOn=aa);a=a.nextScheduledRoot}while(a!==ba)}Z(0,!0)}finally{Tc=!1}}function Z(a,b){Qc();if(b)for(Ub(),jb=aa;null!==ca&&0!==C&&a<=C&&!(Tc&&aa>C);)Sc(ca,C,aa>C),Qc(),Ub(),jb=aa;else for(;null!==ca&&0!==C&&a<=C;)Sc(ca,C,!1),
|
||||
Qc();b&&(Oc=0,Pc=null);0!==C&&Ug(ca,C);Tb=0;we=null;if(null!==kb)for(a=kb,kb=null,b=0;b<a.length;b++){var c=a[b];try{c._onComplete()}catch(d){lb||(lb=!0,Uc=d)}}if(lb)throw a=Uc,Uc=null,lb=!1,a;}function Xg(a,b){w?n("253"):void 0;ca=a;C=b;Sc(a,b,!1);Z(1073741823,!1)}function Sc(a,b,c){w?n("245"):void 0;w=!0;if(c){var d=a.finishedWork;null!==d?Vc(a,d,b):(a.finishedWork=null,d=a.timeoutHandle,-1!==d&&(a.timeoutHandle=-1,Yg(d)),Rg(a,c),d=a.finishedWork,null!==d&&(Nc()?a.finishedWork=d:Vc(a,d,b)))}else d=
|
||||
a.finishedWork,null!==d?Vc(a,d,b):(a.finishedWork=null,d=a.timeoutHandle,-1!==d&&(a.timeoutHandle=-1,Yg(d)),Rg(a,c),d=a.finishedWork,null!==d&&Vc(a,d,b));w=!1}function Vc(a,b,c){var d=a.firstBatch;if(null!==d&&d._expirationTime>=c&&(null===kb?kb=[d]:kb.push(d),d._defer)){a.finishedWork=b;a.expirationTime=0;return}a.finishedWork=null;a===we?Tb++:(we=a,Tb=0);Mc(se,function(){ui(a,b)})}function ie(a){null===ca?n("246"):void 0;ca.expirationTime=0;lb||(lb=!0,Uc=a)}function Zg(a,b){var c=z;z=!0;try{return a(b)}finally{(z=
|
||||
c)||w||Z(1073741823,!1)}}function $g(a,b){if(z&&!Rc){Rc=!0;try{return a(b)}finally{Rc=!1}}return a(b)}function ah(a,b,c){z||w||0===oa||(Z(oa,!1),oa=0);var d=z;z=!0;try{return Mc(te,function(){return a(b,c)})}finally{(z=d)||w||Z(1073741823,!1)}}function bh(a,b,c,d,e){var f=b.current;a:if(c){c=c._reactInternalFiber;b:{2===wb(c)&&1===c.tag?void 0:n("170");var g=c;do{switch(g.tag){case 3:g=g.stateNode.context;break b;case 1:if(E(g.type)){g=g.stateNode.__reactInternalMemoizedMergedChildContext;break b}}g=
|
||||
g.return}while(null!==g);n("171");g=void 0}if(1===c.tag){var h=c.type;if(E(h)){c=Rf(c,h,g);break a}}c=g}else c=va;null===b.context?b.context=c:b.pendingContext=c;b=e;e=Aa(d);e.payload={element:a};b=void 0===b?null:b;null!==b&&(e.callback=b);eb();na(f,e);ya(f,d);return d}function xe(a,b,c,d){var e=b.current,f=ka();e=fb(f,e);return bh(a,b,c,e,d)}function ye(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function Hi(a,b,c){var d=
|
||||
3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:Va,key:null==d?null:""+d,children:a,containerInfo:b,implementation:c}}function Vb(a){var b=1073741822-25*(((1073741822-ka()+500)/25|0)+1);b>=ze&&(b=ze-1);this._expirationTime=ze=b;this._root=a;this._callbacks=this._next=null;this._hasChildren=this._didComplete=!1;this._children=null;this._defer=!0}function mb(){this._callbacks=null;this._didCommit=!1;this._onCommit=this._onCommit.bind(this)}function nb(a,b,c){b=S(3,null,null,
|
||||
b?3:0);a={current:b,containerInfo:a,pendingChildren:null,pingCache:null,earliestPendingTime:0,latestPendingTime:0,earliestSuspendedTime:0,latestSuspendedTime:0,latestPingedTime:0,didError:!1,pendingCommitExpirationTime:0,finishedWork:null,timeoutHandle:-1,context:null,pendingContext:null,hydrate:c,nextExpirationTimeToWorkOn:0,expirationTime:0,firstBatch:null,nextScheduledRoot:null};this._internalRoot=b.stateNode=a}function ob(a){return!(!a||1!==a.nodeType&&9!==a.nodeType&&11!==a.nodeType&&(8!==a.nodeType||
|
||||
" react-mount-point-unstable "!==a.nodeValue))}function Ii(a,b){b||(b=a?9===a.nodeType?a.documentElement:a.firstChild:null,b=!(!b||1!==b.nodeType||!b.hasAttribute("data-reactroot")));if(!b)for(var c;c=a.lastChild;)a.removeChild(c);return new nb(a,!1,b)}function Wc(a,b,c,d,e){var f=c._reactRootContainer;if(f){if("function"===typeof e){var g=e;e=function(){var a=ye(f._internalRoot);g.call(a)}}null!=a?f.legacy_renderSubtreeIntoContainer(a,b,e):f.render(b,e)}else{f=c._reactRootContainer=Ii(c,d);if("function"===
|
||||
typeof e){var h=e;e=function(){var a=ye(f._internalRoot);h.call(a)}}$g(function(){null!=a?f.legacy_renderSubtreeIntoContainer(a,b,e):f.render(b,e)})}return ye(f._internalRoot)}function ch(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;ob(b)?void 0:n("200");return Hi(a,b,null,c)}da?void 0:n("227");var sh=function(a,b,c,d,e,f,g,h,l){var k=Array.prototype.slice.call(arguments,3);try{b.apply(c,k)}catch(m){this.onError(m)}},qb=!1,$b=null,ac=!1,Yc=null,th={onError:function(a){qb=
|
||||
!0;$b=a}},bc=null,Na={},cc=[],Zc={},Oa={},$c={},bd=null,Ue=null,He=null,rb=null,vh=function(a){if(a){var b=a._dispatchListeners,c=a._dispatchInstances;if(Array.isArray(b))for(var d=0;d<b.length&&!a.isPropagationStopped();d++)Ge(a,b[d],c[d]);else b&&Ge(a,b,c);a._dispatchListeners=null;a._dispatchInstances=null;a.isPersistent()||a.constructor.release(a)}},Ae={injectEventPluginOrder:function(a){bc?n("101"):void 0;bc=Array.prototype.slice.call(a);Ee()},injectEventPluginsByName:function(a){var b=!1,c;
|
||||
for(c in a)if(a.hasOwnProperty(c)){var d=a[c];Na.hasOwnProperty(c)&&Na[c]===d||(Na[c]?n("102",c):void 0,Na[c]=d,b=!0)}b&&Ee()}},dh=Math.random().toString(36).slice(2),ea="__reactInternalInstance$"+dh,ec="__reactEventHandlers$"+dh,ra=!("undefined"===typeof window||!window.document||!window.document.createElement),Ra={animationend:fc("Animation","AnimationEnd"),animationiteration:fc("Animation","AnimationIteration"),animationstart:fc("Animation","AnimationStart"),transitionend:fc("Transition","TransitionEnd")},
|
||||
fd={},Le={};ra&&(Le=document.createElement("div").style,"AnimationEvent"in window||(delete Ra.animationend.animation,delete Ra.animationiteration.animation,delete Ra.animationstart.animation),"TransitionEvent"in window||delete Ra.transitionend.transition);var eh=gc("animationend"),fh=gc("animationiteration"),gh=gc("animationstart"),hh=gc("transitionend"),zb="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),
|
||||
qa=null,gd=null,hc=null,B=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.assign;B(J.prototype,{preventDefault:function(){this.defaultPrevented=!0;var a=this.nativeEvent;a&&(a.preventDefault?a.preventDefault():"unknown"!==typeof a.returnValue&&(a.returnValue=!1),this.isDefaultPrevented=ic)},stopPropagation:function(){var a=this.nativeEvent;a&&(a.stopPropagation?a.stopPropagation():"unknown"!==typeof a.cancelBubble&&(a.cancelBubble=!0),this.isPropagationStopped=ic)},persist:function(){this.isPersistent=
|
||||
ic},isPersistent:jc,destructor:function(){var a=this.constructor.Interface,b;for(b in a)this[b]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null;this.isPropagationStopped=this.isDefaultPrevented=jc;this._dispatchInstances=this._dispatchListeners=null}});J.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(a){return a.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null};J.extend=function(a){function b(){return c.apply(this,
|
||||
arguments)}var c=this,d=function(){};d.prototype=c.prototype;d=new d;B(d,b.prototype);b.prototype=d;b.prototype.constructor=b;b.Interface=B({},c.Interface,a);b.extend=c.extend;Ne(b);return b};Ne(J);var Ji=J.extend({data:null}),Ki=J.extend({data:null}),Ah=[9,13,27,32],hd=ra&&"CompositionEvent"in window,Wb=null;ra&&"documentMode"in document&&(Wb=document.documentMode);var Li=ra&&"TextEvent"in window&&!Wb,Se=ra&&(!hd||Wb&&8<Wb&&11>=Wb),Re=String.fromCharCode(32),pa={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",
|
||||
captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",
|
||||
captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},Qe=!1,Sa=!1,Mi={eventTypes:pa,extractEvents:function(a,b,c,d){var e=void 0;var f=void 0;if(hd)b:{switch(a){case "compositionstart":e=pa.compositionStart;break b;case "compositionend":e=pa.compositionEnd;break b;case "compositionupdate":e=pa.compositionUpdate;break b}e=void 0}else Sa?Oe(a,c)&&(e=pa.compositionEnd):"keydown"===a&&229===c.keyCode&&(e=pa.compositionStart);e?(Se&&
|
||||
"ko"!==c.locale&&(Sa||e!==pa.compositionStart?e===pa.compositionEnd&&Sa&&(f=Me()):(qa=d,gd="value"in qa?qa.value:qa.textContent,Sa=!0)),e=Ji.getPooled(e,b,c,d),f?e.data=f:(f=Pe(c),null!==f&&(e.data=f)),Qa(e),f=e):f=null;(a=Li?Bh(a,c):Ch(a,c))?(b=Ki.getPooled(pa.beforeInput,b,c,d),b.data=a,Qa(b)):b=null;return null===f?b:null===b?f:[f,b]}},id=null,Ta=null,Ua=null,Ye=function(a,b){return a(b)},yf=function(a,b,c){return a(b,c)},Ze=function(){},jd=!1,Dh={color:!0,date:!0,datetime:!0,"datetime-local":!0,
|
||||
email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},Ma=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;Ma.hasOwnProperty("ReactCurrentDispatcher")||(Ma.ReactCurrentDispatcher={current:null});var Fh=/^(.*)[\\\/]/,O="function"===typeof Symbol&&Symbol.for,Bc=O?Symbol.for("react.element"):60103,Va=O?Symbol.for("react.portal"):60106,ta=O?Symbol.for("react.fragment"):60107,md=O?Symbol.for("react.strict_mode"):60108,lc=O?Symbol.for("react.profiler"):60114,
|
||||
ff=O?Symbol.for("react.provider"):60109,ef=O?Symbol.for("react.context"):60110,ld=O?Symbol.for("react.concurrent_mode"):60111,od=O?Symbol.for("react.forward_ref"):60112,nd=O?Symbol.for("react.suspense"):60113,pd=O?Symbol.for("react.memo"):60115,gf=O?Symbol.for("react.lazy"):60116,df="function"===typeof Symbol&&Symbol.iterator,Hh=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,
|
||||
hf=Object.prototype.hasOwnProperty,kf={},jf={},A={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){A[a]=new K(a,0,!1,a,null)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];A[b]=new K(b,1,!1,a[1],null)});["contentEditable","draggable","spellCheck","value"].forEach(function(a){A[a]=new K(a,2,!1,
|
||||
a.toLowerCase(),null)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){A[a]=new K(a,2,!1,a,null)});"allowFullScreen async autoFocus autoPlay controls default defer disabled formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){A[a]=new K(a,3,!1,a.toLowerCase(),null)});["checked","multiple","muted","selected"].forEach(function(a){A[a]=new K(a,3,!0,a,null)});["capture",
|
||||
"download"].forEach(function(a){A[a]=new K(a,4,!1,a,null)});["cols","rows","size","span"].forEach(function(a){A[a]=new K(a,6,!1,a,null)});["rowSpan","start"].forEach(function(a){A[a]=new K(a,5,!1,a.toLowerCase(),null)});var Be=/[\-:]([a-z])/g,Ce=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=
|
||||
a.replace(Be,Ce);A[b]=new K(b,1,!1,a,null)});"xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(Be,Ce);A[b]=new K(b,1,!1,a,"http://www.w3.org/1999/xlink")});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(Be,Ce);A[b]=new K(b,1,!1,a,"http://www.w3.org/XML/1998/namespace")});["tabIndex","crossOrigin"].forEach(function(a){A[a]=new K(a,1,!1,a.toLowerCase(),null)});var pf={change:{phasedRegistrationNames:{bubbled:"onChange",
|
||||
captured:"onChangeCapture"},dependencies:"blur change click focus input keydown keyup selectionchange".split(" ")}},tb=null,ub=null,De=!1;ra&&(De=af("input")&&(!document.documentMode||9<document.documentMode));var Ni={eventTypes:pf,_isInputEventSupported:De,extractEvents:function(a,b,c,d){var e=b?Da(b):window,f=void 0,g=void 0,h=e.nodeName&&e.nodeName.toLowerCase();"select"===h||"input"===h&&"file"===e.type?f=Lh:$e(e)?De?f=Ph:(f=Nh,g=Mh):(h=e.nodeName)&&"input"===h.toLowerCase()&&("checkbox"===e.type||
|
||||
"radio"===e.type)&&(f=Oh);if(f&&(f=f(a,b)))return of(f,c,d);g&&g(a,e,b);"blur"===a&&(a=e._wrapperState)&&a.controlled&&"number"===e.type&&ud(e,"number",e.value)}},Xb=J.extend({view:null,detail:null}),Rh={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"},ih=0,jh=0,kh=!1,lh=!1,Yb=Xb.extend({screenX:null,screenY:null,clientX:null,clientY:null,pageX:null,pageY:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,getModifierState:vd,button:null,buttons:null,relatedTarget:function(a){return a.relatedTarget||
|
||||
(a.fromElement===a.srcElement?a.toElement:a.fromElement)},movementX:function(a){if("movementX"in a)return a.movementX;var b=ih;ih=a.screenX;return kh?"mousemove"===a.type?a.screenX-b:0:(kh=!0,0)},movementY:function(a){if("movementY"in a)return a.movementY;var b=jh;jh=a.screenY;return lh?"mousemove"===a.type?a.screenY-b:0:(lh=!0,0)}}),mh=Yb.extend({pointerId:null,width:null,height:null,pressure:null,tangentialPressure:null,tiltX:null,tiltY:null,twist:null,pointerType:null,isPrimary:null}),Zb={mouseEnter:{registrationName:"onMouseEnter",
|
||||
dependencies:["mouseout","mouseover"]},mouseLeave:{registrationName:"onMouseLeave",dependencies:["mouseout","mouseover"]},pointerEnter:{registrationName:"onPointerEnter",dependencies:["pointerout","pointerover"]},pointerLeave:{registrationName:"onPointerLeave",dependencies:["pointerout","pointerover"]}},Oi={eventTypes:Zb,extractEvents:function(a,b,c,d){var e="mouseover"===a||"pointerover"===a,f="mouseout"===a||"pointerout"===a;if(e&&(c.relatedTarget||c.fromElement)||!f&&!e)return null;e=d.window===
|
||||
d?d:(e=d.ownerDocument)?e.defaultView||e.parentWindow:window;f?(f=b,b=(b=c.relatedTarget||c.toElement)?dc(b):null):f=null;if(f===b)return null;var g=void 0,h=void 0,l=void 0,k=void 0;if("mouseout"===a||"mouseover"===a)g=Yb,h=Zb.mouseLeave,l=Zb.mouseEnter,k="mouse";else if("pointerout"===a||"pointerover"===a)g=mh,h=Zb.pointerLeave,l=Zb.pointerEnter,k="pointer";var m=null==f?e:Da(f);e=null==b?e:Da(b);a=g.getPooled(h,f,c,d);a.type=k+"leave";a.target=m;a.relatedTarget=e;c=g.getPooled(l,b,c,d);c.type=
|
||||
k+"enter";c.target=e;c.relatedTarget=m;d=b;if(f&&d)a:{b=f;e=d;k=0;for(g=b;g;g=fa(g))k++;g=0;for(l=e;l;l=fa(l))g++;for(;0<k-g;)b=fa(b),k--;for(;0<g-k;)e=fa(e),g--;for(;k--;){if(b===e||b===e.alternate)break a;b=fa(b);e=fa(e)}b=null}else b=null;e=b;for(b=[];f&&f!==e;){k=f.alternate;if(null!==k&&k===e)break;b.push(f);f=fa(f)}for(f=[];d&&d!==e;){k=d.alternate;if(null!==k&&k===e)break;f.push(d);d=fa(d)}for(d=0;d<b.length;d++)ed(b[d],"bubbled",a);for(d=f.length;0<d--;)ed(f[d],"captured",c);return[a,c]}},
|
||||
Sh=Object.prototype.hasOwnProperty,Pi=J.extend({animationName:null,elapsedTime:null,pseudoElement:null}),Qi=J.extend({clipboardData:function(a){return"clipboardData"in a?a.clipboardData:window.clipboardData}}),Ri=Xb.extend({relatedTarget:null}),Si={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},Ti={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",
|
||||
16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},Ui=Xb.extend({key:function(a){if(a.key){var b=Si[a.key]||a.key;if("Unidentified"!==b)return b}return"keypress"===a.type?(a=nc(a),13===a?"Enter":
|
||||
String.fromCharCode(a)):"keydown"===a.type||"keyup"===a.type?Ti[a.keyCode]||"Unidentified":""},location:null,ctrlKey:null,shiftKey:null,altKey:null,metaKey:null,repeat:null,locale:null,getModifierState:vd,charCode:function(a){return"keypress"===a.type?nc(a):0},keyCode:function(a){return"keydown"===a.type||"keyup"===a.type?a.keyCode:0},which:function(a){return"keypress"===a.type?nc(a):"keydown"===a.type||"keyup"===a.type?a.keyCode:0}}),Vi=Yb.extend({dataTransfer:null}),Wi=Xb.extend({touches:null,targetTouches:null,
|
||||
changedTouches:null,altKey:null,metaKey:null,ctrlKey:null,shiftKey:null,getModifierState:vd}),Xi=J.extend({propertyName:null,elapsedTime:null,pseudoElement:null}),Yi=Yb.extend({deltaX:function(a){return"deltaX"in a?a.deltaX:"wheelDeltaX"in a?-a.wheelDeltaX:0},deltaY:function(a){return"deltaY"in a?a.deltaY:"wheelDeltaY"in a?-a.wheelDeltaY:"wheelDelta"in a?-a.wheelDelta:0},deltaZ:null,deltaMode:null}),Zi=[["abort","abort"],[eh,"animationEnd"],[fh,"animationIteration"],[gh,"animationStart"],["canplay",
|
||||
"canPlay"],["canplaythrough","canPlayThrough"],["drag","drag"],["dragenter","dragEnter"],["dragexit","dragExit"],["dragleave","dragLeave"],["dragover","dragOver"],["durationchange","durationChange"],["emptied","emptied"],["encrypted","encrypted"],["ended","ended"],["error","error"],["gotpointercapture","gotPointerCapture"],["load","load"],["loadeddata","loadedData"],["loadedmetadata","loadedMetadata"],["loadstart","loadStart"],["lostpointercapture","lostPointerCapture"],["mousemove","mouseMove"],
|
||||
["mouseout","mouseOut"],["mouseover","mouseOver"],["playing","playing"],["pointermove","pointerMove"],["pointerout","pointerOut"],["pointerover","pointerOver"],["progress","progress"],["scroll","scroll"],["seeking","seeking"],["stalled","stalled"],["suspend","suspend"],["timeupdate","timeUpdate"],["toggle","toggle"],["touchmove","touchMove"],[hh,"transitionEnd"],["waiting","waiting"],["wheel","wheel"]],vf={},wd={};[["blur","blur"],["cancel","cancel"],["click","click"],["close","close"],["contextmenu",
|
||||
"contextMenu"],["copy","copy"],["cut","cut"],["auxclick","auxClick"],["dblclick","doubleClick"],["dragend","dragEnd"],["dragstart","dragStart"],["drop","drop"],["focus","focus"],["input","input"],["invalid","invalid"],["keydown","keyDown"],["keypress","keyPress"],["keyup","keyUp"],["mousedown","mouseDown"],["mouseup","mouseUp"],["paste","paste"],["pause","pause"],["play","play"],["pointercancel","pointerCancel"],["pointerdown","pointerDown"],["pointerup","pointerUp"],["ratechange","rateChange"],["reset",
|
||||
"reset"],["seeked","seeked"],["submit","submit"],["touchcancel","touchCancel"],["touchend","touchEnd"],["touchstart","touchStart"],["volumechange","volumeChange"]].forEach(function(a){uf(a,!0)});Zi.forEach(function(a){uf(a,!1)});var nh={eventTypes:vf,isInteractiveTopLevelEventType:function(a){a=wd[a];return void 0!==a&&!0===a.isInteractive},extractEvents:function(a,b,c,d){var e=wd[a];if(!e)return null;switch(a){case "keypress":if(0===nc(c))return null;case "keydown":case "keyup":a=Ui;break;case "blur":case "focus":a=
|
||||
Ri;break;case "click":if(2===c.button)return null;case "auxclick":case "dblclick":case "mousedown":case "mousemove":case "mouseup":case "mouseout":case "mouseover":case "contextmenu":a=Yb;break;case "drag":case "dragend":case "dragenter":case "dragexit":case "dragleave":case "dragover":case "dragstart":case "drop":a=Vi;break;case "touchcancel":case "touchend":case "touchmove":case "touchstart":a=Wi;break;case eh:case fh:case gh:a=Pi;break;case hh:a=Xi;break;case "scroll":a=Xb;break;case "wheel":a=
|
||||
Yi;break;case "copy":case "cut":case "paste":a=Qi;break;case "gotpointercapture":case "lostpointercapture":case "pointercancel":case "pointerdown":case "pointermove":case "pointerout":case "pointerover":case "pointerup":a=mh;break;default:a=J}b=a.getPooled(e,b,c,d);Qa(b);return b}},wf=nh.isInteractiveTopLevelEventType,rc=[],qc=!0,Af={},Vh=0,sc="_reactListenersID"+(""+Math.random()).slice(2),$i=ra&&"documentMode"in document&&11>=document.documentMode,Hf={select:{phasedRegistrationNames:{bubbled:"onSelect",
|
||||
captured:"onSelectCapture"},dependencies:"blur contextmenu dragend focus keydown keyup mousedown mouseup selectionchange".split(" ")}},Wa=null,Ad=null,xb=null,zd=!1,aj={eventTypes:Hf,extractEvents:function(a,b,c,d){var e=d.window===d?d.document:9===d.nodeType?d:d.ownerDocument,f;if(!(f=!e)){a:{e=zf(e);f=$c.onSelect;for(var g=0;g<f.length;g++){var h=f[g];if(!e.hasOwnProperty(h)||!e[h]){e=!1;break a}}e=!0}f=!e}if(f)return null;e=b?Da(b):window;switch(a){case "focus":if($e(e)||"true"===e.contentEditable)Wa=
|
||||
e,Ad=b,xb=null;break;case "blur":xb=Ad=Wa=null;break;case "mousedown":zd=!0;break;case "contextmenu":case "mouseup":case "dragend":return zd=!1,Gf(c,d);case "selectionchange":if($i)break;case "keydown":case "keyup":return Gf(c,d)}return null}};Ae.injectEventPluginOrder("ResponderEventPlugin SimpleEventPlugin EnterLeaveEventPlugin ChangeEventPlugin SelectEventPlugin BeforeInputEventPlugin".split(" "));(function(a,b,c){bd=a;Ue=b;He=c})(dd,Je,Da);Ae.injectEventPluginsByName({SimpleEventPlugin:nh,EnterLeaveEventPlugin:Oi,
|
||||
ChangeEventPlugin:Ni,SelectEventPlugin:aj,BeforeInputEventPlugin:Mi});var Xc=void 0,Of=function(a){return"undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)})}:a}(function(a,b){if("http://www.w3.org/2000/svg"!==a.namespaceURI||"innerHTML"in a)a.innerHTML=b;else{Xc=Xc||document.createElement("div");Xc.innerHTML="<svg>"+b+"</svg>";for(b=Xc.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild)}}),
|
||||
Ab=function(a,b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b},yb={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,
|
||||
lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},bj=["Webkit","ms","Moz","O"];Object.keys(yb).forEach(function(a){bj.forEach(function(b){b=b+a.charAt(0).toUpperCase()+a.substring(1);yb[b]=yb[a]})});var Zh=B({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,
|
||||
source:!0,track:!0,wbr:!0}),R=da.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler,Vg=R.unstable_cancelCallback,ue=R.unstable_now,Wg=R.unstable_scheduleCallback,Gi=R.unstable_shouldYield,Mc=R.unstable_runWithPriority,zi=R.unstable_getCurrentPriorityLevel,se=R.unstable_ImmediatePriority,te=R.unstable_UserBlockingPriority,Lg=R.unstable_NormalPriority,Ai=R.unstable_LowPriority,Bi=R.unstable_IdlePriority,ne=null,oe=null,Ei="function"===typeof setTimeout?setTimeout:void 0,Yg="function"===typeof clearTimeout?
|
||||
clearTimeout:void 0,vi=Wg,ti=Vg;new Set;var Id=[],Ya=-1,va={},F={current:va},M={current:!1},Fa=va,Kd=null,Ld=null,S=function(a,b,c,d){return new bi(a,b,c,d)},$f=(new da.Component).refs,zc={isMounted:function(a){return(a=a._reactInternalFiber)?2===wb(a):!1},enqueueSetState:function(a,b,c){a=a._reactInternalFiber;var d=ka();d=fb(d,a);var e=Aa(d);e.payload=b;void 0!==c&&null!==c&&(e.callback=c);eb();na(a,e);ya(a,d)},enqueueReplaceState:function(a,b,c){a=a._reactInternalFiber;var d=ka();d=fb(d,a);var e=
|
||||
Aa(d);e.tag=yg;e.payload=b;void 0!==c&&null!==c&&(e.callback=c);eb();na(a,e);ya(a,d)},enqueueForceUpdate:function(a,b){a=a._reactInternalFiber;var c=ka();c=fb(c,a);var d=Aa(c);d.tag=Ec;void 0!==b&&null!==b&&(d.callback=b);eb();na(a,d);ya(a,c)}},Cc=Array.isArray,hb=ag(!0),ae=ag(!1),Eb={},U={current:Eb},Gb={current:Eb},Fb={current:Eb},db=0,pi=2,Rb=4,ji=8,ri=16,Sb=32,me=64,le=128,Dc=Ma.ReactCurrentDispatcher,Hb=0,xa=null,y=null,W=null,bb=null,G=null,ab=null,Kb=0,X=null,Lb=0,Ib=!1,ia=null,Jb=0,Ud={readContext:T,
|
||||
useCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useLayoutEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V},fi={readContext:T,useCallback:function(a,b){cb().memoizedState=[a,void 0===b?null:b];return a},useContext:T,useEffect:function(a,b){return Xd(516,le|me,a,b)},useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Xd(4,Rb|Sb,fg.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Xd(4,Rb|Sb,a,b)},useMemo:function(a,b){var c=cb();
|
||||
b=void 0===b?null:b;a=a();c.memoizedState=[a,b];return a},useReducer:function(a,b,c){var d=cb();b=void 0!==c?c(b):b;d.memoizedState=d.baseState=b;a=d.queue={last:null,dispatch:null,lastRenderedReducer:a,lastRenderedState:b};a=a.dispatch=hg.bind(null,xa,a);return[d.memoizedState,a]},useRef:function(a){var b=cb();a={current:a};return b.memoizedState=a},useState:function(a){var b=cb();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a=b.queue={last:null,dispatch:null,lastRenderedReducer:dg,
|
||||
lastRenderedState:a};a=a.dispatch=hg.bind(null,xa,a);return[b.memoizedState,a]},useDebugValue:gg},cg={readContext:T,useCallback:function(a,b){var c=Mb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&Sd(b,d[1]))return d[0];c.memoizedState=[a,b];return a},useContext:T,useEffect:function(a,b){return Yd(516,le|me,a,b)},useImperativeHandle:function(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Yd(4,Rb|Sb,fg.bind(null,b,a),c)},useLayoutEffect:function(a,b){return Yd(4,Rb|Sb,
|
||||
a,b)},useMemo:function(a,b){var c=Mb();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&Sd(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a},useReducer:eg,useRef:function(a){return Mb().memoizedState},useState:function(a){return eg(dg,a)},useDebugValue:gg},la=null,gb=null,Ia=!1,gi=Ma.ReactCurrentOwner,ja=!1,de={current:null},Nb=null,Ja=null,Ob=null,ug=0,yg=1,Ec=2,ge=3,za=!1,Og=void 0,pe=void 0,Ng=void 0,Pg=void 0;Og=function(a,b,c,d){for(c=b.child;null!==c;){if(5===c.tag||
|
||||
6===c.tag)a.appendChild(c.stateNode);else if(4!==c.tag&&null!==c.child){c.child.return=c;c=c.child;continue}if(c===b)break;for(;null===c.sibling;){if(null===c.return||c.return===b)return;c=c.return}c.sibling.return=c.return;c=c.sibling}};pe=function(a){};Ng=function(a,b,c,d,e){var f=a.memoizedProps;if(f!==d){var g=b.stateNode;Ha(U.current);a=null;switch(c){case "input":f=sd(g,f);d=sd(g,d);a=[];break;case "option":f=Bd(g,f);d=Bd(g,d);a=[];break;case "select":f=B({},f,{value:void 0});d=B({},d,{value:void 0});
|
||||
a=[];break;case "textarea":f=Cd(g,f);d=Cd(g,d);a=[];break;default:"function"!==typeof f.onClick&&"function"===typeof d.onClick&&(g.onclick=tc)}Ed(c,d);g=c=void 0;var h=null;for(c in f)if(!d.hasOwnProperty(c)&&f.hasOwnProperty(c)&&null!=f[c])if("style"===c){var l=f[c];for(g in l)l.hasOwnProperty(g)&&(h||(h={}),h[g]="")}else"dangerouslySetInnerHTML"!==c&&"children"!==c&&"suppressContentEditableWarning"!==c&&"suppressHydrationWarning"!==c&&"autoFocus"!==c&&(Oa.hasOwnProperty(c)?a||(a=[]):(a=a||[]).push(c,
|
||||
null));for(c in d){var k=d[c];l=null!=f?f[c]:void 0;if(d.hasOwnProperty(c)&&k!==l&&(null!=k||null!=l))if("style"===c)if(l){for(g in l)!l.hasOwnProperty(g)||k&&k.hasOwnProperty(g)||(h||(h={}),h[g]="");for(g in k)k.hasOwnProperty(g)&&l[g]!==k[g]&&(h||(h={}),h[g]=k[g])}else h||(a||(a=[]),a.push(c,h)),h=k;else"dangerouslySetInnerHTML"===c?(k=k?k.__html:void 0,l=l?l.__html:void 0,null!=k&&l!==k&&(a=a||[]).push(c,""+k)):"children"===c?l===k||"string"!==typeof k&&"number"!==typeof k||(a=a||[]).push(c,""+
|
||||
k):"suppressContentEditableWarning"!==c&&"suppressHydrationWarning"!==c&&(Oa.hasOwnProperty(c)?(null!=k&&ha(e,c),a||l===k||(a=[])):(a=a||[]).push(c,k))}h&&(a=a||[]).push("style",h);e=a;(b.updateQueue=e)&&Pb(b)}};Pg=function(a,b,c,d){c!==d&&Pb(b)};var ki="function"===typeof WeakSet?WeakSet:Set,xi="function"===typeof WeakMap?WeakMap:Map,qe=Ma.ReactCurrentDispatcher,Kg=Ma.ReactCurrentOwner,ze=1073741822,Ca=!1,x=null,Y=null,H=0,La=-1,je=!1,p=null,Lc=!1,ke=null,Jc=null,Ic=null,Ba=null,ba=null,I=null,Oc=
|
||||
0,Pc=void 0,w=!1,ca=null,C=0,oa=0,lb=!1,Uc=null,z=!1,Rc=!1,kb=null,ve=ue(),aa=1073741822-(ve/10|0),jb=aa,Ci=50,Tb=0,we=null,Tc=!1;id=function(a,b,c){switch(b){case "input":td(a,c);b=c.name;if("radio"===c.type&&null!=b){for(c=a;c.parentNode;)c=c.parentNode;c=c.querySelectorAll("input[name="+JSON.stringify(""+b)+'][type="radio"]');for(b=0;b<c.length;b++){var d=c[b];if(d!==a&&d.form===a.form){var e=dd(d);e?void 0:n("90");cf(d);td(d,e)}}}break;case "textarea":Jf(a,c);break;case "select":b=c.value,null!=
|
||||
b&&Xa(a,!!c.multiple,b,!1)}};Vb.prototype.render=function(a){this._defer?void 0:n("250");this._hasChildren=!0;this._children=a;var b=this._root._internalRoot,c=this._expirationTime,d=new mb;bh(a,b,null,c,d._onCommit);return d};Vb.prototype.then=function(a){if(this._didComplete)a();else{var b=this._callbacks;null===b&&(b=this._callbacks=[]);b.push(a)}};Vb.prototype.commit=function(){var a=this._root._internalRoot,b=a.firstBatch;this._defer&&null!==b?void 0:n("251");if(this._hasChildren){var c=this._expirationTime;
|
||||
if(b!==this){this._hasChildren&&(c=this._expirationTime=b._expirationTime,this.render(this._children));for(var d=null,e=b;e!==this;)d=e,e=e._next;null===d?n("251"):void 0;d._next=e._next;this._next=b;a.firstBatch=this}this._defer=!1;Xg(a,c);b=this._next;this._next=null;b=a.firstBatch=b;null!==b&&b._hasChildren&&b.render(b._children)}else this._next=null,this._defer=!1};Vb.prototype._onComplete=function(){if(!this._didComplete){this._didComplete=!0;var a=this._callbacks;if(null!==a)for(var b=0;b<a.length;b++)(0,a[b])()}};
|
||||
mb.prototype.then=function(a){if(this._didCommit)a();else{var b=this._callbacks;null===b&&(b=this._callbacks=[]);b.push(a)}};mb.prototype._onCommit=function(){if(!this._didCommit){this._didCommit=!0;var a=this._callbacks;if(null!==a)for(var b=0;b<a.length;b++){var c=a[b];"function"!==typeof c?n("191",c):void 0;c()}}};nb.prototype.render=function(a,b){var c=this._internalRoot,d=new mb;b=void 0===b?null:b;null!==b&&d.then(b);xe(a,c,null,d._onCommit);return d};nb.prototype.unmount=function(a){var b=
|
||||
this._internalRoot,c=new mb;a=void 0===a?null:a;null!==a&&c.then(a);xe(null,b,null,c._onCommit);return c};nb.prototype.legacy_renderSubtreeIntoContainer=function(a,b,c){var d=this._internalRoot,e=new mb;c=void 0===c?null:c;null!==c&&e.then(c);xe(b,d,a,e._onCommit);return e};nb.prototype.createBatch=function(){var a=new Vb(this),b=a._expirationTime,c=this._internalRoot,d=c.firstBatch;if(null===d)c.firstBatch=a,a._next=null;else{for(c=null;null!==d&&d._expirationTime>=b;)c=d,d=d._next;a._next=d;null!==
|
||||
c&&(c._next=a)}return a};(function(a,b,c){Ye=a;yf=b;Ze=c})(Zg,ah,function(){w||0===oa||(Z(oa,!1),oa=0)});var oh={createPortal:ch,findDOMNode:function(a){if(null==a)return null;if(1===a.nodeType)return a;var b=a._reactInternalFiber;void 0===b&&("function"===typeof a.render?n("188"):n("268",Object.keys(a)));a=tf(b);a=null===a?null:a.stateNode;return a},hydrate:function(a,b,c){ob(b)?void 0:n("200");return Wc(null,a,b,!0,c)},render:function(a,b,c){ob(b)?void 0:n("200");return Wc(null,a,b,!1,c)},unstable_renderSubtreeIntoContainer:function(a,
|
||||
b,c,d){ob(c)?void 0:n("200");null==a||void 0===a._reactInternalFiber?n("38"):void 0;return Wc(a,b,c,!1,d)},unmountComponentAtNode:function(a){ob(a)?void 0:n("40");return a._reactRootContainer?($g(function(){Wc(null,null,a,!1,function(){a._reactRootContainer=null})}),!0):!1},unstable_createPortal:function(){return ch.apply(void 0,arguments)},unstable_batchedUpdates:Zg,unstable_interactiveUpdates:ah,flushSync:function(a,b){w?n("187"):void 0;var c=z;z=!0;try{return Tg(a,b)}finally{z=c,Z(1073741823,!1)}},
|
||||
unstable_createRoot:function(a,b){ob(a)?void 0:n("299","unstable_createRoot");return new nb(a,!0,null!=b&&!0===b.hydrate)},unstable_flushControlled:function(a){var b=z;z=!0;try{Tg(a)}finally{(z=b)||w||Z(1073741823,!1)}},__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:{Events:[Je,Da,dd,Ae.injectEventPluginsByName,Zc,Qa,function(a){ad(a,xh)},Ve,We,oc,cd]}};(function(a){var b=a.findFiberByHostInstance;return ai(B({},a,{overrideProps:null,currentDispatcherRef:Ma.ReactCurrentDispatcher,findHostInstanceByFiber:function(a){a=
|
||||
tf(a);return null===a?null:a.stateNode},findFiberByHostInstance:function(a){return b?b(a):null}}))})({findFiberByHostInstance:dc,bundleType:0,version:"16.8.6",rendererPackageName:"react-dom"});var ph={default:oh},qh=ph&&oh||ph;return qh.default||qh});
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/** @license React v16.8.6
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
'use strict';(function(N,q){"object"===typeof exports&&"undefined"!==typeof module?module.exports=q():"function"===typeof define&&define.amd?define(q):N.React=q()})(this,function(){function N(a,b,d,g,p,c,e,h){if(!a){a=void 0;if(void 0===b)a=Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var n=[d,g,p,c,e,h],f=0;a=Error(b.replace(/%s/g,function(){return n[f++]}));a.name="Invariant Violation"}a.framesToPop=1;
|
||||
throw a;}}function q(a){for(var b=arguments.length-1,d="https://reactjs.org/docs/error-decoder.html?invariant="+a,g=0;g<b;g++)d+="&args[]="+encodeURIComponent(arguments[g+1]);N(!1,"Minified React error #"+a+"; visit %s for the full message or use the non-minified dev environment for full errors and additional helpful warnings. ",d)}function t(a,b,d){this.props=a;this.context=b;this.refs=ba;this.updater=d||ca}function da(){}function O(a,b,d){this.props=a;this.context=b;this.refs=ba;this.updater=d||
|
||||
ca}function u(){if(!x){var a=c.expirationTime;C?P():C=!0;D(ta,a)}}function Q(){var a=c,b=c.next;if(c===b)c=null;else{var d=c.previous;c=d.next=b;b.previous=d}a.next=a.previous=null;d=a.callback;b=a.expirationTime;a=a.priorityLevel;var g=f,p=E;f=a;E=b;try{var n=d()}finally{f=g,E=p}if("function"===typeof n)if(n={callback:n,priorityLevel:a,expirationTime:b,next:null,previous:null},null===c)c=n.next=n.previous=n;else{d=null;a=c;do{if(a.expirationTime>=b){d=a;break}a=a.next}while(a!==c);null===d?d=c:d===
|
||||
c&&(c=n,u());b=d.previous;b.next=d.previous=n;n.next=d;n.previous=b}}function F(){if(-1===k&&null!==c&&1===c.priorityLevel){x=!0;try{do Q();while(null!==c&&1===c.priorityLevel)}finally{x=!1,null!==c?u():C=!1}}}function ta(a){x=!0;var b=G;G=a;try{if(a)for(;null!==c;){var d=l();if(c.expirationTime<=d){do Q();while(null!==c&&c.expirationTime<=d)}else break}else if(null!==c){do Q();while(null!==c&&!H())}}finally{x=!1,G=b,null!==c?u():C=!1,F()}}function ea(a,b,d){var g=void 0,p={},c=null,e=null;if(null!=
|
||||
b)for(g in void 0!==b.ref&&(e=b.ref),void 0!==b.key&&(c=""+b.key),b)fa.call(b,g)&&!ha.hasOwnProperty(g)&&(p[g]=b[g]);var h=arguments.length-2;if(1===h)p.children=d;else if(1<h){for(var f=Array(h),k=0;k<h;k++)f[k]=arguments[k+2];p.children=f}if(a&&a.defaultProps)for(g in h=a.defaultProps,h)void 0===p[g]&&(p[g]=h[g]);return{$$typeof:y,type:a,key:c,ref:e,props:p,_owner:R.current}}function ua(a,b){return{$$typeof:y,type:a.type,key:b,ref:a.ref,props:a.props,_owner:a._owner}}function S(a){return"object"===
|
||||
typeof a&&null!==a&&a.$$typeof===y}function va(a){var b={"=":"=0",":":"=2"};return"$"+(""+a).replace(/[=:]/g,function(a){return b[a]})}function ia(a,b,d,g){if(I.length){var c=I.pop();c.result=a;c.keyPrefix=b;c.func=d;c.context=g;c.count=0;return c}return{result:a,keyPrefix:b,func:d,context:g,count:0}}function ja(a){a.result=null;a.keyPrefix=null;a.func=null;a.context=null;a.count=0;10>I.length&&I.push(a)}function T(a,b,d,g){var c=typeof a;if("undefined"===c||"boolean"===c)a=null;var e=!1;if(null===
|
||||
a)e=!0;else switch(c){case "string":case "number":e=!0;break;case "object":switch(a.$$typeof){case y:case wa:e=!0}}if(e)return d(g,a,""===b?"."+U(a,0):b),1;e=0;b=""===b?".":b+":";if(Array.isArray(a))for(var f=0;f<a.length;f++){c=a[f];var h=b+U(c,f);e+=T(c,h,d,g)}else if(null===a||"object"!==typeof a?h=null:(h=ka&&a[ka]||a["@@iterator"],h="function"===typeof h?h:null),"function"===typeof h)for(a=h.call(a),f=0;!(c=a.next()).done;)c=c.value,h=b+U(c,f++),e+=T(c,h,d,g);else"object"===c&&(d=""+a,q("31",
|
||||
"[object Object]"===d?"object with keys {"+Object.keys(a).join(", ")+"}":d,""));return e}function V(a,b,d){return null==a?0:T(a,"",b,d)}function U(a,b){return"object"===typeof a&&null!==a&&null!=a.key?va(a.key):b.toString(36)}function xa(a,b,d){a.func.call(a.context,b,a.count++)}function ya(a,b,d){var g=a.result,c=a.keyPrefix;a=a.func.call(a.context,b,a.count++);Array.isArray(a)?W(a,g,d,function(a){return a}):null!=a&&(S(a)&&(a=ua(a,c+(!a.key||b&&b.key===a.key?"":(""+a.key).replace(la,"$&/")+"/")+
|
||||
d)),g.push(a))}function W(a,b,d,g,c){var e="";null!=d&&(e=(""+d).replace(la,"$&/")+"/");b=ia(b,e,g,c);V(a,ya,b);ja(b)}function m(){var a=ma.current;null===a?q("321"):void 0;return a}var e="function"===typeof Symbol&&Symbol.for,y=e?Symbol.for("react.element"):60103,wa=e?Symbol.for("react.portal"):60106,r=e?Symbol.for("react.fragment"):60107,X=e?Symbol.for("react.strict_mode"):60108,za=e?Symbol.for("react.profiler"):60114,Aa=e?Symbol.for("react.provider"):60109,Ba=e?Symbol.for("react.context"):60110,
|
||||
Ca=e?Symbol.for("react.concurrent_mode"):60111,Da=e?Symbol.for("react.forward_ref"):60112,Ea=e?Symbol.for("react.suspense"):60113,Fa=e?Symbol.for("react.memo"):60115,Ga=e?Symbol.for("react.lazy"):60116,ka="function"===typeof Symbol&&Symbol.iterator,na=Object.getOwnPropertySymbols,Ha=Object.prototype.hasOwnProperty,Ia=Object.prototype.propertyIsEnumerable,J=function(){try{if(!Object.assign)return!1;var a=new String("abc");a[5]="de";if("5"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=
|
||||
0;10>a;a++)b["_"+String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var d={};"abcdefghijklmnopqrst".split("").forEach(function(a){d[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},d)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var d=Object(a);for(var c,e=1;e<arguments.length;e++){var f=Object(arguments[e]);
|
||||
for(var k in f)Ha.call(f,k)&&(d[k]=f[k]);if(na){c=na(f);for(var h=0;h<c.length;h++)Ia.call(f,c[h])&&(d[c[h]]=f[c[h]])}}return d},ca={isMounted:function(a){return!1},enqueueForceUpdate:function(a,b,d){},enqueueReplaceState:function(a,b,d,c){},enqueueSetState:function(a,b,d,c){}},ba={};t.prototype.isReactComponent={};t.prototype.setState=function(a,b){"object"!==typeof a&&"function"!==typeof a&&null!=a?q("85"):void 0;this.updater.enqueueSetState(this,a,b,"setState")};t.prototype.forceUpdate=function(a){this.updater.enqueueForceUpdate(this,
|
||||
a,"forceUpdate")};da.prototype=t.prototype;e=O.prototype=new da;e.constructor=O;J(e,t.prototype);e.isPureReactComponent=!0;var c=null,G=!1,f=3,k=-1,E=-1,x=!1,C=!1,Ja=Date,Ka="function"===typeof setTimeout?setTimeout:void 0,La="function"===typeof clearTimeout?clearTimeout:void 0,oa="function"===typeof requestAnimationFrame?requestAnimationFrame:void 0,pa="function"===typeof cancelAnimationFrame?cancelAnimationFrame:void 0,qa,ra,Y=function(a){qa=oa(function(b){La(ra);a(b)});ra=Ka(function(){pa(qa);
|
||||
a(l())},100)};if("object"===typeof performance&&"function"===typeof performance.now){var Ma=performance;var l=function(){return Ma.now()}}else l=function(){return Ja.now()};e=null;"undefined"!==typeof window?e=window:"undefined"!==typeof global&&(e=global);if(e&&e._schedMock){e=e._schedMock;var D=e[0];var P=e[1];var H=e[2];l=e[3]}else if("undefined"===typeof window||"function"!==typeof MessageChannel){var v=null,Na=function(a){if(null!==v)try{v(a)}finally{v=null}};D=function(a,b){null!==v?setTimeout(D,
|
||||
0,a):(v=a,setTimeout(Na,0,!1))};P=function(){v=null};H=function(){return!1}}else{"undefined"!==typeof console&&("function"!==typeof oa&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),"function"!==typeof pa&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"));var w=null,K=!1,z=-1,A=!1,Z=!1,L=0,
|
||||
M=33,B=33;H=function(){return L<=l()};e=new MessageChannel;var sa=e.port2;e.port1.onmessage=function(a){K=!1;a=w;var b=z;w=null;z=-1;var d=l(),c=!1;if(0>=L-d)if(-1!==b&&b<=d)c=!0;else{A||(A=!0,Y(aa));w=a;z=b;return}if(null!==a){Z=!0;try{a(c)}finally{Z=!1}}};var aa=function(a){if(null!==w){Y(aa);var b=a-L+B;b<B&&M<B?(8>b&&(b=8),B=b<M?M:b):M=b;L=a+B;K||(K=!0,sa.postMessage(void 0))}else A=!1};D=function(a,b){w=a;z=b;Z||0>b?sa.postMessage(void 0):A||(A=!0,Y(aa))};P=function(){w=null;K=!1;z=-1}}var Oa=
|
||||
0,ma={current:null},R={current:null};e={ReactCurrentDispatcher:ma,ReactCurrentOwner:R,assign:J};J(e,{Scheduler:{unstable_cancelCallback:function(a){var b=a.next;if(null!==b){if(b===a)c=null;else{a===c&&(c=b);var d=a.previous;d.next=b;b.previous=d}a.next=a.previous=null}},unstable_shouldYield:function(){return!G&&(null!==c&&c.expirationTime<E||H())},unstable_now:l,unstable_scheduleCallback:function(a,b){var d=-1!==k?k:l();if("object"===typeof b&&null!==b&&"number"===typeof b.timeout)b=d+b.timeout;
|
||||
else switch(f){case 1:b=d+-1;break;case 2:b=d+250;break;case 5:b=d+1073741823;break;case 4:b=d+1E4;break;default:b=d+5E3}a={callback:a,priorityLevel:f,expirationTime:b,next:null,previous:null};if(null===c)c=a.next=a.previous=a,u();else{d=null;var g=c;do{if(g.expirationTime>b){d=g;break}g=g.next}while(g!==c);null===d?d=c:d===c&&(c=a,u());b=d.previous;b.next=d.previous=a;a.next=d;a.previous=b}return a},unstable_runWithPriority:function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=
|
||||
3}var d=f,c=k;f=a;k=l();try{return b()}finally{f=d,k=c,F()}},unstable_next:function(a){switch(f){case 1:case 2:case 3:var b=3;break;default:b=f}var d=f,c=k;f=b;k=l();try{return a()}finally{f=d,k=c,F()}},unstable_wrapCallback:function(a){var b=f;return function(){var d=f,c=k;f=b;k=l();try{return a.apply(this,arguments)}finally{f=d,k=c,F()}}},unstable_getFirstCallbackNode:function(){return c},unstable_pauseExecution:function(){},unstable_continueExecution:function(){null!==c&&u()},unstable_getCurrentPriorityLevel:function(){return f},
|
||||
unstable_IdlePriority:5,unstable_ImmediatePriority:1,unstable_LowPriority:4,unstable_NormalPriority:3,unstable_UserBlockingPriority:2},SchedulerTracing:{__interactionsRef:null,__subscriberRef:null,unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Oa},unstable_subscribe:function(a){},unstable_trace:function(a,b,d){return d()},unstable_unsubscribe:function(a){},unstable_wrap:function(a){return a}}});var fa=Object.prototype.hasOwnProperty,
|
||||
ha={key:!0,ref:!0,__self:!0,__source:!0},la=/\/+/g,I=[];r={Children:{map:function(a,b,d){if(null==a)return a;var c=[];W(a,c,null,b,d);return c},forEach:function(a,b,d){if(null==a)return a;b=ia(null,null,b,d);V(a,xa,b);ja(b)},count:function(a){return V(a,function(){return null},null)},toArray:function(a){var b=[];W(a,b,null,function(a){return a});return b},only:function(a){S(a)?void 0:q("143");return a}},createRef:function(){return{current:null}},Component:t,PureComponent:O,createContext:function(a,
|
||||
b){void 0===b&&(b=null);a={$$typeof:Ba,_calculateChangedBits:b,_currentValue:a,_currentValue2:a,_threadCount:0,Provider:null,Consumer:null};a.Provider={$$typeof:Aa,_context:a};return a.Consumer=a},forwardRef:function(a){return{$$typeof:Da,render:a}},lazy:function(a){return{$$typeof:Ga,_ctor:a,_status:-1,_result:null}},memo:function(a,b){return{$$typeof:Fa,type:a,compare:void 0===b?null:b}},useCallback:function(a,b){return m().useCallback(a,b)},useContext:function(a,b){return m().useContext(a,b)},
|
||||
useEffect:function(a,b){return m().useEffect(a,b)},useImperativeHandle:function(a,b,d){return m().useImperativeHandle(a,b,d)},useDebugValue:function(a,b){},useLayoutEffect:function(a,b){return m().useLayoutEffect(a,b)},useMemo:function(a,b){return m().useMemo(a,b)},useReducer:function(a,b,d){return m().useReducer(a,b,d)},useRef:function(a){return m().useRef(a)},useState:function(a){return m().useState(a)},Fragment:r,StrictMode:X,Suspense:Ea,createElement:ea,cloneElement:function(a,b,d){null===a||
|
||||
void 0===a?q("267",a):void 0;var c=void 0,e=J({},a.props),f=a.key,k=a.ref,h=a._owner;if(null!=b){void 0!==b.ref&&(k=b.ref,h=R.current);void 0!==b.key&&(f=""+b.key);var l=void 0;a.type&&a.type.defaultProps&&(l=a.type.defaultProps);for(c in b)fa.call(b,c)&&!ha.hasOwnProperty(c)&&(e[c]=void 0===b[c]&&void 0!==l?l[c]:b[c])}c=arguments.length-2;if(1===c)e.children=d;else if(1<c){l=Array(c);for(var m=0;m<c;m++)l[m]=arguments[m+2];e.children=l}return{$$typeof:y,type:a.type,key:f,ref:k,props:e,_owner:h}},
|
||||
createFactory:function(a){var b=ea.bind(null,a);b.type=a;return b},isValidElement:S,version:"16.8.6",unstable_ConcurrentMode:Ca,unstable_Profiler:za,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:e};r=(X={default:r},r)||X;return r.default||r});
|
||||
@@ -1,42 +0,0 @@
|
||||
.main {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
}
|
||||
|
||||
.left-thing {
|
||||
background-color: gray;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.left-thing button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right-thing {
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Message widget */
|
||||
.message .author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 400px;
|
||||
background-color: lightblue;
|
||||
margin: 10px 5px;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.remove {
|
||||
float: right;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// Generating demo data
|
||||
//------------------------------------------------------------------------------
|
||||
const AUTHORS = ["Aaron", "David", "Vincent"];
|
||||
const CONTENT = [
|
||||
"Lorem ipsum dolor sit amet",
|
||||
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem",
|
||||
"Excepteur sint occaecat cupidatat non proident"
|
||||
];
|
||||
|
||||
function chooseRandomly(array) {
|
||||
const index = Math.floor(Math.random() * array.length);
|
||||
return array[index];
|
||||
}
|
||||
|
||||
let nextId = 1;
|
||||
export function buildData(n = 1000) {
|
||||
const data = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let id = nextId++;
|
||||
data.push({
|
||||
id: id,
|
||||
author: chooseRandomly(AUTHORS),
|
||||
msg: `${id}: ${chooseRandomly(CONTENT)}`,
|
||||
likes: 0
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Measuring helpers
|
||||
//------------------------------------------------------------------------------
|
||||
let startTime;
|
||||
let lastMeasure;
|
||||
export function startMeasure(descr) {
|
||||
startTime = performance.now();
|
||||
lastMeasure = descr;
|
||||
}
|
||||
export function stopMeasure() {
|
||||
let last = lastMeasure;
|
||||
if (lastMeasure) {
|
||||
window.setTimeout(function() {
|
||||
lastMeasure = null;
|
||||
const stop = performance.now();
|
||||
console.log(`[${last}] took ${Number(stop - startTime).toFixed()}ms`);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter Widget
|
||||
//------------------------------------------------------------------------------
|
||||
Vue.component("likes-counter", {
|
||||
data: function() {
|
||||
return {
|
||||
counter: 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
increment() {
|
||||
this.counter++;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<button @click="increment">Value: {{counter}}</button>
|
||||
</div>`
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Message Widget
|
||||
//------------------------------------------------------------------------------
|
||||
Vue.component("my-message", {
|
||||
props: ["msg"],
|
||||
methods: {
|
||||
removeMessage() {
|
||||
this.$emit("removeMessage", this.msg.id);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="message">
|
||||
<span class="author">{{msg.author}}</span>
|
||||
<span class="msg">{{msg.msg}}</span>
|
||||
<button class="remove" @click="removeMessage">Remove</button>
|
||||
<likes-counter></likes-counter>
|
||||
</div>`
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
const App = {
|
||||
name: "App",
|
||||
data() {
|
||||
return { messages: [] };
|
||||
},
|
||||
methods: {
|
||||
addMessages(n) {
|
||||
startMeasure("add " + n);
|
||||
const newMessages = buildData(n);
|
||||
this.messages.push.apply(this.messages, newMessages);
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
clear() {
|
||||
startMeasure("clear");
|
||||
this.messages = [];
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
updateSomeMessages() {
|
||||
startMeasure("update every 10th");
|
||||
const messages = this.messages;
|
||||
for (let i = 0; i < this.messages.length; i += 10) {
|
||||
messages[i].author += "!!!";
|
||||
}
|
||||
stopMeasure();
|
||||
},
|
||||
|
||||
removeMessage(id) {
|
||||
startMeasure("remove message");
|
||||
const index = this.messages.findIndex(m => m.id === id);
|
||||
this.messages.splice(index, 1);
|
||||
stopMeasure();
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="main">
|
||||
<div class="left-thing">
|
||||
<div>Number of msg: {{messages.length}}</div>
|
||||
<button @click="addMessages(100)">Add 100 messages</button>
|
||||
<button @click="addMessages(1000)">Add 1000 messages</button>
|
||||
<button @click="addMessages(10000)">Add 10000 messages</button>
|
||||
<button @click="addMessages(50000)">Add 50000 messages</button>
|
||||
<button @click="updateSomeMessages">Update every 10th message</button>
|
||||
<button @click="clear">Clear</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<my-message v-for="msg in messages" v-bind:key="msg.id" v-bind:msg="msg" @removeMessage="removeMessage"></my-message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
new Vue({
|
||||
render: h => h(App)
|
||||
}).$mount(`#main`);
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Vue.js Benchmark</title>
|
||||
<link href="../shared/main.css" rel="stylesheet"/>
|
||||
<script src='vue.min.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='main'></div>
|
||||
<script src='app.js' type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
-6
File diff suppressed because one or more lines are too long
@@ -1,27 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OWL Extras</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🦉 OWL Extra Stuff 🦉</h1>
|
||||
|
||||
|
||||
<h2>Benchmarks</h2>
|
||||
<p>The goal is to compare some operations on large lists</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="benchmarks/odoo-widgets-12.0">Odoo Widgets (12.0)</a></li>
|
||||
<li><a href="benchmarks/odoo-widgets-12.3">Odoo Widgets (12.3)</a></li>
|
||||
<li><a href="benchmarks/owl-0.7.0">OWL 0.7.0</a></li>
|
||||
<li><a href="benchmarks/owl-0.8.0">OWL 0.8.0</a></li>
|
||||
<li><a href="benchmarks/owl-master">OWL Master</a></li>
|
||||
<li><a href="benchmarks/vue">Vue</a></li>
|
||||
<li><a href="benchmarks/react">React</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Playground</h2>
|
||||
<a href="playground">Playground</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,325 +0,0 @@
|
||||
import { SAMPLES } from "./samples.js";
|
||||
|
||||
let owlJS;
|
||||
|
||||
async function owlSourceCode() {
|
||||
if (owlJS) {
|
||||
return owlJS;
|
||||
}
|
||||
const result = await fetch("../owl.js");
|
||||
owlJS = await result.text();
|
||||
return owlJS;
|
||||
}
|
||||
|
||||
const MODES = {
|
||||
js: "ace/mode/javascript",
|
||||
css: "ace/mode/css",
|
||||
xml: "ace/mode/xml"
|
||||
};
|
||||
|
||||
const DEFAULT_XML = `<templates>
|
||||
</templates>`;
|
||||
|
||||
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 src="app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const APP_PY = `import sys
|
||||
import thread
|
||||
import webbrowser
|
||||
import time
|
||||
|
||||
import BaseHTTPServer, SimpleHTTPServer
|
||||
|
||||
def start_server():
|
||||
httpd = BaseHTTPServer.HTTPServer(('127.0.0.1', 3600), SimpleHTTPServer.SimpleHTTPRequestHandler)
|
||||
httpd.serve_forever()
|
||||
|
||||
thread.start_new_thread(start_server,())
|
||||
url = 'http://127.0.0.1:3600'
|
||||
webbrowser.open_new(url)
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
`;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tabbed editor
|
||||
//------------------------------------------------------------------------------
|
||||
class TabbedEditor extends owl.Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.template = "tabbed-editor";
|
||||
this.state = {
|
||||
currentTab: this.props.display.split("|")[0]
|
||||
};
|
||||
this.tabs = {
|
||||
js: this.props.display.includes("js"),
|
||||
xml: this.props.display.includes("xml"),
|
||||
css: this.props.display.includes("css")
|
||||
};
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.editor = ace.edit(this.refs.editor);
|
||||
|
||||
// remove this for xml/css (?)
|
||||
this.editor.session.setOption("useWorker", false);
|
||||
this.editor.setValue(this.props[this.state.currentTab], -1);
|
||||
this.editor.setFontSize("13px");
|
||||
this.editor.setTheme("ace/theme/monokai");
|
||||
this.editor.session.setMode(MODES[this.state.currentTab]);
|
||||
const tabSize = this.state.currentTab === "xml" ? 2 : 4;
|
||||
this.editor.session.setOption("tabSize", tabSize);
|
||||
this.editor.on("blur", () => {
|
||||
const editorValue = this.editor.getValue();
|
||||
const propsValue = this.props[this.state.currentTab];
|
||||
if (editorValue !== propsValue) {
|
||||
this.trigger("updateCode", {
|
||||
type: this.state.currentTab,
|
||||
value: editorValue
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
patched() {
|
||||
if (this.editor) {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
this.editor.setValue(this.props[this.state.currentTab], -1);
|
||||
}
|
||||
}
|
||||
|
||||
willUnmount() {
|
||||
this.editor.destroy();
|
||||
delete this.editor;
|
||||
}
|
||||
|
||||
setTab(tab) {
|
||||
this.editor.setValue(this.props[tab], -1);
|
||||
|
||||
const mode = MODES[tab];
|
||||
this.editor.session.setMode(mode);
|
||||
const tabSize = tab === "xml" ? 2 : 4;
|
||||
this.editor.session.setOption("tabSize", tabSize);
|
||||
this.state.currentTab = tab;
|
||||
}
|
||||
|
||||
onMouseDown(ev) {
|
||||
if (ev.target.tagName === "DIV") {
|
||||
let y = ev.clientY;
|
||||
const resizer = ev => {
|
||||
const delta = ev.clientY - y;
|
||||
y = ev.clientY;
|
||||
this.trigger("updatePanelHeight", { delta });
|
||||
};
|
||||
document.body.addEventListener("mousemove", resizer);
|
||||
document.body.addEventListener("mouseup", () => {
|
||||
document.body.removeEventListener("mousemove", resizer);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// MAIN APP
|
||||
//------------------------------------------------------------------------------
|
||||
class App extends owl.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.template = "playground";
|
||||
this.version = owl._version;
|
||||
this.SAMPLES = SAMPLES;
|
||||
this.widgets = { TabbedEditor };
|
||||
|
||||
this.state = {
|
||||
js: SAMPLES[0].code,
|
||||
css: SAMPLES[0].css || "",
|
||||
xml: SAMPLES[0].xml || DEFAULT_XML,
|
||||
error: false,
|
||||
displayWelcome: true,
|
||||
splitLayout: true,
|
||||
leftPaneWidth: Math.ceil(window.innerWidth / 2),
|
||||
topPanelHeight: null
|
||||
};
|
||||
}
|
||||
|
||||
displayError(error) {
|
||||
this.state.error = error;
|
||||
if (error) {
|
||||
setTimeout(() => {
|
||||
this.refs.content.innerHTML = "";
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async runCode() {
|
||||
this.state.displayWelcome = false;
|
||||
// check templates
|
||||
var qweb = new owl.QWeb();
|
||||
var error = false;
|
||||
const sanitizedXML = this.state.xml.replace(/<!--[\s\S]*?-->/g, "");
|
||||
try {
|
||||
qweb.loadTemplates(sanitizedXML);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.displayError(error.message);
|
||||
return;
|
||||
} else {
|
||||
this.state.error = false;
|
||||
}
|
||||
|
||||
// create iframe
|
||||
const iframe = document.createElement("iframe");
|
||||
|
||||
iframe.onload = () => {
|
||||
const doc = iframe.contentDocument;
|
||||
// inject js
|
||||
const owlScript = doc.createElement("script");
|
||||
owlScript.type = "text/javascript";
|
||||
owlScript.src = "../owl.js";
|
||||
owlScript.addEventListener("load", () => {
|
||||
const script = doc.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
const content = `window.TEMPLATES = \`${sanitizedXML}\`\n${
|
||||
this.state.js
|
||||
}`;
|
||||
script.innerHTML = content;
|
||||
const errorHandler = e => this.displayError(e.message);
|
||||
iframe.contentWindow.addEventListener("error", errorHandler);
|
||||
setTimeout(function() {
|
||||
if (iframe.contentWindow) {
|
||||
iframe.contentWindow.removeEventListener("error", errorHandler);
|
||||
}
|
||||
}, 100);
|
||||
doc.body.appendChild(script);
|
||||
});
|
||||
doc.head.appendChild(owlScript);
|
||||
|
||||
// inject css
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = this.state.css;
|
||||
doc.head.appendChild(style);
|
||||
};
|
||||
this.refs.content.innerHTML = "";
|
||||
this.refs.content.appendChild(iframe);
|
||||
}
|
||||
|
||||
setSample(ev) {
|
||||
const sample = SAMPLES.find(s => s.description === ev.target.value);
|
||||
this.state.js = sample.code;
|
||||
this.state.css = sample.css || "";
|
||||
this.state.xml = sample.xml || DEFAULT_XML;
|
||||
}
|
||||
|
||||
get leftPaneStyle() {
|
||||
return `width:${this.state.leftPaneWidth}px`;
|
||||
}
|
||||
|
||||
get rightPaneStyle() {
|
||||
return `width:${window.innerWidth - 6 - this.state.leftPaneWidth}px`;
|
||||
}
|
||||
|
||||
get topEditorStyle() {
|
||||
return `flex: 0 0 ${this.state.topPanelHeight}px`;
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
const resizer = ev => {
|
||||
this.state.leftPaneWidth = ev.clientX;
|
||||
};
|
||||
|
||||
document.body.addEventListener("mousemove", resizer);
|
||||
for (let iframe of document.getElementsByTagName("iframe")) {
|
||||
iframe.classList.add("disabled");
|
||||
}
|
||||
|
||||
document.body.addEventListener("mouseup", () => {
|
||||
document.body.removeEventListener("mousemove", resizer);
|
||||
for (let iframe of document.getElementsByTagName("iframe")) {
|
||||
iframe.classList.remove("disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
updateCode(ev) {
|
||||
this.state[ev.type] = ev.value;
|
||||
}
|
||||
toggleLayout() {
|
||||
this.state.splitLayout = !this.state.splitLayout;
|
||||
}
|
||||
updatePanelHeight(ev) {
|
||||
if (!ev.delta) {
|
||||
return;
|
||||
}
|
||||
let height = this.state.topPanelHeight;
|
||||
if (!height) {
|
||||
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
|
||||
}
|
||||
this.state.topPanelHeight = height + ev.delta;
|
||||
}
|
||||
|
||||
async downloadCode() {
|
||||
await owl.utils.loadJS("libs/FileSaver.min.js");
|
||||
await owl.utils.loadJS("libs/jszip.min.js");
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
const JS = `async function startApp() {
|
||||
// Loading templates
|
||||
let TEMPLATES;
|
||||
try {
|
||||
TEMPLATES = await owl.utils.loadTemplates('app.xml');
|
||||
} catch(e) {
|
||||
document.write(\`This app requires a static server. If you have python installed, try 'python app.py'\`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Application code
|
||||
${this.state.js.split('\n').map(l => l === '' ? '' : ' ' + l).join('\n')}
|
||||
}
|
||||
|
||||
// wait for DOM ready before starting
|
||||
owl.utils.whenReady(startApp);`;
|
||||
|
||||
zip.file("app.js", JS);
|
||||
zip.file("app.css", this.state.css);
|
||||
zip.file("app.py", APP_PY);
|
||||
zip.file("app.xml", this.state.xml);
|
||||
zip.file("index.html", DEFAULT_HTML);
|
||||
zip.file("owl.js", owlSourceCode());
|
||||
zip.generateAsync({ type: "blob" }).then(function(content) {
|
||||
saveAs(content, "app.zip");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Application initialization
|
||||
//------------------------------------------------------------------------------
|
||||
document.title = `${document.title} (v${owl._version})`;
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const templates = await owl.utils.loadTemplates("templates.xml");
|
||||
const qweb = new owl.QWeb(templates);
|
||||
const env = { qweb };
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
<templates>
|
||||
<div t-name="tabbed-editor" class="tabbed-editor" t-att-style="props.style">
|
||||
<div class="tabBar" t-att-class="{resizeable: props.resizeable}" t-on-mousedown="onMouseDown">
|
||||
<a t-if="tabs.js" class="tab flash" t-att-class="{active: state.currentTab==='js'}" t-on-click="setTab('js')">JS</a>
|
||||
<a t-if="tabs.xml" class="tab flash" t-att-class="{active: state.currentTab==='xml'}" t-on-click="setTab('xml')">XML</a>
|
||||
<a t-if="tabs.css" class="tab flash" t-att-class="{active: state.currentTab==='css'}" t-on-click="setTab('css')">CSS</a>
|
||||
</div>
|
||||
<div class="code-editor" t-ref="'editor'"></div>
|
||||
</div>
|
||||
|
||||
<div t-name="playground" class="playground">
|
||||
<div class="left-bar" t-att-style="leftPaneStyle" t-att-class="{split: state.splitLayout}">
|
||||
<div class="menubar">
|
||||
<a class="btn run-code flash" t-on-click="runCode" title="Execute this Code">▶ Run</a>
|
||||
<select t-on-change="setSample">
|
||||
<option t-foreach="SAMPLES" t-as="sample" t-key="sample_index">
|
||||
<t t-esc="sample.description"/>
|
||||
</option>
|
||||
</select>
|
||||
<a class="btn flash" t-on-click="downloadCode" title="Download a Zip with this Code"><i class="fas fa-download"></i></a>
|
||||
<a class="layout-selector flash" t-on-click="toggleLayout" title="Toggle Layout"><i class="fas" t-att-class="state.splitLayout ? 'fa-toggle-on' : 'fa-toggle-off'"></i></a>
|
||||
</div>
|
||||
<t t-if="!state.splitLayout">
|
||||
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js|xml|css'}" t-on-updateCode="updateCode"/>
|
||||
</t>
|
||||
<t t-else="1">
|
||||
<t t-widget="TabbedEditor" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'js', style:topEditorStyle}" t-on-updateCode="updateCode"/>
|
||||
<div class="separator horizontal"/>
|
||||
<t t-widget="TabbedEditor" t-keepalive="1" t-props="{js:state.js, css:state.css, xml: state.xml, display: 'xml|css', resizeable: true}" t-on-updateCode="updateCode" t-on-updatePanelHeight="updatePanelHeight"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="separator vertical" t-on-mousedown="onMouseDown"/>
|
||||
<div class="right-pane" t-att-style="rightPaneStyle">
|
||||
<div class="welcome" t-if="state.displayWelcome">
|
||||
<div>🦉 Odoo Web Library 🦉</div>
|
||||
<div>v<t t-esc="version"/></div>
|
||||
<div class="url"><a href="https://github.com/odoo/owl">https://github.com/odoo/owl</a></div>
|
||||
<div class="note">Note: these examples require a recent browser to work without a transpilation step. </div>
|
||||
</div>
|
||||
<div t-if="state.error" class="error">
|
||||
<t t-esc="state.error"/>
|
||||
</div>
|
||||
<div class="content" t-ref="'content'"/>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -1,6 +0,0 @@
|
||||
# 🦉 Extra Stuff 🦉
|
||||
|
||||
To help work/improve/learn with OWL, we have here:
|
||||
|
||||
- a benchmarks application
|
||||
- a playground application
|
||||
@@ -1,34 +0,0 @@
|
||||
import sys
|
||||
import thread
|
||||
import webbrowser
|
||||
import time
|
||||
|
||||
import BaseHTTPServer
|
||||
import SimpleHTTPServer
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 8000
|
||||
|
||||
|
||||
class OWLHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == '/extras/owl.js':
|
||||
self.path = '/dist/owl.js'
|
||||
return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
|
||||
|
||||
|
||||
def start_server():
|
||||
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), OWLHandler)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
thread.start_new_thread(start_server, ())
|
||||
url = 'http://{0}:{1}/extras'.format(HOST, PORT)
|
||||
webbrowser.open_new(url)
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
+50
-24
@@ -1,56 +1,78 @@
|
||||
{
|
||||
"name": "owl",
|
||||
"version": "0.9.0",
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.0.0-beta-10",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "src/index.ts",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"browser": "dist/owl.iife.js",
|
||||
"module": "dist/owl.es.js",
|
||||
"types": "dist/types/owl.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.18.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
|
||||
"build:js:es5": "tsc --target es5 --module es6 --outDir dist/owl src/*",
|
||||
"build:bundle": "rollup -c",
|
||||
"build": "npm run build:js && npm run build:bundle",
|
||||
"build:es5": "npm run build:js:es5 && npm run build:bundle",
|
||||
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
||||
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
|
||||
"build:bundle": "rollup -c --failAfterWarnings",
|
||||
"build:runtime": "rollup -c --failAfterWarnings runtime",
|
||||
"build:compiler": "rollup -c --failAfterWarnings compiler",
|
||||
"build": "npm run build:bundle",
|
||||
"test": "jest",
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
||||
"test:watch": "jest --watch",
|
||||
"preextras": "npm run build",
|
||||
"extras": "python extras/server.py"
|
||||
"playground:serve": "python3 tools/server.py || python tools/server.py",
|
||||
"playground": "npm run build && npm run playground:serve",
|
||||
"preplayground:watch": "npm run build",
|
||||
"playground:watch": "npm-run-all --parallel playground: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",
|
||||
"compile_templates": "node tools/compile_xml.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/odoo/owl.git"
|
||||
},
|
||||
"author": "Odoo",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"license": "LGPL-3.0-only",
|
||||
"bugs": {
|
||||
"url": "https://github.com/odoo/owl/issues"
|
||||
},
|
||||
"homepage": "https://github.com/odoo/owl#readme",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.3.12",
|
||||
"@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",
|
||||
"jest": "^23.6.0",
|
||||
"github-api": "^3.3.0",
|
||||
"jest": "^27.1.0",
|
||||
"jest-diff": "^27.3.1",
|
||||
"jest-environment-jsdom": "^27.1.0",
|
||||
"live-server": "^1.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"rollup": "^1.6.0",
|
||||
"rollup-plugin-typescript2": "^0.20.1",
|
||||
"prettier": "2.4.1",
|
||||
"rollup": "^2.56.3",
|
||||
"rollup-plugin-dts": "^4.2.2",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
"sass": "^1.16.1",
|
||||
"snabbdom-to-html": "^5.1.1",
|
||||
"source-map-support": "^0.5.10",
|
||||
"ts-jest": "^23.10.5",
|
||||
"typescript": "^3.2.2",
|
||||
"ts-jest": "^27.0.5",
|
||||
"typescript": "4.5.2",
|
||||
"uglify-es": "^3.3.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"jest-environment-jsdom": "^24.7.1",
|
||||
"snabbdom": "^0.7.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom",
|
||||
"roots": [
|
||||
"<rootDir>/src",
|
||||
"<rootDir>/tests"
|
||||
],
|
||||
"setupFiles": [
|
||||
"./tests/mocks/mockEventTarget.js"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.ts?$": "ts-jest"
|
||||
},
|
||||
@@ -64,5 +86,9 @@
|
||||
"json",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
}
|
||||
|
||||
+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.
|
||||
|
||||
|
||||
+82
-10
@@ -1,14 +1,86 @@
|
||||
import { version } from "./package.json";
|
||||
import pkg from "./package.json";
|
||||
import git from "git-rev-sync";
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import dts from "rollup-plugin-dts";
|
||||
|
||||
// rollup.config.js
|
||||
export default {
|
||||
input: "dist/owl/index.js",
|
||||
output: {
|
||||
file: "dist/owl.js",
|
||||
format: "iife",
|
||||
let input, output;
|
||||
|
||||
const outro = `
|
||||
__info__.version = '${pkg.version}';
|
||||
__info__.date = '${new Date().toISOString()}';
|
||||
__info__.hash = '${git.short()}';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
`;
|
||||
|
||||
switch (process.argv[4]) {
|
||||
case "compiler":
|
||||
input = "src/compiler/index.ts",
|
||||
output = [
|
||||
getConfigForFormat('cjs', 'dist/compiler.js', ''),
|
||||
]
|
||||
break;
|
||||
case "runtime":
|
||||
input = "src/runtime/index.ts";
|
||||
output = [
|
||||
getConfigForFormat('esm', addSuffix(pkg.module, 'runtime'), outro),
|
||||
getConfigForFormat('cjs', addSuffix(pkg.main, 'runtime'), outro),
|
||||
getConfigForFormat('iife', addSuffix(pkg.browser, 'runtime'), outro),
|
||||
getConfigForFormat('iife', addSuffix(pkg.browser, 'runtime'), outro, true),
|
||||
]
|
||||
break;
|
||||
default:
|
||||
input = "src/index.ts",
|
||||
output = [
|
||||
getConfigForFormat('esm', pkg.module, outro),
|
||||
getConfigForFormat('cjs', pkg.main, outro),
|
||||
getConfigForFormat('iife', pkg.browser, outro),
|
||||
getConfigForFormat('iife', pkg.browser, outro, true),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate from a string depicting a path a new path for the minified version.
|
||||
* @param {string} pkgFileName file name
|
||||
*/
|
||||
function addSuffix(pkgFileName, suffix) {
|
||||
const parts = pkgFileName.split('.');
|
||||
parts.splice(parts.length - 1, 0, suffix);
|
||||
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, outro, minified = false) {
|
||||
return {
|
||||
file: minified ? addSuffix(generatedFileName, "min") : generatedFileName,
|
||||
format: format,
|
||||
name: "owl",
|
||||
extend: true,
|
||||
outro: `exports._version = '${version}';\nexports._date = '${new Date().toISOString()}';\nexports._hash = '${git.short()}';\nexports._url = 'https://github.com/odoo/owl';`
|
||||
}
|
||||
};
|
||||
outro: outro,
|
||||
freeze: false,
|
||||
plugins: minified ? [terser()] : [],
|
||||
indent: ' ', // indent with 4 spaces
|
||||
};
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
input,
|
||||
output,
|
||||
plugins: [
|
||||
typescript({
|
||||
useTsconfigDeclarationDir: true
|
||||
}),
|
||||
]
|
||||
},
|
||||
{
|
||||
input: "dist/types/index.d.ts",
|
||||
output: [{ file: "dist/types/owl.d.ts", format: "es" }],
|
||||
plugins: [dts()],
|
||||
},
|
||||
];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
import type { TemplateSet } from "../runtime/template_set";
|
||||
import type { BDom } from "../runtime/blockdom";
|
||||
import { CodeGenerator, Config } from "./code_generator";
|
||||
import { parse } from "./parser";
|
||||
|
||||
export type Template = (context: any, vnode: any, key?: string) => BDom;
|
||||
|
||||
export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Template;
|
||||
|
||||
interface CompileOptions extends Config {
|
||||
name?: string;
|
||||
}
|
||||
export function compile(
|
||||
template: string | Element,
|
||||
options: CompileOptions = {}
|
||||
): TemplateFunction {
|
||||
// parsing
|
||||
const ast = parse(template);
|
||||
|
||||
// some work
|
||||
const hasSafeContext =
|
||||
template instanceof Node
|
||||
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
|
||||
: !template.includes("t-set") && !template.includes("t-call");
|
||||
|
||||
// code generation
|
||||
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
||||
const code = codeGenerator.generateCode();
|
||||
// template function
|
||||
return new Function("app, bdom, helpers", code) as TemplateFunction;
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* 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: { [key: string]: string } = 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. Currently we don't support delete and void
|
||||
const OPERATORS =
|
||||
"...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~".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: any) {
|
||||
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;
|
||||
let error: any;
|
||||
let current = expr;
|
||||
|
||||
try {
|
||||
while (token) {
|
||||
current = current.trim();
|
||||
if (current) {
|
||||
for (let tokenizer of TOKENIZERS) {
|
||||
token = tokenizer(current);
|
||||
if (token) {
|
||||
result.push(token);
|
||||
current = current.slice(token.size || token.value.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
token = false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error = e; // Silence all errors and throw a generic error below
|
||||
}
|
||||
if (current.length || error) {
|
||||
throw new Error(`Tokenizer error: could not tokenize \`${expr}\``);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Expression "evaluator"
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const isLeftSeparator = (token: Token) =>
|
||||
token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
|
||||
const isRightSeparator = (token: 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): Token[] {
|
||||
const localVars = new Set<string>();
|
||||
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: any) => compileExpr(expr));
|
||||
}
|
||||
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!;
|
||||
localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
|
||||
}
|
||||
j--;
|
||||
}
|
||||
} else {
|
||||
localVars.add(token.value); //] = { id: token.value, expr: token.value };
|
||||
}
|
||||
}
|
||||
|
||||
if (isVar) {
|
||||
token.varName = token.value;
|
||||
if (!localVars.has(token.value)) {
|
||||
token.originalValue = token.value;
|
||||
token.value = `ctx['${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" && token.varName && localVars.has(token.value)) {
|
||||
token.originalValue = token.value;
|
||||
token.value = `_${token.value}`;
|
||||
token.isLocal = true;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// Leading spaces are trimmed during tokenization, so they need to be added back for some values
|
||||
const paddedValues = new Map([["in ", " in "]]);
|
||||
|
||||
export function compileExpr(expr: string): string {
|
||||
return compileExprToArray(expr)
|
||||
.map((t) => paddedValues.get(t.value) || t.value)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
|
||||
|
||||
export function replaceDynamicParts(s: string, replacer: (s: string) => string) {
|
||||
let matches = s.match(INTERP_REGEXP);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
|
||||
}
|
||||
|
||||
let r = s.replace(
|
||||
INTERP_REGEXP,
|
||||
(s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}"
|
||||
);
|
||||
return "`" + r + "`";
|
||||
}
|
||||
export function interpolate(s: string): string {
|
||||
return replaceDynamicParts(s, compileExpr);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,436 +0,0 @@
|
||||
import { EventBus } from "./event_bus";
|
||||
import { Observer } from "./observer";
|
||||
import { QWeb, CompiledTemplate } from "./qweb";
|
||||
import { h, patch, VNode } from "./vdom";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface Env {
|
||||
qweb: QWeb;
|
||||
}
|
||||
|
||||
interface Meta<T extends Env, Props> {
|
||||
readonly id: number;
|
||||
vnode: VNode | null;
|
||||
isStarted: boolean;
|
||||
isMounted: boolean;
|
||||
isDestroyed: boolean;
|
||||
parent: Component<T, any, any> | null;
|
||||
children: { [key: number]: Component<T, any, any> };
|
||||
// children mapping: from templateID to widgetID
|
||||
// should it be a map number => Widget?
|
||||
cmap: { [key: number]: number };
|
||||
|
||||
renderId: number;
|
||||
renderProps: Props | null;
|
||||
renderPromise: Promise<VNode> | null;
|
||||
boundHandlers: { [key: number]: any };
|
||||
observer?: Observer;
|
||||
render?: CompiledTemplate;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Widget
|
||||
//------------------------------------------------------------------------------
|
||||
let nextId = 1;
|
||||
|
||||
export class Component<
|
||||
T extends Env,
|
||||
Props extends {},
|
||||
State extends {}
|
||||
> extends EventBus {
|
||||
readonly __owl__: Meta<Env, Props>;
|
||||
template: string = "default";
|
||||
inlineTemplate: string | null = null;
|
||||
|
||||
get el(): HTMLElement | null {
|
||||
return this.__owl__.vnode ? (<any>this).__owl__.vnode.elm : null;
|
||||
}
|
||||
|
||||
env: T;
|
||||
state?: State;
|
||||
props: Props;
|
||||
refs: {
|
||||
[key: string]: Component<T, any, any> | HTMLElement | undefined;
|
||||
} = {};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates an instance of Component.
|
||||
*
|
||||
* The root widget of a component tree needs an environment:
|
||||
*
|
||||
* ```javascript
|
||||
* const root = new RootWidget(env, props);
|
||||
* ```
|
||||
*
|
||||
* Every other widget simply needs a reference to its parent:
|
||||
*
|
||||
* ```javascript
|
||||
* const child = new SomeWidget(parent, props);
|
||||
* ```
|
||||
*
|
||||
* Note that most of the time, only the root widget needs to be created by
|
||||
* hand. Other widgets should be created automatically by the framework (with
|
||||
* the t-widget directive in a template)
|
||||
*/
|
||||
constructor(parent: Component<T, any, any> | T, props?: Props) {
|
||||
super();
|
||||
|
||||
// is this a good idea?
|
||||
// Pro: if props is empty, we can create easily a widget
|
||||
// Con: this is not really safe
|
||||
// Pro: but creating widget (by a template) is always unsafe anyway
|
||||
this.props = <Props>props || <Props>{};
|
||||
let id: number = nextId++;
|
||||
let p: Component<T, any, any> | null = null;
|
||||
if (parent instanceof Component) {
|
||||
p = parent;
|
||||
this.env = parent.env;
|
||||
parent.__owl__.children[id] = this;
|
||||
} else {
|
||||
this.env = parent;
|
||||
}
|
||||
this.__owl__ = {
|
||||
id: id,
|
||||
vnode: null,
|
||||
isStarted: false,
|
||||
isMounted: false,
|
||||
isDestroyed: false,
|
||||
parent: p,
|
||||
children: {},
|
||||
cmap: {},
|
||||
renderId: 1,
|
||||
renderPromise: null,
|
||||
renderProps: props || null,
|
||||
boundHandlers: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 widget 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
|
||||
*
|
||||
* The return value of willPatch will be given to the patched function.
|
||||
*/
|
||||
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 widget 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.
|
||||
*
|
||||
* The snapshot parameter is the result of the call to willPatch.
|
||||
*/
|
||||
patched(snapshot: any) {}
|
||||
|
||||
/**
|
||||
* 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() {}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async mount(target: HTMLElement): Promise<void> {
|
||||
const vnode = await this._prepare();
|
||||
if (this.__owl__.isDestroyed) {
|
||||
// widget was destroyed before we get here...
|
||||
return;
|
||||
}
|
||||
this._patch(vnode);
|
||||
target.appendChild(this.el!);
|
||||
|
||||
if (document.body.contains(target)) {
|
||||
this._callMounted();
|
||||
}
|
||||
}
|
||||
|
||||
_callMounted() {
|
||||
const children = this.__owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (!comp.__owl__.isMounted && this.el!.contains(comp.el)) {
|
||||
comp._callMounted();
|
||||
}
|
||||
}
|
||||
this.__owl__.isMounted = true;
|
||||
this.mounted();
|
||||
}
|
||||
|
||||
_callWillUnmount() {
|
||||
this.willUnmount();
|
||||
this.__owl__.isMounted = false;
|
||||
const children = this.__owl__.children;
|
||||
for (let id in children) {
|
||||
const comp = children[id];
|
||||
if (comp.__owl__.isMounted) {
|
||||
comp._callWillUnmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (this.__owl__.isMounted) {
|
||||
this._callWillUnmount();
|
||||
this.el!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async render(force: boolean = false, patchQueue?: any[]): Promise<void> {
|
||||
if (this.__owl__.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
const shouldCallPatchHooks: boolean = !patchQueue;
|
||||
if (shouldCallPatchHooks) {
|
||||
patchQueue = [];
|
||||
}
|
||||
const renderVDom = this._render(force, patchQueue);
|
||||
const renderId = this.__owl__.renderId;
|
||||
const vnode = await renderVDom;
|
||||
if (renderId === this.__owl__.renderId) {
|
||||
// we only update the vnode and the actual DOM if no other rendering
|
||||
// occurred between now and when the render method was initially called.
|
||||
if (shouldCallPatchHooks) {
|
||||
for (let i = 0; i < patchQueue!.length; i++) {
|
||||
const c = patchQueue![i];
|
||||
c.__owl__.willPatchVal = c.willPatch();
|
||||
}
|
||||
}
|
||||
this._patch(vnode);
|
||||
if (shouldCallPatchHooks) {
|
||||
for (let i = patchQueue!.length - 1; i >= 0; i--) {
|
||||
const c = patchQueue![i];
|
||||
c.patched(c.__owl__.willPatchVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.__owl__.isDestroyed) {
|
||||
const el = this.el;
|
||||
this._destroy(this.__owl__.parent);
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_destroy(parent) {
|
||||
const isMounted = this.__owl__.isMounted;
|
||||
if (isMounted) {
|
||||
this.willUnmount();
|
||||
this.__owl__.isMounted = false;
|
||||
}
|
||||
const children = Object.values(this.__owl__.children);
|
||||
for (let child of children) {
|
||||
child._destroy(this);
|
||||
}
|
||||
if (parent) {
|
||||
let id = this.__owl__.id;
|
||||
delete parent.__owl__.children[id];
|
||||
this.__owl__.parent = null;
|
||||
}
|
||||
this.clear();
|
||||
this.__owl__.isDestroyed = true;
|
||||
delete this.__owl__.vnode;
|
||||
}
|
||||
|
||||
shouldUpdate(nextProps: Props): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the correct way to update the environment of a widget. Doing
|
||||
* this will cause a full rerender of the widget and its children, so this is
|
||||
* an operation that should not be done frequently.
|
||||
*
|
||||
* A good usecase for updating the environment would be to update some mostly
|
||||
* static config keys, such as a boolean to determine if we are in mobile
|
||||
* mode or not.
|
||||
*/
|
||||
async updateEnv(nextEnv: Partial<T>): Promise<void> {
|
||||
if (this.__owl__.parent && this.__owl__.parent.env === this.env) {
|
||||
this.env = Object.create(this.env);
|
||||
}
|
||||
Object.assign(this.env, nextEnv);
|
||||
if (this.__owl__.isMounted) {
|
||||
await this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
set(target: any, key: string | number, value: any) {
|
||||
this.__owl__.observer!.set(target, key, value);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async _updateProps(
|
||||
nextProps: Props,
|
||||
forceUpdate: boolean = false,
|
||||
patchQueue: any[]
|
||||
): Promise<void> {
|
||||
const shouldUpdate = forceUpdate || this.shouldUpdate(nextProps);
|
||||
if (shouldUpdate) {
|
||||
await this.willUpdateProps(nextProps);
|
||||
this.props = nextProps;
|
||||
await this.render(false, patchQueue);
|
||||
}
|
||||
}
|
||||
|
||||
_patch(vnode) {
|
||||
this.__owl__.renderPromise = null;
|
||||
if (this.__owl__.vnode) {
|
||||
this.__owl__.vnode = patch(this.__owl__.vnode, vnode);
|
||||
} else {
|
||||
this.__owl__.vnode = patch(document.createElement(vnode.sel!), vnode);
|
||||
}
|
||||
}
|
||||
_prepare(): Promise<VNode> {
|
||||
this.__owl__.renderProps = this.props;
|
||||
this.__owl__.renderPromise = this._prepareAndRender();
|
||||
return this.__owl__.renderPromise;
|
||||
}
|
||||
|
||||
async _prepareAndRender(): Promise<VNode> {
|
||||
await this.willStart();
|
||||
if (this.__owl__.isDestroyed) {
|
||||
return Promise.resolve(h("div"));
|
||||
}
|
||||
this.__owl__.isStarted = true;
|
||||
if (this.inlineTemplate) {
|
||||
this.env.qweb.addTemplate(this.inlineTemplate, this.inlineTemplate, true);
|
||||
}
|
||||
this.__owl__.render = this.env.qweb.render.bind(
|
||||
this.env.qweb,
|
||||
this.inlineTemplate || this.template
|
||||
);
|
||||
this._observeState();
|
||||
return this._render();
|
||||
}
|
||||
async _render(
|
||||
force: boolean = false,
|
||||
patchQueue: any[] = []
|
||||
): Promise<VNode> {
|
||||
if (this.__owl__.isMounted) {
|
||||
patchQueue.push(this);
|
||||
}
|
||||
this.__owl__.renderId++;
|
||||
const promises: Promise<void>[] = [];
|
||||
if (this.__owl__.observer) {
|
||||
this.__owl__.observer.allowMutations = false;
|
||||
}
|
||||
let vnode = this.__owl__.render!(this, {
|
||||
promises,
|
||||
handlers: this.__owl__.boundHandlers,
|
||||
forceUpdate: force,
|
||||
patchQueue
|
||||
});
|
||||
if (this.__owl__.observer) {
|
||||
this.__owl__.observer.allowMutations = true;
|
||||
}
|
||||
|
||||
// this part is critical for the patching process to be done correctly. The
|
||||
// tricky part is that a child widget can be rerendered on its own, which
|
||||
// will update its own vnode representation without the knowledge of the
|
||||
// parent widget. With this, we make sure that the parent widget will be
|
||||
// able to patch itself properly after
|
||||
vnode.key = this.__owl__.id;
|
||||
this.__owl__.renderProps = this.props;
|
||||
this.__owl__.renderPromise = Promise.all(promises).then(() => vnode);
|
||||
return this.__owl__.renderPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called by qweb t-widget directive
|
||||
*/
|
||||
_mount(vnode: VNode, elm: HTMLElement): VNode {
|
||||
this.__owl__.vnode = patch(elm, vnode);
|
||||
if (
|
||||
this.__owl__.parent &&
|
||||
this.__owl__.parent.__owl__.isMounted &&
|
||||
!this.__owl__.isMounted
|
||||
) {
|
||||
this._callMounted();
|
||||
}
|
||||
return this.__owl__.vnode;
|
||||
}
|
||||
|
||||
__mount() {
|
||||
if (!this.__owl__.isMounted) {
|
||||
this.__owl__.isMounted = true;
|
||||
this.mounted();
|
||||
}
|
||||
}
|
||||
|
||||
_observeState() {
|
||||
if (this.state) {
|
||||
this.__owl__.observer = new Observer();
|
||||
this.__owl__.observer.observe(this.state);
|
||||
this.__owl__.observer.notifyCB = this.render.bind(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* We define here a simple event bus: it can
|
||||
* - emit events
|
||||
* - add/remove listeners.
|
||||
*
|
||||
* This is a useful pattern of communication in some cases.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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 widget 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 sub of subs) {
|
||||
sub.callback.call(sub.owner, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all subscriptions.
|
||||
*/
|
||||
clear() {
|
||||
this.subscriptions = {};
|
||||
}
|
||||
}
|
||||
+15
-7
@@ -1,8 +1,16 @@
|
||||
export { Component } from "./component";
|
||||
export { EventBus } from "./event_bus";
|
||||
export { Observer } from "./observer";
|
||||
export { QWeb } from "./qweb";
|
||||
export { connect, Store } from "./store";
|
||||
import * as _utils from "./utils";
|
||||
import { TemplateSet } from "./runtime/template_set";
|
||||
import { compile } from "./compiler";
|
||||
|
||||
export const utils = _utils;
|
||||
export * from "./runtime";
|
||||
|
||||
TemplateSet.prototype._compileTemplate = function _compileTemplate(
|
||||
name: string,
|
||||
template: string | Element
|
||||
) {
|
||||
return compile(template, {
|
||||
name,
|
||||
dev: this.dev,
|
||||
translateFn: this.translateFn,
|
||||
translatableAttributes: this.translatableAttributes,
|
||||
});
|
||||
};
|
||||
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// Observer
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const methodsToPatch = [
|
||||
"push",
|
||||
"pop",
|
||||
"shift",
|
||||
"unshift",
|
||||
"splice",
|
||||
"sort",
|
||||
"reverse"
|
||||
];
|
||||
|
||||
const ArrayProto = Array.prototype;
|
||||
const ModifiedArrayProto = Object.create(ArrayProto);
|
||||
|
||||
for (let method of methodsToPatch) {
|
||||
const initialMethod = ArrayProto[method];
|
||||
ModifiedArrayProto[method] = function(...args) {
|
||||
if (!this.__observer__.allowMutations) {
|
||||
throw new Error(`Array cannot be changed here")`);
|
||||
}
|
||||
this.__observer__.notifyChange();
|
||||
this.__owl__.rev++;
|
||||
let parent = this;
|
||||
do {
|
||||
parent.__owl__.deepRev++;
|
||||
} while ((parent = parent.__owl__.parent));
|
||||
let inserted;
|
||||
switch (method) {
|
||||
case "push":
|
||||
case "unshift":
|
||||
inserted = args;
|
||||
break;
|
||||
case "splice":
|
||||
inserted = args.slice(2);
|
||||
break;
|
||||
}
|
||||
if (inserted) {
|
||||
for (let elem of inserted) {
|
||||
this.__observer__.observe(elem, this);
|
||||
}
|
||||
}
|
||||
return initialMethod.call(this, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
export class Observer {
|
||||
rev: number = 1;
|
||||
allowMutations: boolean = true;
|
||||
dirty: boolean = false;
|
||||
|
||||
notifyCB() {}
|
||||
notifyChange() {
|
||||
this.rev++;
|
||||
this.dirty = true;
|
||||
Promise.resolve().then(() => {
|
||||
if (this.dirty) {
|
||||
this.dirty = false;
|
||||
this.notifyCB();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
observe(value: any, parent?: any) {
|
||||
if (value === null) {
|
||||
// fun fact: typeof null === 'object'
|
||||
return;
|
||||
}
|
||||
if (typeof value !== "object") {
|
||||
return;
|
||||
}
|
||||
if ("__owl__" in value) {
|
||||
// already observed
|
||||
value.__owl__.parent = parent;
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
this._observeArr(value, parent);
|
||||
} else {
|
||||
this._observeObj(value, parent);
|
||||
}
|
||||
}
|
||||
|
||||
set(target: any, key: number | string, value: any) {
|
||||
this._addProp(target, key, value);
|
||||
target.__owl__.rev++;
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
_observeObj<T extends { __owl__?: any }>(obj: T, parent?: any) {
|
||||
const keys = Object.keys(obj);
|
||||
obj.__owl__ = { rev: 1, deepRev: 1, parent };
|
||||
Object.defineProperty(obj, "__owl__", { enumerable: false });
|
||||
for (let key of keys) {
|
||||
this._addProp(obj, key, obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
_observeArr(arr: Array<any>, parent?: any) {
|
||||
(<any>arr).__owl__ = { rev: 1, deepRev: 1, parent };
|
||||
Object.defineProperty(arr, "__owl__", { enumerable: false });
|
||||
(<any>arr).__proto__ = Object.create(ModifiedArrayProto);
|
||||
(<any>arr).__proto__.__observer__ = this;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
this.observe(arr[i], arr);
|
||||
}
|
||||
}
|
||||
|
||||
_addProp<T extends { __owl__?: any }>(
|
||||
obj: T,
|
||||
key: string | number,
|
||||
value: any
|
||||
) {
|
||||
var self = this;
|
||||
Object.defineProperty(obj, key, {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return value;
|
||||
},
|
||||
set(newVal) {
|
||||
if (newVal !== value) {
|
||||
if (!self.allowMutations) {
|
||||
throw new Error(
|
||||
`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`
|
||||
);
|
||||
}
|
||||
value = newVal;
|
||||
self.observe(newVal, obj);
|
||||
obj.__owl__.rev!++;
|
||||
let parent = obj;
|
||||
do {
|
||||
parent.__owl__.deepRev++;
|
||||
} while ((parent = parent.__owl__.parent) && parent !== obj);
|
||||
self.notifyChange();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.observe(value, obj);
|
||||
}
|
||||
}
|
||||
-1198
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
import { Component, ComponentConstructor, Props } from "./component";
|
||||
import { ComponentNode } from "./component_node";
|
||||
import { nodeErrorHandlers } from "./error_handling";
|
||||
import { Fiber, MountOptions } from "./fibers";
|
||||
import { Scheduler } from "./scheduler";
|
||||
import { validateProps } from "./template_helpers";
|
||||
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
||||
import { validateTarget } from "./utils";
|
||||
|
||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||
|
||||
export interface Env {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface AppConfig<P, E> extends TemplateSetConfig {
|
||||
props?: P;
|
||||
env?: E;
|
||||
test?: boolean;
|
||||
warnIfNoStaticProps?: boolean;
|
||||
}
|
||||
|
||||
let hasBeenLogged = false;
|
||||
|
||||
export const DEV_MSG = () => {
|
||||
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
||||
|
||||
return `Owl is running in 'dev' mode.
|
||||
|
||||
This is not suitable for production use.
|
||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
||||
};
|
||||
|
||||
export class App<
|
||||
T extends abstract new (...args: any) => any = any,
|
||||
P extends object = any,
|
||||
E = any
|
||||
> extends TemplateSet {
|
||||
static validateTarget = validateTarget;
|
||||
|
||||
Root: ComponentConstructor<P, E>;
|
||||
props: P;
|
||||
env: E;
|
||||
scheduler = new Scheduler();
|
||||
root: ComponentNode<P, E> | null = null;
|
||||
warnIfNoStaticProps: boolean;
|
||||
|
||||
constructor(Root: ComponentConstructor<P, E>, config: AppConfig<P, E> = {}) {
|
||||
super(config);
|
||||
this.Root = Root;
|
||||
if (config.test) {
|
||||
this.dev = true;
|
||||
}
|
||||
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||||
if (this.dev && !config.test && !hasBeenLogged) {
|
||||
console.info(DEV_MSG());
|
||||
hasBeenLogged = true;
|
||||
}
|
||||
const env = config.env || {};
|
||||
const descrs = Object.getOwnPropertyDescriptors(env);
|
||||
this.env = Object.freeze(Object.create(Object.getPrototypeOf(env), descrs));
|
||||
this.props = config.props || ({} as P);
|
||||
}
|
||||
|
||||
mount(target: HTMLElement, options?: MountOptions): Promise<Component<P, E> & InstanceType<T>> {
|
||||
App.validateTarget(target);
|
||||
if (this.dev) {
|
||||
validateProps(this.Root, this.props, { __owl__: { app: this } });
|
||||
}
|
||||
const node = this.makeNode(this.Root, this.props);
|
||||
const prom = this.mountNode(node, target, options);
|
||||
this.root = node;
|
||||
return prom;
|
||||
}
|
||||
|
||||
makeNode(Component: ComponentConstructor, props: any): ComponentNode {
|
||||
return new ComponentNode(Component, props, this, null, null);
|
||||
}
|
||||
|
||||
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
|
||||
const promise: any = new Promise((resolve, reject) => {
|
||||
let isResolved = false;
|
||||
// manually set a onMounted callback.
|
||||
// that way, we are independant from the current node.
|
||||
node.mounted.push(() => {
|
||||
resolve(node.component);
|
||||
isResolved = true;
|
||||
});
|
||||
|
||||
// Manually add the last resort error handler on the node
|
||||
let handlers = nodeErrorHandlers.get(node);
|
||||
if (!handlers) {
|
||||
handlers = [];
|
||||
nodeErrorHandlers.set(node, handlers);
|
||||
}
|
||||
handlers.unshift((e) => {
|
||||
if (isResolved) {
|
||||
console.error(e);
|
||||
} else {
|
||||
reject(e);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
node.mountComponent(target, options);
|
||||
return promise;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.root) {
|
||||
this.scheduler.flush();
|
||||
this.root.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
createComponent<P extends Props>(
|
||||
name: string | null,
|
||||
isStatic: boolean,
|
||||
hasSlotsProp: boolean,
|
||||
hasDynamicPropList: boolean,
|
||||
hasNoProp: boolean
|
||||
) {
|
||||
const isDynamic = !isStatic;
|
||||
function _arePropsDifferent(props1: Props, props2: Props): boolean {
|
||||
for (let k in props1) {
|
||||
if (props1[k] !== props2[k]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return hasDynamicPropList && Object.keys(props1).length !== Object.keys(props2).length;
|
||||
}
|
||||
const arePropsDifferent = hasSlotsProp
|
||||
? (_1: any, _2: any) => true
|
||||
: hasNoProp
|
||||
? (_1: any, _2: any) => false
|
||||
: _arePropsDifferent;
|
||||
const updateAndRender = ComponentNode.prototype.updateAndRender;
|
||||
const initiateRender = ComponentNode.prototype.initiateRender;
|
||||
|
||||
return (props: P, key: string, ctx: ComponentNode, parent: any, C: any) => {
|
||||
let children = ctx.children;
|
||||
let node: any = children[key];
|
||||
if (isDynamic && node && node.component.constructor !== C) {
|
||||
node = undefined;
|
||||
}
|
||||
const parentFiber = ctx.fiber!;
|
||||
if (node) {
|
||||
if (arePropsDifferent(node.props, props) || parentFiber.deep || node.forceNextRender) {
|
||||
node.forceNextRender = false;
|
||||
updateAndRender.call(node, props, parentFiber);
|
||||
}
|
||||
} else {
|
||||
// new component
|
||||
if (isStatic) {
|
||||
C = parent.constructor.components[name as any];
|
||||
if (!C) {
|
||||
throw new Error(`Cannot find the definition of component "${name}"`);
|
||||
} else if (!(C.prototype instanceof Component)) {
|
||||
throw new Error(
|
||||
`"${name}" is not a Component. It must inherit from the Component class`
|
||||
);
|
||||
}
|
||||
}
|
||||
node = new ComponentNode(C, props, this, ctx, key);
|
||||
children[key] = node;
|
||||
initiateRender.call(node, new Fiber(node, parentFiber));
|
||||
}
|
||||
parentFiber.childrenMap[key] = node;
|
||||
return node;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function mount<
|
||||
T extends abstract new (...args: any) => any = any,
|
||||
P extends object = any,
|
||||
E = any
|
||||
>(
|
||||
C: T & ComponentConstructor<P, E>,
|
||||
target: HTMLElement,
|
||||
config: AppConfig<P, E> & MountOptions = {}
|
||||
): Promise<Component<P, E> & InstanceType<T>> {
|
||||
return new App(C, config).mount(target, config);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { Setter } from "./block_compiler";
|
||||
|
||||
const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype;
|
||||
const tokenList = DOMTokenList.prototype;
|
||||
const tokenListAdd = tokenList.add;
|
||||
const tokenListRemove = tokenList.remove;
|
||||
const isArray = Array.isArray;
|
||||
const { split, trim } = String.prototype;
|
||||
const wordRegexp = /\s+/;
|
||||
|
||||
/**
|
||||
* We regroup here all code related to updating attributes in a very loose sense:
|
||||
* attributes, properties and classs are all managed by the functions in this
|
||||
* file.
|
||||
*/
|
||||
|
||||
function setAttribute(this: HTMLElement, key: string, value: any) {
|
||||
switch (value) {
|
||||
case false:
|
||||
case undefined:
|
||||
removeAttribute.call(this, key);
|
||||
break;
|
||||
case true:
|
||||
elemSetAttribute.call(this, key, "");
|
||||
break;
|
||||
default:
|
||||
elemSetAttribute.call(this, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
||||
return function (this: HTMLElement, value: any) {
|
||||
setAttribute.call(this, attr, value);
|
||||
};
|
||||
}
|
||||
|
||||
export function attrsSetter(this: HTMLElement, attrs: any) {
|
||||
if (isArray(attrs)) {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
} else {
|
||||
for (let k in attrs) {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
||||
if (isArray(attrs)) {
|
||||
const name = attrs[0];
|
||||
const val = attrs[1];
|
||||
if (name === oldAttrs[0]) {
|
||||
if (val === oldAttrs[1]) {
|
||||
return;
|
||||
}
|
||||
setAttribute.call(this, name, val);
|
||||
} else {
|
||||
removeAttribute.call(this, oldAttrs[0]);
|
||||
setAttribute.call(this, name, val);
|
||||
}
|
||||
} else {
|
||||
for (let k in oldAttrs) {
|
||||
if (!(k in attrs)) {
|
||||
removeAttribute.call(this, k);
|
||||
}
|
||||
}
|
||||
for (let k in attrs) {
|
||||
const val = attrs[k];
|
||||
if (val !== oldAttrs[k]) {
|
||||
setAttribute.call(this, k, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toClassObj(expr: string | number | { [c: string]: any }) {
|
||||
const result: { [c: string]: any } = {};
|
||||
switch (typeof expr) {
|
||||
case "string":
|
||||
// we transform here a list of classes into an object:
|
||||
// 'hey you' becomes {hey: true, you: true}
|
||||
const str = trim.call(expr);
|
||||
if (!str) {
|
||||
return {};
|
||||
}
|
||||
let words = split.call(str, wordRegexp);
|
||||
for (let i = 0, l = words.length; i < l; i++) {
|
||||
result[words[i]] = true;
|
||||
}
|
||||
return result;
|
||||
case "object":
|
||||
// this is already an object but we may need to split keys:
|
||||
// {'a': true, 'b c': true} should become {a: true, b: true, c: true}
|
||||
for (let key in expr as any) {
|
||||
const value = (expr as any)[key];
|
||||
if (value) {
|
||||
key = trim.call(key);
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
const words = split.call(key, wordRegexp);
|
||||
for (let word of words) {
|
||||
result[word] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
case "undefined":
|
||||
return {};
|
||||
case "number":
|
||||
return { [expr as number]: true };
|
||||
default:
|
||||
return { [expr as any]: true };
|
||||
}
|
||||
}
|
||||
|
||||
export function setClass(this: HTMLElement, val: any) {
|
||||
val = val === "" ? {} : toClassObj(val);
|
||||
// add classes
|
||||
const cl = this.classList;
|
||||
for (let c in val) {
|
||||
tokenListAdd.call(cl, c);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateClass(this: HTMLElement, val: any, oldVal: any) {
|
||||
oldVal = oldVal === "" ? {} : toClassObj(oldVal);
|
||||
val = val === "" ? {} : toClassObj(val);
|
||||
const cl = this.classList;
|
||||
// remove classes
|
||||
for (let c in oldVal) {
|
||||
if (!(c in val)) {
|
||||
tokenListRemove.call(cl, c);
|
||||
}
|
||||
}
|
||||
// add classes
|
||||
for (let c in val) {
|
||||
if (!(c in oldVal)) {
|
||||
tokenListAdd.call(cl, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function makePropSetter(name: string): Setter<HTMLElement> {
|
||||
return function setProp(this: HTMLElement, value: any) {
|
||||
// support 0, fallback to empty string for other falsy values
|
||||
(this as any)[name] = value === 0 ? 0 : value || "";
|
||||
};
|
||||
}
|
||||
|
||||
export function isProp(tag: string, key: string): boolean {
|
||||
switch (tag) {
|
||||
case "input":
|
||||
return (
|
||||
key === "checked" ||
|
||||
key === "indeterminate" ||
|
||||
key === "value" ||
|
||||
key === "readonly" ||
|
||||
key === "disabled"
|
||||
);
|
||||
case "option":
|
||||
return key === "selected" || key === "disabled";
|
||||
case "textarea":
|
||||
return key === "value" || key === "readonly" || key === "disabled";
|
||||
case "select":
|
||||
return key === "value" || key === "disabled";
|
||||
case "button":
|
||||
case "optgroup":
|
||||
return key === "disabled";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
import {
|
||||
attrsSetter,
|
||||
attrsUpdater,
|
||||
createAttrUpdater,
|
||||
isProp,
|
||||
makePropSetter,
|
||||
setClass,
|
||||
updateClass,
|
||||
} from "./attributes";
|
||||
import { config } from "./config";
|
||||
import { createEventHandler } from "./events";
|
||||
import type { VNode } from "./index";
|
||||
import { VMulti } from "./multi";
|
||||
import { toText } from "./text";
|
||||
|
||||
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
|
||||
const nodeProto = Node.prototype;
|
||||
const elementProto = Element.prototype;
|
||||
const characterDataProto = CharacterData.prototype;
|
||||
|
||||
const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
|
||||
const nodeGetFirstChild = getDescriptor(nodeProto, "firstChild").get!;
|
||||
const nodeGetNextSibling = getDescriptor(nodeProto, "nextSibling").get!;
|
||||
|
||||
const NO_OP = () => {};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Main compiler code
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type BlockType = (data?: any[], children?: VNode[]) => VNode;
|
||||
|
||||
const cache: { [key: string]: BlockType } = {};
|
||||
|
||||
/**
|
||||
* Compiling blocks is a multi-step process:
|
||||
*
|
||||
* 1. build an IntermediateTree from the HTML element. This intermediate tree
|
||||
* is a binary tree structure that encode dynamic info sub nodes, and the
|
||||
* path required to reach them
|
||||
* 2. process the tree to build a block context, which is an object that aggregate
|
||||
* all dynamic info in a list, and also, all ref indexes.
|
||||
* 3. process the context to build appropriate builder/setter functions
|
||||
* 4. make a dynamic block class, which will efficiently collect references and
|
||||
* create/update dynamic locations/children
|
||||
*
|
||||
* @param str
|
||||
* @returns a new block type, that can build concrete blocks
|
||||
*/
|
||||
export function createBlock(str: string): BlockType {
|
||||
if (str in cache) {
|
||||
return cache[str];
|
||||
}
|
||||
|
||||
// step 0: prepare html base element
|
||||
const doc = new DOMParser().parseFromString(`<t>${str}</t>`, "text/xml");
|
||||
const node = doc.firstChild!.firstChild!;
|
||||
if (config.shouldNormalizeDom) {
|
||||
normalizeNode(node as any);
|
||||
}
|
||||
|
||||
// step 1: prepare intermediate tree
|
||||
const tree = buildTree(node);
|
||||
|
||||
// step 2: prepare block context
|
||||
const context = buildContext(tree);
|
||||
|
||||
// step 3: build the final block class
|
||||
const template = tree.el as HTMLElement;
|
||||
const Block = buildBlock(template, context);
|
||||
cache[str] = Block;
|
||||
return Block;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helper
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function normalizeNode(node: HTMLElement | Text) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
if (!/\S/.test((node as Text).textContent!)) {
|
||||
(node as Text).remove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
if ((node as HTMLElement).tagName === "pre") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (let i = node.childNodes.length - 1; i >= 0; --i) {
|
||||
normalizeNode(node.childNodes.item(i) as any);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// building a intermediate tree
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface DynamicInfo {
|
||||
idx: number;
|
||||
refIdx?: number;
|
||||
type: "text" | "child" | "handler" | "attribute" | "attributes" | "ref";
|
||||
isOnlyChild?: boolean;
|
||||
name?: string;
|
||||
tag?: string;
|
||||
event?: string;
|
||||
}
|
||||
|
||||
interface IntermediateTree {
|
||||
parent: IntermediateTree | null;
|
||||
firstChild: IntermediateTree | null;
|
||||
nextSibling: IntermediateTree | null;
|
||||
el: Node;
|
||||
info: DynamicInfo[];
|
||||
isRef?: boolean;
|
||||
refIdx?: number;
|
||||
refN: number;
|
||||
currentNS: string | null;
|
||||
}
|
||||
|
||||
function buildTree(
|
||||
node: Node,
|
||||
parent: IntermediateTree | null = null,
|
||||
domParentTree: IntermediateTree | null = null
|
||||
): IntermediateTree {
|
||||
switch (node.nodeType) {
|
||||
case Node.ELEMENT_NODE: {
|
||||
// HTMLElement
|
||||
let currentNS = domParentTree && domParentTree.currentNS;
|
||||
const tagName = (node as Element).tagName;
|
||||
let el: Node | undefined = undefined;
|
||||
const info: DynamicInfo[] = [];
|
||||
if (tagName.startsWith("block-text-")) {
|
||||
const index = parseInt(tagName.slice(11), 10);
|
||||
info.push({ type: "text", idx: index });
|
||||
el = document.createTextNode("");
|
||||
}
|
||||
if (tagName.startsWith("block-child-")) {
|
||||
if (!domParentTree!.isRef) {
|
||||
addRef(domParentTree!);
|
||||
}
|
||||
const index = parseInt(tagName.slice(12), 10);
|
||||
info.push({ type: "child", idx: index });
|
||||
el = document.createTextNode("");
|
||||
}
|
||||
const attrs = (node as Element).attributes;
|
||||
const ns = attrs.getNamedItem("block-ns");
|
||||
if (ns) {
|
||||
attrs.removeNamedItem("block-ns");
|
||||
currentNS = ns.value;
|
||||
}
|
||||
if (!el) {
|
||||
el = currentNS
|
||||
? document.createElementNS(currentNS, tagName)
|
||||
: document.createElement(tagName);
|
||||
}
|
||||
if (el instanceof Element) {
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
const attrName = attrs[i].name;
|
||||
const attrValue = attrs[i].value;
|
||||
if (attrName.startsWith("block-handler-")) {
|
||||
const idx = parseInt(attrName.slice(14), 10);
|
||||
info.push({
|
||||
type: "handler",
|
||||
idx,
|
||||
event: attrValue,
|
||||
});
|
||||
} else if (attrName.startsWith("block-attribute-")) {
|
||||
const idx = parseInt(attrName.slice(16), 10);
|
||||
info.push({
|
||||
type: "attribute",
|
||||
idx,
|
||||
name: attrValue,
|
||||
tag: tagName,
|
||||
});
|
||||
} else if (attrName === "block-attributes") {
|
||||
info.push({
|
||||
type: "attributes",
|
||||
idx: parseInt(attrValue, 10),
|
||||
});
|
||||
} else if (attrName === "block-ref") {
|
||||
info.push({
|
||||
type: "ref",
|
||||
idx: parseInt(attrValue, 10),
|
||||
});
|
||||
} else {
|
||||
el.setAttribute(attrs[i].name, attrValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tree: IntermediateTree = {
|
||||
parent,
|
||||
firstChild: null,
|
||||
nextSibling: null,
|
||||
el,
|
||||
info,
|
||||
refN: 0,
|
||||
currentNS,
|
||||
};
|
||||
|
||||
if (node.firstChild) {
|
||||
const childNode = node.childNodes[0];
|
||||
if (
|
||||
node.childNodes.length === 1 &&
|
||||
childNode.nodeType === Node.ELEMENT_NODE &&
|
||||
(childNode as Element).tagName.startsWith("block-child-")
|
||||
) {
|
||||
const tagName = (childNode as Element).tagName;
|
||||
const index = parseInt(tagName.slice(12), 10);
|
||||
info.push({ idx: index, type: "child", isOnlyChild: true });
|
||||
} else {
|
||||
tree.firstChild = buildTree(node.firstChild, tree, tree);
|
||||
el.appendChild(tree.firstChild.el);
|
||||
let curNode: Node | null = node.firstChild;
|
||||
let curTree: IntermediateTree | null = tree.firstChild;
|
||||
while ((curNode = curNode.nextSibling)) {
|
||||
curTree.nextSibling = buildTree(curNode, curTree, tree);
|
||||
el.appendChild(curTree.nextSibling.el);
|
||||
curTree = curTree.nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tree.info.length) {
|
||||
addRef(tree);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
case Node.TEXT_NODE:
|
||||
case Node.COMMENT_NODE: {
|
||||
// text node or comment node
|
||||
const el =
|
||||
node.nodeType === Node.TEXT_NODE
|
||||
? document.createTextNode(node.textContent!)
|
||||
: document.createComment(node.textContent!);
|
||||
return {
|
||||
parent: parent,
|
||||
firstChild: null,
|
||||
nextSibling: null,
|
||||
el,
|
||||
info: [],
|
||||
refN: 0,
|
||||
currentNS: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error("boom");
|
||||
}
|
||||
|
||||
function addRef(tree: IntermediateTree) {
|
||||
tree.isRef = true;
|
||||
do {
|
||||
tree.refN++;
|
||||
} while ((tree = tree.parent as any));
|
||||
}
|
||||
|
||||
function parentTree(tree: IntermediateTree): IntermediateTree | null {
|
||||
let parent = tree.parent;
|
||||
while (parent && parent.nextSibling === tree) {
|
||||
tree = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Building a block context
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface RefCollector {
|
||||
idx: number;
|
||||
prevIdx: number;
|
||||
getVal: Function;
|
||||
}
|
||||
|
||||
export type Setter<T = any> = (this: T, value: any) => void;
|
||||
export type Updater<T = any> = (this: T, value: any, oldVal: any) => void;
|
||||
|
||||
interface Location {
|
||||
refIdx: number;
|
||||
setData: Setter;
|
||||
updateData: Updater;
|
||||
}
|
||||
|
||||
interface IndexedLocation extends Location {
|
||||
idx: number;
|
||||
}
|
||||
|
||||
interface Child {
|
||||
parentRefIdx: number;
|
||||
afterRefIdx?: number;
|
||||
isOnlyChild?: boolean;
|
||||
}
|
||||
|
||||
interface BlockCtx {
|
||||
refN: number;
|
||||
collectors: RefCollector[];
|
||||
locations: IndexedLocation[];
|
||||
children: Child[];
|
||||
cbRefs: number[];
|
||||
refList: (() => void)[][];
|
||||
}
|
||||
|
||||
function buildContext(tree: IntermediateTree, ctx?: BlockCtx, fromIdx?: number): BlockCtx {
|
||||
if (!ctx) {
|
||||
const children = new Array(tree.info.filter((v) => v.type === "child").length);
|
||||
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN, refList: [] };
|
||||
fromIdx = 0;
|
||||
}
|
||||
if (tree.refN) {
|
||||
const initialIdx = fromIdx!;
|
||||
const isRef = tree.isRef;
|
||||
const firstChild = tree.firstChild ? tree.firstChild.refN : 0;
|
||||
const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0;
|
||||
|
||||
//node
|
||||
if (isRef) {
|
||||
for (let info of tree.info) {
|
||||
info.refIdx = initialIdx!;
|
||||
}
|
||||
tree.refIdx = initialIdx!;
|
||||
updateCtx(ctx, tree);
|
||||
fromIdx!++;
|
||||
}
|
||||
|
||||
// right
|
||||
if (nextSibling) {
|
||||
const idx = fromIdx! + firstChild;
|
||||
ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling });
|
||||
buildContext(tree.nextSibling!, ctx, idx);
|
||||
}
|
||||
|
||||
// left
|
||||
if (firstChild) {
|
||||
ctx.collectors.push({ idx: fromIdx!, prevIdx: initialIdx, getVal: nodeGetFirstChild });
|
||||
buildContext(tree.firstChild!, ctx, fromIdx!);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
|
||||
for (let info of tree.info) {
|
||||
switch (info.type) {
|
||||
case "text":
|
||||
ctx.locations.push({
|
||||
idx: info.idx,
|
||||
refIdx: info.refIdx!,
|
||||
setData: setText,
|
||||
updateData: setText,
|
||||
});
|
||||
break;
|
||||
case "child":
|
||||
if (info.isOnlyChild) {
|
||||
// tree is the parentnode here
|
||||
ctx.children[info.idx] = {
|
||||
parentRefIdx: info.refIdx!,
|
||||
isOnlyChild: true,
|
||||
};
|
||||
} else {
|
||||
// tree is the anchor text node
|
||||
ctx.children[info.idx] = {
|
||||
parentRefIdx: parentTree(tree)!.refIdx!,
|
||||
afterRefIdx: info.refIdx!,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "attribute": {
|
||||
const refIdx = info.refIdx!;
|
||||
let updater: any;
|
||||
let setter: any;
|
||||
if (isProp(info.tag!, info.name!)) {
|
||||
const setProp = makePropSetter(info.name!);
|
||||
setter = setProp;
|
||||
updater = setProp;
|
||||
} else if (info.name === "class") {
|
||||
setter = setClass;
|
||||
updater = updateClass;
|
||||
} else {
|
||||
setter = createAttrUpdater(info.name!);
|
||||
updater = setter;
|
||||
}
|
||||
ctx.locations.push({
|
||||
idx: info.idx,
|
||||
refIdx,
|
||||
setData: setter,
|
||||
updateData: updater,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "attributes":
|
||||
ctx.locations.push({
|
||||
idx: info.idx,
|
||||
refIdx: info.refIdx!,
|
||||
setData: attrsSetter,
|
||||
updateData: attrsUpdater,
|
||||
});
|
||||
break;
|
||||
case "handler": {
|
||||
const { setup, update } = createEventHandler(info.event!);
|
||||
ctx.locations.push({
|
||||
idx: info.idx,
|
||||
refIdx: info.refIdx!,
|
||||
setData: setup,
|
||||
updateData: update,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ref":
|
||||
const index = ctx.cbRefs.push(info.idx) - 1;
|
||||
ctx.locations.push({
|
||||
idx: info.idx,
|
||||
refIdx: info.refIdx!,
|
||||
setData: makeRefSetter(index, ctx.refList),
|
||||
updateData: NO_OP,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// building the concrete block class
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function buildBlock(template: HTMLElement, ctx: BlockCtx): BlockType {
|
||||
let B = createBlockClass(template, ctx);
|
||||
|
||||
if (ctx.cbRefs.length) {
|
||||
const cbRefs = ctx.cbRefs;
|
||||
const refList = ctx.refList;
|
||||
let cbRefsNumber = cbRefs.length;
|
||||
B = class extends B {
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
refList.push(new Array(cbRefsNumber));
|
||||
super.mount(parent, afterNode);
|
||||
for (let cbRef of refList.pop()!) {
|
||||
cbRef();
|
||||
}
|
||||
}
|
||||
remove() {
|
||||
super.remove();
|
||||
for (let cbRef of cbRefs) {
|
||||
let fn = (this as any).data[cbRef];
|
||||
fn(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (ctx.children.length) {
|
||||
B = class extends B {
|
||||
children: (VNode | undefined)[] | undefined;
|
||||
constructor(data?: any[], children?: VNode[]) {
|
||||
super(data);
|
||||
this.children = children;
|
||||
}
|
||||
};
|
||||
B.prototype.beforeRemove = VMulti.prototype.beforeRemove;
|
||||
return (data?: any[], children: (VNode | undefined)[] = []) => new B(data, children);
|
||||
}
|
||||
|
||||
return (data?: any[]) => new B(data);
|
||||
}
|
||||
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
type BlockClass = Constructor<VNode<any>>;
|
||||
|
||||
function createBlockClass(template: HTMLElement, ctx: BlockCtx): BlockClass {
|
||||
const { refN, collectors, children } = ctx;
|
||||
const colN = collectors.length;
|
||||
ctx.locations.sort((a, b) => a.idx - b.idx);
|
||||
const locations: Location[] = ctx.locations.map((loc) => ({
|
||||
refIdx: loc.refIdx,
|
||||
setData: loc.setData,
|
||||
updateData: loc.updateData,
|
||||
}));
|
||||
const locN = locations.length;
|
||||
const childN = children.length;
|
||||
const childrenLocs = children;
|
||||
const isDynamic = refN > 0;
|
||||
|
||||
// these values are defined here to make them faster to lookup in the class
|
||||
// block scope
|
||||
const nodeCloneNode = nodeProto.cloneNode;
|
||||
const nodeInsertBefore = nodeProto.insertBefore;
|
||||
const elementRemove = elementProto.remove;
|
||||
|
||||
class Block {
|
||||
el: HTMLElement | undefined;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
data: any[] | undefined;
|
||||
children?: (VNode | undefined)[];
|
||||
refs: Node[] | undefined;
|
||||
|
||||
constructor(data?: any[]) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
beforeRemove() {}
|
||||
|
||||
remove() {
|
||||
elementRemove.call(this.el);
|
||||
}
|
||||
|
||||
firstNode(): Node {
|
||||
return this.el!;
|
||||
}
|
||||
|
||||
moveBefore(other: Block | null, afterNode: Node | null) {
|
||||
const target = other ? other.el! : afterNode;
|
||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
||||
}
|
||||
|
||||
toString() {
|
||||
const div = document.createElement("div");
|
||||
this.mount(div, null);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
const el = nodeCloneNode.call(template, true) as HTMLElement;
|
||||
nodeInsertBefore.call(parent, el, afterNode);
|
||||
this.el = el;
|
||||
this.parentEl = parent;
|
||||
}
|
||||
patch(other: Block, withBeforeRemove: boolean) {}
|
||||
}
|
||||
|
||||
if (isDynamic) {
|
||||
Block.prototype.mount = function mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
const el = nodeCloneNode.call(template, true);
|
||||
// collecting references
|
||||
const refs: Node[] = new Array(refN);
|
||||
this.refs = refs;
|
||||
refs[0] = el;
|
||||
for (let i = 0; i < colN; i++) {
|
||||
const w = collectors[i];
|
||||
refs[w.idx] = w.getVal.call(refs[w.prevIdx]);
|
||||
}
|
||||
|
||||
// applying data to all update points
|
||||
if (locN) {
|
||||
const data = this.data!;
|
||||
for (let i = 0; i < locN; i++) {
|
||||
const loc = locations[i];
|
||||
loc.setData.call(refs[loc.refIdx], data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
nodeInsertBefore.call(parent, el, afterNode);
|
||||
|
||||
// preparing all children
|
||||
if (childN) {
|
||||
const children = this.children;
|
||||
for (let i = 0; i < childN; i++) {
|
||||
const child = children![i];
|
||||
if (child) {
|
||||
const loc = childrenLocs[i];
|
||||
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
|
||||
child.isOnlyChild = loc.isOnlyChild;
|
||||
child.mount(refs[loc.parentRefIdx] as any, afterNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.el = el as HTMLElement;
|
||||
this.parentEl = parent;
|
||||
};
|
||||
|
||||
Block.prototype.patch = function patch(other: Block, withBeforeRemove: boolean) {
|
||||
if (this === other) {
|
||||
return;
|
||||
}
|
||||
const refs = this.refs!;
|
||||
// update texts/attributes/
|
||||
if (locN) {
|
||||
const data1 = this.data!;
|
||||
const data2 = other.data!;
|
||||
for (let i = 0; i < locN; i++) {
|
||||
const val1 = data1[i];
|
||||
const val2 = data2[i];
|
||||
if (val1 !== val2) {
|
||||
const loc = locations[i];
|
||||
loc.updateData.call(refs[loc.refIdx], val2, val1);
|
||||
}
|
||||
}
|
||||
this.data = data2;
|
||||
}
|
||||
|
||||
// update children
|
||||
if (childN) {
|
||||
let children1 = this.children;
|
||||
const children2 = other.children;
|
||||
for (let i = 0; i < childN; i++) {
|
||||
const child1 = children1![i];
|
||||
const child2 = children2![i];
|
||||
if (child1) {
|
||||
if (child2) {
|
||||
child1.patch(child2, withBeforeRemove);
|
||||
} else {
|
||||
if (withBeforeRemove) {
|
||||
child1.beforeRemove();
|
||||
}
|
||||
child1.remove();
|
||||
children1![i] = undefined;
|
||||
}
|
||||
} else if (child2) {
|
||||
const loc = childrenLocs[i];
|
||||
const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null;
|
||||
child2.mount(refs[loc.parentRefIdx] as any, afterNode);
|
||||
children1![i] = child2;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return Block;
|
||||
}
|
||||
|
||||
function setText(this: Text, value: any) {
|
||||
characterDataSetData.call(this, toText(value));
|
||||
}
|
||||
|
||||
function makeRefSetter(index: number, refs: (() => void)[][]): Setter<HTMLElement> {
|
||||
return function setRef(this: HTMLElement, fn: any) {
|
||||
refs[refs.length - 1][index] = () => fn(this);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export function filterOutModifiersFromData(dataList: any[]): { modifiers: string[]; data: any[] } {
|
||||
dataList = dataList.slice();
|
||||
const modifiers = [];
|
||||
let elm;
|
||||
while ((elm = dataList[0]) && typeof elm === "string") {
|
||||
modifiers.push(dataList.shift());
|
||||
}
|
||||
return { modifiers, data: dataList };
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// whether or not blockdom should normalize DOM whenever a block is created.
|
||||
// Normalizing dom mean removing empty text nodes (or containing only spaces)
|
||||
shouldNormalizeDom: true,
|
||||
|
||||
// this is the main event handler. Every event handler registered with blockdom
|
||||
// will go through this function, giving it the data registered in the block
|
||||
// and the event
|
||||
mainEventHandler: (data: any, ev: Event, currentTarget?: EventTarget | null): boolean => {
|
||||
if (typeof data === "function") {
|
||||
data(ev);
|
||||
} else if (Array.isArray(data)) {
|
||||
data = filterOutModifiersFromData(data).data;
|
||||
data[0](data[1], ev);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import { createEventHandler } from "./events";
|
||||
import type { VNode } from "./index";
|
||||
|
||||
type EventsSpec = { [name: string]: number };
|
||||
|
||||
type Catcher = (child: VNode, handlers: any[]) => VNode;
|
||||
|
||||
export function createCatcher(eventsSpec: EventsSpec): Catcher {
|
||||
const n = Object.keys(eventsSpec).length;
|
||||
|
||||
class VCatcher {
|
||||
child: VNode;
|
||||
handlerData: any[];
|
||||
handlerFns: any[] = [];
|
||||
|
||||
parentEl?: HTMLElement | undefined;
|
||||
afterNode: Text | null = null;
|
||||
|
||||
constructor(child: VNode, handlers: any[]) {
|
||||
this.child = child;
|
||||
this.handlerData = handlers;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
this.parentEl = parent;
|
||||
this.child.mount(parent, afterNode);
|
||||
this.afterNode = document.createTextNode("");
|
||||
parent.insertBefore(this.afterNode, afterNode);
|
||||
this.wrapHandlerData();
|
||||
for (let name in eventsSpec) {
|
||||
const index = eventsSpec[name];
|
||||
const handler = createEventHandler(name);
|
||||
this.handlerFns[index] = handler;
|
||||
handler.setup.call(parent, this.handlerData[index]);
|
||||
}
|
||||
}
|
||||
|
||||
wrapHandlerData() {
|
||||
for (let i = 0; i < n; i++) {
|
||||
let handler = this.handlerData[i];
|
||||
// handler = [...mods, fn, comp], so we need to replace second to last elem
|
||||
let idx = handler.length - 2;
|
||||
let origFn = handler[idx];
|
||||
const self = this;
|
||||
handler[idx] = function (ev: any) {
|
||||
const target = ev.target;
|
||||
let currentNode: any = self.child.firstNode();
|
||||
const afterNode = self.afterNode;
|
||||
while (currentNode !== afterNode) {
|
||||
if (currentNode.contains(target)) {
|
||||
return origFn.call(this, ev);
|
||||
}
|
||||
currentNode = currentNode.nextSibling;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
moveBefore(other: VCatcher | null, afterNode: Node | null) {
|
||||
this.child.moveBefore(other ? other.child : null, afterNode);
|
||||
this.parentEl!.insertBefore(this.afterNode!, afterNode);
|
||||
}
|
||||
|
||||
patch(other: VCatcher, withBeforeRemove: boolean) {
|
||||
if (this === other) {
|
||||
return;
|
||||
}
|
||||
this.handlerData = other.handlerData;
|
||||
this.wrapHandlerData();
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.handlerFns[i].update.call(this.parentEl!, this.handlerData[i]);
|
||||
}
|
||||
|
||||
this.child.patch(other.child, withBeforeRemove);
|
||||
}
|
||||
|
||||
beforeRemove() {
|
||||
this.child.beforeRemove();
|
||||
}
|
||||
|
||||
remove() {
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.handlerFns[i].remove.call(this.parentEl!);
|
||||
}
|
||||
this.child.remove();
|
||||
this.afterNode!.remove();
|
||||
}
|
||||
|
||||
firstNode(): Node | undefined {
|
||||
return this.child.firstNode();
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.child.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return function (child: VNode, handlers: any[]): VNode<VCatcher> {
|
||||
return new VCatcher(child, handlers);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { config } from "./config";
|
||||
|
||||
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
||||
|
||||
interface EventHandlerCreator {
|
||||
setup: EventHandlerSetter;
|
||||
update: EventHandlerSetter;
|
||||
remove: (this: HTMLElement) => void;
|
||||
}
|
||||
|
||||
export function createEventHandler(rawEvent: string): EventHandlerCreator {
|
||||
const eventName = rawEvent.split(".")[0];
|
||||
const capture = rawEvent.includes(".capture");
|
||||
if (rawEvent.includes(".synthetic")) {
|
||||
return createSyntheticHandler(eventName, capture);
|
||||
} else {
|
||||
return createElementHandler(eventName, capture);
|
||||
}
|
||||
}
|
||||
|
||||
// Native listener
|
||||
let nextNativeEventId = 1;
|
||||
function createElementHandler(evName: string, capture: boolean = false): EventHandlerCreator {
|
||||
let eventKey = `__event__${evName}_${nextNativeEventId++}`;
|
||||
if (capture) {
|
||||
eventKey = `${eventKey}_capture`;
|
||||
}
|
||||
|
||||
function listener(ev: Event) {
|
||||
const currentTarget = ev.currentTarget;
|
||||
if (!currentTarget || !document.contains(currentTarget as HTMLElement)) return;
|
||||
const data = (currentTarget as any)[eventKey];
|
||||
if (!data) return;
|
||||
config.mainEventHandler(data, ev, currentTarget);
|
||||
}
|
||||
|
||||
function setup(this: HTMLElement, data: any) {
|
||||
(this as any)[eventKey] = data;
|
||||
this.addEventListener(evName, listener, { capture });
|
||||
}
|
||||
|
||||
function remove(this: HTMLElement) {
|
||||
delete (this as any)[eventKey];
|
||||
this.removeEventListener(evName, listener, { capture });
|
||||
}
|
||||
function update(this: HTMLElement, data: any) {
|
||||
(this as any)[eventKey] = data;
|
||||
}
|
||||
|
||||
return { setup, update, remove };
|
||||
}
|
||||
|
||||
// Synthetic handler: a form of event delegation that allows placing only one
|
||||
// listener per event type.
|
||||
let nextSyntheticEventId = 1;
|
||||
function createSyntheticHandler(evName: string, capture: boolean = false): EventHandlerCreator {
|
||||
let eventKey = `__event__synthetic_${evName}`;
|
||||
if (capture) {
|
||||
eventKey = `${eventKey}_capture`;
|
||||
}
|
||||
setupSyntheticEvent(evName, eventKey, capture);
|
||||
const currentId = nextSyntheticEventId++;
|
||||
function setup(this: HTMLElement, data: any) {
|
||||
const _data = (this as any)[eventKey] || {};
|
||||
_data[currentId] = data;
|
||||
(this as any)[eventKey] = _data;
|
||||
}
|
||||
|
||||
function remove(this: HTMLElement) {
|
||||
delete (this as any)[eventKey];
|
||||
}
|
||||
|
||||
return { setup, update: setup, remove };
|
||||
}
|
||||
|
||||
function nativeToSyntheticEvent(eventKey: string, event: Event) {
|
||||
let dom = event.target;
|
||||
while (dom !== null) {
|
||||
const _data = (dom as any)[eventKey];
|
||||
if (_data) {
|
||||
for (const data of Object.values(_data)) {
|
||||
const stopped = config.mainEventHandler(data, event, dom);
|
||||
if (stopped) return;
|
||||
}
|
||||
}
|
||||
dom = (dom as any).parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {};
|
||||
|
||||
function setupSyntheticEvent(evName: string, eventKey: string, capture: boolean = false) {
|
||||
if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {
|
||||
return;
|
||||
}
|
||||
document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), {
|
||||
capture,
|
||||
});
|
||||
CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { VNode } from "./index";
|
||||
|
||||
const nodeProto = Node.prototype;
|
||||
|
||||
const nodeInsertBefore = nodeProto.insertBefore;
|
||||
const nodeRemoveChild = nodeProto.removeChild;
|
||||
|
||||
class VHtml {
|
||||
html: string;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
content: ChildNode[] = [];
|
||||
|
||||
constructor(html: string) {
|
||||
this.html = html;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
this.parentEl = parent;
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = this.html;
|
||||
this.content = [...(template.content.childNodes as any)];
|
||||
for (let elem of this.content) {
|
||||
nodeInsertBefore.call(parent, elem, afterNode);
|
||||
}
|
||||
if (!this.content.length) {
|
||||
const textNode = document.createTextNode("");
|
||||
this.content.push(textNode);
|
||||
nodeInsertBefore.call(parent, textNode, afterNode);
|
||||
}
|
||||
}
|
||||
|
||||
moveBefore(other: VHtml | null, afterNode: Node | null) {
|
||||
const target = other ? other.content[0] : afterNode;
|
||||
const parent = this.parentEl;
|
||||
for (let elem of this.content) {
|
||||
nodeInsertBefore.call(parent, elem, target);
|
||||
}
|
||||
}
|
||||
|
||||
patch(other: VHtml) {
|
||||
if (this === other) {
|
||||
return;
|
||||
}
|
||||
const html2 = other.html;
|
||||
if (this.html !== html2) {
|
||||
const parent = this.parentEl;
|
||||
// insert new html in front of current
|
||||
const afterNode = this.content[0];
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = html2;
|
||||
const content = [...(template.content.childNodes as any)];
|
||||
for (let elem of content) {
|
||||
nodeInsertBefore.call(parent, elem, afterNode);
|
||||
}
|
||||
if (!content.length) {
|
||||
const textNode = document.createTextNode("");
|
||||
content.push(textNode);
|
||||
nodeInsertBefore.call(parent, textNode, afterNode);
|
||||
}
|
||||
|
||||
// remove current content
|
||||
this.remove();
|
||||
this.content = content;
|
||||
this.html = other.html;
|
||||
}
|
||||
}
|
||||
|
||||
beforeRemove() {}
|
||||
|
||||
remove() {
|
||||
const parent = this.parentEl;
|
||||
for (let elem of this.content) {
|
||||
nodeRemoveChild.call(parent, elem);
|
||||
}
|
||||
}
|
||||
|
||||
firstNode(): Node {
|
||||
return this.content[0]!;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.html;
|
||||
}
|
||||
}
|
||||
|
||||
export function html(str: string): VNode<VHtml> {
|
||||
return new VHtml(str);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export { config } from "./config";
|
||||
|
||||
export { toggler } from "./toggler";
|
||||
export { createBlock } from "./block_compiler";
|
||||
export { list } from "./list";
|
||||
export { multi } from "./multi";
|
||||
export { text, comment } from "./text";
|
||||
export { html } from "./html";
|
||||
export { createCatcher } from "./event_catcher";
|
||||
|
||||
export interface VNode<T = any> {
|
||||
mount(parent: HTMLElement, afterNode: Node | null): void;
|
||||
moveBefore(other: T | null, afterNode: Node | null): void;
|
||||
patch(other: T, withBeforeRemove: boolean): void;
|
||||
beforeRemove(): void;
|
||||
remove(): void;
|
||||
firstNode(): Node | undefined;
|
||||
|
||||
el?: undefined | HTMLElement | Text;
|
||||
parentEl?: undefined | HTMLElement;
|
||||
isOnlyChild?: boolean | undefined;
|
||||
key?: any;
|
||||
}
|
||||
|
||||
export type BDom = VNode<any>;
|
||||
|
||||
export function mount(vnode: VNode, fixture: HTMLElement, afterNode: Node | null = null) {
|
||||
vnode.mount(fixture, afterNode);
|
||||
}
|
||||
|
||||
export function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove: boolean = false) {
|
||||
vnode1.patch(vnode2, withBeforeRemove);
|
||||
}
|
||||
|
||||
export function remove(vnode: VNode, withBeforeRemove: boolean = false) {
|
||||
if (withBeforeRemove) {
|
||||
vnode.beforeRemove();
|
||||
}
|
||||
vnode.remove();
|
||||
}
|
||||
|
||||
export function withKey(vnode: VNode, key: any) {
|
||||
vnode.key = key;
|
||||
return vnode;
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import type { VNode } from "./index";
|
||||
|
||||
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
|
||||
const nodeProto = Node.prototype;
|
||||
|
||||
const nodeInsertBefore = nodeProto.insertBefore;
|
||||
const nodeAppendChild = nodeProto.appendChild;
|
||||
const nodeRemoveChild = nodeProto.removeChild;
|
||||
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// List Node
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class VList {
|
||||
children: VNode[];
|
||||
anchor: Node | undefined;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
isOnlyChild?: boolean | undefined;
|
||||
|
||||
constructor(children: VNode[]) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
const children = this.children;
|
||||
const _anchor = document.createTextNode("");
|
||||
this.anchor = _anchor;
|
||||
nodeInsertBefore.call(parent, _anchor, afterNode);
|
||||
const l = children.length;
|
||||
if (l) {
|
||||
const mount = children[0].mount;
|
||||
for (let i = 0; i < l; i++) {
|
||||
mount.call(children[i], parent, _anchor);
|
||||
}
|
||||
}
|
||||
|
||||
this.parentEl = parent;
|
||||
}
|
||||
|
||||
moveBefore(other: VList | null, afterNode: Node | null) {
|
||||
if (other) {
|
||||
const next = other!.children[0];
|
||||
afterNode = (next ? next.firstNode() : other!.anchor) || null;
|
||||
}
|
||||
const children = this.children;
|
||||
for (let i = 0, l = children.length; i < l; i++) {
|
||||
children[i].moveBefore(null, afterNode);
|
||||
}
|
||||
this.parentEl!.insertBefore(this.anchor!, afterNode);
|
||||
}
|
||||
|
||||
patch(other: VList, withBeforeRemove: boolean) {
|
||||
if (this === other) {
|
||||
return;
|
||||
}
|
||||
const ch1 = this.children;
|
||||
const ch2: VNode[] = other.children;
|
||||
if (ch2.length === 0 && ch1.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.children = ch2;
|
||||
const proto = ch2[0] || ch1[0];
|
||||
const {
|
||||
mount: cMount,
|
||||
patch: cPatch,
|
||||
remove: cRemove,
|
||||
beforeRemove,
|
||||
moveBefore: cMoveBefore,
|
||||
firstNode: cFirstNode,
|
||||
} = proto;
|
||||
|
||||
const _anchor = this.anchor!;
|
||||
const isOnlyChild = this.isOnlyChild;
|
||||
const parent = this.parentEl!;
|
||||
|
||||
// fast path: no new child => only remove
|
||||
if (ch2.length === 0 && isOnlyChild) {
|
||||
if (withBeforeRemove) {
|
||||
for (let i = 0, l = ch1.length; i < l; i++) {
|
||||
beforeRemove.call(ch1[i]);
|
||||
}
|
||||
}
|
||||
|
||||
nodeSetTextContent.call(parent, "");
|
||||
nodeAppendChild.call(parent, _anchor);
|
||||
return;
|
||||
}
|
||||
|
||||
let startIdx1 = 0;
|
||||
let startIdx2 = 0;
|
||||
let startVn1 = ch1[0];
|
||||
let startVn2 = ch2[0];
|
||||
|
||||
let endIdx1 = ch1.length - 1;
|
||||
let endIdx2 = ch2.length - 1;
|
||||
let endVn1 = ch1[endIdx1];
|
||||
let endVn2 = ch2[endIdx2];
|
||||
|
||||
let mapping: any = undefined;
|
||||
|
||||
while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) {
|
||||
// -------------------------------------------------------------------
|
||||
if (startVn1 === null) {
|
||||
startVn1 = ch1[++startIdx1];
|
||||
continue;
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
if (endVn1 === null) {
|
||||
endVn1 = ch1[--endIdx1];
|
||||
continue;
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
let startKey1 = startVn1.key;
|
||||
let startKey2 = startVn2.key;
|
||||
if (startKey1 === startKey2) {
|
||||
cPatch.call(startVn1, startVn2, withBeforeRemove);
|
||||
ch2[startIdx2] = startVn1;
|
||||
startVn1 = ch1[++startIdx1];
|
||||
startVn2 = ch2[++startIdx2];
|
||||
continue;
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
let endKey1 = endVn1.key;
|
||||
let endKey2 = endVn2.key;
|
||||
if (endKey1 === endKey2) {
|
||||
cPatch.call(endVn1, endVn2, withBeforeRemove);
|
||||
ch2[endIdx2] = endVn1;
|
||||
endVn1 = ch1[--endIdx1];
|
||||
endVn2 = ch2[--endIdx2];
|
||||
continue;
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
if (startKey1 === endKey2) {
|
||||
// bnode moved right
|
||||
cPatch.call(startVn1, endVn2, withBeforeRemove);
|
||||
ch2[endIdx2] = startVn1;
|
||||
const nextChild = ch2[endIdx2 + 1];
|
||||
cMoveBefore.call(startVn1, nextChild, _anchor);
|
||||
startVn1 = ch1[++startIdx1];
|
||||
endVn2 = ch2[--endIdx2];
|
||||
continue;
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
if (endKey1 === startKey2) {
|
||||
// bnode moved left
|
||||
cPatch.call(endVn1, startVn2, withBeforeRemove);
|
||||
ch2[startIdx2] = endVn1;
|
||||
const nextChild = ch1[startIdx1];
|
||||
cMoveBefore.call(endVn1, nextChild, _anchor);
|
||||
endVn1 = ch1[--endIdx1];
|
||||
startVn2 = ch2[++startIdx2];
|
||||
continue;
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
mapping = mapping || createMapping(ch1, startIdx1, endIdx1);
|
||||
let idxInOld = mapping[startKey2];
|
||||
if (idxInOld === undefined) {
|
||||
cMount.call(startVn2, parent, cFirstNode.call(startVn1) || null);
|
||||
} else {
|
||||
const elmToMove = ch1[idxInOld];
|
||||
cMoveBefore.call(elmToMove, startVn1, null);
|
||||
cPatch.call(elmToMove, startVn2, withBeforeRemove);
|
||||
ch2[startIdx2] = elmToMove;
|
||||
ch1[idxInOld] = null as any;
|
||||
}
|
||||
startVn2 = ch2[++startIdx2];
|
||||
}
|
||||
// ---------------------------------------------------------------------
|
||||
if (startIdx1 <= endIdx1 || startIdx2 <= endIdx2) {
|
||||
if (startIdx1 > endIdx1) {
|
||||
const nextChild = ch2[endIdx2 + 1];
|
||||
const anchor = nextChild ? cFirstNode.call(nextChild) || null : _anchor;
|
||||
for (let i = startIdx2; i <= endIdx2; i++) {
|
||||
cMount.call(ch2[i], parent, anchor);
|
||||
}
|
||||
} else {
|
||||
for (let i = startIdx1; i <= endIdx1; i++) {
|
||||
let ch = ch1[i];
|
||||
if (ch) {
|
||||
if (withBeforeRemove) {
|
||||
beforeRemove.call(ch);
|
||||
}
|
||||
cRemove.call(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeRemove() {
|
||||
const children = this.children;
|
||||
const l = children.length;
|
||||
if (l) {
|
||||
const beforeRemove = children[0].beforeRemove;
|
||||
for (let i = 0; i < l; i++) {
|
||||
beforeRemove.call(children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
const { parentEl, anchor } = this;
|
||||
if (this.isOnlyChild) {
|
||||
nodeSetTextContent.call(parentEl, "");
|
||||
} else {
|
||||
const children = this.children;
|
||||
const l = children.length;
|
||||
if (l) {
|
||||
const remove = children[0].remove;
|
||||
for (let i = 0; i < l; i++) {
|
||||
remove.call(children[i]);
|
||||
}
|
||||
}
|
||||
nodeRemoveChild.call(parentEl, anchor!);
|
||||
}
|
||||
}
|
||||
|
||||
firstNode(): Node | undefined {
|
||||
const child = this.children[0];
|
||||
return child ? child.firstNode() : undefined;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.children.map((c) => c!.toString()).join("");
|
||||
}
|
||||
}
|
||||
|
||||
export function list(children: VNode[]): VNode<VList> {
|
||||
return new VList(children);
|
||||
}
|
||||
|
||||
function createMapping(ch1: any[], startIdx1: number, endIdx2: number): { [key: string]: any } {
|
||||
let mapping: any = {};
|
||||
for (let i = startIdx1; i <= endIdx2; i++) {
|
||||
mapping[ch1[i].key] = i;
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { VNode } from "./index";
|
||||
|
||||
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
|
||||
const nodeProto = Node.prototype;
|
||||
const nodeInsertBefore = nodeProto.insertBefore;
|
||||
const nodeSetTextContent = getDescriptor(nodeProto, "textContent").set!;
|
||||
const nodeRemoveChild = nodeProto.removeChild;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Multi NODE
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export class VMulti {
|
||||
children: (VNode | undefined)[];
|
||||
anchors?: Node[] | undefined;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
isOnlyChild?: boolean | undefined;
|
||||
|
||||
constructor(children: (VNode | undefined)[]) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
const children = this.children;
|
||||
const l = children.length;
|
||||
const anchors = new Array(l);
|
||||
for (let i = 0; i < l; i++) {
|
||||
let child = children[i];
|
||||
if (child) {
|
||||
child.mount(parent, afterNode);
|
||||
} else {
|
||||
const childAnchor = document.createTextNode("");
|
||||
anchors[i] = childAnchor;
|
||||
nodeInsertBefore.call(parent, childAnchor, afterNode);
|
||||
}
|
||||
}
|
||||
this.anchors = anchors;
|
||||
this.parentEl = parent;
|
||||
}
|
||||
|
||||
moveBefore(other: VMulti | null, afterNode: Node | null) {
|
||||
if (other) {
|
||||
const next = other!.children[0];
|
||||
afterNode = (next ? next.firstNode() : other!.anchors![0]) || null;
|
||||
}
|
||||
const children = this.children;
|
||||
const parent = this.parentEl;
|
||||
const anchors = this.anchors;
|
||||
for (let i = 0, l = children.length; i < l; i++) {
|
||||
let child = children[i];
|
||||
if (child) {
|
||||
child.moveBefore(null, afterNode);
|
||||
} else {
|
||||
const anchor = anchors![i];
|
||||
nodeInsertBefore.call(parent, anchor, afterNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patch(other: VMulti, withBeforeRemove: boolean) {
|
||||
if (this === other) {
|
||||
return;
|
||||
}
|
||||
const children1 = this.children;
|
||||
const children2 = other.children;
|
||||
const anchors = this.anchors!;
|
||||
const parentEl = this.parentEl!;
|
||||
for (let i = 0, l = children1.length; i < l; i++) {
|
||||
const vn1 = children1[i];
|
||||
const vn2 = children2[i];
|
||||
if (vn1) {
|
||||
if (vn2) {
|
||||
vn1.patch(vn2, withBeforeRemove);
|
||||
} else {
|
||||
const afterNode = vn1.firstNode()!;
|
||||
const anchor = document.createTextNode("");
|
||||
anchors[i] = anchor;
|
||||
nodeInsertBefore.call(parentEl, anchor, afterNode);
|
||||
if (withBeforeRemove) {
|
||||
vn1.beforeRemove();
|
||||
}
|
||||
vn1.remove();
|
||||
children1[i] = undefined;
|
||||
}
|
||||
} else if (vn2) {
|
||||
children1[i] = vn2;
|
||||
const anchor = anchors[i];
|
||||
vn2.mount(parentEl, anchor);
|
||||
nodeRemoveChild.call(parentEl, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeRemove() {
|
||||
const children = this.children;
|
||||
for (let i = 0, l = children.length; i < l; i++) {
|
||||
const child = children[i];
|
||||
if (child) {
|
||||
child.beforeRemove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
const parentEl = this.parentEl;
|
||||
if (this.isOnlyChild) {
|
||||
nodeSetTextContent.call(parentEl, "");
|
||||
} else {
|
||||
const children = this.children;
|
||||
const anchors = this.anchors;
|
||||
for (let i = 0, l = children.length; i < l; i++) {
|
||||
const child = children[i];
|
||||
if (child) {
|
||||
child.remove();
|
||||
} else {
|
||||
nodeRemoveChild.call(parentEl, anchors![i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstNode(): Node | undefined {
|
||||
const child = this.children[0];
|
||||
return child ? child.firstNode() : this.anchors![0];
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.children.map((c) => (c ? c!.toString() : "")).join("");
|
||||
}
|
||||
}
|
||||
|
||||
export function multi(children: (VNode | undefined)[]): VNode<VMulti> {
|
||||
return new VMulti(children);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { VNode } from "./index";
|
||||
|
||||
const getDescriptor = (o: any, p: any) => Object.getOwnPropertyDescriptor(o, p)!;
|
||||
const nodeProto = Node.prototype;
|
||||
const characterDataProto = CharacterData.prototype;
|
||||
|
||||
const nodeInsertBefore = nodeProto.insertBefore;
|
||||
const characterDataSetData = getDescriptor(characterDataProto, "data").set!;
|
||||
const nodeRemoveChild = nodeProto.removeChild;
|
||||
|
||||
abstract class VSimpleNode {
|
||||
text: string | String;
|
||||
parentEl?: HTMLElement | undefined;
|
||||
el?: any;
|
||||
|
||||
constructor(text: string | String) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
mountNode(node: Node, parent: HTMLElement, afterNode: Node | null) {
|
||||
this.parentEl = parent;
|
||||
nodeInsertBefore.call(parent, node, afterNode);
|
||||
this.el = node;
|
||||
}
|
||||
|
||||
moveBefore(other: VText | null, afterNode: Node | null) {
|
||||
const target = other ? other.el! : afterNode;
|
||||
nodeInsertBefore.call(this.parentEl, this.el!, target);
|
||||
}
|
||||
|
||||
beforeRemove() {}
|
||||
|
||||
remove() {
|
||||
nodeRemoveChild.call(this.parentEl, this.el!);
|
||||
}
|
||||
|
||||
firstNode(): Node {
|
||||
return this.el!;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
|
||||
class VText extends VSimpleNode {
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode);
|
||||
}
|
||||
|
||||
patch(other: VText) {
|
||||
const text2 = other.text;
|
||||
if (this.text !== text2) {
|
||||
characterDataSetData.call(this.el!, toText(text2));
|
||||
this.text = text2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VComment extends VSimpleNode {
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
this.mountNode(document.createComment(toText(this.text)), parent, afterNode);
|
||||
}
|
||||
|
||||
patch() {}
|
||||
}
|
||||
|
||||
export function text(str: string | String): VNode<VText> {
|
||||
return new VText(str);
|
||||
}
|
||||
|
||||
export function comment(str: string): VNode<VComment> {
|
||||
return new VComment(str);
|
||||
}
|
||||
|
||||
export function toText(value: any): string {
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
return value;
|
||||
case "number":
|
||||
return String(value);
|
||||
case "boolean":
|
||||
return value ? "true" : "false";
|
||||
default:
|
||||
return value || "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { VNode } from "./index";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Toggler node
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class VToggler {
|
||||
key: string;
|
||||
child: VNode;
|
||||
|
||||
parentEl?: HTMLElement | undefined;
|
||||
|
||||
constructor(key: string, child: VNode) {
|
||||
this.key = key;
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||
this.parentEl = parent;
|
||||
this.child.mount(parent, afterNode);
|
||||
}
|
||||
|
||||
moveBefore(other: VToggler | null, afterNode: Node | null) {
|
||||
this.child.moveBefore(other ? other.child : null, afterNode);
|
||||
}
|
||||
|
||||
patch(other: VToggler, withBeforeRemove: boolean) {
|
||||
if (this === other) {
|
||||
return;
|
||||
}
|
||||
let child1 = this.child;
|
||||
let child2 = other.child;
|
||||
if (this.key === other.key) {
|
||||
child1.patch(child2, withBeforeRemove);
|
||||
} else {
|
||||
child2.mount(this.parentEl!, child1.firstNode()!);
|
||||
if (withBeforeRemove) {
|
||||
child1.beforeRemove();
|
||||
}
|
||||
child1.remove();
|
||||
this.child = child2;
|
||||
this.key = other.key;
|
||||
}
|
||||
}
|
||||
|
||||
beforeRemove() {
|
||||
this.child.beforeRemove();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.child.remove();
|
||||
}
|
||||
|
||||
firstNode(): Node | undefined {
|
||||
return this.child.firstNode();
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.child.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export function toggler(key: string, child: VNode): VNode<VToggler> {
|
||||
return new VToggler(key, child);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Schema } from "./validation";
|
||||
import type { ComponentNode } from "./component_node";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Component Class
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export type Props = { [key: string]: any };
|
||||
|
||||
interface StaticComponentProperties {
|
||||
template: string;
|
||||
defaultProps?: any;
|
||||
props?: Schema;
|
||||
components?: { [componentName: string]: ComponentConstructor };
|
||||
}
|
||||
|
||||
export type ComponentConstructor<P extends Props = any, E = any> = (new (
|
||||
props: P,
|
||||
env: E,
|
||||
node: ComponentNode
|
||||
) => Component<P, E>) &
|
||||
StaticComponentProperties;
|
||||
|
||||
export class Component<Props = any, Env = any> {
|
||||
static template: string = "";
|
||||
static props?: any;
|
||||
static defaultProps?: any;
|
||||
|
||||
props: Props;
|
||||
env: Env;
|
||||
__owl__: ComponentNode;
|
||||
|
||||
constructor(props: Props, env: Env, node: ComponentNode) {
|
||||
this.props = props;
|
||||
this.env = env;
|
||||
this.__owl__ = node;
|
||||
}
|
||||
|
||||
setup() {}
|
||||
|
||||
render(deep: boolean = false) {
|
||||
this.__owl__.render(deep === true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import type { App, Env } from "./app";
|
||||
import { BDom, VNode } from "./blockdom";
|
||||
import { Component, ComponentConstructor, Props } from "./component";
|
||||
import { fibersInError, handleError } from "./error_handling";
|
||||
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
||||
import {
|
||||
clearReactivesForCallback,
|
||||
getSubscriptions,
|
||||
NonReactive,
|
||||
Reactive,
|
||||
reactive,
|
||||
TARGET,
|
||||
} from "./reactivity";
|
||||
import { STATUS } from "./status";
|
||||
import { Callback } from "./utils";
|
||||
|
||||
let currentNode: ComponentNode | null = null;
|
||||
|
||||
export function getCurrent(): ComponentNode {
|
||||
if (!currentNode) {
|
||||
throw new Error("No active component (a hook function should only be called in 'setup')");
|
||||
}
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
export function useComponent(): Component {
|
||||
return currentNode!.component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default props (only top level).
|
||||
*/
|
||||
function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>) {
|
||||
for (let propName in defaultProps) {
|
||||
if (props[propName] === undefined) {
|
||||
(props as any)[propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Integration with reactivity system (useState)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const renderFunctions = new WeakMap<ComponentNode, Callback>();
|
||||
/**
|
||||
* Creates a reactive object that will be observed by the current component.
|
||||
* Reading data from the returned object (eg during rendering) will cause the
|
||||
* component to subscribe to that data and be rerendered when it changes.
|
||||
*
|
||||
* @param state the state to observe
|
||||
* @returns a reactive object that will cause the component to re-render on
|
||||
* relevant changes
|
||||
* @see reactive
|
||||
*/
|
||||
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
||||
const node = getCurrent();
|
||||
let render = renderFunctions.get(node)!;
|
||||
if (!render) {
|
||||
render = node.render.bind(node, false);
|
||||
renderFunctions.set(node, render);
|
||||
// manual implementation of onWillDestroy to break cyclic dependency
|
||||
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
||||
}
|
||||
return reactive(state, render);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Component VNode class
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type LifecycleHook = Function;
|
||||
|
||||
export class ComponentNode<P extends Props = any, E = any> implements VNode<ComponentNode<P, E>> {
|
||||
el?: HTMLElement | Text | undefined;
|
||||
app: App;
|
||||
fiber: Fiber | null = null;
|
||||
component: Component<P, E>;
|
||||
bdom: BDom | null = null;
|
||||
status: STATUS = STATUS.NEW;
|
||||
forceNextRender: boolean = false;
|
||||
parentKey: string | null;
|
||||
props: P;
|
||||
|
||||
renderFn: Function;
|
||||
parent: ComponentNode | null;
|
||||
level: number;
|
||||
childEnv: Env;
|
||||
children: { [key: string]: ComponentNode } = Object.create(null);
|
||||
refs: any = {};
|
||||
|
||||
willStart: LifecycleHook[] = [];
|
||||
willUpdateProps: LifecycleHook[] = [];
|
||||
willUnmount: LifecycleHook[] = [];
|
||||
mounted: LifecycleHook[] = [];
|
||||
willPatch: LifecycleHook[] = [];
|
||||
patched: LifecycleHook[] = [];
|
||||
willDestroy: LifecycleHook[] = [];
|
||||
|
||||
constructor(
|
||||
C: ComponentConstructor<P, E>,
|
||||
props: P,
|
||||
app: App,
|
||||
parent: ComponentNode | null,
|
||||
parentKey: string | null
|
||||
) {
|
||||
currentNode = this;
|
||||
this.app = app;
|
||||
this.parent = parent;
|
||||
this.props = props;
|
||||
this.parentKey = parentKey;
|
||||
this.level = parent ? parent.level + 1 : 0;
|
||||
const defaultProps = C.defaultProps;
|
||||
props = Object.assign({}, props);
|
||||
if (defaultProps) {
|
||||
applyDefaultProps(props, defaultProps);
|
||||
}
|
||||
const env = (parent && parent.childEnv) || app.env;
|
||||
this.childEnv = env;
|
||||
for (const key in props) {
|
||||
const prop = props[key];
|
||||
if (prop && typeof prop === "object" && prop[TARGET]) {
|
||||
props[key] = useState(prop);
|
||||
}
|
||||
}
|
||||
this.component = new C(props, env, this);
|
||||
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
|
||||
this.component.setup();
|
||||
currentNode = null;
|
||||
}
|
||||
|
||||
mountComponent(target: any, options?: MountOptions) {
|
||||
const fiber = new MountFiber(this, target, options);
|
||||
this.app.scheduler.addFiber(fiber);
|
||||
this.initiateRender(fiber);
|
||||
}
|
||||
|
||||
async initiateRender(fiber: Fiber | MountFiber) {
|
||||
this.fiber = fiber;
|
||||
if (this.mounted.length) {
|
||||
fiber.root!.mounted.push(fiber);
|
||||
}
|
||||
const component = this.component;
|
||||
if (this.willStart.length) {
|
||||
let willStartResults;
|
||||
try {
|
||||
willStartResults = this.willStart.map((f) => f.call(component));
|
||||
} catch (e) {
|
||||
handleError({ node: this, error: e });
|
||||
return;
|
||||
}
|
||||
if (willStartResults.some((result) => result instanceof Promise)) {
|
||||
try {
|
||||
await Promise.all(willStartResults);
|
||||
} catch (e) {
|
||||
handleError({ node: this, error: e });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.status === STATUS.NEW && this.fiber === fiber) {
|
||||
fiber.render();
|
||||
}
|
||||
}
|
||||
|
||||
async render(deep: boolean) {
|
||||
let current = this.fiber;
|
||||
if (current && (current.root!.locked || current as any).bdom === true) {
|
||||
this.app.scheduler.requestAnimationFrame(() => this.render(deep));
|
||||
return;
|
||||
}
|
||||
if (current) {
|
||||
if (!current.bdom && !fibersInError.has(current)) {
|
||||
if (deep) {
|
||||
// we want the render from this point on to be with deep=true
|
||||
current.deep = deep;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// if current rendering was with deep=true, we want this one to be the same
|
||||
deep = deep || current.deep;
|
||||
} else if (!this.bdom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fiber = makeRootFiber(this);
|
||||
fiber.deep = deep;
|
||||
this.fiber = fiber;
|
||||
const scheduler = this.app.scheduler;
|
||||
|
||||
scheduler.requestAnimationFrame(() => {
|
||||
if (this.status === STATUS.DESTROYED) {
|
||||
return;
|
||||
}
|
||||
// We only want to actually render the component if the following two
|
||||
// conditions are true:
|
||||
// * this.fiber: it could be null, in which case the render has been cancelled
|
||||
// * (current || !fiber.parent): if current is not null, this means that the
|
||||
// render function was called when a render was already occurring. In this
|
||||
// case, the pending rendering was cancelled, and the fiber needs to be
|
||||
// rendered to complete the work. If current is null, we check that the
|
||||
// fiber has no parent. If that is the case, the fiber was downgraded from
|
||||
// a root fiber to a child fiber in the previous microtick, because it was
|
||||
// embedded in a rendering coming from above, so the fiber will be rendered
|
||||
// in the next microtick anyway, so we should not render it again.
|
||||
if (this.fiber === fiber && (current || !fiber.parent)) {
|
||||
fiber.render();
|
||||
}
|
||||
});
|
||||
scheduler.addFiber(fiber);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
let shouldRemove = this.status === STATUS.MOUNTED;
|
||||
this._destroy();
|
||||
if (shouldRemove) {
|
||||
this.bdom!.remove();
|
||||
}
|
||||
}
|
||||
|
||||
_destroy() {
|
||||
const component = this.component;
|
||||
if (this.status === STATUS.MOUNTED) {
|
||||
for (let cb of this.willUnmount) {
|
||||
cb.call(component);
|
||||
}
|
||||
}
|
||||
for (let child of Object.values(this.children)) {
|
||||
child._destroy();
|
||||
}
|
||||
if (this.willDestroy.length) {
|
||||
try {
|
||||
for (let cb of this.willDestroy) {
|
||||
cb.call(component);
|
||||
}
|
||||
} catch (e) {
|
||||
handleError({ error: e, node: this });
|
||||
}
|
||||
}
|
||||
this.status = STATUS.DESTROYED;
|
||||
}
|
||||
|
||||
async updateAndRender(props: P, parentFiber: Fiber) {
|
||||
const rawProps = props;
|
||||
props = Object.assign({}, props);
|
||||
// update
|
||||
const fiber = makeChildFiber(this, parentFiber);
|
||||
this.fiber = fiber;
|
||||
const component = this.component;
|
||||
const defaultProps = (component.constructor as any).defaultProps;
|
||||
if (defaultProps) {
|
||||
applyDefaultProps(props, defaultProps);
|
||||
}
|
||||
|
||||
currentNode = this;
|
||||
for (const key in props) {
|
||||
const prop = props[key];
|
||||
if (prop && typeof prop === "object" && prop[TARGET]) {
|
||||
props[key] = useState(prop);
|
||||
}
|
||||
}
|
||||
currentNode = null;
|
||||
if (this.willUpdateProps.length) {
|
||||
const willUpdatePropsResults = this.willUpdateProps.map((f) => f.call(component, props));
|
||||
if (willUpdatePropsResults.some((res) => res instanceof Promise)) {
|
||||
await Promise.all(willUpdatePropsResults);
|
||||
}
|
||||
if (fiber !== this.fiber) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
component.props = props;
|
||||
this.props = rawProps;
|
||||
const parentRoot = parentFiber.root!;
|
||||
if (this.willPatch.length) {
|
||||
parentRoot.willPatch.push(fiber);
|
||||
}
|
||||
if (this.patched.length) {
|
||||
parentRoot.patched.push(fiber);
|
||||
}
|
||||
fiber.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a child that has dom that is not yet updated, and update it. This
|
||||
* method is meant to be used only in the context of repatching the dom after
|
||||
* a mounted hook failed and was handled.
|
||||
*/
|
||||
updateDom() {
|
||||
if (!this.fiber) {
|
||||
return;
|
||||
}
|
||||
if (this.bdom === this.fiber!.bdom) {
|
||||
// If the error was handled by some child component, we need to find it to
|
||||
// apply its change
|
||||
for (let k in this.children) {
|
||||
const child = this.children[k];
|
||||
child.updateDom();
|
||||
}
|
||||
} else {
|
||||
// if we get here, this is the component that handled the error and rerendered
|
||||
// itself, so we can simply patch the dom
|
||||
this.bdom!.patch(this.fiber!.bdom, false);
|
||||
this.fiber!.appliedToDom = true;
|
||||
this.fiber = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Block DOM methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
firstNode(): Node | undefined {
|
||||
const bdom = this.bdom;
|
||||
return bdom ? bdom.firstNode() : undefined;
|
||||
}
|
||||
|
||||
mount(parent: HTMLElement, anchor: ChildNode) {
|
||||
const bdom = this.fiber!.bdom!;
|
||||
this.bdom = bdom;
|
||||
bdom.mount(parent, anchor);
|
||||
this.status = STATUS.MOUNTED;
|
||||
this.fiber!.appliedToDom = true;
|
||||
this.children = this.fiber!.childrenMap;
|
||||
this.fiber = null;
|
||||
}
|
||||
|
||||
moveBefore(other: ComponentNode | null, afterNode: Node | null) {
|
||||
this.bdom!.moveBefore(other ? other.bdom : null, afterNode);
|
||||
}
|
||||
|
||||
patch() {
|
||||
if (this.fiber && this.fiber.parent) {
|
||||
// we only patch here renderings coming from above. renderings initiated
|
||||
// by the component will be patched independently in the appropriate
|
||||
// fiber.complete
|
||||
this._patch();
|
||||
}
|
||||
}
|
||||
_patch() {
|
||||
let hasChildren = false;
|
||||
for (let _k in this.children) {
|
||||
hasChildren = true;
|
||||
break;
|
||||
}
|
||||
const fiber = this.fiber!;
|
||||
this.children = fiber.childrenMap;
|
||||
this.bdom!.patch(fiber.bdom!, hasChildren);
|
||||
fiber.appliedToDom = true;
|
||||
this.fiber = null;
|
||||
}
|
||||
|
||||
beforeRemove() {
|
||||
this._destroy();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.bdom!.remove();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Some debug helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
get name(): string {
|
||||
return this.component.constructor.name;
|
||||
}
|
||||
|
||||
get subscriptions(): ReturnType<typeof getSubscriptions> {
|
||||
const render = renderFunctions.get(this);
|
||||
return render ? getSubscriptions(render) : [];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user