mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
720 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21b07afb60 | |||
| 13c3178760 | |||
| 2b565ce1b4 | |||
| cfcf2c6714 | |||
| 92174559a1 | |||
| 840348892f | |||
| b988a68b6f | |||
| 7869a1f2c6 | |||
| 9e81d14d50 | |||
| 0e4a55ba09 | |||
| 6e9b68dafa | |||
| ba7c9063c0 | |||
| 3a361876ad | |||
| 58b8572f0a | |||
| c23637e6d8 | |||
| 4a4b1fbba5 | |||
| 536d9e1762 | |||
| 6ed68372a6 | |||
| efd934d2b1 | |||
| dabc24cee3 | |||
| 7f49796a07 | |||
| 757dffefac | |||
| bdfe058279 | |||
| 345c44b952 | |||
| ea74739d46 | |||
| 93b53d8017 | |||
| c2284bc6f5 | |||
| 48650da62e | |||
| 2edd6cb8f3 | |||
| 4b0a37c542 | |||
| 50c0e30936 | |||
| e97335d7ab | |||
| 6ea40a66da | |||
| 2806cda420 | |||
| cb0b525f32 | |||
| 30d28670e8 | |||
| 7b761a2f8b | |||
| d834bb2ac4 | |||
| 6ae92fa4b3 | |||
| 3ceb118ace | |||
| fa426b7fc0 | |||
| d7403871fc | |||
| 6aeff21d9a | |||
| 429e145b68 | |||
| fb6aab8b70 | |||
| 9e285e1e0a | |||
| 63bfdfb8db | |||
| d4c4fe853e | |||
| 9163bb1e08 | |||
| a187a376a0 | |||
| 06ea6d2490 | |||
| 04334e23d7 | |||
| 4efe1ae166 | |||
| ad4673cb36 | |||
| e611c20ab4 | |||
| d60a5a414e | |||
| d47dcf6be2 | |||
| 3e70b17da1 | |||
| c718d8e6c6 | |||
| a122a94180 | |||
| ca139166ab | |||
| ae71db28e4 | |||
| 779fc6b02b | |||
| 84913593b2 | |||
| 8d4eb21536 | |||
| 4bdfaf96f3 | |||
| 233c953243 | |||
| ee5ad354ff | |||
| 4e4b85e2a9 | |||
| 6479631983 | |||
| 2a0c410014 | |||
| bb4aecf638 | |||
| a1adfd5a1b | |||
| dd4848f602 | |||
| b25f476c22 | |||
| 90cdb97b49 | |||
| 7d7568d254 | |||
| 06d207f60e | |||
| ff564421da | |||
| c3695ec8db | |||
| 6e0834ce5e | |||
| d1425c7100 | |||
| e01fe301c0 | |||
| ba2fe3ff55 | |||
| 700d574314 | |||
| ba32772a92 | |||
| e4a0277f68 | |||
| abe3748825 | |||
| ee64de8b3f | |||
| ed831320db | |||
| 5f1e1e189f | |||
| 55ef9ca116 | |||
| 642ecf0ccd | |||
| c9cc789f1d | |||
| b8649f1add | |||
| e2d98e4c26 | |||
| e6bb4ef286 | |||
| 7ac20f4fc2 | |||
| 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 |
@@ -0,0 +1,27 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Node.js CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, owl-next ]
|
||||||
|
|
||||||
|
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
|
||||||
+14
-1
@@ -1,5 +1,6 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
@@ -14,8 +15,20 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
#ide's
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Extras temp file
|
# Extras temp file
|
||||||
/extras/owl.js
|
/tools/owl.js
|
||||||
|
|
||||||
|
release-notes.md
|
||||||
|
|
||||||
|
.rpt2_cache
|
||||||
|
|
||||||
|
# useful in some cases
|
||||||
|
/temp
|
||||||
+588
@@ -0,0 +1,588 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
**Components**
|
||||||
|
|
||||||
|
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
|
||||||
|
- new `useEffect` hook
|
||||||
|
- new `onDestroyed`, `onWillRender` and `onRendered` hooks
|
||||||
|
- 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: component.el may be a text node, and is no longer `null` ([details](#9-componentel-may-be-a-text-node-and-is-no-longer-null))
|
||||||
|
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
|
||||||
|
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
|
||||||
|
- breaking: `t-on` does not work on components any more ([details](#12-t-on-does-not-work-on-components-any-more))
|
||||||
|
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
|
||||||
|
|
||||||
|
**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))
|
||||||
|
|
||||||
|
**Slots**
|
||||||
|
|
||||||
|
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
|
||||||
|
|
||||||
|
**Miscellaneous**
|
||||||
|
|
||||||
|
- improved performance
|
||||||
|
- much simpler code
|
||||||
|
- finer grained reactivity: owl 2 tracks change per key/component
|
||||||
|
- finer grained reactivity: sub components can reobserve state
|
||||||
|
- new App class to encapsulate a root Owl component (with the config for that application)
|
||||||
|
- new `Memo` component
|
||||||
|
- breaking: `Context` is removed ([details](#15-context-is-removed))
|
||||||
|
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
|
||||||
|
- 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: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
|
||||||
|
- 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: `useSubEnv` only applies to child components ([details](#27-usesubenv-only-applies-to-child-components))
|
||||||
|
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
|
||||||
|
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
|
||||||
|
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
Migration code: remove the `shouldUpdate` methods. Then, maybe the following
|
||||||
|
ideas may help:
|
||||||
|
|
||||||
|
- try to organize the state/architecture to minimize the number of state updates
|
||||||
|
- take advantage of the finer reactivity system. For example, if we have a list
|
||||||
|
of items, with a component for each item, we can write this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Item extends Component {
|
||||||
|
setup() {
|
||||||
|
this.item = useState(this.props.item); // and only use this, not props.item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Doing so will make it that each `Item` component will register itself as an
|
||||||
|
observer of its own item, and will be the only component being rerendered when
|
||||||
|
its item object is updated.
|
||||||
|
- use the `Memo` component to wrap some piece of template. `Memo` memoize its
|
||||||
|
content, and only update itself if its props are different (shallow comparison):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Memo a="state.a" b="state.b">
|
||||||
|
<t t-esc="state.a"/>
|
||||||
|
<t t-esc="state.b"/>
|
||||||
|
<t t-esc="state.c"/>
|
||||||
|
</Memo>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. component.el may be a text node, and is no longer `null`
|
||||||
|
|
||||||
|
This comes from the fact that Owl 2 supports fragments (arbitrary content). When
|
||||||
|
it is not defined, it was `null` in Owl 1 and is `undefined` in owl 2.
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
|
### 12. `t-on` does not work on components any more
|
||||||
|
|
||||||
|
In owl 1, it was possible to bind an event listener on a component tag in a
|
||||||
|
template:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<SomeComponent t-on-some-event="doSomething"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
This does not work any more.
|
||||||
|
|
||||||
|
Rationale: with the support of fragments, there is no longer a canonical html
|
||||||
|
element that we can refer. So, this makes it difficult to implement correctly
|
||||||
|
and efficiently. Also, we noticed in practice that the event system was an issue
|
||||||
|
in some cases, when components need to communicate before they are mounted. In
|
||||||
|
those cases, the better solution is to directly use a callback. Also, another
|
||||||
|
conceptual issue with this is that it kind of breaks the component encapsulation.
|
||||||
|
The child component kind of leak its own implementation to the outside world.
|
||||||
|
|
||||||
|
Migration: a quick fix that may work in some cases is to simply bind the event
|
||||||
|
handler on a parent htmlelement. A better way to do it, if possible, is to change
|
||||||
|
the component API to accept explicitely a callback as props.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<SomeComponent onSomeEvent="doSomething"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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(...)`
|
||||||
|
|
||||||
|
### 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. `useSubEnv` only applies to child components
|
||||||
|
|
||||||
|
In Owl 1, a call to `useSubEnv` would define a new environment for the children
|
||||||
|
AND the component. It now only defines an environment for the children.
|
||||||
|
|
||||||
|
Rationale: This was a subtle cause for bugs: some code had to be rrun
|
||||||
|
before the call to `useSubEnv`, otherwise it could interfere with the sub environment.
|
||||||
|
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
@@ -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,19 +1,85 @@
|
|||||||
<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_
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
The Odoo Web Library (OWL) is a small
|
The Odoo Web Library (OWL) is a smallish (~<20kb gzipped) UI framework intended to
|
||||||
UI framework intended to be the basis for the [Odoo](https://www.odoo.com/) Web Client, and hopefully many
|
be the basis for the [Odoo](https://www.odoo.com/) Web Client. Owl is a modern
|
||||||
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.
|
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 reactivity system based on hooks,
|
||||||
|
- concurrent mode by default,
|
||||||
|
|
||||||
## OWL's design principles
|
Owl components are defined with ES6 classes, they use QWeb templates, an
|
||||||
|
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
|
||||||
|
asynchronous.
|
||||||
|
|
||||||
|
**Try it online!** An online playground is available at
|
||||||
|
[https://odoo.github.io/owl/playground](https://odoo.github.io/owl/playground)
|
||||||
|
to let you experiment with the Owl framework. There are some code examples to
|
||||||
|
showcase some interesting features.
|
||||||
|
|
||||||
|
Owl is currently stable. Possible future changes are explained in the
|
||||||
|
[roadmap](roadmap.md).
|
||||||
|
|
||||||
|
## Why Owl?
|
||||||
|
|
||||||
|
Why did Odoo decide to make Yet Another Framework? This is really a question
|
||||||
|
that deserves [a long answer](doc/miscellaneous/why_owl.md). But in short, we believe that
|
||||||
|
while the current state of the art frameworks are excellent, they are not
|
||||||
|
optimized for our use case, and there is still room for something else.
|
||||||
|
|
||||||
|
If you are interested in a comparison with React or Vue, you will
|
||||||
|
find some more additional information [here](doc/miscellaneous/comparison.md).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Here is a short example to illustrate interactive components:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { Component, useState, mount } = owl;
|
||||||
|
const { xml } = owl.tags;
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<button t-on-click="state.value++">
|
||||||
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span>Hello Owl</span>
|
||||||
|
<Counter />
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
static components = { Counter };
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(App, { target: document.body });
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the counter component is made reactive with the [`useState` hook](doc/reference/hooks.md#usestate).
|
||||||
|
Also, all examples here uses the [`xml` helper](doc/reference/tags.md#xml-tag) to define inline templates.
|
||||||
|
But this is not mandatory, many applications will load templates separately.
|
||||||
|
|
||||||
|
More interesting examples can be found on the
|
||||||
|
[playground](https://odoo.github.io/owl/playground) application.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
OWL is designed to be used in highly dynamic applications where changing
|
OWL is designed to be used in highly dynamic applications where changing
|
||||||
requirements are common, code needs to be maintained by large teams.
|
requirements are common and code needs to be maintained by large teams.
|
||||||
|
|
||||||
- **XML based**: templates are based on the XML format, which allows interesting
|
- **XML based**: templates are based on the XML format, which allows interesting
|
||||||
applications. For example, they could be stored in a database and modified
|
applications. For example, they could be stored in a database and modified
|
||||||
@@ -26,62 +92,39 @@ requirements are common, code needs to be maintained by large teams.
|
|||||||
for various reasons (security/deployment/dynamic modules/specific assets tools),
|
for various reasons (security/deployment/dynamic modules/specific assets tools),
|
||||||
it is not ok to use standard web tools based on `npm`.
|
it is not ok to use standard web tools based on `npm`.
|
||||||
|
|
||||||
Owl is not designed to be fast or small (even though it is quite good on those
|
Owl is not designed to be fast nor small (even though it is quite good on those
|
||||||
two topics). If you are interested in a comparison with React or Vue, you will
|
two topics). It is a no nonsense framework to build applications. There is only
|
||||||
find some more information [here](doc/comparison.md).
|
one way to define components (with classes). There is no black magic. It just
|
||||||
|
works.
|
||||||
|
|
||||||
# Example
|
|
||||||
|
|
||||||
Here is a short example to illustrate interactive widgets:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
class ClickCounter extends owl.Component {
|
|
||||||
state = { value: 0 };
|
|
||||||
|
|
||||||
increment() {
|
|
||||||
this.state.value++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEMPLATES = `
|
|
||||||
<button t-name="ClickCounter" t-on-click="increment">
|
|
||||||
Click Me! [<t t-esc="state.value"/>]
|
|
||||||
</button>`;
|
|
||||||
|
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
|
||||||
const counter = new ClickCounter({ qweb });
|
|
||||||
counter.mount(document.body);
|
|
||||||
```
|
|
||||||
|
|
||||||
More interesting examples can be found on the [playground](https://odoo.github.io/owl/playground) application.
|
|
||||||
|
|
||||||
## Installing/Building
|
|
||||||
|
|
||||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
|
||||||
|
|
||||||
- [owl-0.11.0.js](https://github.com/odoo/owl/releases/download/v0.11.0/owl.js)
|
|
||||||
- [owl-0.11.0.min.js](https://github.com/odoo/owl/releases/download/v0.11.0/owl.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)) |
|
|
||||||
| `npm run extras:watch` | same as `extras`, but with a watcher to rebuild owl |
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The complete documentation can be found [here](doc/readme.md). The most important sections are:
|
A complete documentation for Owl can be found here:
|
||||||
|
|
||||||
- [Quick Start](doc/quick_start.md)
|
- [Main documentation page](doc/readme.md).
|
||||||
- [Component](doc/component.md)
|
|
||||||
- [QWeb](doc/qweb.md)
|
Some of the most important pages are:
|
||||||
|
|
||||||
|
- [Tutorial: TodoList application](doc/learning/tutorial_todoapp.md)
|
||||||
|
- [How to start an Owl project](doc/learning/quick_start.md)
|
||||||
|
- [QWeb templating language](doc/reference/qweb_templating_language.md)
|
||||||
|
- [Component](doc/reference/component.md)
|
||||||
|
- [Hooks](doc/reference/hooks.md)
|
||||||
|
|
||||||
|
|
||||||
|
## Installing Owl
|
||||||
|
|
||||||
|
Owl is available on `npm` and can be installed with the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install @odoo/owl
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||||
|
|
||||||
|
- [owl-1.4.7](https://github.com/odoo/owl/releases/tag/v1.4.7)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
OWL is [GPL licensed](./LICENSE).
|
OWL is [LGPL 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,458 +0,0 @@
|
|||||||
# 🦉 OWL Component 🦉
|
|
||||||
|
|
||||||
## Content
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Example](#example)
|
|
||||||
- [Composition](#composition)
|
|
||||||
- [Reference](#reference)
|
|
||||||
- [Properties](#properties)
|
|
||||||
- [Methods](#methods)
|
|
||||||
- [Lifecycle](#lifecycle)
|
|
||||||
- [Semantics](#semantics)
|
|
||||||
- [Asynchronous rendering](#asynchronous-rendering)
|
|
||||||
|
|
||||||
## 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 (which needs to be preloaded in QWeb).
|
|
||||||
Rendering a component generates a virtual dom representation
|
|
||||||
of the widget, which is then patched to the DOM, in order 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 {
|
|
||||||
state = { value: 0 };
|
|
||||||
|
|
||||||
increment() {
|
|
||||||
this.state.value++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<button t-name="ClickCounter" t-on-click="increment">
|
|
||||||
Click Me! [<t t-esc="state.value"/>]
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that this code is written in ESNext style, so it will only run on the
|
|
||||||
latest browsers without a transpilation step.
|
|
||||||
|
|
||||||
This example show how a component should be defined: it simply subclasses the
|
|
||||||
Component class. If no `template` key is defined, then
|
|
||||||
Owl will use the component's name as template name. Here,
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Composition
|
|
||||||
|
|
||||||
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 t-name="ParentWidget">
|
|
||||||
<span>some text</span>
|
|
||||||
<t t-widget="MyWidget" t-props="{info: 13}"/>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
class ParentWidget extends owl.Component {
|
|
||||||
widgets = { MyWidget: MyWidget};
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, the `ParentWidget`'s template creates 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.
|
|
||||||
|
|
||||||
**CSS and style:** there is some specific support to allow the parent to declare
|
|
||||||
additional css classes or style for the sub widget: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
|
||||||
root widget element.
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<div t-name="ParentWidget">
|
|
||||||
<t t-widget="MyWidget" class="someClass" style="font-weight:bold;" t-props="{info: 13}"/>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Warning: there is a small caveat with dynamic class attributes: since Owl needs
|
|
||||||
to be able to add/remove proper classes whenever necessary, it needs to be aware
|
|
||||||
of the possible classes. Otherwise, it will not be able to make the difference
|
|
||||||
between a valid css class added by the component, or some custom code, and a
|
|
||||||
class that need to be removed. This is why we only support the explicit syntax
|
|
||||||
with a class object:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<t t-widget="MyWidget" t-att-class="{a: state.flagA, b: state.flagB}" />
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
Be aware that the name of the component may be significant: if a component does
|
|
||||||
not define a `template` key, then Owl will lookup in QWeb to
|
|
||||||
find a template with the component name (or one of its ancestor).
|
|
||||||
|
|
||||||
### 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, optional): if given, this is the name of the QWeb template that will render
|
|
||||||
the component.
|
|
||||||
|
|
||||||
- **`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)** | just after component is rendered and added to the DOM |
|
|
||||||
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
|
|
||||||
| **[willPatch](#willpatch)** | just before the DOM is patched |
|
|
||||||
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
|
|
||||||
| **[willUnmount](#willUnmount)** | just before removing component from DOM |
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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`.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
- widget `B` is created
|
|
||||||
1. `willStart` is called on `B`
|
|
||||||
2. template `B` is rendered
|
|
||||||
- widget `C` is created
|
|
||||||
1. `willStart` is called on `C`
|
|
||||||
2. template `C` is rendered
|
|
||||||
- widget `D` is created
|
|
||||||
1. `willStart` is called on `D`
|
|
||||||
2. template `D` is rendered
|
|
||||||
- widget `E` is created
|
|
||||||
1. `willStart` is called on `E`
|
|
||||||
2. template `E` is rendered
|
|
||||||
|
|
||||||
3. widget `A` is patched into a detached DOM element. This will create the actual
|
|
||||||
widget `A` DOM structure. The patching process will cause recursively the
|
|
||||||
patching of the `B`, `C`, `D` and `E` DOM trees. (so the actual full DOM tree is created
|
|
||||||
in one pass)
|
|
||||||
|
|
||||||
4. the widget `A` root element is actually appended to `document.body`
|
|
||||||
|
|
||||||
5. The method `mounted` is called recursively on all widgets in the following
|
|
||||||
order: `B`, `D`, `E`, `C`, `A`.
|
|
||||||
|
|
||||||
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
|
|
||||||
button in `C`, and this results in a state update, which is supposed to:
|
|
||||||
|
|
||||||
- update `D`,
|
|
||||||
- remove `E`,
|
|
||||||
- add new widget `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
|
|
||||||
|
|
||||||
- widget `D` is updated:
|
|
||||||
1. hook `willUpdateProps` is called on `D` (async)
|
|
||||||
2. template `D` is rerendered
|
|
||||||
- widget `F` is created:
|
|
||||||
1. hook `willStart` is called on `E` (async)
|
|
||||||
2. template `F` is rendered
|
|
||||||
|
|
||||||
3. `willPatch` hooks are called recursively on widgets `C`, `D` (not on `F`,
|
|
||||||
because it is not mounted yet)
|
|
||||||
|
|
||||||
4. widget `C` is patched, which will cause recursively:
|
|
||||||
|
|
||||||
2. `willUnmount` hook on `E`, then destruction of `E`,
|
|
||||||
3. (initial) patching of `F`, then hook `mounted` is called on `F`
|
|
||||||
|
|
||||||
5. patching of `D`
|
|
||||||
|
|
||||||
6. `patched` hooks are called on `D`, `C`
|
|
||||||
|
|
||||||
## 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 widget can delay the rendering (initial and subsequent) of the whole
|
|
||||||
application
|
|
||||||
- for a given widget, 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 widgets:
|
|
||||||
|
|
||||||
1. minimize the use of asynchronous widgets!
|
|
||||||
2. Maybe move the asynchronous logic in a store, which then triggers (mostly)
|
|
||||||
synchronous renderings
|
|
||||||
3. Lazy loading external libraries is a good use case for async rendering. This
|
|
||||||
is mostly fine, because we can assume that it will only takes a fraction of a
|
|
||||||
second, and only once (see `owl.utils.loadJS`)
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# 🦉 How to test Components 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Unit Tests](#unit-tests)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
It is a good practice to test applications and components to ensure that they
|
||||||
|
behave as expected. There are many ways to test a user interface: manual
|
||||||
|
testing, integration testing, unit testing, ...
|
||||||
|
|
||||||
|
In this section, we will discuss how to write unit tests for components.
|
||||||
|
|
||||||
|
## Unit Tests
|
||||||
|
|
||||||
|
Writing unit tests for Owl components really depends on the testing framework
|
||||||
|
used in a project. But usually, it involves the following steps:
|
||||||
|
|
||||||
|
- create a test file: for example `SomeComponent.test.js`,
|
||||||
|
- in that file, import the code for `SomeComponent`,
|
||||||
|
- add a test case:
|
||||||
|
- create a real DOM element to use as test fixture,
|
||||||
|
- create a test environment
|
||||||
|
- create an instance of `SomeComponent`, mount it to the fixture
|
||||||
|
- interact with the component and assert some properties.
|
||||||
|
|
||||||
|
To help with this, it is useful to have a `helper.js` file that contains some
|
||||||
|
common utility functions:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export function makeTestFixture() {
|
||||||
|
let fixture = document.createElement("div");
|
||||||
|
document.body.appendChild(fixture);
|
||||||
|
return fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextTick() {
|
||||||
|
let requestAnimationFrame = owl.Component.scheduler.requestAnimationFrame;
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
setTimeout(() => requestAnimationFrame(() => resolve()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeTestEnv() {
|
||||||
|
// application specific. It needs a way to load actual templates
|
||||||
|
const templates = ...;
|
||||||
|
|
||||||
|
return {
|
||||||
|
qweb: new QWeb(templates),
|
||||||
|
..., // each service can be mocked here
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With such a file, a typical test suite for Jest will look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// in SomeComponent.test.js
|
||||||
|
import { SomeComponent } from "../../src/ui/SomeComponent";
|
||||||
|
import { nextTick, makeTestFixture, makeTestEnv} from '../helpers';
|
||||||
|
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
let fixture: HTMLElement;
|
||||||
|
let env: Env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
env = makeTestEnv();
|
||||||
|
// we set here the default environment for each component created in the test
|
||||||
|
Component.env = env;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
describe("SomeComponent", () => {
|
||||||
|
test("component behaves as expected", async () => {
|
||||||
|
const props = {...}; // depends on the component
|
||||||
|
const comp = await mount(SomeComponent, { target: fixture, props });
|
||||||
|
|
||||||
|
// do some assertions
|
||||||
|
expect(...).toBe(...);
|
||||||
|
|
||||||
|
fixture.querySelector('button').click();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// some other assertions
|
||||||
|
expect(...).toBe(...);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that Owl does wait for the next animation frame to actually update the DOM.
|
||||||
|
This is why it is necessary to wait with the `nextTick` (or other methods) to
|
||||||
|
make sure that the DOM is up-to-date.
|
||||||
|
|
||||||
|
It is sometimes useful to wait until Owl is completely done updating components
|
||||||
|
(in particular, if we have a highly concurrent user interface). This next
|
||||||
|
helper simply polls every 20ms the internal Owl task queue and returns a promise
|
||||||
|
which resolves when it is empty:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function afterUpdates() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let timer = setTimeout(poll, 20);
|
||||||
|
let counter = 0;
|
||||||
|
function poll() {
|
||||||
|
counter++;
|
||||||
|
if (owl.Component.scheduler.tasks.length) {
|
||||||
|
if (counter > 10) {
|
||||||
|
reject(new Error("timeout"));
|
||||||
|
} else {
|
||||||
|
timer = setTimeout(poll);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 🦉 How to write Single File Components 🦉
|
||||||
|
|
||||||
|
It is very useful to group code by feature instead of by type of file. It makes
|
||||||
|
it easier to scale application to larger size.
|
||||||
|
|
||||||
|
To do so, Owl has two small helpers that make it easy to define a
|
||||||
|
template or a stylesheet inside a javascript (or typescript) file: the
|
||||||
|
[`xml`](../reference/tags.md#xml-tag) and [`css`](../reference/tags.md#css-tag)
|
||||||
|
helper.
|
||||||
|
|
||||||
|
This means that the template, the style and the javascript code can be defined in
|
||||||
|
the same file. For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Component } = owl;
|
||||||
|
const { xml, css } = owl.tags;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// TEMPLATE
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const TEMPLATE = xml/* xml */ `
|
||||||
|
<div class="main">
|
||||||
|
<Sidebar/>
|
||||||
|
<Content />
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// STYLE
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const STYLE = css/* css */ `
|
||||||
|
.main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// CODE
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
class Main extends Component {
|
||||||
|
static template = TEMPLATE;
|
||||||
|
static style = STYLE;
|
||||||
|
static components = { Sidebar, Content };
|
||||||
|
|
||||||
|
// rest of component...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the above example has an inline xml comment, just after the `xml` call.
|
||||||
|
This is useful for some editor plugins, such as the VS Code addon
|
||||||
|
`Comment tagged template`, which, if installed, add syntax highlighting to the
|
||||||
|
content of the template string.
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# 🦉 Quick Overview 🦉
|
||||||
|
|
||||||
|
Owl components in an application are used to define a (dynamic) tree of components.
|
||||||
|
|
||||||
|
```
|
||||||
|
Root
|
||||||
|
/ \
|
||||||
|
A B
|
||||||
|
/ \
|
||||||
|
C D
|
||||||
|
```
|
||||||
|
|
||||||
|
**State:** each component can manage its own local state. It is a simple ES6
|
||||||
|
class, there are no special rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<button t-on-click="increment">
|
||||||
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
state = { value: 0 };
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The example above shows a component with a local state. Note that since there
|
||||||
|
is nothing magical to the `state` object, we need to manually call the `render`
|
||||||
|
function whenever we update it. This can quickly become annoying (and not
|
||||||
|
efficient if we do it too much). There is a better way: using the `useState`
|
||||||
|
hook, which transforms an object into a reactive version of itself:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<button t-on-click="increment">
|
||||||
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the `t-on-click` handler can even be replaced by an inline statement:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-on-click="state.value++">
|
||||||
|
```
|
||||||
|
|
||||||
|
**Props:** sub components often needs some information from their parents. This
|
||||||
|
is done by adding the required information to the template. This will then be
|
||||||
|
accessible by the sub component in the `props` object. Note that there is an
|
||||||
|
important rule here: the information contained in the `props` object is not
|
||||||
|
owned by the sub component, and should never be modified.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<div>Hello <t t-esc="props.name"/></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<Child name="'Owl'" />
|
||||||
|
<Child name="'Framework'" />
|
||||||
|
</div>`;
|
||||||
|
static components = { Child };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Communication:** there are multiple ways to communicate information between
|
||||||
|
components. However, the two most important ways are the following:
|
||||||
|
|
||||||
|
- from parent to children: by using `props`,
|
||||||
|
- from a children to one of its parent: by triggering events.
|
||||||
|
|
||||||
|
The following example illustrate both mechanisms:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class OrderLine extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-click="add">
|
||||||
|
<div><t t-esc="props.line.name"/></div>
|
||||||
|
<div>Quantity: <t t-esc="props.line.quantity"/></div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
add() {
|
||||||
|
this.trigger("add-to-order", { line: this.props.line });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div t-on-add-to-order="addToOrder">
|
||||||
|
<OrderLine
|
||||||
|
t-foreach="orders"
|
||||||
|
t-as="line"
|
||||||
|
line="line" />
|
||||||
|
</div>`;
|
||||||
|
static components = { OrderLine };
|
||||||
|
orders = useState([
|
||||||
|
{ id: 1, name: "Coffee", quantity: 0 },
|
||||||
|
{ id: 2, name: "Tea", quantity: 0 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
addToOrder(event) {
|
||||||
|
const line = event.detail.line;
|
||||||
|
line.quantity++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `OrderLine` component trigger a `add-to-order` event. This
|
||||||
|
will generate a DOM event which will bubble along the DOM tree. It will then be
|
||||||
|
intercepted by the parent component, which will then get the line (from the
|
||||||
|
`detail` key) and then increment its quantity. See the page on [event handling](../reference/event_handling.md)
|
||||||
|
for more details on how events work.
|
||||||
|
|
||||||
|
Note that this example would have also worked if the `OrderLine` component
|
||||||
|
directly modifies the `line` object. However, this is not a good practice: this
|
||||||
|
only works because the `props` object received by the child component is reactive,
|
||||||
|
so the child component is then coupled to the parents implementation.
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
# 🦉 How to start an Owl project 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Simple html file](#simple-html-file)
|
||||||
|
- [With a static server](#with-a-static-server)
|
||||||
|
- [Standard Javascript project](#standard-javascript-project)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Each software project has its specific needs. Many of these needs can be solved
|
||||||
|
with some tooling: `webpack`, `gulp`, css preprocessor, bundlers, transpilers, ...
|
||||||
|
|
||||||
|
Because of that, it is usually not simple to just start a project. Some
|
||||||
|
frameworks provide their own tooling to help with that. But then, you have to
|
||||||
|
integrate and learn how these applications work.
|
||||||
|
|
||||||
|
Owl is designed to be used with no tooling at all. Because of that, Owl can
|
||||||
|
"easily" be integrated in a modern build toolchain. In this section, we will
|
||||||
|
discuss a few different setups to start a project. Each of these setups has
|
||||||
|
advantages and disadvantages in different situations.
|
||||||
|
|
||||||
|
## Simple html file
|
||||||
|
|
||||||
|
The simplest possible setup is the following: a simple javascript file with your
|
||||||
|
code. To do that, let us create the following file structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
hello_owl/
|
||||||
|
index.html
|
||||||
|
owl.js
|
||||||
|
app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
The file `owl.js` can be downloaded from the last release published at
|
||||||
|
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
|
||||||
|
is a single javascript file which export all Owl into the global `owl` object.
|
||||||
|
|
||||||
|
Now, `index.html` should contain the following:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello Owl</title>
|
||||||
|
<script src="owl.js"></script>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
And `app.js` should look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Component, mount } = owl;
|
||||||
|
const { xml } = owl.tags;
|
||||||
|
const { whenReady } = owl.utils;
|
||||||
|
|
||||||
|
// Owl Components
|
||||||
|
class App extends Component {
|
||||||
|
static template = xml`<div>Hello Owl</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup code
|
||||||
|
function setup() {
|
||||||
|
mount(App, target: { document.body })
|
||||||
|
}
|
||||||
|
|
||||||
|
whenReady(setup);
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, simply loading this html file in a browser should display a welcome message.
|
||||||
|
This setup is not fancy, but it is extremely simple. There are no tooling at
|
||||||
|
all required. It can be slightly optimized by using the minified build of Owl.
|
||||||
|
|
||||||
|
## With a static server
|
||||||
|
|
||||||
|
The previous setup has a big disadvantage: the application code is located in a
|
||||||
|
single file. Obviously, we could split it in several files and add multiple
|
||||||
|
`<script>` tags in the html page, but then we need to make sure the script are
|
||||||
|
inserted in the proper order, we need to export each file content in global
|
||||||
|
variables and we lose autocompletion across files.
|
||||||
|
|
||||||
|
There is a low tech solution to this issue: using native javascript modules.
|
||||||
|
This however has a requirement: for security reasons, browsers will not accept
|
||||||
|
modules on content served through the `file` protocol. This means that we need
|
||||||
|
to use a static server.
|
||||||
|
|
||||||
|
Let us start a new project with the following file structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
hello_owl/
|
||||||
|
src/
|
||||||
|
app.js
|
||||||
|
index.html
|
||||||
|
main.js
|
||||||
|
owl.js
|
||||||
|
```
|
||||||
|
|
||||||
|
As previously, the file `owl.js` can be downloaded from the last release published at
|
||||||
|
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
|
||||||
|
|
||||||
|
Now, `index.html` should contain the following:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello Owl</title>
|
||||||
|
<script src="owl.js"></script>
|
||||||
|
<script src="main.js" type="module"></script>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
Not that the `main.js` script tag has the `type="module"` attribute. This means
|
||||||
|
that the browser will parse the script as a module, and load all its dependencies.
|
||||||
|
|
||||||
|
Here is the content of `app.js` and `main.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// app.js ----------------------------------------------------------------------
|
||||||
|
const { Component, mount } = owl;
|
||||||
|
const { xml } = owl.tags;
|
||||||
|
|
||||||
|
export class App extends Component {
|
||||||
|
static template = xml`<div>Hello Owl</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// main.js ---------------------------------------------------------------------
|
||||||
|
import { App } from "./app.js";
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
mount(App, { target: document.body });
|
||||||
|
}
|
||||||
|
|
||||||
|
owl.utils.whenReady(setup);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `main.js` file import the `app.js` file. Note that the import statement has
|
||||||
|
a `.js` suffix, which is important. Most text editor can understand this syntax
|
||||||
|
and will provide autocompletion.
|
||||||
|
|
||||||
|
Now, to execute this code, we need to serve the `src` folder statically. A low
|
||||||
|
tech way to do that is to use for example the python `SimpleHTTPServer` feature:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd src
|
||||||
|
$ python -m SimpleHTTPServer 8022 # now content is available at localhost:8022
|
||||||
|
```
|
||||||
|
|
||||||
|
Another more "javascripty" way to do it is to create a `npm` application. To do
|
||||||
|
that, we can add the following `package.json` file at the root of the project:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "hello_owl",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Starting Owl app",
|
||||||
|
"main": "src/index.html",
|
||||||
|
"scripts": {
|
||||||
|
"serve": "serve src"
|
||||||
|
},
|
||||||
|
"author": "John",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"serve": "^11.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can now install the `serve` tool with the command `npm install`, and then,
|
||||||
|
start a static server with the simple `npm run serve` command.
|
||||||
|
|
||||||
|
## Standard Javascript project
|
||||||
|
|
||||||
|
The previous setup works, and is certainly good for some usecases, including
|
||||||
|
quick prototyping. However, it lacks some useful features, such as livereload,
|
||||||
|
a test suite, or bundling the code in a single file.
|
||||||
|
|
||||||
|
Each of these features, and many others, can be done in many different ways.
|
||||||
|
Since it is really not trivial to configure such a project, we provide here an
|
||||||
|
example that can be used as a starting point.
|
||||||
|
|
||||||
|
Our standard Owl project has the following file structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
hello_owl/
|
||||||
|
public/
|
||||||
|
index.html
|
||||||
|
src/
|
||||||
|
components/
|
||||||
|
App.js
|
||||||
|
main.js
|
||||||
|
tests/
|
||||||
|
components/
|
||||||
|
App.test.js
|
||||||
|
helpers.js
|
||||||
|
.gitignore
|
||||||
|
package.json
|
||||||
|
webpack.config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
This project as a `public` folder, meant to contain all static assets, such as
|
||||||
|
images and styles. The `src` folder has the javascript source code, and finally,
|
||||||
|
`tests` contains the test suite.
|
||||||
|
|
||||||
|
Here is the content of `index.html`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hello Owl</title>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that there are no `<script>` tag here. They will be injected by webpack.
|
||||||
|
Now, let's have a look at the javascript files:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/components/App.js -------------------------------------------------------
|
||||||
|
import { Component, tags, useState } from "@odoo/owl";
|
||||||
|
|
||||||
|
const { xml } = tags;
|
||||||
|
|
||||||
|
export class App extends Component {
|
||||||
|
static template = xml`<div t-on-click="update">Hello <t t-esc="state.text"/></div>`;
|
||||||
|
state = useState({ text: "Owl" });
|
||||||
|
update() {
|
||||||
|
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/main.js -----------------------------------------------------------------
|
||||||
|
import { utils, mount } from "@odoo/owl";
|
||||||
|
import { App } from "./components/App";
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
mount(App, { target: document.body });
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.whenReady(setup);
|
||||||
|
|
||||||
|
// tests/components/App.test.js ------------------------------------------------
|
||||||
|
import { App } from "../../src/components/App";
|
||||||
|
import { makeTestFixture, nextTick, click } from "../helpers";
|
||||||
|
import { mount } from "@odoo/owl";
|
||||||
|
|
||||||
|
let fixture;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = makeTestFixture();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("App", () => {
|
||||||
|
test("Works as expected...", async () => {
|
||||||
|
await mount(App, { target: fixture });
|
||||||
|
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
|
||||||
|
|
||||||
|
click(fixture, "div");
|
||||||
|
await nextTick();
|
||||||
|
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// tests/helpers.js ------------------------------------------------------------
|
||||||
|
import { Component } from "@odoo/owl";
|
||||||
|
import "regenerator-runtime/runtime";
|
||||||
|
|
||||||
|
export async function nextTick() {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
setTimeout(() => Component.scheduler.requestAnimationFrame(() => resolve()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeTestFixture() {
|
||||||
|
let fixture = document.createElement("div");
|
||||||
|
document.body.appendChild(fixture);
|
||||||
|
return fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function click(elem, selector) {
|
||||||
|
elem.querySelector(selector).dispatchEvent(new Event("click"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, here is the configuration files `.gitignore`, `package.json` and
|
||||||
|
`webpack.config.js`:
|
||||||
|
|
||||||
|
```
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "hello_owl",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Demo app",
|
||||||
|
"main": "src/index.html",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"dev": "webpack-dev-server --mode development"
|
||||||
|
},
|
||||||
|
"author": "Someone",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.8.4",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
|
"babel-jest": "^25.1.0",
|
||||||
|
"babel-loader": "^8.0.6",
|
||||||
|
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||||
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"jest": "^25.1.0",
|
||||||
|
"regenerator-runtime": "^0.13.3",
|
||||||
|
"serve": "^11.3.0",
|
||||||
|
"webpack": "^4.41.5",
|
||||||
|
"webpack-cli": "^3.3.10",
|
||||||
|
"webpack-dev-server": "^3.10.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@odoo/owl": "^1.0.4"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"plugins": ["@babel/plugin-proposal-class-properties"],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"plugins": ["transform-es2015-modules-commonjs"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"verbose": false,
|
||||||
|
"testRegex": "(/tests/.*(test|spec))\\.js?$",
|
||||||
|
"moduleFileExtensions": ["js"],
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.[t|j]sx?$": "babel-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
const path = require("path");
|
||||||
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
|
const host = process.env.HOST || "localhost";
|
||||||
|
|
||||||
|
module.exports = function (env, argv) {
|
||||||
|
const mode = argv.mode || "development";
|
||||||
|
return {
|
||||||
|
mode: mode,
|
||||||
|
entry: "./src/main.js",
|
||||||
|
output: {
|
||||||
|
filename: "main.js",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.jsx?$/,
|
||||||
|
loader: "babel-loader",
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".js", ".jsx"],
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
contentBase: path.resolve(__dirname, "public/index.html"),
|
||||||
|
compress: true,
|
||||||
|
hot: true,
|
||||||
|
host,
|
||||||
|
port: 3000,
|
||||||
|
publicPath: "/",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
inject: true,
|
||||||
|
template: path.resolve(__dirname, "public/index.html"),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
With this setup, we can now use the following script commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build # build the full application in prod mode in dist/
|
||||||
|
|
||||||
|
npm run dev # start a dev server with livereload
|
||||||
|
|
||||||
|
npm run test # run the jest test suite
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,256 @@
|
|||||||
|
# Comparison with Vue/React
|
||||||
|
|
||||||
|
OWL, React and Vue have the same main feature: they allow developers to build
|
||||||
|
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
|
||||||
|
|
||||||
|
In this page, we try to highlight some of these differences. Obviously, a lot of
|
||||||
|
effort was put to be fair. However, if you disagree with some of the points
|
||||||
|
discussed, feel free to open an issue/submit a PR to correct this text.
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Size](#size)
|
||||||
|
- [Class Based](#class-based)
|
||||||
|
- [Tooling/Build Step](#toolingbuild-step)
|
||||||
|
- [Templating](#templating)
|
||||||
|
- [Asynchronous rendering](#asynchronous-rendering)
|
||||||
|
- [Reactiveness](#reactiveness)
|
||||||
|
- [State Management](#state-management)
|
||||||
|
- [Hooks](#hooks)
|
||||||
|
|
||||||
|
## Size
|
||||||
|
|
||||||
|
OWL is intended to be small and to work at a slightly lower level of abstraction
|
||||||
|
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
|
||||||
|
|
||||||
|
| Framework | Size (minified, gzipped) |
|
||||||
|
| ------------------------ | ------------------------ |
|
||||||
|
| OWL | 18kb |
|
||||||
|
| Vue + VueX | 30kb |
|
||||||
|
| Vue + VueX + Vue Router | 39kb |
|
||||||
|
| React + ReactDOM + Redux | 40kb |
|
||||||
|
| jQuery | 30kb |
|
||||||
|
|
||||||
|
Note that those comparisons are not entirely fair, because we do not compare
|
||||||
|
the same exact set of features. For example, VueX and Vue Router support more
|
||||||
|
advanced use cases.
|
||||||
|
|
||||||
|
## Class Based
|
||||||
|
|
||||||
|
Both React and Vue moved away from defining components with classes. They prefer
|
||||||
|
a more functional approach, in particular, with the new `hooks` mechanisms.
|
||||||
|
|
||||||
|
This has some advantages and disadvantages. But the end result is that React
|
||||||
|
and Vue both offers multiple different ways of defining new components. In
|
||||||
|
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
||||||
|
components are fast enough for all our usecases, and making it as simple as
|
||||||
|
possible for developers is more valuable (for us).
|
||||||
|
|
||||||
|
Also, functions or class based components are more than just syntax. Functions
|
||||||
|
come with a mindset of composition and class are about inheritance. Clearly,
|
||||||
|
both of these are important mechanisms for reusing code. Also, one does not
|
||||||
|
exclude the other.
|
||||||
|
|
||||||
|
It certainly looks like the world of UI frameworks is moving toward composition,
|
||||||
|
for many very good reasons. Owl is still good at composition (for example,
|
||||||
|
Owl supports slots, which is the primary mechanism to make generic reusable
|
||||||
|
components). But it can also use inheritance (and this is very important since
|
||||||
|
templates can also be inherited with `xpaths` transformations).
|
||||||
|
|
||||||
|
## Tooling/Build step
|
||||||
|
|
||||||
|
OWL is designed to be easy to use in a standalone way. For various reasons,
|
||||||
|
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
|
||||||
|
be used by simply adding a script tag to a page.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="owl.min.js" />
|
||||||
|
```
|
||||||
|
|
||||||
|
In comparison, React encourages using JSX, which necessitate a build step, and
|
||||||
|
most Vue applications uses single file components, which also necessitate a build step.
|
||||||
|
|
||||||
|
On the flipside, external tooling may make it harder to use in some case, but it
|
||||||
|
also brings a lot of benefits. And React/Vue have both a large ecosystem.
|
||||||
|
|
||||||
|
Note that since Owl is not dependant on any external tool nor libraries, it is
|
||||||
|
very easy to integrate into any build toolchain. Also, since we cannot rely on
|
||||||
|
additional tools, we made a lot of effort to make the most of the web platform.
|
||||||
|
|
||||||
|
For example, Owl uses the standard `xml` parser that comes with every browser.
|
||||||
|
Because of that, Owl did not have to write its own template parser. Another
|
||||||
|
example is the [`xml`](../reference/tags.md#xml-tag) tag helper function, which makes use of
|
||||||
|
native template literals to allow in a natural way to write `xml` templates
|
||||||
|
directly in the javascript code. This can be easily integrated with editor
|
||||||
|
plugins to have autocompletion inside the template.
|
||||||
|
|
||||||
|
## Templating
|
||||||
|
|
||||||
|
OWL uses its own QWeb engine, which compiles templates on the
|
||||||
|
frontend, as they are needed. This is extremely convenient for our use case, in
|
||||||
|
particular because templates are described in XML files, and can be modified by
|
||||||
|
XPaths. Since Odoo is at its heart a modular application, this is an important
|
||||||
|
feature for us.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<button t-on-click="increment">Click Me! [<t t-esc="state.value"/>]</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Vue is actually kind of similar. Its template language is kind of close to QWeb,
|
||||||
|
with the `v` replaced by the `t`. However, it is also more fully featured. For
|
||||||
|
example, Vue templates have slots, or event modifiers. A large difference is that
|
||||||
|
most Vue applications will need to be built ahead of time, to compile the templates
|
||||||
|
into javascript functions. Note that Vue has a separate build which includes the
|
||||||
|
template compiler.
|
||||||
|
|
||||||
|
In contrast, most React applications do not use a templating language, but write
|
||||||
|
some JSX code, which is precompiled into plain JavaScript by a build step. This
|
||||||
|
example is done with the (kind of outdated) React class system:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
class Clock extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This has the advantage of having the full power of Javascript, but is less
|
||||||
|
structured than a template language. Note that the tooling is quite impressive:
|
||||||
|
there is a syntax highlighter for jsx here on github!
|
||||||
|
|
||||||
|
By comparison, here is the equivalent Owl component, written with the
|
||||||
|
[`xml`](../reference/tags.md#xml-tag) tag helper:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Clock extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<h2>It is {props.date.toLocaleTimeString()}.</h2>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Asynchronous Rendering
|
||||||
|
|
||||||
|
This is actually a big difference between OWL and React/Vue: components in OWL
|
||||||
|
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
|
||||||
|
|
||||||
|
- `willStart` (before the component starts rendering)
|
||||||
|
- `willUpdateProps` (before new props are set)
|
||||||
|
|
||||||
|
Both these methods can be implemented and return a promise. The rendering will
|
||||||
|
then wait for these promises to be completed before patching the DOM. This is
|
||||||
|
useful for some use cases: for example, a component may want to fetch an external
|
||||||
|
library (a calendar component may need a specialized calendar rendering library),
|
||||||
|
in its willStart hook.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class MyCalendarComponent extends owl.Component {
|
||||||
|
...
|
||||||
|
|
||||||
|
willStart() {
|
||||||
|
return utils.lazyLoad('static/libs/fullcalendar/fullcalendar.js');
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This may be dangerous (to stop the rendering waiting for the network), but it is
|
||||||
|
extremely powerful as well, as demonstrated by the Odoo Web Client.
|
||||||
|
|
||||||
|
Lazy loading static libraries can obviously be done with React/Vue, but it is
|
||||||
|
more convoluted. For example, in Vue, you need to use a dynamic import keyword
|
||||||
|
that needs to be transpiled at build time in order for the component to be loaded
|
||||||
|
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
|
||||||
|
|
||||||
|
## Reactiveness
|
||||||
|
|
||||||
|
React has a simple model: whenever the state changes, it is
|
||||||
|
replaced with a new state (via the `setState` method). Then, the DOM is patched.
|
||||||
|
This is simple, efficient, and a little bit awkward to write.
|
||||||
|
|
||||||
|
Vue is a little bit different: it replace magically the properties in the state
|
||||||
|
by getters/setters. With that, it can notify components whenever the state that
|
||||||
|
they read was changed.
|
||||||
|
|
||||||
|
Owl is closer to vue: it also tracks magically the state properties, but it does
|
||||||
|
only increment an internal counter whenever it changes. Note that it is done
|
||||||
|
with a `Proxy`, which means that it is totally transparent to the developers.
|
||||||
|
Adding new keys is supported. Once any part of the state has been changed, a
|
||||||
|
rendering is scheduled in the next microtask tick (promise queue).
|
||||||
|
|
||||||
|
## 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,32 @@
|
|||||||
|
# 🦉 Rendering Pipeline 🦉
|
||||||
|
|
||||||
|
We explain here how Owl is designed, from the perspective of its rendering
|
||||||
|
pipeline.
|
||||||
|
|
||||||
|
Warning: these notes are technical by nature, and intended for people working
|
||||||
|
on Owl (or interested in understanding its design).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A rendering occurs in two phases:
|
||||||
|
|
||||||
|
- virtual rendering: this generates the virtual dom in memory, asynchronously
|
||||||
|
- patch: applies a virtual tree to the screen (synchronously)
|
||||||
|
|
||||||
|
There are several classes involved in a rendering:
|
||||||
|
|
||||||
|
- components
|
||||||
|
- a scheduler
|
||||||
|
- fibers: small objects containing some metadata, associated with a rendering of
|
||||||
|
a specific component
|
||||||
|
|
||||||
|
Components are organized in a dynamic component tree, visible in the user
|
||||||
|
interface. Whenever a rendering is initiated in a component `C`:
|
||||||
|
|
||||||
|
- a fiber is created on `C` with the rendering props information
|
||||||
|
- the virtual rendering phase starts on C (will asynchronously render all the
|
||||||
|
child components)
|
||||||
|
- the fiber is added to the scheduler, which will poll continuously, every
|
||||||
|
animation frame, if the fiber is done
|
||||||
|
- once it is done, the scheduler will call the task callback, which will apply
|
||||||
|
the patch (if it was not cancelled in the meantime).
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 🦉 VDom 🦉
|
||||||
|
|
||||||
|
Owl is a declarative component system: we declare the structure of the component
|
||||||
|
tree, and Owl will translate that to a list of imperative operations. This
|
||||||
|
translation is done by a virtual dom. This is the low level layer of Owl, most
|
||||||
|
developer will not need to call directly the virtual dom functions.
|
||||||
|
|
||||||
|
The main idea behind a virtual dom is to keep a in-memory representation of the
|
||||||
|
DOM (called a virtual node), and whenever some change is needed, to regenerate
|
||||||
|
a new representation, compute the difference between the old and the new, then
|
||||||
|
apply the changes.
|
||||||
|
|
||||||
|
`vdom` exports two functions:
|
||||||
|
|
||||||
|
- `h`: create a new virtual node
|
||||||
|
- `patch`: compare two virtual nodes, and apply the difference.
|
||||||
|
|
||||||
|
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# 🦉 Why Owl ? 🦉
|
||||||
|
|
||||||
|
The common wisdom is that one should not reinvent the wheel, because that would
|
||||||
|
waste effort and resources. It is certainly true in many cases. A javascript
|
||||||
|
framework is a considerable investment, so it is quite logical to ask the question:
|
||||||
|
why did Odoo decide to make OWL instead of using a standard/well known framework,
|
||||||
|
such as React or Vue?
|
||||||
|
|
||||||
|
As you might expect, the answer to that question is not simple. But most of the
|
||||||
|
reasons discussed in this page are a consequence from a single fact: Odoo is
|
||||||
|
extremely modular.
|
||||||
|
|
||||||
|
This means, for example, that the core parts of Odoo are not aware, before runtime,
|
||||||
|
of what files will be loaded/executed, or what will be the state of the UI. Because
|
||||||
|
of that, Odoo cannot rely on a standard build toolchain. Also, this implies that
|
||||||
|
the core parts of Odoo need to be extremely generic. In other words, Odoo is not
|
||||||
|
really an application with a user interface. It is an application which generates
|
||||||
|
a dynamic user interface. And most frameworks are not up to the task.
|
||||||
|
|
||||||
|
Betting on Owl was not an easy choice to make, because there certainly are a lot
|
||||||
|
of conflicting needs that we want to carefully balance. Choosing anything other
|
||||||
|
than a well known framework is bound to be controversial. This page will explain
|
||||||
|
some of the reason why we still believe that building Owl is a worthwile
|
||||||
|
endeavour.
|
||||||
|
|
||||||
|
## Strategy
|
||||||
|
|
||||||
|
It is true that we want to keep control of our technology, in the sense that we
|
||||||
|
do not want to depend on Facebook or Google, or any other large (or small)
|
||||||
|
company. If they decide to change their license, or to go in a direction that
|
||||||
|
will not work for us, this may be a problem. This is even more true because
|
||||||
|
Odoo is not a conventional javascript application, and our needs are probably
|
||||||
|
quite different as most other applications.
|
||||||
|
|
||||||
|
## Class components
|
||||||
|
|
||||||
|
It is clear that the biggest frameworks are moving away from class components.
|
||||||
|
There is an implicit assumption that class components are terrible, and that
|
||||||
|
functional programming is the way to go. React even goes as far as to say that
|
||||||
|
classes are confusing for developers.
|
||||||
|
|
||||||
|
While there is some truth to that, and to the fact that composition is certainly
|
||||||
|
a good mechanism for code reuse, we believe that classes and inheritance are
|
||||||
|
important tools.
|
||||||
|
|
||||||
|
Sharing code between generic components with inheritance is the way Odoo built
|
||||||
|
its web client. And it is clear that inheritance is not the root of all evils.
|
||||||
|
It is often a perfectly simple and appropriate solution. What matter most is
|
||||||
|
the architectural decisions.
|
||||||
|
|
||||||
|
Also, Odoo has another specific use out of class components: each method of a
|
||||||
|
class provides an extension point for addons. This may not be a clean architecture
|
||||||
|
pattern, but it is a pragmatic decision that served Odoo well: classes are
|
||||||
|
sometimes monkey-patched to add behaviour from the outside. A little bit like
|
||||||
|
mixins, but from the outside.
|
||||||
|
|
||||||
|
Using React or Vue would make it significantly harder to monkey patch components,
|
||||||
|
because a lot of the state is hidden in their internals.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
React or Vue have a huge community, and a lot of effort have been made into their
|
||||||
|
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
|
||||||
|
since the assets are totally dynamic (and could change whenever the user installs
|
||||||
|
or removes an addon), we need to have all that kind of tooling on the production
|
||||||
|
servers. This is certainly not ideal.
|
||||||
|
|
||||||
|
Also, this makes it very complicated to setup Vue or React tools: Odoo code is
|
||||||
|
not a simple file that import other files. It changes all the time, assets
|
||||||
|
are bundled differently in different contexts. This is the reason why Odoo has
|
||||||
|
its own module system, which are resolved at runtime, by the browser. The
|
||||||
|
dynamic nature of Odoo means that we often need to delay work as late as possible
|
||||||
|
(in other word, we want a JIT user interface!)
|
||||||
|
|
||||||
|
Our ideal framework has minimal (mandatory) tooling, which makes it easier to
|
||||||
|
deploy. Using React without JSX, or Vue without vue file is not very appealing.
|
||||||
|
|
||||||
|
At the same time, Owl is designed to solve this issue: it compiles templates
|
||||||
|
by the browser, it doesn't need much code for that, since we use the XML parser
|
||||||
|
built into each browser. Owl works with or without any additional tooling. It
|
||||||
|
can use template strings to write single file components, and is easy to integrate
|
||||||
|
in any html page, with a simple `<script>` tag.
|
||||||
|
|
||||||
|
## Template based
|
||||||
|
|
||||||
|
Odoo stores templates as XML documents in a database. This is very powerful, since
|
||||||
|
this allow the use of xpaths to customize other templates. This is a very
|
||||||
|
important feature of odoo, and one of the key to Odoo modularity.
|
||||||
|
|
||||||
|
Because of that, we still expect to write our templates in an XML document.
|
||||||
|
Weirdly enough, no major framework uses XML to store templates, even though it
|
||||||
|
is extremely convenient.
|
||||||
|
|
||||||
|
So, using React or Vue means that we need to make a template compiler. For React,
|
||||||
|
that would be a compiler that would take a QWeb template, and convert it to a
|
||||||
|
React render function. For Vue, it would convert it to a Vue template. Then
|
||||||
|
we need to bundle the vue template compiler as well.
|
||||||
|
|
||||||
|
Not only this would be complex (compiling a templating language into another is
|
||||||
|
not an easy task), but it would negatively impact the developer experience as
|
||||||
|
well. Writing Vue or React components in a QWeb template would certainly be
|
||||||
|
awkward, and very confusing.
|
||||||
|
|
||||||
|
## Developer Experience
|
||||||
|
|
||||||
|
This brings us to the following point: developer experience. We see this choice
|
||||||
|
as an investment for the future, and we want to make onboarding developers as
|
||||||
|
easy as possible.
|
||||||
|
|
||||||
|
While many javascript professionals clearly think that react/vue is not difficult
|
||||||
|
(which is true to some extent), it is alsy true that many non js specialists are
|
||||||
|
overwhelmed with the frontend world: functional components, hooks, and many other
|
||||||
|
fancy words. Also, what is available in the compilation context may be difficult,
|
||||||
|
there is a lot of black magic going on in pretty much every framework. Vue
|
||||||
|
somehow join various namespaces into one, under the hood, and add various internal
|
||||||
|
keys. Svelte transform the code. React require that state transformations are
|
||||||
|
deep, and not shallow.
|
||||||
|
|
||||||
|
Owl is trying very hard to have a simple and familiar API. It uses classes. Its
|
||||||
|
reactivity system is explicit, not implicit. The scoping rules are obvious. In
|
||||||
|
case of doubt, we err on the side of not implementing a feature.
|
||||||
|
|
||||||
|
It is certainly different from React or Vue, but at the same time, kind of
|
||||||
|
familiar for experienced developers.
|
||||||
|
|
||||||
|
## JIT compilation
|
||||||
|
|
||||||
|
There is also a clear trend in the frontend world to compile code
|
||||||
|
as much as possible ahead of time. Most frameworks will compile templates ahead
|
||||||
|
of time. And now Svelte is trying to compile the JS code away, so it can remove
|
||||||
|
itself from the bundle.
|
||||||
|
|
||||||
|
This is certainly reasonable for many usecases. However, this is not what Odoo
|
||||||
|
needs: Odoo will fetch templates from the database and need to compile them only
|
||||||
|
at the last possible moment, so we can apply all necessary xpaths.
|
||||||
|
|
||||||
|
Even more: Odoo needs to be able to generate (and compile) templates at runtime.
|
||||||
|
Currently, Odoo form views interpret an xml description. But the form view code
|
||||||
|
then needs to do a lot of complicated operations. With Owl, we will be able to
|
||||||
|
transform a view description into a QWeb template, then compile that and use it
|
||||||
|
immediately.
|
||||||
|
|
||||||
|
## Reactivity
|
||||||
|
|
||||||
|
There are other design choices that we feel are not optimal in other frameworks.
|
||||||
|
For example, the reactivity system. We like the way Vue did it, but it has a
|
||||||
|
flaw: it is not really optional. There is actually a way to opt out of the reactivity
|
||||||
|
system by freezing the state, but then, it is freezed.
|
||||||
|
|
||||||
|
And there certainly are situations where we need a state, which is not read-only,
|
||||||
|
and not observed. For example, imagine a spreadsheet component. It may have a
|
||||||
|
very large internal state, and it knows exactly when it needs to be rendered
|
||||||
|
(basically, whenever the user performs some action). Then, observing its state
|
||||||
|
is a net performance loss, both for the CPU and the memory.
|
||||||
|
|
||||||
|
## Concurrency
|
||||||
|
|
||||||
|
Many applications are happy to simply display a spinner whenever a new asynchronous
|
||||||
|
action is performed, but Odoo wants a different user experience: most asynchronous
|
||||||
|
state changes are not displayed until ready. This is sometimes called a concurrent
|
||||||
|
mode: the UI is rendered in memory, and displayed only when it is ready (and
|
||||||
|
only if it has not been cancelled by subsequent user actions).
|
||||||
|
|
||||||
|
React has now an experimental concurrent mode, but it was not ready when Owl
|
||||||
|
started. Vue has not really an equivalent API (suspense is not what we need).
|
||||||
|
|
||||||
|
Also, React concurrent mode is complex to use. Concurrency was one of the rare
|
||||||
|
strong point of the former Odoo js framework (widgets), and we feel that Owl has
|
||||||
|
now a very strong concurrent mode, which is simple and powerful at the same time.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This lengthy discussion showed that there are many small and not so small reasons
|
||||||
|
that current standard frameworks are not tailored to our needs. It is perfectly
|
||||||
|
fine, because they each chose a different set of tradeoffs.
|
||||||
|
|
||||||
|
However, we feel that there is still room in the framework world for something
|
||||||
|
that is different. For a framework that makes choices compatible with Odoo.
|
||||||
|
|
||||||
|
And that is why we built Owl 🦉.
|
||||||
@@ -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();
|
|
||||||
```
|
|
||||||
-594
@@ -1,594 +0,0 @@
|
|||||||
# 🦉 QWeb 🦉
|
|
||||||
|
|
||||||
## Content
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [QWeb Engine](#qweb-engine)
|
|
||||||
- [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)
|
|
||||||
- [Dynamic attributes (`t-att` and `t-attf` directives)](#dynamic-attributes-t-att-and-t-attf-directives)
|
|
||||||
- [`t-attf` directive (dynamic attributes)](#t-att-directive-dynamic-attributes)
|
|
||||||
- [`t-call` directive (sub templates)](#t-call-directive-sub-templates)
|
|
||||||
|
|
||||||
- [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-directive)
|
|
||||||
- [`t-mounted` directive](#t-mounted-directive)
|
|
||||||
- [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 Engine
|
|
||||||
|
|
||||||
This section is about the javascript code that implements the `QWeb` specification.
|
|
||||||
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
|
|
||||||
instantiated:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const qweb = new owl.QWeb();
|
|
||||||
```
|
|
||||||
|
|
||||||
It's API is quite simple:
|
|
||||||
|
|
||||||
- **`constructor(data)`**: constructor. Takes an optional string to add initial
|
|
||||||
templates (see `addTemplates` for more information on format of the string).
|
|
||||||
|
|
||||||
```js
|
|
||||||
const qweb = new owl.QWeb(TEMPLATES);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`addTemplate(name, xmlStr)`**: add a specific template.
|
|
||||||
|
|
||||||
```js
|
|
||||||
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
|
||||||
attribute).
|
|
||||||
|
|
||||||
```js
|
|
||||||
const TEMPLATES = `
|
|
||||||
<templates>
|
|
||||||
<div t-name="App" class="main">main</div>
|
|
||||||
<div t-name="OtherWidget">other widget</div>
|
|
||||||
</templates>`;
|
|
||||||
qweb.addTemplates(TEMPLATES);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
|
|
||||||
which is a virtual representation of the DOM (see [vdom doc](vdom.md)).
|
|
||||||
|
|
||||||
```js
|
|
||||||
const vnode = qweb.render("App", widget);
|
|
||||||
```
|
|
||||||
|
|
||||||
## QWeb Specification
|
|
||||||
|
|
||||||
We define in this section the specification of how `QWeb` templates should be
|
|
||||||
rendered.
|
|
||||||
|
|
||||||
### 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>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dynamic attributes (`t-att` and `t-attf` directives)
|
|
||||||
|
|
||||||
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> -->
|
|
||||||
```
|
|
||||||
|
|
||||||
There is another way to format a string attribute: the `t-attf-` directive. With
|
|
||||||
it, you get string interpolation:
|
|
||||||
|
|
||||||
```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> -->
|
|
||||||
```
|
|
||||||
|
|
||||||
For historical reason, there is an alternate form of string interpolation:
|
|
||||||
|
|
||||||
```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> -->
|
|
||||||
```
|
|
||||||
|
|
||||||
### `t-call` directive (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>
|
|
||||||
```
|
|
||||||
|
|
||||||
## JS/OWL Specific Extensions
|
|
||||||
|
|
||||||
### `t-on` directive
|
|
||||||
|
|
||||||
In a component's template, it is useful to be able to register handlers on some
|
|
||||||
elements to some specific events. This
|
|
||||||
is what makes a template _alive_. There are two different use cases.
|
|
||||||
|
|
||||||
1. Register an event handler on a DOM node
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<button t-on-click="someMethod">Do something</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
This will be roughly translated in javascript like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
button.addEventListener("click", widget.someMethod.bind(widget));
|
|
||||||
```
|
|
||||||
|
|
||||||
The suffix (`click` in this example) is simply the name of the actual DOM
|
|
||||||
event.
|
|
||||||
|
|
||||||
2. Register an event handler on a component. This will not capture a DOM event,
|
|
||||||
but rather a _business_ event:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<t t-widget="MyWidget" t-on-menuLoaded="someMethod"/>
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
class MyWidget {
|
|
||||||
someWhere() {
|
|
||||||
const payload = ...;
|
|
||||||
this.trigger('menuLoaded', payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here, the parent widget will receive the payload in its `someMethod` handler,
|
|
||||||
whenever the event is triggered.
|
|
||||||
|
|
||||||
The `t-on` directive also allows to prebind some arguments. For example,
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<button t-on-click="someMethod(expr)">Do something</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
|
|
||||||
from the rendering context.
|
|
||||||
|
|
||||||
### Component: `t-widget`, `t-props`
|
|
||||||
|
|
||||||
The `t-widget` and the `t-props` directives are the key to a declarative component
|
|
||||||
system. They allow a template to define where and how a sub widget is created
|
|
||||||
and/or updated. For example:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<div t-name="ParentWidget">
|
|
||||||
<t t-widget="ChildWidget" t-props="{count: state.val}"/>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
class ParentWidget {
|
|
||||||
widgets = { ChildWidget };
|
|
||||||
state = { val: 4 };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Whenever the template is rendered, it will automatically create the subwidget
|
|
||||||
`ChildWidget` at the correct place. It needs to find the reference to the
|
|
||||||
actual component class in the special `widgets` key.
|
|
||||||
|
|
||||||
In this example, the child widget will receive the object `{count: 4}` in its
|
|
||||||
constructor. This will be assigned to the `props` variable, which can be accessed
|
|
||||||
on the widget (and also, in the template). Whenever the state is updated, then
|
|
||||||
the subwidget will also be updated automatically.
|
|
||||||
|
|
||||||
### `t-ref` directive
|
|
||||||
|
|
||||||
The `t-ref` directive helps a component keep reference to some inside part of it.
|
|
||||||
Like the `t-on` directive, it can work either on a DOM node, or on a component:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<div>
|
|
||||||
<div t-ref="someDiv"/>
|
|
||||||
<t t-widget="SubWidget" t-ref="someWidget"/>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, the widget will be able to access the `div` and the component
|
|
||||||
inside the special `refs` variable:
|
|
||||||
|
|
||||||
```js
|
|
||||||
this.refs.someDiv;
|
|
||||||
this.refs.someWidget;
|
|
||||||
```
|
|
||||||
|
|
||||||
This is useful for various usecases: for example, integrating with an external
|
|
||||||
library that needs to render itself inside an actual DOM node. Or for calling
|
|
||||||
some method on a sub widget.
|
|
||||||
|
|
||||||
Note: if used on a component, the reference will be set in the `refs`
|
|
||||||
variable between `willPatch` and `patched`.
|
|
||||||
|
|
||||||
### `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 0.5s;
|
|
||||||
}
|
|
||||||
.fade-enter,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: more information on animations are available [here](doc/animations.md).
|
|
||||||
|
|
||||||
### `t-mounted` directive
|
|
||||||
|
|
||||||
The `t-mounted` directive allows to register a callback to execute whenever the node
|
|
||||||
is inserted into the DOM.
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<div><input t-ref="someInput" t-mounted="focusMe"/></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
class MyWidget extends owl.Component {
|
|
||||||
...
|
|
||||||
focusMe() {
|
|
||||||
this.refs.someInput.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
+48
-10
@@ -1,16 +1,54 @@
|
|||||||
# 🦉 OWL Documentation 🦉
|
# 🦉 OWL Documentation 🦉
|
||||||
|
|
||||||
|
## Learning Owl
|
||||||
|
|
||||||
|
Are you new to Owl? This is the place to start!
|
||||||
|
|
||||||
|
- [Tutorial: create a TodoList application](learning/tutorial_todoapp.md)
|
||||||
|
- [Quick Overview](learning/overview.md)
|
||||||
|
- [How to start an Owl project](learning/quick_start.md)
|
||||||
|
- [How to test Components](learning/how_to_test.md)
|
||||||
|
- [How to write Single File Components](learning/how_to_write_sfc.md)
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
- [Component](component.md)
|
You will find here a complete reference of every feature, class or object
|
||||||
- [QWeb](qweb.md)
|
provided by Owl.
|
||||||
- [Store](store.md)
|
|
||||||
- [Observer](observer.md)
|
|
||||||
- [Virtual DOM](vdom.md)
|
|
||||||
- [Utils](utils.md)
|
|
||||||
|
|
||||||
## Miscellaneous
|
- [Animations](reference/animations.md)
|
||||||
- [Quick Start](quick_start.md)
|
- [Browser](reference/browser.md)
|
||||||
- [Animations](animations.md)
|
- [Component](reference/component.md)
|
||||||
- [Comparison with React/Vue](comparison.md)
|
- [Content](reference/content.md)
|
||||||
|
- [Concurrency Model](reference/concurrency_model.md)
|
||||||
|
- [Configuration](reference/config.md)
|
||||||
|
- [Context](reference/context.md)
|
||||||
|
- [Environment](reference/environment.md)
|
||||||
|
- [Event Bus](reference/event_bus.md)
|
||||||
|
- [Event Handling](reference/event_handling.md)
|
||||||
|
- [Error Handling](reference/error_handling.md)
|
||||||
|
- [Hooks](reference/hooks.md)
|
||||||
|
- [Mounting a component](reference/mounting.md)
|
||||||
|
- [Miscellaneous Components](reference/misc.md)
|
||||||
|
- [Observer](reference/observer.md)
|
||||||
|
- [Props](reference/props.md)
|
||||||
|
- [Props Validation](reference/props_validation.md)
|
||||||
|
- [QWeb Templating Language](reference/qweb_templating_language.md)
|
||||||
|
- [QWeb Engine](reference/qweb_engine.md)
|
||||||
|
- [Slots](reference/slots.md)
|
||||||
|
- [Tags](reference/tags.md)
|
||||||
|
- [Utils](reference/utils.md)
|
||||||
|
|
||||||
|
## Other Topics
|
||||||
|
|
||||||
|
This section provides miscellaneous document that explains some topics
|
||||||
|
which cannot be considered either a tutorial, or reference documentation.
|
||||||
|
|
||||||
|
- [Owl architecture: the Virtual DOM](miscellaneous/vdom.md)
|
||||||
|
- [Owl architecture: the rendering pipeline](miscellaneous/rendering.md)
|
||||||
|
- [Comparison with React/Vue](miscellaneous/comparison.md)
|
||||||
|
- [Why did Odoo built Owl?](miscellaneous/why_owl.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Found an issue in the documentation? A broken link? Some outdated information?
|
||||||
|
Please open an issue or submit a PR!
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 🦉 Animations 🦉
|
||||||
|
|
||||||
|
Animation is a complex topic. There are many different use cases, and many
|
||||||
|
solutions and technologies. Owl only supports some basic use cases.
|
||||||
|
|
||||||
|
## Simple CSS effects
|
||||||
|
|
||||||
|
Sometimes, using pure CSS is enough. For these use cases, Owl is not really
|
||||||
|
necessary: it just needs to render a DOM element with a specific class. For
|
||||||
|
example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<a class="btn flash" t-on-click="doSomething">Click</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
with the following CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
btn {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash {
|
||||||
|
transition: background 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash:active {
|
||||||
|
background-color: #41454a;
|
||||||
|
transition: background 0s;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
will produce a nice flash effect whenever the user clicks (or activates with the
|
||||||
|
keyboard) the button.
|
||||||
|
|
||||||
|
## CSS Transitions
|
||||||
|
|
||||||
|
A more complex situation occurs when we want to transition an element in or out
|
||||||
|
of the page. For example, we may want a fade-in and fade-out effect.
|
||||||
|
|
||||||
|
The `t-transition` directive is here to help us. It works on html elements and
|
||||||
|
on components, by adding and removing some css classes.
|
||||||
|
|
||||||
|
To perform useful transition effects, whenever an element appears or disappears,
|
||||||
|
it is necessary to add/remove some css style or class at some precise moment in
|
||||||
|
the lifetime of a node. Since this is not easy to do by hand, Owl `t-transition`
|
||||||
|
directive is there to help.
|
||||||
|
|
||||||
|
Whenever a node has a `t-transition` directive, with a `name` value, the following
|
||||||
|
sequence of events will happen:
|
||||||
|
|
||||||
|
At node insertion:
|
||||||
|
|
||||||
|
- the css classes `name-enter` and `name-enter-active` will be added directly
|
||||||
|
when the node is inserted into the DOM.
|
||||||
|
- on the next animation frame: the css class `name-enter` will be removed and the
|
||||||
|
class `name-enter-to` will be added (so they can be used to trigger css
|
||||||
|
transition effects).
|
||||||
|
- at the end of the transition, `name-enter-to` and `name-enter-active` will be removed.
|
||||||
|
|
||||||
|
At node destruction:
|
||||||
|
|
||||||
|
- the css classes `name-leave` and `name-leave-active` will be added before the
|
||||||
|
node is removed to the DOM.
|
||||||
|
- on the next animation frame: the css class `name-leave` will be removed and the
|
||||||
|
class `name-leave-to` will be added (so they can be used to trigger css
|
||||||
|
transition effects).
|
||||||
|
- at the end of the transition, `name-leave-to` and `name-leave-active` will be removed.
|
||||||
|
|
||||||
|
For example, a simple fade in/out effect can be done with this:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<div t-if="state.flag" class="square" t-transition="fade">Hello</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
||||||
|
.fade-enter,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `t-transition` directive can be applied on a node element or on a component.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
Owl does not support more than one transition on a single node, so the
|
||||||
|
`t-transition` expression must be a single value (i.e. no space allowed).
|
||||||
|
|
||||||
|
## SCSS Mixins
|
||||||
|
|
||||||
|
If you use SCSS, you can use mixins to make generic animations. Here is an exemple with a fade in / fade out animation:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
@mixin animation-fade($time, $name) {
|
||||||
|
.#{$name}_fade-enter-active,
|
||||||
|
.#{$name}_fade-active {
|
||||||
|
transition: all $time;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$name}_fade-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$name}_fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
@include animation-fade(0.5s, "o_notification");
|
||||||
|
```
|
||||||
|
|
||||||
|
You can now have in your template:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<SomeTag t-transition="o_notification_fade"/>
|
||||||
|
```
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 🦉 Browser 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Browser Content](#browser-content)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The browser object contains some browser native APIs, such as `setTimeout`, that
|
||||||
|
are used by Owl and its utility functions. They are exposed with the intent of
|
||||||
|
making them mockable if necessary.
|
||||||
|
|
||||||
|
```js
|
||||||
|
owl.browser.setTimeout === window.setTimeout; // return true
|
||||||
|
```
|
||||||
|
|
||||||
|
For now, this object contains some functions that are not used by Owl. They
|
||||||
|
will eventually be removed in Owl 2.0.
|
||||||
|
|
||||||
|
## Browser Content
|
||||||
|
|
||||||
|
More specifically, the `browser` object contains the following methods and objects:
|
||||||
|
|
||||||
|
- `setTimeout`
|
||||||
|
- `clearTimeout`
|
||||||
|
- `setInterval`
|
||||||
|
- `clearInterval`
|
||||||
|
- `requestAnimationFrame`
|
||||||
|
- `random`
|
||||||
|
- `Date`
|
||||||
|
- `fetch`
|
||||||
|
- `localStorage`
|
||||||
@@ -0,0 +1,855 @@
|
|||||||
|
# 🦉 OWL Component 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Reference](#reference)
|
||||||
|
- [Reactive System](#reactive-system)
|
||||||
|
- [Properties](#properties)
|
||||||
|
- [Static Properties](#static-properties)
|
||||||
|
- [Methods](#methods)
|
||||||
|
- [Lifecycle](#lifecycle)
|
||||||
|
- [`constructor(parent, props)`](#constructorparent-props)
|
||||||
|
- [`setup()`](#setup)
|
||||||
|
- [`willStart()`](#willstart)
|
||||||
|
- [`mounted()`](#mounted)
|
||||||
|
- [`willUpdateProps(nextProps)`](#willupdatepropsnextprops)
|
||||||
|
- [`willPatch()`](#willpatch)
|
||||||
|
- [`patched(snapshot)`](#patchedsnapshot)
|
||||||
|
- [`willUnmount()`](#willunmount)
|
||||||
|
- [`catchError(error)`](#catcherrorerror)
|
||||||
|
- [Root Component](#root-component)
|
||||||
|
- [Composition](#composition)
|
||||||
|
- [Form Input Bindings](#form-input-bindings)
|
||||||
|
- [References](#references)
|
||||||
|
- [Dynamic sub components](#dynamic-sub-components)
|
||||||
|
- [Functional Components](#functional-components)
|
||||||
|
- [SVG Components](#svg-components)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
OWL components are the building blocks for user interface. They are designed to be:
|
||||||
|
|
||||||
|
1. **declarative:** the user interface should be described in terms of the state
|
||||||
|
of the application, not as a sequence of imperative steps.
|
||||||
|
|
||||||
|
2. **composable:** each component can seamlessly be created in a parent component by
|
||||||
|
a simple tag or directive in its template.
|
||||||
|
|
||||||
|
3. **asynchronous rendering:** the framework will transparently wait for each
|
||||||
|
sub components to be ready before applying the rendering. It uses native promises
|
||||||
|
under the hood.
|
||||||
|
|
||||||
|
4. **uses QWeb as a template system:** the templates are described in XML
|
||||||
|
and follow the QWeb specification. This is a requirement for Odoo.
|
||||||
|
|
||||||
|
OWL components are defined as a subclass of Component. The rendering is
|
||||||
|
exclusively done by a [QWeb](qweb_templating_language.md) template (which needs to be preloaded in QWeb).
|
||||||
|
Rendering a component generates a virtual dom representation
|
||||||
|
of the component, which is then patched to the DOM, in order to apply the changes in an efficient way.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Let us have a look at a simple component:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
|
||||||
|
class ClickCounter extends owl.Component {
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-name="ClickCounter" t-on-click="increment">
|
||||||
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this code is written in ESNext style, so it will only run on the
|
||||||
|
latest browsers without a transpilation step.
|
||||||
|
|
||||||
|
This example shows how a component should be defined: it simply subclasses the
|
||||||
|
Component class. If no static `template` key is defined, then
|
||||||
|
Owl will use the component's name as template name. Here,
|
||||||
|
a state object is defined, by using the `useState` hook. It is not mandatory to use the state object, but it is certainly encouraged. The result of the `useState` call is
|
||||||
|
[observed](observer.md), and any change to it will cause a rerendering.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
An Owl component is a small class which represents a component or some UI element.
|
||||||
|
It exists in the context of an [environment](environment.md) (`env`), which is propagated from a
|
||||||
|
parent to its children. The environment needs to have a [QWeb](qweb_templating_language.md) instance, which
|
||||||
|
will be used to render the component template.
|
||||||
|
|
||||||
|
Be aware that the name of the component may be significant: if a component does
|
||||||
|
not define a `template` key, then Owl will lookup in QWeb to
|
||||||
|
find a template with the component name (or one of its ancestors).
|
||||||
|
|
||||||
|
### Reactive system
|
||||||
|
|
||||||
|
OWL components are normal javascript classes. So, changing a component internal
|
||||||
|
state does nothing more:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
|
||||||
|
state = { value: 0 };
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Clicking on the `Counter` component defined above will call the `increment`
|
||||||
|
method, but it will not rerender the component. To fix that, one could add an
|
||||||
|
explicit call to `render` in `increment`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
However, it may be simple in this case, but it quickly become cumbersome, as a
|
||||||
|
component get more complex, and its internal state is modified by more than one
|
||||||
|
method.
|
||||||
|
|
||||||
|
A better way is to use the reactive system: by using the `useState` hook (see the
|
||||||
|
[hooks](hooks.md) section for more details), one can make Owl react to state
|
||||||
|
changes. The `useState` hook generates a proxy version of an object
|
||||||
|
(this is done by an [observer](observer.md)), which allows the component to
|
||||||
|
react to any change. So, the `Counter` example above can be improved like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`<div t-on-click="increment"><t t-esc="state.value"/></div>`;
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Obviously, we can call the `useState` hook more than once:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
|
||||||
|
class Counter extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span t-on-click="increment(counter1)"><t t-esc="counter1.value"/></span>
|
||||||
|
<span t-on-click="increment(counter2)"><t t-esc="counter2.value"/></span>
|
||||||
|
</div>`;
|
||||||
|
counter1 = useState({ value: 0 });
|
||||||
|
counter2 = useState({ value: 0 });
|
||||||
|
|
||||||
|
increment(counter) {
|
||||||
|
counter.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that hooks are subject to one important [rule](hooks.md#one-rule): they need
|
||||||
|
to be called in the constructor.
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
- **`el`** (HTMLElement | null): reference to the DOM root node of the element. It is `null` when the
|
||||||
|
component is not mounted.
|
||||||
|
|
||||||
|
- **`env`** (Object): the component [environment](environment.md), which contains a QWeb instance.
|
||||||
|
|
||||||
|
- **`props`** (Object): this is an object containing all the properties given by
|
||||||
|
the parent to a child component. For example, in the following situation,
|
||||||
|
the parent component gives a `user` and a `color` value to the `ChildComponent`.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<ChildComponent user="state.user" color="color">
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `props` are owned by the parent, not by the component.
|
||||||
|
As such, it should not ever be modified by the component (otherwise you risk
|
||||||
|
unintended effects, since the parent may not be aware of the change)!!
|
||||||
|
|
||||||
|
The `props` can be modified dynamically by the parent. In that case, the
|
||||||
|
component will go through the following lifecycle methods: `willUpdateProps`,
|
||||||
|
`willPatch` and `patched`.
|
||||||
|
|
||||||
|
### Static Properties
|
||||||
|
|
||||||
|
- **`template`** (string, optional): if given, this is the name of the QWeb template that will render the component. Note that there is a helper `xml` to
|
||||||
|
make it easy to define an inline template.
|
||||||
|
|
||||||
|
* **`components`** (Object, optional): if given, this is an object that contains
|
||||||
|
the classes of any sub components needed by the template. This is the main way
|
||||||
|
used by Owl to be able to create sub components.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent extends owl.Component {
|
||||||
|
static components = { SubComponent };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* **`props`** (Object, optional): if given, this is an object that describes the
|
||||||
|
type and shape of the (actual) props given to the component. If Owl mode is
|
||||||
|
`dev`, this will be used to validate the props each time the component is
|
||||||
|
created/updated. See [Props Validation](props_validation.md) for more information.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
static props = {
|
||||||
|
initialValue: Number,
|
||||||
|
optional: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`defaultProps`** (Object, optional): if given, this object define default
|
||||||
|
values for (top-level) props. Whenever `props` are given to the object, they
|
||||||
|
will be altered to add default value (if missing). Note that it does not
|
||||||
|
change the initial object, a new object will be created instead.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
static defaultProps = {
|
||||||
|
initialValue: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`style`** (string, optional): it should be the return value of the [`css` tag](tags.md#css-tag),
|
||||||
|
which is used to inject stylesheet whenever the component is visible on the
|
||||||
|
screen.
|
||||||
|
|
||||||
|
There is another static property defined on the `Component` class: `current`.
|
||||||
|
This property is set to the currently being defined component (in the constructor).
|
||||||
|
This is the way [hooks](hooks.md) are able to get a reference to the target
|
||||||
|
component.
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
We explain here all the public methods of the `Component` class.
|
||||||
|
|
||||||
|
- **`mount(target, options)`** (async): this is the main way a
|
||||||
|
component is added to the DOM: the root component is mounted to a target
|
||||||
|
HTMLElement (or document fragment). Obviously, this is asynchronous, since each children need to be
|
||||||
|
created as well. Most applications will need to call `mount` exactly once, on
|
||||||
|
the root component.
|
||||||
|
|
||||||
|
The `options` argument is an optional object with a `position` key. The
|
||||||
|
`position` key can have three possible values: `first-child`, `last-child`, `self`.
|
||||||
|
|
||||||
|
- `first-child`: with this option, the component will be prepended inside the target,
|
||||||
|
- `last-child` (default value): with this option, the component will be
|
||||||
|
appended in the target element,
|
||||||
|
- `self`: the target will be used as the root element for the component. This
|
||||||
|
means that the target has to be an HTMLElement (and not a document fragment).
|
||||||
|
In this situation, it is possible that the component cannot be unmounted. For
|
||||||
|
example, if its target is `document.body`.
|
||||||
|
|
||||||
|
Note that if a component is mounted, unmounted and remounted, it will be
|
||||||
|
automatically re-rendered to ensure that changes in its state (or something
|
||||||
|
in the environment) will be taken into account.
|
||||||
|
|
||||||
|
If a component is mounted inside an element or a fragment which is not in the
|
||||||
|
DOM, then it will be rendered fully, but not active: the `mounted` hooks will
|
||||||
|
not be called. This is sometimes useful if we want to load an application in
|
||||||
|
memory. In that case, we need to mount the root component again in an element
|
||||||
|
which is in the DOM:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const app = new App();
|
||||||
|
await app.mount(document.createDocumentFragment());
|
||||||
|
// app is rendered in memory, but not active
|
||||||
|
await app.mount(document.body);
|
||||||
|
// app is now visible
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the normal way of mounting an application is by using the `mount`
|
||||||
|
method on a component class, not by creating the instance by hand. See the
|
||||||
|
documentation on [mounting applications](mounting.md).
|
||||||
|
|
||||||
|
* **`unmount()`**: in case a component needs to be detached/removed from the DOM, this
|
||||||
|
method can be used. Most applications should not call `unmount`, this is more
|
||||||
|
useful to the underlying component system.
|
||||||
|
|
||||||
|
* **`render()`** (async): calling this method directly will cause a rerender. Note
|
||||||
|
that this should be very rare to have to do it manually, the Owl framework is
|
||||||
|
most of the time responsible for doing that at an appropriate moment.
|
||||||
|
|
||||||
|
Note that the render method is asynchronous, so one cannot observe the updated
|
||||||
|
DOM in the same stack frame.
|
||||||
|
|
||||||
|
* **`shouldUpdate(nextProps)`**: this method is called each time a component's props
|
||||||
|
are updated. It returns a boolean, which indicates if the component should
|
||||||
|
ignore a props update. If it returns false, then `willUpdateProps` will not
|
||||||
|
be called, and no rendering will occur. Its default implementation is to
|
||||||
|
always return true. Note that this is an optimization, similar to React's `shouldComponentUpdate`. Most of the time, this should not be used, but it
|
||||||
|
can be useful if we are handling large number of components. Since this is an
|
||||||
|
optimization, Owl has the freedom to ignore the result of `shouldUpdate` in
|
||||||
|
some cases (for example, if a component is remounted, or if we want to force
|
||||||
|
a full rerender of the UI). However, if `shouldUpdate` returns true, then Owl
|
||||||
|
provides the guarantee that the component will be rendered at some point in
|
||||||
|
the future (except if the component is destroyed or if some part of the UI crashes).
|
||||||
|
|
||||||
|
* **`destroy()`**. As its name suggests, this method will remove the component,
|
||||||
|
and perform all necessary cleanup, such as unmounting the component, its children,
|
||||||
|
removing the parent/children relationship. This method should almost never be
|
||||||
|
called directly (except maybe on the root component), but should be done by the
|
||||||
|
framework instead.
|
||||||
|
|
||||||
|
Obviously, these methods are reserved for Owl, and should not be used by Owl
|
||||||
|
users, unless they want to override them. Also, Owl reserves all method names
|
||||||
|
starting with `__`, in order to prevent possible future conflicts with user code
|
||||||
|
whenever Owl needs to change.
|
||||||
|
|
||||||
|
### Lifecycle
|
||||||
|
|
||||||
|
A solid and robust component system needs useful hooks/methods to help
|
||||||
|
developers write components. Here is a complete description of the lifecycle of
|
||||||
|
a owl component:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| ------------------------------------------------ | ----------------------------------------------------------- |
|
||||||
|
| **[setup](#setup)** | setup |
|
||||||
|
| **[willStart](#willstart)** | async, before first rendering |
|
||||||
|
| **[mounted](#mounted)** | just after component is rendered and added to the DOM |
|
||||||
|
| **[willUpdateProps](#willupdatepropsnextprops)** | async, before props update |
|
||||||
|
| **[willPatch](#willpatch)** | just before the DOM is patched |
|
||||||
|
| **[patched](#patchedsnapshot)** | just after the DOM is patched |
|
||||||
|
| **[willUnmount](#willunmount)** | just before removing component from DOM |
|
||||||
|
| **[catchError](#catcherrorerror)** | catch errors (see [error handling page](error_handling.md)) |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- hooks call order is precisely defined: `[willX]` hooks are called first on parent,
|
||||||
|
then on children, and `[Xed]` are called in the reverse order: first children,
|
||||||
|
then parent.
|
||||||
|
- no hook method should ever be called manually. They are supposed to be
|
||||||
|
called by the owl framework whenever it is required.
|
||||||
|
|
||||||
|
#### `constructor(parent, props)`
|
||||||
|
|
||||||
|
The constructor is not exactly a hook, it is the regular,
|
||||||
|
normal, constructor of the component. Since it is not a hook, you need to make
|
||||||
|
sure that `super` is called.
|
||||||
|
|
||||||
|
This is usually where you would set the initial state and the template of the
|
||||||
|
component.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
this.state = useState({someValue: true});
|
||||||
|
this.template = 'mytemplate';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that with ESNext class fields, the constructor method does not need to be
|
||||||
|
implemented in most cases:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ClickCounter extends owl.Component {
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Hook functions can be called in the constructor.
|
||||||
|
|
||||||
|
#### `setup()`
|
||||||
|
|
||||||
|
_setup_ is run just after the component is constructed. It is a lifecycle method,
|
||||||
|
very similar to the _constructor_, except that it does not receive any argument.
|
||||||
|
|
||||||
|
It is a valid method to call hook functions. Note that one of the main reason to
|
||||||
|
have the `setup` hook in the component lifecycle is to make it possible to
|
||||||
|
monkey patch it. It is a common need in the Odoo ecosystem.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
setup() {
|
||||||
|
useSetupAutofocus();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `willStart()`
|
||||||
|
|
||||||
|
willStart is an asynchronous hook that can be implemented to
|
||||||
|
perform some action before the initial rendering of a component.
|
||||||
|
|
||||||
|
It will be called exactly once before the initial rendering. It is useful
|
||||||
|
in some cases, for example, to load external assets (such as a JS library)
|
||||||
|
before the component is rendered. Another use case is to load data from a server.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async willStart() {
|
||||||
|
await owl.utils.loadJS("my-awesome-lib.js");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point, the component is not yet rendered. Note that a slow `willStart` method will slow down the rendering of the user
|
||||||
|
interface. Therefore, some care should be made to make this method as
|
||||||
|
fast as possible.
|
||||||
|
|
||||||
|
#### `mounted()`
|
||||||
|
|
||||||
|
`mounted` is called each time a component is attached to the
|
||||||
|
DOM, after the initial rendering and possibly later if the component was unmounted
|
||||||
|
and remounted. At this point, the component is considered _active_. This is a good place to add some listeners, or to interact with the
|
||||||
|
DOM, if the component needs to perform some measure for example.
|
||||||
|
|
||||||
|
It is the opposite of `willUnmount`. If a component has been mounted, it will
|
||||||
|
always be unmounted at some point in the future.
|
||||||
|
|
||||||
|
The mounted method will be called recursively on each of its children. First,
|
||||||
|
the parent, then all its children.
|
||||||
|
|
||||||
|
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
|
||||||
|
Doing so will cause a rerender, which will not be perceptible by the user, but
|
||||||
|
will slightly slow down the component.
|
||||||
|
|
||||||
|
#### `willUpdateProps(nextProps)`
|
||||||
|
|
||||||
|
The willUpdateProps is an asynchronous hook, called just before new props
|
||||||
|
are set. This is useful if the component needs to perform an asynchronous task,
|
||||||
|
depending on the props (for example, assuming that the props are
|
||||||
|
some record Id, fetching the record data).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
willUpdateProps(nextProps) {
|
||||||
|
return this.loadData({id: nextProps.id});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This hook is not called during the first render (but willStart is called
|
||||||
|
and performs a similar job).
|
||||||
|
|
||||||
|
#### `willPatch()`
|
||||||
|
|
||||||
|
The willPatch hook is called just before the DOM patching process starts.
|
||||||
|
It is not called on the initial render. This is useful to read
|
||||||
|
information from the DOM. For example, the current position of the
|
||||||
|
scrollbar.
|
||||||
|
|
||||||
|
Note that modifying the state is not allowed here. This method is called just
|
||||||
|
before an actual DOM patch, and is only intended to be used to save some local
|
||||||
|
DOM state. Also, it will not be called if the component is not in the DOM.
|
||||||
|
|
||||||
|
#### `patched(snapshot)`
|
||||||
|
|
||||||
|
This hook is called whenever a component did actually update its DOM (most
|
||||||
|
likely via a change in its state/props or environment).
|
||||||
|
|
||||||
|
This method is not called on the initial render. It is useful to interact
|
||||||
|
with the DOM (for example, through an external library) whenever the
|
||||||
|
component was patched. Note that this hook will not be called if the component is
|
||||||
|
not in the DOM.
|
||||||
|
|
||||||
|
Updating the component state in this hook is possible, but not encouraged.
|
||||||
|
One needs to be careful, because updates here will create an additional rendering, which in
|
||||||
|
turn will cause other calls to the `patched` method. So, we need to be particularly
|
||||||
|
careful at avoiding endless cycles.
|
||||||
|
|
||||||
|
#### `willUnmount()`
|
||||||
|
|
||||||
|
willUnmount is a hook that is called each time just before a component is unmounted from
|
||||||
|
the DOM. This is a good place to remove listeners, for example.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
mounted() {
|
||||||
|
this.env.bus.on('someevent', this, this.doSomething);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.bus.off('someevent', this, this.doSomething);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the opposite method of `mounted`.
|
||||||
|
|
||||||
|
#### `catchError(error)`
|
||||||
|
|
||||||
|
The `catchError` method is useful when we need to intercept and properly react
|
||||||
|
to (rendering) errors that occur in some sub components. See the page on
|
||||||
|
[error handling](error_handling.md).
|
||||||
|
|
||||||
|
### Root Component
|
||||||
|
|
||||||
|
Most of the time, an Owl component will be created automatically by a tag (or the `t-component`
|
||||||
|
directive) in a template. There is however an obvious exception: the root component
|
||||||
|
of an Owl application has to be created manually:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class App extends owl.Component { ... }
|
||||||
|
|
||||||
|
const app = new App();
|
||||||
|
app.mount(document.body);
|
||||||
|
```
|
||||||
|
|
||||||
|
The root component does not have a parent nor `props` (see note below). It will be setup with an
|
||||||
|
[environment](environment.md) (either the `env` defined on its class, or a
|
||||||
|
default empty environment).
|
||||||
|
|
||||||
|
Note: a root component can however be given a `props` object in its constructor,
|
||||||
|
like this: `new App(null, {some: 'object'});`. It will not be a true `props`
|
||||||
|
object, managed by Owl (so, for example, it will never be updated).
|
||||||
|
|
||||||
|
### Composition
|
||||||
|
|
||||||
|
The example above shows a QWeb template with a sub component. In a template,
|
||||||
|
components are declared with a tagname corresponding to the class name. It has
|
||||||
|
to be capitalized.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<span>some text</span>
|
||||||
|
<MyComponent info="13" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent extends owl.Component {
|
||||||
|
static components = { MyComponent: MyComponent};
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `ParentComponent`'s template creates a component `MyComponent` just
|
||||||
|
after the span. The `info` key will be added to the subcomponent's `props`. Each
|
||||||
|
`props` is a string which represents a javascript (QWeb) expression, so it is
|
||||||
|
dynamic. If it is necessary to give a string, this can be done by quoting it:
|
||||||
|
`someString="'somevalue'"`.
|
||||||
|
|
||||||
|
Note that the rendering context for the template is the component itself. This means
|
||||||
|
that the template can access `state` (if it exists), `props`, `env`, or any
|
||||||
|
methods defined in the component.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<ChildComponent count="state.val" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent {
|
||||||
|
static components = { ChildComponent };
|
||||||
|
state = useState({ val: 4 });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Whenever the template is rendered, it will automatically create the subcomponent
|
||||||
|
`ChildComponent` at the correct place. It needs to find the reference to the
|
||||||
|
actual component class in the special static `components` key, or the class registered in
|
||||||
|
QWeb's global registry (see `register` function of QWeb). It first looks inside
|
||||||
|
the static `components` key, then fallbacks on the global registry.
|
||||||
|
|
||||||
|
_Props_: In this example, the child component will receive the object `{count: 4}` in its
|
||||||
|
constructor. This will be assigned to the `props` variable, which can be accessed
|
||||||
|
on the component (and also, in the template). Whenever the state is updated, then
|
||||||
|
the sub component will also be updated automatically. See the [props section](props.md)
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
**CSS and style:** Owl allows the parent to declare
|
||||||
|
additional css classes or style for the sub component: css declared in `class`, `style`, `t-att-class` or `t-att-style` will be added to the
|
||||||
|
root component element.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<MyComponent class="someClass" style="font-weight:bold;" info="13" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Warning: there is a small caveat with dynamic class attributes: since Owl needs
|
||||||
|
to be able to add/remove proper classes whenever necessary, it needs to be aware
|
||||||
|
of the possible classes. Otherwise, it will not be able to make the difference
|
||||||
|
between a valid css class added by the component, or other custom code, and a
|
||||||
|
class that need to be removed. This is why we only support the explicit syntax
|
||||||
|
with a class object:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<MyComponent t-att-class="{a: state.flagA, b: state.flagB}" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Input Bindings
|
||||||
|
|
||||||
|
It is very common to need to be able to read the value out of an html `input` (or
|
||||||
|
`textarea`, or `select`) in order to use it (note: it does not need to be in a
|
||||||
|
form!). A possible way to do this is to do it by hand:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Form extends owl.Component {
|
||||||
|
state = useState({ text: "" });
|
||||||
|
|
||||||
|
_updateInputValue(event) {
|
||||||
|
this.state.text = event.target.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<input t-on-input="_updateInputValue" />
|
||||||
|
<span t-esc="state.text" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
This works. However, this requires a little bit of _plumbing_ code. Also, the
|
||||||
|
plumbing code is slightly different if you need to interact with a checkbox,
|
||||||
|
or with radio buttons, or with select tags.
|
||||||
|
|
||||||
|
To help with this situation, Owl has a builtin directive `t-model`: its value
|
||||||
|
should be an observed value in the component (usually `state.someValue`). With
|
||||||
|
the `t-model` directive, we can write a shorter code, equivalent to the previous
|
||||||
|
example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Form extends owl.Component {
|
||||||
|
state = { text: "" };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<input t-model="state.text" />
|
||||||
|
<span t-esc="state.text" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
|
||||||
|
`<input type="radio">`, `<textarea>` and `<select>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<div>Text in an input: <input t-model="state.someVal"/></div>
|
||||||
|
<div>Textarea: <textarea t-model="state.otherVal"/></div>
|
||||||
|
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
|
||||||
|
<div>Selection:
|
||||||
|
<select t-model="state.color">
|
||||||
|
<option value="">Select a color</option>
|
||||||
|
<option value="red">Red</option>
|
||||||
|
<option value="blue">Blue</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Selection with radio buttons:
|
||||||
|
<span>
|
||||||
|
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
|
||||||
|
<label for="red">Red</label>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
|
||||||
|
<label for="blue">Blue</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Like event handling, the `t-model` directive accepts the following modifiers:
|
||||||
|
|
||||||
|
| Modifier | Description |
|
||||||
|
| --------- | -------------------------------------------------------------------- |
|
||||||
|
| `.lazy` | update the value on the `change` event (default is on `input` event) |
|
||||||
|
| `.number` | try to parse the value to a number (using `parseFloat`) |
|
||||||
|
| `.trim` | trim the resulting value |
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<input t-model.lazy="state.someVal" />
|
||||||
|
```
|
||||||
|
|
||||||
|
These modifiers can be combined. For instance, `t-model.lazy.number` will only
|
||||||
|
update a number whenever the change is done.
|
||||||
|
|
||||||
|
Note: the online playground has an example to show how it works.
|
||||||
|
|
||||||
|
### References
|
||||||
|
|
||||||
|
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||||
|
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||||
|
tagged by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
|
||||||
|
more detail.
|
||||||
|
|
||||||
|
As a short example, here is how we could set the focus on a given input:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<input t-ref="input"/>
|
||||||
|
<button t-on-click="focusInput">Click</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { useRef } from "owl/hooks";
|
||||||
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
inputRef = useRef("input");
|
||||||
|
|
||||||
|
focusInput() {
|
||||||
|
this.inputRef.el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `useRef` hook can also be used to get a reference to an instance of a sub
|
||||||
|
component rendered by Owl. In that case, we need to access it with the `comp`
|
||||||
|
property instead of `el`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<SubComponent t-ref="sub"/>
|
||||||
|
<button t-on-click="doSomething">Click</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { useRef } from "owl/hooks";
|
||||||
|
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static components = { SubComponent };
|
||||||
|
subRef = useRef("sub");
|
||||||
|
|
||||||
|
doSomething() {
|
||||||
|
this.subRef.comp.doSomeThingElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that these two examples uses the suffix `ref` to name the reference. This
|
||||||
|
is not mandatory, but it is a useful convention, so we do not forget to access
|
||||||
|
it with the `el` or `comp` suffix.
|
||||||
|
|
||||||
|
### Dynamic sub components
|
||||||
|
|
||||||
|
It is not common, but sometimes we need a dynamic component name. In this case,
|
||||||
|
the `t-component` directive can also be used to accept dynamic values with string interpolation (like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) directive):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<t t-component="ChildComponent{{id}}" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent {
|
||||||
|
static components = { ChildComponent1, ChildComponent2 };
|
||||||
|
state = { id: 1 };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There is an even more dynamic way to use `t-component`: its value can be an
|
||||||
|
expression evaluating to an actual component class. In that case, this is the
|
||||||
|
class that will be used to create the component:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class A extends Component<any, any, any> {
|
||||||
|
static template = xml`<span>child a</span>`;
|
||||||
|
}
|
||||||
|
class B extends Component<any, any, any> {
|
||||||
|
static template = xml`<span>child b</span>`;
|
||||||
|
}
|
||||||
|
class App extends Component<any, any, any> {
|
||||||
|
static template = xml`<t t-component="myComponent" t-key="state.child"/>`;
|
||||||
|
|
||||||
|
state = { child: "a" };
|
||||||
|
|
||||||
|
get myComponent() {
|
||||||
|
return this.state.child === "a" ? A : B;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the component `App` selects dynamically the concrete sub
|
||||||
|
component class.
|
||||||
|
|
||||||
|
Note that the `t-component` directive can only be used on `<t>` nodes.
|
||||||
|
|
||||||
|
### Functional Components
|
||||||
|
|
||||||
|
Owl does not exactly have functional components. However, there is an extremely
|
||||||
|
close alternative: calling sub templates.
|
||||||
|
|
||||||
|
A stateless functional component in react is usually some kind of function that
|
||||||
|
maps props to a virtual dom (often with `jsx`). So, basically, almost like a
|
||||||
|
template rendered with `props`. In Owl, this can be done by
|
||||||
|
simply defining a template, that will access the `props` object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const Welcome = xml`<h1>Hello, <t t-esc="props.name"/></h1>`;
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-call=${Welcome}/>
|
||||||
|
<div>something</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The way this works is that sub templates are inlined, and have access to the
|
||||||
|
ambient context. They can therefore access `props`, and any other part of the
|
||||||
|
caller component.
|
||||||
|
|
||||||
|
### SVG Components
|
||||||
|
|
||||||
|
Owl components can be used to generate dynamic SVG graphs:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Node extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<g>
|
||||||
|
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
|
||||||
|
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
|
||||||
|
<t t-set="childx" t-value="props.x + 100"/>
|
||||||
|
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
|
||||||
|
<t t-foreach="props.node.children || []" t-as="child">
|
||||||
|
<t t-set="childy" t-value="props.y + child_index*height"/>
|
||||||
|
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
|
||||||
|
<Node x="childx" y="childy" node="child" height="height"/>
|
||||||
|
</t>
|
||||||
|
</g>
|
||||||
|
`;
|
||||||
|
static components = { Node };
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootNode extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<svg height="180">
|
||||||
|
<Node node="graph" x="10" y="20" height="180"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
static components = { Node };
|
||||||
|
graph = {
|
||||||
|
label: "a",
|
||||||
|
children: [
|
||||||
|
{ label: "b" },
|
||||||
|
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
|
||||||
|
{ label: "f", children: [{ label: "g" }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This `RootNode` component will then display a live SVG representation of the
|
||||||
|
graph described by the `graph` property. Note that there is a recursive structure
|
||||||
|
here: the `Node` component uses itself as a subcomponent.
|
||||||
|
|
||||||
|
Note that since SVG needs to be handled in a specific way (its namespace needs
|
||||||
|
to be properly set), there is a small constraint for Owl components: if an owl
|
||||||
|
component is supposed to be a part of an svg graph, then its root node needs to
|
||||||
|
be a `g` tag, so Owl can properly set the namespace.
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# 🦉 Concurrency Model 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Rendering Components](#rendering-components)
|
||||||
|
- [Semantics](#semantics)
|
||||||
|
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Owl was designed from the very beginning with asynchronous components. This comes
|
||||||
|
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
|
||||||
|
methods, it is possible to build complex highly concurrent applications.
|
||||||
|
|
||||||
|
Owl concurrent mode has several benefits: it makes it possible to delay the
|
||||||
|
rendering until some asynchronous operation is complete, it makes it possible
|
||||||
|
to lazy load libraries, while keeping the previous screen completely functional.
|
||||||
|
It is also good for performance reasons: Owl uses it to only apply the result of
|
||||||
|
many different renderings only once in an animation frame. Owl can cancel
|
||||||
|
a rendering that is no longer relevant, restart it, reuse it in some cases.
|
||||||
|
|
||||||
|
But even though using concurrency is quite simple (and is the default behaviour),
|
||||||
|
asynchrony is difficult, because it introduces an additional dimension that
|
||||||
|
vastly increase the complexity of an application. This section will explain
|
||||||
|
how Owl manages this complexity, how concurrent rendering works in a general way.
|
||||||
|
|
||||||
|
## Rendering Components
|
||||||
|
|
||||||
|
The word _rendering_ is a little vague, so, let us explain more precisely the
|
||||||
|
process by which Owl components are displayed on a screen.
|
||||||
|
|
||||||
|
When a component is mounted or updated, a new rendering is started. It has
|
||||||
|
two phases: _virtual rendering_ and _patching_.
|
||||||
|
|
||||||
|
### Virtual rendering
|
||||||
|
|
||||||
|
This phase represent the process of rendering a template, in memory, which create a virtual representation of the desired component html). The output of this phase is a
|
||||||
|
virtual DOM.
|
||||||
|
|
||||||
|
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
|
||||||
|
will need to be called), or updated (which is done with the `willUpdateProps`
|
||||||
|
method). This is completely a recursive process: a component is the root of a
|
||||||
|
component tree, and each sub component needs to be (virtually) rendered.
|
||||||
|
|
||||||
|
### Patching
|
||||||
|
|
||||||
|
Once a rendering is complete, it will be applied on the next animation frame.
|
||||||
|
This is done synchronously: the whole component tree is patched to the real
|
||||||
|
DOM.
|
||||||
|
|
||||||
|
## Semantics
|
||||||
|
|
||||||
|
We give here an informal description of the way components are created/updated
|
||||||
|
in an application. Here, ordered lists describe actions that are executed
|
||||||
|
sequentially, bullet lists describe actions that are executed in parallel.
|
||||||
|
|
||||||
|
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
|
||||||
|
|
||||||
|
```
|
||||||
|
A
|
||||||
|
/ \
|
||||||
|
B C
|
||||||
|
/ \
|
||||||
|
D E
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is what happen whenever we mount the root
|
||||||
|
component (with some code like `app.mount(document.body)`).
|
||||||
|
|
||||||
|
1. `willStart` is called on `A`
|
||||||
|
|
||||||
|
2. when it is done, template `A` is rendered.
|
||||||
|
|
||||||
|
- component `B` is created
|
||||||
|
1. `willStart` is called on `B`
|
||||||
|
2. template `B` is rendered
|
||||||
|
- component `C` is created
|
||||||
|
1. `willStart` is called on `C`
|
||||||
|
2. template `C` is rendered
|
||||||
|
- component `D` is created
|
||||||
|
1. `willStart` is called on `D`
|
||||||
|
2. template `D` is rendered
|
||||||
|
- component `E` is created
|
||||||
|
1. `willStart` is called on `E`
|
||||||
|
2. template `E` is rendered
|
||||||
|
|
||||||
|
3. each components are patched into a detached DOM element, in the following order:
|
||||||
|
`E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created
|
||||||
|
in one pass)
|
||||||
|
|
||||||
|
4. the component `A` root element is actually appended to `document.body`
|
||||||
|
|
||||||
|
5. The method `mounted` is called recursively on all components in the following
|
||||||
|
order: `E`, `D`, `C`, `B`, `A`.
|
||||||
|
|
||||||
|
**Scenario 2: rerendering a component**. Now, let's assume that the user clicked on some
|
||||||
|
button in `C`, and this results in a state update, which is supposed to:
|
||||||
|
|
||||||
|
- update `D`,
|
||||||
|
- remove `E`,
|
||||||
|
- add new component `F`.
|
||||||
|
|
||||||
|
So, the component tree should look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
A
|
||||||
|
/ \
|
||||||
|
B C
|
||||||
|
/ \
|
||||||
|
D F
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is what Owl will do:
|
||||||
|
|
||||||
|
1. because of a state change, the method `render` is called on `C`
|
||||||
|
2. template `C` is rendered again
|
||||||
|
|
||||||
|
- component `D` is updated:
|
||||||
|
1. hook `willUpdateProps` is called on `D` (async)
|
||||||
|
2. template `D` is rerendered
|
||||||
|
- component `F` is created:
|
||||||
|
1. hook `willStart` is called on `F` (async)
|
||||||
|
2. template `F` is rendered
|
||||||
|
|
||||||
|
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
|
||||||
|
because it is not mounted yet)
|
||||||
|
|
||||||
|
4. components `F`, `D` are patched in that order
|
||||||
|
|
||||||
|
5. component `C` is patched, which will cause recursively:
|
||||||
|
|
||||||
|
1. `willUnmount` hook on `E`
|
||||||
|
2. destruction of `E`,
|
||||||
|
|
||||||
|
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
|
||||||
|
|
||||||
|
Tags are very small helpers to make it easy to write inline templates. There is
|
||||||
|
only one currently available tag: `xml`, but we plan to add other tags later,
|
||||||
|
such as a `css` tag, which will be used to write [single file components](../learning/how_to_write_sfc.md).
|
||||||
|
|
||||||
|
### Asynchronous Rendering
|
||||||
|
|
||||||
|
Working with asynchronous code always adds a lot of complexity to a system. Whenever
|
||||||
|
different parts of a system are active at the same time, one needs to think
|
||||||
|
carefully about all possible interactions. Clearly, this is also true for Owl
|
||||||
|
components.
|
||||||
|
|
||||||
|
There are two different common problems with Owl asynchronous rendering model:
|
||||||
|
|
||||||
|
- any component can delay the rendering (initial and subsequent) of the whole
|
||||||
|
application
|
||||||
|
- for a given component, there are two independant situations that will trigger an
|
||||||
|
asynchronous rerendering: a change in the state, or a change in the props.
|
||||||
|
These changes may be done at different times, and Owl has no way of knowing
|
||||||
|
how to reconcile the resulting renderings.
|
||||||
|
|
||||||
|
Here are a few tips on how to work with asynchronous components:
|
||||||
|
|
||||||
|
1. Minimize the use of asynchronous components!
|
||||||
|
2. Lazy loading external libraries is a good use case for async rendering. This
|
||||||
|
is mostly fine, because we can assume that it will only takes a fraction of a
|
||||||
|
second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs))
|
||||||
|
3. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When
|
||||||
|
this component is met, a new rendering
|
||||||
|
sub tree is created, such that the rendering of that component (and its
|
||||||
|
children) is not tied to the rendering of the rest of the interface. It can
|
||||||
|
be used on an asynchronous component, to prevent it from delaying the
|
||||||
|
rendering of the whole interface, or on a synchronous one, such that its
|
||||||
|
rendering isn't delayed by other (asynchronous) components. Note that this
|
||||||
|
directive has no effect on the first rendering, but only on subsequent ones
|
||||||
|
(triggered by state or props changes).
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<SyncChild />
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 🦉 Config 🦉
|
||||||
|
|
||||||
|
The Owl framework is designed to work in many situations. However, it is
|
||||||
|
sometimes necessary to customize some behaviour. This is done by using the
|
||||||
|
global `config` object. It provides two settings:
|
||||||
|
|
||||||
|
- [`mode`](#mode) (default value: `prod`),
|
||||||
|
- [`enableTransitions`](#enabletransitions) (default value: `true`).
|
||||||
|
|
||||||
|
## Mode
|
||||||
|
|
||||||
|
By default, Owl is in _production_ mode, this means that it will try to do its
|
||||||
|
job fast, and skip some expensive operations. However, it is sometimes necessary
|
||||||
|
to have better information on what is going on, this is the purpose
|
||||||
|
of the `dev` mode.
|
||||||
|
|
||||||
|
Owl has a mode flag, in `owl.config.mode`. Its default value is `prod`, but
|
||||||
|
it can be set to `dev`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
owl.config.mode = "dev";
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that templates compiled with the `prod` settings will not be recompiled.
|
||||||
|
So, changing this setting is best done at startup.
|
||||||
|
|
||||||
|
An important job done by the `dev` mode is to validate props for each component
|
||||||
|
creation and update. Also, extra props will cause an error.
|
||||||
|
|
||||||
|
## `enableTransitions`
|
||||||
|
|
||||||
|
Transitions are usually nice, but they can cause issues in some specific cases,
|
||||||
|
such as automated tests. It is uncomfortable having to wait for a transition
|
||||||
|
to end before moving to the next step.
|
||||||
|
|
||||||
|
To solve this issue, Owl can be configured to ignore the `t-transition` directive.
|
||||||
|
To do that, one only needs to set the `enableTransitions` flag to false:
|
||||||
|
|
||||||
|
```js
|
||||||
|
owl.config.enableTransitions = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it suffers from the same drawback as the "dev" mode: all compiled
|
||||||
|
templates, if any, will keep their current behaviours.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 🦉 Owl Content 🦉
|
||||||
|
|
||||||
|
Here is a complete visual representation of everything exported by the `owl`
|
||||||
|
global object.
|
||||||
|
|
||||||
|
For example, `Component` is available at `owl.Component` and `EventBus` is
|
||||||
|
exported as `owl.core.EventBus`.
|
||||||
|
|
||||||
|
```
|
||||||
|
browser
|
||||||
|
Component misc
|
||||||
|
Context AsyncRoot
|
||||||
|
QWeb Portal
|
||||||
|
mount
|
||||||
|
useState tags
|
||||||
|
config css
|
||||||
|
mode xml
|
||||||
|
core utils
|
||||||
|
EventBus debounce
|
||||||
|
Observer escape
|
||||||
|
hooks loadJS
|
||||||
|
onWillStart loadFile
|
||||||
|
onMounted shallowEqual
|
||||||
|
onWillUpdateProps whenReady
|
||||||
|
onWillPatch
|
||||||
|
onPatched
|
||||||
|
onWillUnmount
|
||||||
|
useContext
|
||||||
|
useState
|
||||||
|
useRef
|
||||||
|
useComponent
|
||||||
|
useEnv
|
||||||
|
useSubEnv
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that for convenience, the `useState` hook is also exported at the root of the `owl` object.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# 🦉 Context 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Reference](#reference)
|
||||||
|
- [`Context`](#context)
|
||||||
|
- [`useContext`](#usecontext)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `Context` object provides a way to share data between an arbitrary number
|
||||||
|
of components. Usually, data is passed from a parent to its children component,
|
||||||
|
but when we have to deal with some mostly global information, this can be
|
||||||
|
annoying, since each component will need to pass the information to each children,
|
||||||
|
even though some or most of them will not use the information.
|
||||||
|
|
||||||
|
With a `Context` object, each component can subscribe (with the `useContext` hook)
|
||||||
|
to its state, and will be updated whenever the context state is updated.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Assume that we have an application with various components which needs to render
|
||||||
|
differently depending on the size of the device. Here is how we could proceed
|
||||||
|
to make sure that the information is properly shared. First, let us create a
|
||||||
|
context, and add it to the environment:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const deviceContext = new Context({ isMobile: true });
|
||||||
|
App.env.deviceContext = deviceContext;
|
||||||
|
```
|
||||||
|
|
||||||
|
If we want to make it completely responsive, we need to update its value whenever
|
||||||
|
the size of the screen is updated:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isMobile = () => window.innerWidth <= 768;
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
owl.utils.debounce(() => {
|
||||||
|
const state = deviceContext.state;
|
||||||
|
if (state.isMobile !== isMobile()) {
|
||||||
|
state.isMobile = !state.isMobile;
|
||||||
|
}
|
||||||
|
}, 15)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, each component that want can subscribe and render differently depending on the
|
||||||
|
fact that we are in a mobile or desktop mode.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-if=device.isMobile>
|
||||||
|
some simplified user interface
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
a more advanced user interface
|
||||||
|
</t>
|
||||||
|
</div>`;
|
||||||
|
device = useContext(this.env.deviceContext);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
### `Context`
|
||||||
|
|
||||||
|
A `Context` object should be created with a state object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const someContext = new Context({ some: "key" });
|
||||||
|
```
|
||||||
|
|
||||||
|
Its state is now available in the `state` key:
|
||||||
|
|
||||||
|
```js
|
||||||
|
someContext.state.some = "other key";
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the way some global code (such as the responsive code above) should
|
||||||
|
read and update the context state. However, components should not ever read the
|
||||||
|
context state directly from the context, they should instead use the `useContext`
|
||||||
|
hook to properly register themselves to state changes.
|
||||||
|
|
||||||
|
Note that the `Context` hook is different from the React version. For example,
|
||||||
|
there is no concept of provider/consumer. So, the `Context` feature does not
|
||||||
|
by itself allow the use of a different context state depending on the component
|
||||||
|
place in the component tree. However, this functionality can be obtained, if
|
||||||
|
necessary, with the use of sub environment.
|
||||||
|
|
||||||
|
### `useContext`
|
||||||
|
|
||||||
|
The `useContext` hook is the normal way for a component to register themselve
|
||||||
|
to context state changes. The `useContext` method returns the context state:
|
||||||
|
|
||||||
|
```js
|
||||||
|
device = useContext(this.env.deviceContext);
|
||||||
|
```
|
||||||
|
|
||||||
|
It is a simple observed state (with an owl `Observer`), which contains the shared
|
||||||
|
information.
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# 🦉 Environment 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Setting an Environment](#setting-an-environment)
|
||||||
|
- [Using a sub environment](#using-a-sub-environment)
|
||||||
|
- [Content of an Environment](#content-of-an-environment)
|
||||||
|
- [Special keys](#special-keys)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
An environment is an object which contains a [`QWeb` instance](qweb_engine.md). Whenever
|
||||||
|
a root component is created, it is assigned an environment (see
|
||||||
|
[below](#setting-an-environment) for more info on this). This environment is
|
||||||
|
then automatically given to each sub component (and accessible in the `this.env`
|
||||||
|
property).
|
||||||
|
|
||||||
|
```
|
||||||
|
Root
|
||||||
|
/ \
|
||||||
|
A B
|
||||||
|
```
|
||||||
|
|
||||||
|
This way, all components share the same `QWeb` instance. Owl internally requires
|
||||||
|
that the environment has a `qweb` key which maps to a
|
||||||
|
[`QWeb`](qweb_engine.md) instance. This is the QWeb instance that will be used to
|
||||||
|
render each templates in this specific component tree. Note that if no `QWeb`
|
||||||
|
instance is provided, Owl will simply generate it on the fly.
|
||||||
|
|
||||||
|
The environment is mostly static. Each application is free to add anything to
|
||||||
|
the environment, which is very useful, since this can be accessed by each sub
|
||||||
|
component.
|
||||||
|
|
||||||
|
## Setting an environment
|
||||||
|
|
||||||
|
An Owl application needs an [environment](environment.md) to be executed. The
|
||||||
|
environment has an important key: the [QWeb](qweb_engine.md) instance, which will render
|
||||||
|
all templates.
|
||||||
|
|
||||||
|
Whenever a root component `App` is mounted, Owl will setup a valid environment by
|
||||||
|
following the next steps:
|
||||||
|
|
||||||
|
- take the `env` object defined on `App.env` (if no `env` was explicitly setup,
|
||||||
|
this will return the empty `env` object defined on `Component`)
|
||||||
|
- if `env.qweb` is not set, then Owl will create a `QWeb` instance.
|
||||||
|
|
||||||
|
The correct way to customize an environment is to simply set it up on the root
|
||||||
|
component class, before the first component is created:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const env = {
|
||||||
|
_t: myTranslateFunction,
|
||||||
|
user: {...},
|
||||||
|
services: {
|
||||||
|
...
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mount(App, { target: document.body, env });
|
||||||
|
```
|
||||||
|
|
||||||
|
It is also possible to simply share an environment between all root components,
|
||||||
|
by simply doing this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Component.env = myEnv; // will be the default env for all components
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this environment is the global owl environment for an application. The
|
||||||
|
next section explains how to extend an environment for a specific sub component
|
||||||
|
and its children.
|
||||||
|
|
||||||
|
## Using a sub environment
|
||||||
|
|
||||||
|
It is sometimes useful to add one (or more) specific keys to the environment,
|
||||||
|
from the perspective of a specific component and its children. In that case, the
|
||||||
|
solution presented above will not work, since it sets the global environment.
|
||||||
|
|
||||||
|
There is a hook for this situation: [`useSubEnv`](hooks.md#usesubenv).
|
||||||
|
|
||||||
|
```js
|
||||||
|
class FormComponent extends Component {
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
useSubEnv({ myKey: someValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Content of an Environment
|
||||||
|
|
||||||
|
Some good use cases for additional keys in the environment are:
|
||||||
|
|
||||||
|
- some configuration keys,
|
||||||
|
- session information,
|
||||||
|
- generic services (such as doing rpcs).
|
||||||
|
|
||||||
|
Doing it this way means that components are easily testable: we can simply
|
||||||
|
create a test environment with mock services.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function myEnv() {
|
||||||
|
const templates = await loadTemplates();
|
||||||
|
const qweb = new QWeb({ templates });
|
||||||
|
const session = getSession();
|
||||||
|
|
||||||
|
return {
|
||||||
|
_t: myTranslateFunction,
|
||||||
|
session: session,
|
||||||
|
qweb: qweb,
|
||||||
|
services: {
|
||||||
|
localStorage: localStorage,
|
||||||
|
rpc: rpc,
|
||||||
|
},
|
||||||
|
debug: false,
|
||||||
|
inMobileMode: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
const env = await myEnv();
|
||||||
|
mount(App, { target: document.body, env });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special Keys
|
||||||
|
|
||||||
|
There are two special key/value added by Owl if not provided in the environment:
|
||||||
|
the `QWeb` instance and a `browser` object:
|
||||||
|
|
||||||
|
- `qweb` will be set to an empty `QWeb` instance. This is absolutely necessary
|
||||||
|
for Owl to be able to render anything
|
||||||
|
- `browser`: this is an object that contains some common access points to the
|
||||||
|
browser methods with a side effect. See [browser](browser.md) for more information. Note that the browser object will be removed from the environment in Owl 2.0.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# 🦉 Error Handling 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Reference](#reference)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
By default, whenever an error occurs in the rendering of an Owl application, we
|
||||||
|
destroy the whole application. Otherwise, we cannot offer any guarantee on the
|
||||||
|
state of the resulting component tree. It might be hopelessly corrupted, but
|
||||||
|
without any user-visible state.
|
||||||
|
|
||||||
|
Clearly, it sometimes is a little bit extreme to destroy the application. This
|
||||||
|
is why we have a builtin mechanism to handle rendering errors (and errors coming
|
||||||
|
from lifecycle hooks): the `catchError` hook.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
For example, here is how we could implement an `ErrorBoundary` component:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ErrorBoundary">
|
||||||
|
<t t-if="state.error">
|
||||||
|
Error handled
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<t t-slot="default" />
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ErrorBoundary extends Component {
|
||||||
|
state = useState({ error: false });
|
||||||
|
|
||||||
|
catchError() {
|
||||||
|
this.state.error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Using the `ErrorBoundary` is then extremely simple:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ErrorBoundary><SomeOtherComponent/></ErrorBoundary>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that we need to be careful here: the fallback UI should not throw any
|
||||||
|
error, otherwise we risk going into an infinite loop (also, see the page on
|
||||||
|
[slots](slots.md) for more information on the `t-slot` directive).
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
Whenever the `catchError` lifecycle hook is implemented, all errors coming from
|
||||||
|
sub components rendering and/or lifecycle method calls will be caught and given
|
||||||
|
to the `catchError` method. This allows us to properly handle the error, and to
|
||||||
|
not break the application.
|
||||||
|
|
||||||
|
There are important things to know:
|
||||||
|
|
||||||
|
- If an error that occured in the internal rendering cycle is not caught, then
|
||||||
|
Owl will destroy the full application. This is done on purpose, because Owl
|
||||||
|
cannot guarantee that the state is not corrupted from this point on.
|
||||||
|
|
||||||
|
- errors coming from event handlers are NOT managed by `catchError` or any other
|
||||||
|
owl mechanism. This is up to the application developer to properly recover
|
||||||
|
from an error
|
||||||
|
|
||||||
|
Also, it may be useful to know that whenever an error is caught, it is then
|
||||||
|
broadcasted to the application by an event on the `qweb` instance. It may be
|
||||||
|
useful, for example, to log the error somewhere.
|
||||||
|
|
||||||
|
```js
|
||||||
|
env.qweb.on("error", null, function (error) {
|
||||||
|
// do something
|
||||||
|
// react to the error
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 🦉 Event Bus 🦉
|
||||||
|
|
||||||
|
It is sometimes useful to use a `Bus` to communicate informations between various
|
||||||
|
parts of the code. Owl has a very simple bus class, which manages subscriptions,
|
||||||
|
triggering events, and callbacks.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const bus = new owl.core.EventBus();
|
||||||
|
|
||||||
|
bus.on("some-event", null, function (...args) {
|
||||||
|
console.log(...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
bus.trigger("some-event", 1, 2, 3);
|
||||||
|
// [1,2,3] will be logged to the console
|
||||||
|
```
|
||||||
|
|
||||||
|
Its API is:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| -------------------------------- | --------------------------------- |
|
||||||
|
| `on(eventType, owner, callback)` | add a listener |
|
||||||
|
| `off(eventType, owner)` | remove all listeners for an owner |
|
||||||
|
| `trigger(eventType, ...args)` | trigger an event |
|
||||||
|
| `clear` | remove all subscriptions |
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# 🦉 Event Handling 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Event Handling](#event-handling)
|
||||||
|
- [Business DOM Events](#business-dom-events)
|
||||||
|
- [Inline Event Handlers](#inline-event-handlers)
|
||||||
|
- [Modifiers](#modifiers)
|
||||||
|
|
||||||
|
## Event Handling
|
||||||
|
|
||||||
|
In a component's template, it is useful to be able to register handlers on DOM
|
||||||
|
elements to some specific events. This is what makes a template _alive_. There
|
||||||
|
are four different use cases.
|
||||||
|
|
||||||
|
1. Register an event handler on a DOM node (_pure_ DOM event)
|
||||||
|
2. Register an event handler on a component (_pure_ DOM event)
|
||||||
|
3. Register an event handler on a DOM node (_business_ DOM event)
|
||||||
|
4. Register an event handler on a component (_business_ DOM event)
|
||||||
|
|
||||||
|
A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-on-click="someMethod">Do something</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
This will be roughly translated in javascript like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
button.addEventListener("click", component.someMethod.bind(component));
|
||||||
|
```
|
||||||
|
|
||||||
|
The suffix (`click` in this example) is simply the name of the actual DOM
|
||||||
|
event.
|
||||||
|
|
||||||
|
## Business DOM Events
|
||||||
|
|
||||||
|
A _business_ DOM event is triggered by a call to `trigger` on a component.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<MyComponent t-on-menu-loaded="someMethod" />
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent {
|
||||||
|
someWhere() {
|
||||||
|
const payload = ...;
|
||||||
|
this.trigger('menu-loaded', payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The call to `trigger` generates an `OwlEvent`, a subclass of [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
|
||||||
|
with an additional attribute `originalComponent` (the component that triggered
|
||||||
|
the event). The generated event is of type `menu-loaded` and dispatches it on
|
||||||
|
the component's DOM element (`this.el`). The event bubbles and is cancelable.
|
||||||
|
The parent component listening to event `menu-loaded` will receive the payload
|
||||||
|
in its `someMethod` handler (in the `detail` property of the event), whenever
|
||||||
|
the event is triggered.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent {
|
||||||
|
someMethod(ev) {
|
||||||
|
const payload = ev.detail;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By convention, we use KebabCase for the name of _business_ events.
|
||||||
|
|
||||||
|
The `t-on` directive allows to prebind its arguments. For example,
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-on-click="someMethod(expr)">Do something</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `expr` is a valid Owl expression, so it could be `true` or some variable
|
||||||
|
from the rendering context.
|
||||||
|
|
||||||
|
### Type Hinting
|
||||||
|
|
||||||
|
Note that if you work with Typescript, the `trigger` method is generic on the type of the payload.
|
||||||
|
|
||||||
|
You can then describe the type of the event, so you will see typing errors...
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
this.trigger<MyCustomPayload>("my-custom-event", payload);
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
myCustomEventHandler(ev: OwlEvent<MyCustomPayload>) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inline Event Handlers
|
||||||
|
|
||||||
|
One can also directly specify inline statements. For example,
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-on-click="state.counter++">Increment counter</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `state` must be defined in the rendering context (typically the component)
|
||||||
|
as it will be translated to:
|
||||||
|
|
||||||
|
```js
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
context.state.counter++;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Warning: inline expressions are evaluated in the context of the template. This
|
||||||
|
means that they can access the component methods and properties. But if they set
|
||||||
|
a key, the inline statement will actually not modify the component, but a key in
|
||||||
|
a sub scope.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-on-click="value = 1">Set value to 1 (does not work!!!)</button>
|
||||||
|
<button t-on-click="state.value = 1">Set state.value to 1 (work as expected)</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modifiers
|
||||||
|
|
||||||
|
In order to remove the DOM event details from the event handlers (like calls to
|
||||||
|
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
|
||||||
|
specified as additional suffixes of the `t-on` directive.
|
||||||
|
|
||||||
|
| Modifier | Description |
|
||||||
|
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `.stop` | calls `event.stopPropagation()` before calling the method |
|
||||||
|
| `.prevent` | calls `event.preventDefault()` before calling the method |
|
||||||
|
| `.self` | calls the method only if the `event.target` is the element itself |
|
||||||
|
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-on-click.stop="someMethod">Do something</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
|
||||||
|
the order may matter. For instance `t-on-click.prevent.self` will prevent all
|
||||||
|
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
|
||||||
|
itself.
|
||||||
|
|
||||||
|
Finally, empty handlers are tolerated as they could be defined only to apply
|
||||||
|
modifiers. For example,
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<button t-on-click.stop="">Do something</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
This will simply stop the propagation of the event.
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
# 🦉 Hooks 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example: Mouse Position](#example-mouse-position)
|
||||||
|
- [Example: Autofocus](#example-autofocus)
|
||||||
|
- [Reference](#reference)
|
||||||
|
- [One Rule](#one-rule)
|
||||||
|
- [`useState`](#usestate)
|
||||||
|
- [`onMounted`](#onmounted)
|
||||||
|
- [`onWillUnmount`](#onwillunmount)
|
||||||
|
- [`onWillPatch`](#onwillpatch)
|
||||||
|
- [`onPatched`](#onpatched)
|
||||||
|
- [`onWillStart`](#onwillstart)
|
||||||
|
- [`onWillUpdateProps`](#onwillupdateprops)
|
||||||
|
- [`useContext`](#usecontext)
|
||||||
|
- [`useRef`](#useref)
|
||||||
|
- [`useSubEnv`](#usesubenv)
|
||||||
|
- [`useExternalListener`](#useexternallistener)
|
||||||
|
- [`useComponent`](#usecomponent)
|
||||||
|
- [`useEnv`](#useenv)
|
||||||
|
- [Making customized hooks](#making-customized-hooks)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Hooks were popularised by React as a way to solve the following issues:
|
||||||
|
|
||||||
|
- help reusing stateful logic between components
|
||||||
|
- help organizing code by feature in complex components
|
||||||
|
- use state in functional components, without writing a class.
|
||||||
|
|
||||||
|
Owl hooks serve the same purpose, except that they work for class components
|
||||||
|
(note: React hooks do not work on class components, and maybe because of that,
|
||||||
|
there seems to be the misconception that hooks are in opposition to class. This
|
||||||
|
is clearly not true, as shown by Owl hooks).
|
||||||
|
|
||||||
|
Hooks work beautifully with Owl components: they solve the problems mentioned
|
||||||
|
above, and in particular, they are the perfect way to make your component
|
||||||
|
reactive.
|
||||||
|
|
||||||
|
## Example: mouse position
|
||||||
|
|
||||||
|
Here is the classical example of a non trivial hook to track the mouse position.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { useState, onMounted, onWillUnmount } = owl.hooks;
|
||||||
|
|
||||||
|
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||||
|
// position
|
||||||
|
function useMouse() {
|
||||||
|
const position = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
function update(e) {
|
||||||
|
position.x = e.clientX;
|
||||||
|
position.y = e.clientY;
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener("mousemove", update);
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
window.removeEventListener("mousemove", update);
|
||||||
|
});
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main root component
|
||||||
|
class App extends owl.Component {
|
||||||
|
static template = xml`
|
||||||
|
<div t-name="App">
|
||||||
|
<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// this hooks is bound to the 'mouse' property.
|
||||||
|
mouse = useMouse();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||||
|
a convention.
|
||||||
|
|
||||||
|
## Example: autofocus
|
||||||
|
|
||||||
|
Hooks can be combined to create the desired effect. For example, the following
|
||||||
|
hook combines the `useRef` hook with the `onPatched` and `onMounted` functions
|
||||||
|
to create an easy way to focus an input whenever it appears in the DOM:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function useAutofocus(name) {
|
||||||
|
let ref = useRef(name);
|
||||||
|
let isInDom = false;
|
||||||
|
function updateFocus() {
|
||||||
|
if (!isInDom && ref.el) {
|
||||||
|
isInDom = true;
|
||||||
|
ref.el.focus();
|
||||||
|
} else if (isInDom && !ref.el) {
|
||||||
|
isInDom = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPatched(updateFocus);
|
||||||
|
onMounted(updateFocus);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This hook takes the name of a valid `t-ref` directive, which should be present
|
||||||
|
in the template. It then checks whenever the component is mounted or patched if
|
||||||
|
the reference is not valid, and in this case, it will focus the node element.
|
||||||
|
This hook can be used like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<input />
|
||||||
|
<input t-ref="myinput"/>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
useAutofocus("myinput");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
### One rule
|
||||||
|
|
||||||
|
There is only one rule: every hook for a component has to be called in the
|
||||||
|
constructor, in the _setup_ method, or in class fields:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ok
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// also ok
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// also ok
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not ok: this is executed after the constructor is called
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
async willStart() {
|
||||||
|
this.state = useState({ value: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, the `useState` hook does not need to be given a reference to
|
||||||
|
the component. This is possible because there is a way to get a reference to the
|
||||||
|
current component: the `Component.current` static property is the reference to the
|
||||||
|
component instance that is currently being created.
|
||||||
|
|
||||||
|
Hooks need to be called in the constructor to ensure that this reference is
|
||||||
|
properly set. This is also a good thing for performance reasons (Owl can use
|
||||||
|
this to optimize its implementation), and for a clean architecture (this makes
|
||||||
|
it easier for developers to understand what is really happening in a component).
|
||||||
|
|
||||||
|
### `useState`
|
||||||
|
|
||||||
|
The `useState` hook is certainly the most important hook for Owl components:
|
||||||
|
this is what allows a component to be reactive, to react to state change.
|
||||||
|
|
||||||
|
The `useState` hook has to be given an object or an array, and will return
|
||||||
|
an observed version of it (using a `Proxy`).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
|
||||||
|
class Counter extends owl.Component {
|
||||||
|
static template = xml`
|
||||||
|
<button t-on-click="increment">
|
||||||
|
Click Me! [<t t-esc="state.value"/>]
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
state = useState({ value: 0 });
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.state.value++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It is important to remember that `useState` only works with objects or arrays. It
|
||||||
|
is necessary, since Owl needs to react to a change in state.
|
||||||
|
|
||||||
|
### `onMounted`
|
||||||
|
|
||||||
|
`onMounted` is not a user hook, but is a building block designed to help make useful
|
||||||
|
abstractions. `onMounted` registers a callback, which will be called when the component
|
||||||
|
is mounted (see example on top of this page).
|
||||||
|
|
||||||
|
### `onWillUnmount`
|
||||||
|
|
||||||
|
`onWillUnmount` is not a user hook, but is a building block designed to help make useful
|
||||||
|
abstractions. `onWillUnmount` registers a callback, which will be called when the component
|
||||||
|
is unmounted (see example on top of this page).
|
||||||
|
|
||||||
|
### `onWillPatch`
|
||||||
|
|
||||||
|
`onWillPatch` is not a user hook, but is a building block designed to help make useful
|
||||||
|
abstractions. `onWillPatch` registers a callback, which will be called just
|
||||||
|
before the component patched.
|
||||||
|
|
||||||
|
### `onPatched`
|
||||||
|
|
||||||
|
`onPatched` is not a user hook, but is a building block designed to help make useful
|
||||||
|
abstractions. `onPatched` registers a callback, which will be called just
|
||||||
|
after the component patched.
|
||||||
|
|
||||||
|
### `onWillStart`
|
||||||
|
|
||||||
|
`onWillStart` is an asynchronous hook. This means that the function registered
|
||||||
|
in the hook will be run just before the component is first rendered and can return a
|
||||||
|
promise, to express the fact that it is an asynchronous operation.
|
||||||
|
|
||||||
|
Note that if there are more than one `onWillStart` registered callback, then they
|
||||||
|
will all be run in parallel.
|
||||||
|
|
||||||
|
It can be used to load some initial data. For example, the following hook will
|
||||||
|
automatically load some data from the server, and return an object that will
|
||||||
|
be ready whenever the component is rendered:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function useLoader() {
|
||||||
|
const component = Component.current;
|
||||||
|
const record = useState({});
|
||||||
|
onWillStart(async () => {
|
||||||
|
const recordId = component.props.id;
|
||||||
|
Object.assign(record, await fetchSomeRecord(recordId));
|
||||||
|
});
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this example does not update the record value whenever props are
|
||||||
|
updated. For that situation, we need to use the `onWillUpdateProps` hook.
|
||||||
|
|
||||||
|
### `onWillUpdateProps`
|
||||||
|
|
||||||
|
Just like `onWillStart`, `onWillUpdateProps` is an asynchronous hook. It is
|
||||||
|
designed to be run whenever the component props are updated. This could be
|
||||||
|
useful to perform some asynchronous task such as fetching updated data.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function useLoader() {
|
||||||
|
const component = Component.current;
|
||||||
|
const record = useState({});
|
||||||
|
|
||||||
|
async function updateRecord(id) {
|
||||||
|
Object.assign(record, await fetchSomeRecord(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
onWillStart(() => updateRecord(component.props.id));
|
||||||
|
onWillUpdateProps((nextProps) => updateRecord(nextProps.id));
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if there are more than one `onWillUpdateProps` registered callback,
|
||||||
|
then they will all be run in parallel.
|
||||||
|
|
||||||
|
### `useContext`
|
||||||
|
|
||||||
|
See [`useContext`](context.md#usecontext) for reference documentation.
|
||||||
|
|
||||||
|
### `useRef`
|
||||||
|
|
||||||
|
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||||
|
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||||
|
tagged by the `t-ref` directive:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<div t-ref="someDiv"/>
|
||||||
|
<SubComponent t-ref="someComponent"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the component will be able to access the `div` and the component
|
||||||
|
`SubComponent` using the `useRef` hook:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Parent extends Component {
|
||||||
|
subRef = useRef("someComponent");
|
||||||
|
divRef = useRef("someDiv");
|
||||||
|
|
||||||
|
someMethod() {
|
||||||
|
// here, if component is mounted, refs are active:
|
||||||
|
// - this.divRef.el is the div HTMLElement
|
||||||
|
// - this.subRef.comp is the instance of the sub component
|
||||||
|
// - this.subRef.el is the root HTML node of the sub component (i.e. this.subRef.comp.el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As shown by the example above, html elements are accessed by using the `el`
|
||||||
|
key, and components references are accessed with `comp`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- if used on a component, the reference will be set in the `refs`
|
||||||
|
variable between `willPatch` and `patched`,
|
||||||
|
- on a component, accessing `ref.el` will get the root node of the component.
|
||||||
|
|
||||||
|
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||||
|
(like the [`t-attf-`](qweb_templating_language.md#dynamic-attributes) and
|
||||||
|
`t-component` directives). For example,
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the references need to be set like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.ref1 = useRef("component_1");
|
||||||
|
this.ref2 = useRef("component_2");
|
||||||
|
```
|
||||||
|
|
||||||
|
References are only guaranteed to be active while the parent component is mounted.
|
||||||
|
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||||
|
|
||||||
|
### `useSubEnv`
|
||||||
|
|
||||||
|
The environment is sometimes useful to share some common information between
|
||||||
|
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
|
||||||
|
|
||||||
|
For example, if we have a form view component, maybe we would like to make some
|
||||||
|
`model` object available to all sub components, but not to the whole application.
|
||||||
|
This is where the `useSubEnv` hook may be useful: it lets a component add some
|
||||||
|
information to the environment in a way that only the component and its children
|
||||||
|
can access it:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class FormComponent extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
const model = makeModel();
|
||||||
|
useSubEnv({ model });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `useSubEnv` takes one argument: an object which contains some key/value that
|
||||||
|
will be added to the parent environment. Note that it will extend, not replace
|
||||||
|
the parent environment. And of course, the parent environment will not be
|
||||||
|
affected.
|
||||||
|
|
||||||
|
### `useExternalListener`
|
||||||
|
|
||||||
|
The `useExternalListener` hook helps solve a very common problem: adding and removing
|
||||||
|
a listener on some target whenever a component is mounted/unmounted. For example,
|
||||||
|
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
|
||||||
|
to be closed:
|
||||||
|
|
||||||
|
```js
|
||||||
|
useExternalListener(window, "click", this.closeMenu);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Making customized hooks
|
||||||
|
|
||||||
|
Hooks are a wonderful way to organize the code of a complex component by feature
|
||||||
|
instead of by lifecycle methods. They are like mixins, except that they can be
|
||||||
|
easily composed together.
|
||||||
|
|
||||||
|
But, like every good things in life, hooks should be used with moderation. They are
|
||||||
|
not the solution to every problem.
|
||||||
|
|
||||||
|
- they may be overkill: if your component needs to perform some action specific
|
||||||
|
to itself (so, the specific code does not need to be shared), there is nothing
|
||||||
|
wrong with a simple class method:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// maybe overkill
|
||||||
|
class A extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
useMySpecificHook();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ok
|
||||||
|
class B extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.performSpecificTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the second solution is easier to extend in sub components.
|
||||||
|
|
||||||
|
- they may be harder to test: if a customized hook injects some external side
|
||||||
|
effect dependency, then it is harder to test without doing some non obvious
|
||||||
|
manipulation. For example, assume that we want to give a reference to a
|
||||||
|
router in a `useRouter` hook. We could do this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const router = new Router(...);
|
||||||
|
|
||||||
|
function useRouter() {
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, this does not _hook_ into the internal of the component. It
|
||||||
|
simply returns a global object, which is difficult to mock.
|
||||||
|
|
||||||
|
A better way would be to do something like this: get the reference from the
|
||||||
|
environment.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function useRouter() {
|
||||||
|
const env = useEnv();
|
||||||
|
return env.router;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This means that we give control to the application developer to create the
|
||||||
|
router, which is good, so they can set it up, subclass it, ... And then, to
|
||||||
|
test our components, we can just add a mock router in the environment.
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# 🦉 Miscellaneous 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Portal](#portal)
|
||||||
|
- [AsyncRoot](#asyncroot)
|
||||||
|
|
||||||
|
## `Portal`
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
The component `Portal` is meant to be used as a transparent way to 'teleport' a piece
|
||||||
|
of DOM to the node represented by its sole `target` props.
|
||||||
|
|
||||||
|
This component aims at helping the implementation of the needed infrastructure
|
||||||
|
for modals (as in `bootstrap-modal`).
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
The content it will teleport is defined within the `<Portal>` node and
|
||||||
|
internally uses the `default` [Slot](slots.md).
|
||||||
|
|
||||||
|
This slot must contain only **one** node, which in turn can have as many children as necessary.
|
||||||
|
|
||||||
|
The element under which the content will be teleported is represented as a selector
|
||||||
|
by the `target` props which only accepts a string as value.
|
||||||
|
|
||||||
|
The `target` props only supports static selector, and is not meant to be passed to `Portal`
|
||||||
|
as a variable. Namely, `<Portal target="'body'" />` is the intended use.
|
||||||
|
By contrast, `<Portal target="state.target" />` is not supported.
|
||||||
|
|
||||||
|
The component `Portal` has no particular state, rather it is meant to be a slave to its parent,
|
||||||
|
and ultimately just a way for the parent to teleport a piece of its own DOM elsewhere.
|
||||||
|
|
||||||
|
The `Portal`'s root node is always `<portal/>` and is placed where the teleported content
|
||||||
|
_would have_ been. It is this element that the [teleported events](#expected-behaviors) are re-directed on.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
The canonic use-case is to implement a Dialog, where a Component may choose to break the natural
|
||||||
|
workflow to help the user put in some data, which it could use later on.
|
||||||
|
|
||||||
|
JavaScript:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Component, mount } = owl;
|
||||||
|
const { Portal } = owl.misc;
|
||||||
|
|
||||||
|
class TeleportedComponent extends Component {}
|
||||||
|
class App extends Component {
|
||||||
|
static components = { Portal, TeleportedComponent };
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(App, { target: document.body });
|
||||||
|
```
|
||||||
|
|
||||||
|
XML:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<templates>
|
||||||
|
<div t-name="TeleportedComponent">
|
||||||
|
<span>I will move soon enough</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="App">
|
||||||
|
<span>I am like the rest of us</span>
|
||||||
|
<Portal target="'body'">
|
||||||
|
<TeleportedComponent />
|
||||||
|
</Portal>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `Portal` component will teleport the `TeleportedComponent`'s `div` as a child of the `body`.
|
||||||
|
`TeleportedComponent` is acting as a Dialog here.
|
||||||
|
|
||||||
|
The resulting DOM will look like:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<span>I am like the rest of us</span>
|
||||||
|
<portal></portal>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>I will move soon enough</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Behaviors
|
||||||
|
|
||||||
|
The teleported piece is updated as any other `Component`'s DOM and in the same sequence.
|
||||||
|
Namely the teleported piece will be updated in function of its parents components, and patched as
|
||||||
|
a normal child.
|
||||||
|
|
||||||
|
The [_business_ events](event_handling.md#business-dom-events) triggered by a child component will be stopped
|
||||||
|
to not bubble outside of the `target`. They will, on the other hand, be re-directed onto the
|
||||||
|
`Portal`'s root node and bubble up the DOM as if it were triggered by a regular child component.
|
||||||
|
|
||||||
|
Beware that those re-directed events are copies of the original event.
|
||||||
|
They have:
|
||||||
|
|
||||||
|
- The same payload.
|
||||||
|
- The same `originalComponent` than their original counterpart,
|
||||||
|
that is the actual Component that triggered it.
|
||||||
|
- A **different** `target` property than their original counterpart.
|
||||||
|
The `target` of a re-directed event is necessarily the `Portal`'s root node.
|
||||||
|
|
||||||
|
Pure DOM events do not follow this pattern and are free to bubble their natural, unaltered way
|
||||||
|
up to the `body`.
|
||||||
|
|
||||||
|
## `AsyncRoot`
|
||||||
|
|
||||||
|
When this component is used, a new rendering sub tree is created, such that the
|
||||||
|
rendering of that component (and its children) is not tied to the rendering of
|
||||||
|
the rest of the interface. It can be used on an asynchronous component, to
|
||||||
|
prevent it from delaying the rendering of the whole interface, or on a
|
||||||
|
synchronous one, such that its rendering isn't delayed by other (asynchronous)
|
||||||
|
components. Note that this directive has no effect on the first rendering, but
|
||||||
|
only on subsequent ones (triggered by state or props changes).
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<SyncChild />
|
||||||
|
<AsyncRoot>
|
||||||
|
<AsyncChild/>
|
||||||
|
</AsyncRoot>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `AsyncRoot` assumes that there is exactly one root node inside it. It can
|
||||||
|
be a dom node or a component.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 🦉 Mounting an application 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [API](#api)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Mounting an Owl application is done by using the `mount` method (available in
|
||||||
|
`owl.mount` if you are using the iife build, or it can be directly imported
|
||||||
|
from `owl` if you are using a module system):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const mount = { owl }; // if owl is available as an object
|
||||||
|
|
||||||
|
const env = { ... };
|
||||||
|
const app = await mount(MyComponent, { target: document.body, env });
|
||||||
|
```
|
||||||
|
|
||||||
|
Another example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const config = {
|
||||||
|
env: ...,
|
||||||
|
props: ...,
|
||||||
|
target: document.body,
|
||||||
|
position: "self",
|
||||||
|
};
|
||||||
|
const app = await mount(App, config);
|
||||||
|
```
|
||||||
|
|
||||||
|
A common way to initialize an application is to first setup an environment,
|
||||||
|
then to call the `mount` method.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Mount takes two parameters:
|
||||||
|
|
||||||
|
- `C`, which should be a component class (NOT instance),
|
||||||
|
- `params`, which is an object with the following keys:
|
||||||
|
- `target (HTMLElement | DocumentFragment)`: the target of the mount operation
|
||||||
|
- `env (optional, Env)` an environment
|
||||||
|
- `position (optional, "first-child" | "last-child" | "self")` the position
|
||||||
|
where it should be mounted (see below for more informations)
|
||||||
|
- `props (optional, any)`: some initial values that are given as props. Useful
|
||||||
|
when the root component is configurable, or when testing sub components
|
||||||
|
|
||||||
|
Here are the various positions supported by Owl:
|
||||||
|
|
||||||
|
- `first-child`: with this option, the component will be prepended inside the target,
|
||||||
|
- `last-child` (default value): with this option, the component will be
|
||||||
|
appended in the target element,
|
||||||
|
- `self`: the target will be used as the root element for the component. This
|
||||||
|
means that the target has to be an HTMLElement (and not a document fragment).
|
||||||
|
In this situation, it is possible that the component cannot be unmounted. For
|
||||||
|
example, if its target is `document.body`.
|
||||||
|
|
||||||
|
The `mount` method returns a promise that resolves to the instance of the created
|
||||||
|
component.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 🦉 Observer 🦉
|
||||||
|
|
||||||
|
Owl needs to be able to react to state changes. For example, whenever the state
|
||||||
|
of a component is changed, Owl needs to rerender it. To help with that, there is
|
||||||
|
an Observer class. Its job is to observe the state of an object (or array), and
|
||||||
|
to react to any change. The observer is implemented with the native `Proxy`
|
||||||
|
object. Note that this means that it will not work on older browsers.
|
||||||
|
|
||||||
|
Note that the `Observer` is used by the `useState` and `useContext` hooks. This
|
||||||
|
is the way most Owl applications will create observers. For the majority of
|
||||||
|
use cases, there is no need to directly instantiate an observer.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
For example, this code will display `update` in the console:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const observer = new owl.core.Observer();
|
||||||
|
observer.notifyCB = () => console.log("update");
|
||||||
|
const obj = observer.observe({ a: { b: 1 } });
|
||||||
|
|
||||||
|
obj.a.b = 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
This example shows that an observer can observe nested properties.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
**observe** An observer can observe multiple values with the `observe` method.
|
||||||
|
This method takes an object or an array as its argument and will return a proxy
|
||||||
|
(which is mapped to the initial object/array). With this proxy, the observer
|
||||||
|
can detect whenever any internal value is changed.
|
||||||
|
|
||||||
|
**Registering a callback** Whenever an observer sees a state change, it will
|
||||||
|
call its `notifyCB` method. No additional information is given to the callback.
|
||||||
|
|
||||||
|
**deepRevNumber** Each observed value has an internal revision number, which
|
||||||
|
is incremented every time the value is observed. Sometimes, it can be useful
|
||||||
|
to obtain that number:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const observer = new owl.core.Observer();
|
||||||
|
const obj = observer.observe({ a: { b: 1 } });
|
||||||
|
|
||||||
|
observer.revNumber(obj.a); // 1
|
||||||
|
obj.a.b = 2;
|
||||||
|
|
||||||
|
observer.revNumber(obj.a); // 2
|
||||||
|
```
|
||||||
|
|
||||||
|
The `revNumber` can also return 0, which indicates that the value is not
|
||||||
|
observed.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 🦉 Props 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Definition](#definition)
|
||||||
|
- [Good Practices](#good-practices)
|
||||||
|
- [Dynamic Props](#dynamic-props)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In Owl, `props` (short for _properties_) is an object which contains every piece
|
||||||
|
of data given to a component by its parent.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Child extends Component {
|
||||||
|
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
|
||||||
|
static components = { Child };
|
||||||
|
state = useState({ a: "fromparent" });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `Child` component receives two props from its parent: `a`
|
||||||
|
and `b`. They are collected into a `props` object by Owl, with each value being
|
||||||
|
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
|
||||||
|
`props.b` is equal to `'string'`.
|
||||||
|
|
||||||
|
Note that `props` is an object that only makes sense from the perspective of the
|
||||||
|
child component.
|
||||||
|
|
||||||
|
## Definition
|
||||||
|
|
||||||
|
The `props` object is made of every attributes defined on the template, with the
|
||||||
|
following exceptions:
|
||||||
|
|
||||||
|
- every attribute starting with `t-` are not props (they are QWeb directives),
|
||||||
|
- `style` and `class` attributes are excluded as well (they are applied by Owl on
|
||||||
|
the root element of the component).
|
||||||
|
|
||||||
|
In the following example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<ComponentA a="state.a" b="'string'"/>
|
||||||
|
<ComponentB t-if="state.flag" model="model"/>
|
||||||
|
<ComponentC style="color:red;" class="left-pane" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
the `props` object contains the following keys:
|
||||||
|
|
||||||
|
- for `ComponentA`: `a` and `b`,
|
||||||
|
- for `ComponentB`: `model`,
|
||||||
|
- for `ComponentC`: empty object
|
||||||
|
|
||||||
|
## Good Practices
|
||||||
|
|
||||||
|
A `props` object is a collection of values that come from the parent. As such,
|
||||||
|
they are owned by the parent, and should never be modified by the child:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends Component {
|
||||||
|
constructor(parent, props) {
|
||||||
|
super(parent, props);
|
||||||
|
props.a.b = 43; // Never do that!!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Props should be considered readonly, from the perspective of the child component.
|
||||||
|
If there is a need to modify them, then the request to update them should be
|
||||||
|
sent to the parent (for example, with an event).
|
||||||
|
|
||||||
|
Any value can go in a props. Strings, objects, classes, or even callbacks could
|
||||||
|
be given to a child component (but then, in the case of callbacks, communicating
|
||||||
|
with events seems more appropriate).
|
||||||
|
|
||||||
|
## Dynamic Props
|
||||||
|
|
||||||
|
The `t-props` directive can be used to specify totally dynamic props:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="ParentComponent">
|
||||||
|
<Child t-props="some.obj"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ParentComponent {
|
||||||
|
static components = { Child };
|
||||||
|
some = { obj: { a: 1, b: 2 } };
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# 🦉 Props Validation 🦉
|
||||||
|
|
||||||
|
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
|
||||||
|
|
||||||
|
- hard to tell how a component should be used, by looking at its code.
|
||||||
|
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
|
||||||
|
|
||||||
|
A props type system solves both issues, by describing the types and shapes
|
||||||
|
of the props. Here is how it works in Owl:
|
||||||
|
|
||||||
|
- `props` key is a static key (so, different from `this.props` in a component instance)
|
||||||
|
- it is optional: it is ok for a component to not define a `props` key.
|
||||||
|
- props are validated whenever a component is created/updated
|
||||||
|
- props are only validated in `dev` mode (see [config page](config.md#mode))
|
||||||
|
- if a key does not match the description, an error is thrown
|
||||||
|
- it validates keys defined in (static) `props`. Additional keys given by the
|
||||||
|
parent will cause an error.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class ComponentA extends owl.Component {
|
||||||
|
static props = ['id', 'url'];
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComponentB extends owl.Component {
|
||||||
|
static props = {
|
||||||
|
count: {type: Number},
|
||||||
|
messages: {
|
||||||
|
type: Array,
|
||||||
|
element: {type: Object, shape: {id: Boolean, text: String }
|
||||||
|
},
|
||||||
|
date: Date,
|
||||||
|
combinedVal: [Number, Boolean]
|
||||||
|
};
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- it is an object or a list of strings
|
||||||
|
- a list of strings is a simplified props definition, which only lists the name
|
||||||
|
of the props. Also, if the name ends with `?`, it is considered optional.
|
||||||
|
- all props are by default required, unless they are defined with `optional: true`
|
||||||
|
(in that case, validation is only done if there is a value)
|
||||||
|
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
|
||||||
|
constructor functions (so, if you have a `Person` class, it can be used as a type)
|
||||||
|
- arrays are homogeneous (all elements have the same type/shape)
|
||||||
|
|
||||||
|
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
|
||||||
|
|
||||||
|
- a boolean: indicate that the props exists, and is mandatory.
|
||||||
|
- a constructor: this should describe the type, for example: `id: Number` describe
|
||||||
|
the props `id` as a number
|
||||||
|
- a list of constructors. In that case, this means that we allow more than one
|
||||||
|
type. For example, `id: [Number, String]` means that `id` can be either a string
|
||||||
|
or a number.
|
||||||
|
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
|
||||||
|
- `type`: the main type of the prop being validated
|
||||||
|
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
|
||||||
|
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||||
|
- `validate`: this is a function which should return a boolean to determine if
|
||||||
|
the value is valid or not. Useful for custom validation logic.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// only the existence of those 3 keys is documented
|
||||||
|
static props = ['message', 'id', 'date'];
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// size is optional
|
||||||
|
static props = ['message', 'size?'];
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
static props = {
|
||||||
|
messageIds: {type: Array, element: Number}, // list of number
|
||||||
|
otherArr: {type: Array}, // just array. no validation is made on sub elements
|
||||||
|
otherArr2: Array, // same as otherArr
|
||||||
|
someObj: {type: Object}, // just an object, no internal validation
|
||||||
|
someObj2: {
|
||||||
|
type: Object,
|
||||||
|
shape: {
|
||||||
|
id: Number,
|
||||||
|
name: {type: String, optional: true},
|
||||||
|
url: String
|
||||||
|
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||||
|
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||||
|
someVal: [Boolean, Date], // either a boolean or a date
|
||||||
|
otherValue: true, // indicates that it is a prop
|
||||||
|
kindofsmallnumber: {
|
||||||
|
type: Number,
|
||||||
|
validate: n => (0 <= n && n <= 10)
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
validate: e => ["small", "medium", "large"].includes(e)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# 🦉 QWeb Engine 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Reference](#reference)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
|
||||||
|
templating engine used by Odoo. The QWeb class in the OWL project is an
|
||||||
|
implementation of that specification with a few interesting points:
|
||||||
|
|
||||||
|
- it compiles templates into functions that output a virtual DOM instead of a
|
||||||
|
string. This is necessary for the component system.
|
||||||
|
- it has a few extra directives: `t-component`, `t-on`, ...
|
||||||
|
|
||||||
|
We present in this section the engine, not the templating language.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
This section is about the javascript code that implements the `QWeb` specification.
|
||||||
|
Owl exports a `QWeb` class in `owl.QWeb`. To use it, it just needs to be
|
||||||
|
instantiated:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const qweb = new owl.QWeb();
|
||||||
|
```
|
||||||
|
|
||||||
|
Its API is quite simple:
|
||||||
|
|
||||||
|
- **`constructor(config)`**: constructor. Takes an optional configuration object
|
||||||
|
with an optional `templates` string to add initial
|
||||||
|
templates (see `addTemplates` for more information on format of the string)
|
||||||
|
and an optional `translateFn` translate function (see the section on
|
||||||
|
[translations](#translations)).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const qweb = new owl.QWeb({ templates: TEMPLATES, translateFn: _t });
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`addTemplate(name, xmlStr, allowDuplicate)`**: add a specific template.
|
||||||
|
|
||||||
|
```js
|
||||||
|
qweb.addTemplate("mytemplate", "<div>hello</div>");
|
||||||
|
```
|
||||||
|
|
||||||
|
If the optional `allowDuplicate` is set to `true`, then `QWeb` will simply
|
||||||
|
ignore templates added for a second time. Otherwise, `QWeb` will crash.
|
||||||
|
|
||||||
|
- **`addTemplates(xmlStr)`**: add a list of templates (identified by `t-name`
|
||||||
|
attribute).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const TEMPLATES = `
|
||||||
|
<templates>
|
||||||
|
<div t-name="App" class="main">main</div>
|
||||||
|
<div t-name="OtherComponent">other component</div>
|
||||||
|
</templates>`;
|
||||||
|
qweb.addTemplates(TEMPLATES);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`render(name, context, extra)`**: renders a template. This returns a `vnode`,
|
||||||
|
which is a virtual representation of the DOM (see [vdom doc](../miscellaneous/vdom.md)).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const vnode = qweb.render("App", component);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`renderToString(name, context)`**: renders a template, but returns an html
|
||||||
|
string.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const str = qweb.renderToString("someTemplate", somecontext);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`registerTemplate(name, template)`**: static function to register a global
|
||||||
|
QWeb template. This is useful for commonly used components accross the
|
||||||
|
application, and for making a template available to an application without
|
||||||
|
having a reference to the actual QWeb instance.
|
||||||
|
|
||||||
|
```js
|
||||||
|
QWeb.registerTemplate("mytemplate", `<div>some template</div>`);
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`registerComponent(name, Component)`**: static function to register an OWL Component
|
||||||
|
to QWeb's global registry. Globally registered Components can be used in
|
||||||
|
templates (see the `t-component` directive). This is useful for commonly used
|
||||||
|
components accross the application.
|
||||||
|
|
||||||
|
```js
|
||||||
|
class Dialog extends owl.Component { ... }
|
||||||
|
QWeb.registerComponent("Dialog", Dialog);
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
class ParentComponent extends owl.Component { ... }
|
||||||
|
qweb.addTemplate("ParentComponent", "<div><Dialog/></div>");
|
||||||
|
```
|
||||||
|
|
||||||
|
In some way, a `QWeb` instance is the core of an Owl application. It is the only
|
||||||
|
mandatory element of an [environment](environment.md). As such, it
|
||||||
|
has an extra responsibility: it can act as an event bus for internal communication
|
||||||
|
between Owl classes. This is the reason why `QWeb` actually extends [EventBus](event_bus.md).
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
take care of this and "cherry-pick" 8464a1b04e7469434f9dcb3d68a543f58cb61b8e
|
||||||
|
|
||||||
|
If properly setup, Owl QWeb engine can translate all rendered templates. To do
|
||||||
|
so, it needs a translate function, which takes a string and returns a string.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const translations = {
|
||||||
|
hello: "bonjour",
|
||||||
|
yes: "oui",
|
||||||
|
no: "non",
|
||||||
|
};
|
||||||
|
const translateFn = (str) => translations[str] || str;
|
||||||
|
|
||||||
|
const qweb = new QWeb({ translateFn });
|
||||||
|
```
|
||||||
|
|
||||||
|
Once setup, all rendered templates will be translated using `translateFn`:
|
||||||
|
|
||||||
|
- each text node will be replaced with its translation,
|
||||||
|
- each of the following attribute values will be translated as well: `title`,
|
||||||
|
`placeholder`, `label` and `alt`,
|
||||||
|
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||||
|
if its value is `off`.
|
||||||
|
|
||||||
|
So, with the above `translateFn`, the following templates:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>hello</div>
|
||||||
|
<div t-translation="off">hello</div>
|
||||||
|
<div>Are you sure?</div>
|
||||||
|
<input placeholder="hello" other="yes"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
will be rendered as:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>bonjour</div>
|
||||||
|
<div>hello</div>
|
||||||
|
<div>Are you sure?</div>
|
||||||
|
<input placeholder="bonjour" other="yes"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the translation is done during the compilation of the template, not
|
||||||
|
when it is rendered.
|
||||||
@@ -0,0 +1,613 @@
|
|||||||
|
# 🦉 QWeb Templating Language🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Directives](#directives)
|
||||||
|
- [Reference](#reference)
|
||||||
|
- [White Spaces](#white-spaces)
|
||||||
|
- [Root Nodes](#root-nodes)
|
||||||
|
- [Expression Evaluation](#expression-evaluation)
|
||||||
|
- [Static html Nodes](#static-html-nodes)
|
||||||
|
- [Outputting Data](#outputting-data)
|
||||||
|
- [Setting Variables](#setting-variables)
|
||||||
|
- [Conditionals](#conditionals)
|
||||||
|
- [Dynamic Attributes](#dynamic-attributes)
|
||||||
|
- [Dynamic Class Attribute](#dynamic-class-attribute)
|
||||||
|
- [Dynamic Tag Names](#dynamic-tag-names)
|
||||||
|
- [Loops](#loops)
|
||||||
|
- [Rendering Sub Templates](#rendering-sub-templates)
|
||||||
|
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
||||||
|
- [Translations](#translations)
|
||||||
|
- [Debugging](#debugging)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
[QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) is the primary
|
||||||
|
templating engine used by Odoo. It is based on the XML format, and used
|
||||||
|
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
||||||
|
generate a virtual dom representation of the HTML.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<span t-if="somecondition">Some string</span>
|
||||||
|
<ul t-else="">
|
||||||
|
<li t-foreach="messages" t-as="message">
|
||||||
|
<t t-esc="message"/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Template directives are specified as XML attributes prefixed with `t-`, for
|
||||||
|
instance `t-if` for conditionals, with elements and other attributes being
|
||||||
|
rendered directly.
|
||||||
|
|
||||||
|
To avoid element rendering, a placeholder element `<t>` is also available, which
|
||||||
|
executes its directive but doesn’t generate any output in and of itself.
|
||||||
|
|
||||||
|
We present in this section the templating language, including its Owl specific
|
||||||
|
extensions.
|
||||||
|
|
||||||
|
## Directives
|
||||||
|
|
||||||
|
For reference, here is a list of all standard QWeb directives:
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| ------------------------------ | -------------------------------------------------------------- |
|
||||||
|
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||||
|
| `t-raw` | [Outputting value, without escaping](#outputting-data) |
|
||||||
|
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||||
|
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||||
|
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||||
|
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||||
|
| `t-call` | [Rendering sub templates](#rendering-sub-templates) |
|
||||||
|
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||||
|
| `t-translation` | [Disabling the translation of a node](#translations) |
|
||||||
|
| `t-name` | [Defining a template (not really a directive)](qweb_engine.md) |
|
||||||
|
|
||||||
|
The component system in Owl requires additional directives, to express various
|
||||||
|
needs. Here is a list of all Owl specific directives:
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| ------------------------ | ------------------------------------------------------------------------------- |
|
||||||
|
| `t-component`, `t-props` | [Defining a sub component](component.md#composition) |
|
||||||
|
| `t-ref` | [Setting a reference to a dom node or a sub component](component.md#references) |
|
||||||
|
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
|
||||||
|
| `t-on-*` | [Event handling](event_handling.md) |
|
||||||
|
| `t-transition` | [Defining an animation](animations.md#css-transitions) |
|
||||||
|
| `t-slot` | [Rendering a slot](slots.md) |
|
||||||
|
| `t-model` | [Form input bindings](component.md#form-input-bindings) |
|
||||||
|
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
### White Spaces
|
||||||
|
|
||||||
|
White spaces in a template are handled in a special way:
|
||||||
|
|
||||||
|
- consecutive whitespaces are always condensed to a single whitespace
|
||||||
|
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||||
|
- the previous rules do not apply if we are in a `<pre>` tag
|
||||||
|
|
||||||
|
### Root Nodes
|
||||||
|
|
||||||
|
For many reasons, Owl QWeb templates should have a single root node. More
|
||||||
|
precisely, the result of a template rendering should have a single root node:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!–– not ok: two root nodes ––>
|
||||||
|
<t>
|
||||||
|
<div>foo</div>
|
||||||
|
<div>bar</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!–– ok: result has one single root node ––>
|
||||||
|
<t>
|
||||||
|
<div t-if="someCondition">foo</div>
|
||||||
|
<span t-else="">bar</span>
|
||||||
|
</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
Extra root nodes will actually be ignored (even though they will be rendered
|
||||||
|
in memory).
|
||||||
|
|
||||||
|
Note: this does not apply to subtemplates (see the `t-call` directive). In that
|
||||||
|
case, they will be inlined in the main template, and can actually have many
|
||||||
|
root nodes.
|
||||||
|
|
||||||
|
### Expression Evaluation
|
||||||
|
|
||||||
|
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||||
|
the javascript expression will be replaced with a lookup in the context (so, the
|
||||||
|
component). For example, `a + b.c(d)` will be converted into:
|
||||||
|
|
||||||
|
```js
|
||||||
|
context["a"] + context["b"].c(context["d"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
It is useful to explain the various rules that apply on these expressions:
|
||||||
|
|
||||||
|
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div><p t-if="1 + 2 === 3">ok</p></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
is valid, but the following is not valid:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. it can use anything in the rendering context (typically, the component):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p t-if="user.birthday === today()">Happy bithday!</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
is valid, and will read the `user` object from the context, and call the
|
||||||
|
`today` function.
|
||||||
|
|
||||||
|
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
||||||
|
`&` or `|`. This is useful to make sure that we still write valid XML.
|
||||||
|
|
||||||
|
| Word | replaced with |
|
||||||
|
| ----- | ------------- |
|
||||||
|
| `and` | `&&` |
|
||||||
|
| `or` | `\|\|` |
|
||||||
|
| `gt` | `>` |
|
||||||
|
| `gte` | `>=` |
|
||||||
|
| `lt` | `<` |
|
||||||
|
| `lte` | `<=` |
|
||||||
|
|
||||||
|
So, one can write this:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static Html Nodes
|
||||||
|
|
||||||
|
Normal, regular html nodes are rendered into themselves:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>hello</div> <!–– rendered as itself ––>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Outputting Data
|
||||||
|
|
||||||
|
The `t-esc` directive is necessary whenever you want to add a dynamic text
|
||||||
|
expression in a template. The text is escaped to avoid security issues.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p><t t-esc="value"/></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
rendered with the value `value` set to `42` in the rendering context yields:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p>42</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `t-raw` directive is almost the same as `t-esc`, but without the escaping.
|
||||||
|
This is mostly useful to inject a raw html string somewhere. Obviously, this
|
||||||
|
is unsafe to do in general, and should only be used for strings known to be safe.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p><t t-raw="value"/></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
rendered with the value `value` set to `<span>foo</span>` in the rendering context yields:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p><span>foo</span></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that since the content of the expression is not known beforehand, the `t-raw`
|
||||||
|
directive has to parse the html (and convert it to a virtual dom structure) for
|
||||||
|
each rendering. So, it will be much slower than a regular template. It is
|
||||||
|
therefore advised to limit the use of `t-raw` whenever possible.
|
||||||
|
|
||||||
|
### Setting Variables
|
||||||
|
|
||||||
|
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||||
|
|
||||||
|
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
|
||||||
|
|
||||||
|
1. a `t-value` attribute containing an expression, and the result of its
|
||||||
|
evaluation will be set:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-set="foo" t-value="2 + 1"/>
|
||||||
|
<t t-esc="foo"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
will print `3`. Note that the evaluation is done at rendering time, not at
|
||||||
|
compilte time.
|
||||||
|
|
||||||
|
2. if there is no `t-value` attribute, the node’s body is saved and its value is
|
||||||
|
set as the variable’s value:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-set="foo">
|
||||||
|
<li>ok</li>
|
||||||
|
</t>
|
||||||
|
<t t-esc="foo"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
will generate `<li>ok</li>` (the content is escaped as we used the `t-esc` directive)
|
||||||
|
|
||||||
|
The `t-set` directive acts like a regular variable in most programming language.
|
||||||
|
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
|
||||||
|
|
||||||
|
### Conditionals
|
||||||
|
|
||||||
|
The `t-if` directive is useful to conditionally render something. It evaluates
|
||||||
|
the expression given as attribute value, and then acts accordingly.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<t t-if="condition">
|
||||||
|
<p>ok</p>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The element is rendered if the condition (evaluated with the current rendering
|
||||||
|
context) is true:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<p>ok</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
but if the condition is false it is removed from the result:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The conditional rendering applies to the bearer of the directive, which does not
|
||||||
|
have to be `<t>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<p t-if="condition">ok</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
will give the same results as the previous example.
|
||||||
|
|
||||||
|
Extra conditional branching directives `t-elif` and `t-else` are also available:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
<p t-if="user.birthday == today()">Happy bithday!</p>
|
||||||
|
<p t-elif="user.login == 'root'">Welcome master!</p>
|
||||||
|
<p t-else="">Welcome!</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Attributes
|
||||||
|
|
||||||
|
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
|
||||||
|
evaluate an expression (at rendering time) and bind an attribute to its result:
|
||||||
|
|
||||||
|
For example, if we have `id` set to 32 in the rendering context,
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
If an expression evaluates to a falsy value, it will not be set at all:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-att-foo="false"/> <!-- result: <div></div> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
It is sometimes convenient to format an attribute with string interpolation. In
|
||||||
|
that case, the `t-attf-` directive can be used. It is useful when we need to mix
|
||||||
|
literal and dynamic elements, such as css classes.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
|
||||||
|
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
If we need completely dynamic attribute names, then there is an additional
|
||||||
|
directive: `t-att`, which takes either an object (with keys mapping to their
|
||||||
|
values) or a pair `[key, value]`. For example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></div> -->
|
||||||
|
|
||||||
|
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic class attribute
|
||||||
|
|
||||||
|
For convenience, Owl supports a special case for the `t-att-class` case: one can
|
||||||
|
use an object with keys describing the classes, and values boolean value denoting
|
||||||
|
if the class is or is not present:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
|
||||||
|
|
||||||
|
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it can be combined with normal class attribute:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic tag names
|
||||||
|
|
||||||
|
When writing generic components or templates, the specific concrete tag for an
|
||||||
|
HTML element is not known yet. In those situations, the `t-tag` directive is
|
||||||
|
useful. It simply evaluates dynamically an expression to use as a tag name. The
|
||||||
|
template:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-tag="tag">
|
||||||
|
<span>content</span>
|
||||||
|
</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
will be rendered as `<div><span>content</span></div>` if the `tag` context key
|
||||||
|
is set to `div`.
|
||||||
|
|
||||||
|
### Loops
|
||||||
|
|
||||||
|
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
||||||
|
collection to iterate on, and a second parameter `t-as` providing the name to use
|
||||||
|
for the current item of the iteration:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-foreach="[1, 2, 3]" t-as="i">
|
||||||
|
<p><t t-esc="i"/></p>
|
||||||
|
</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
will be rendered as:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p>1</p>
|
||||||
|
<p>2</p>
|
||||||
|
<p>3</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Like conditions, `t-foreach` applies to the element bearing the directive’s attribute, and
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p t-foreach="[1, 2, 3]" t-as="i">
|
||||||
|
<t t-esc="i"/>
|
||||||
|
</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
is equivalent to the previous example.
|
||||||
|
|
||||||
|
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||||
|
or an object (the current item will be the current key).
|
||||||
|
|
||||||
|
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||||
|
variables for various data points (note: `$as` will be replaced with the name
|
||||||
|
passed to `t-as`):
|
||||||
|
|
||||||
|
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||||
|
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||||
|
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||||
|
- `$as_first`: whether the current item is the first of the iteration
|
||||||
|
(equivalent to `$as_index == 0`)
|
||||||
|
- `$as_last`: whether the current item is the last of the iteration
|
||||||
|
(equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be
|
||||||
|
available
|
||||||
|
|
||||||
|
These extra variables provided and all new variables created into the `t-foreach`
|
||||||
|
are only available in the scope of the `t-foreach`. If the variable exists outside
|
||||||
|
the context of the `t-foreach`, the value is copied at the end of the foreach
|
||||||
|
into the global context.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-set="existing_variable" t-value="false"/>
|
||||||
|
<!-- existing_variable now False -->
|
||||||
|
|
||||||
|
<p t-foreach="Array(3)" t-as="i">
|
||||||
|
<t t-set="existing_variable" t-value="true"/>
|
||||||
|
<t t-set="new_variable" t-value="true"/>
|
||||||
|
<!-- existing_variable and new_variable now true -->
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- existing_variable always true -->
|
||||||
|
<!-- new_variable undefined -->
|
||||||
|
```
|
||||||
|
|
||||||
|
Even though Owl tries to be as declarative as possible, the DOM does not fully
|
||||||
|
expose its state declaratively in the DOM tree. For example, the scrolling state,
|
||||||
|
the current user selection, the focused element or the state of an input are not
|
||||||
|
set as attribute in the DOM tree. This is why we use a virtual dom
|
||||||
|
algorithm to keep the actual DOM node as much as possible.
|
||||||
|
|
||||||
|
However, in some situations, this is not enough, and we need to help Owl decide
|
||||||
|
if an element is actually the same, or is a different element with the same
|
||||||
|
properties.
|
||||||
|
|
||||||
|
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
|
||||||
|
and we render them in this template:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
|
||||||
|
and rerender the template, Owl needs to know what the intent is:
|
||||||
|
|
||||||
|
- should Owl actually swap the DOM nodes,
|
||||||
|
- or should it keep the DOM nodes, but with an updated text content?
|
||||||
|
|
||||||
|
This might look trivial, but it actually matters. These two possibilities lead
|
||||||
|
to different results in some cases. For example, if the user selected the text
|
||||||
|
of the first `p`, swapping them will keep the selection while updating the
|
||||||
|
text content will not.
|
||||||
|
|
||||||
|
There are many other cases where this is important: `input` tags with their
|
||||||
|
value, css classes and animations, scroll position...
|
||||||
|
|
||||||
|
So, the `t-key` directive is used to give an identity to an element. It allows
|
||||||
|
Owl to understand if different elements of a list are actually different or not.
|
||||||
|
|
||||||
|
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
|
||||||
|
Then, the template could look like this:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `t-key` directive is useful for lists (`t-foreach`). A key should be
|
||||||
|
a unique number or string (objects will not work: they will be cast to the
|
||||||
|
`"[object Object]"` string, which is obviously not unique).
|
||||||
|
|
||||||
|
Also, the key can be set on a `t` tag or on its children. The following variations
|
||||||
|
are all equivalent:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<p t-foreach="items" t-as="item" t-key="item.id">
|
||||||
|
<t t-esc="item.text"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<t t-foreach="items" t-as="item" t-key="item.id">
|
||||||
|
<p t-esc="item.text"/>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-foreach="items" t-as="item">
|
||||||
|
<p t-key="item.id" t-esc="item.text"/>
|
||||||
|
</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is no `t-key` directive, Owl will use the index as a default key.
|
||||||
|
|
||||||
|
Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
|
||||||
|
not work with other iterables, such as `Set`. However, it is only a matter of
|
||||||
|
using the `...` javascript operator. For example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-foreach="...items" t-as="item">...</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `...` operator will convert the `Set` (or any other iterables) into a list,
|
||||||
|
which will work with Owl QWeb.
|
||||||
|
|
||||||
|
### Rendering Sub Templates
|
||||||
|
|
||||||
|
QWeb templates can be used for top level rendering, but they can also be used
|
||||||
|
from within another template (to avoid duplication or give names to parts of
|
||||||
|
templates), using the `t-call` directive:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="other-template">
|
||||||
|
<p><t t-value="var"/></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="main-template">
|
||||||
|
<t t-set="var" t-value="owl"/>
|
||||||
|
<t t-call="other-template"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
|
||||||
|
template is rendered with the execution context of the parent. The sub template
|
||||||
|
is actually inlined in the main template, but in a sub scope: variables defined
|
||||||
|
in the sub template do not escape.
|
||||||
|
|
||||||
|
Sometimes, one might want to pass information to the sub template. In that case,
|
||||||
|
the content of the body of the `t-call` directive is available as a special
|
||||||
|
magic variable `0`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-name="other-template">
|
||||||
|
This template was called with content:
|
||||||
|
<t t-raw="0"/>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div t-name="main-template">
|
||||||
|
<t t-call="other-template">
|
||||||
|
<em>content</em>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
will result in :
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div>
|
||||||
|
This template was called with content:
|
||||||
|
<em>content</em>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
This can be used to define variables scoped to a sub template:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-call="other-template">
|
||||||
|
<t t-set="var" t-value="1"/>
|
||||||
|
</t>
|
||||||
|
<!-- "var" does not exist here -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic sub templates
|
||||||
|
|
||||||
|
The `t-call` directive can also be used to dynamically call a sub template,
|
||||||
|
using string interpolation. For example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="main-template">
|
||||||
|
<t t-call="{{template}}">
|
||||||
|
<em>content</em>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the name of the template is obtained from the `template` value in the
|
||||||
|
template rendering context.
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
By default, QWeb specify that templates should be translated. If this behaviour
|
||||||
|
is not wanted, there is a `t-translation` directive which can turn off
|
||||||
|
translations (if it is set to the `off` value), with the following rules:
|
||||||
|
|
||||||
|
- each text node will be replaced with its translation,
|
||||||
|
- each of the following attribute values will be translated as well: `title`,
|
||||||
|
`placeholder`, `label` and `alt`,
|
||||||
|
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||||
|
if its value is `off`.
|
||||||
|
|
||||||
|
See [here](qweb_engine.md#translations) for more information on how to setup a
|
||||||
|
translate function in Owl QWeb.
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
The javascript QWeb implementation provides two useful debugging directives:
|
||||||
|
|
||||||
|
`t-debug` adds a debugger statement during template rendering:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-if="a_test">
|
||||||
|
<t t-debug=""/>
|
||||||
|
</t>
|
||||||
|
```
|
||||||
|
|
||||||
|
will stop execution if the browser dev tools are open.
|
||||||
|
|
||||||
|
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-set="foo" t-value="42"/>
|
||||||
|
<t t-log="foo"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
will print 42 to the console.
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# 🦉 Slots 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Example](#example)
|
||||||
|
- [Reference](#reference)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Owl is a template based component system. There is therefore a need to be able
|
||||||
|
to make generic components. For example, imagine a generic `Dialog`
|
||||||
|
component, which is able to display some arbitrary content.
|
||||||
|
|
||||||
|
Obviously, we want to use this component everywhere in our application, to
|
||||||
|
display various different content. The `Dialog` component is technically the
|
||||||
|
owner of its content, but is only a container. The user of the `Dialog` is
|
||||||
|
the component that want to _inject_ something inside the `Dialog`. This is
|
||||||
|
exactly what slots are for.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
To make generic components, it is useful to be able for a parent component to _inject_
|
||||||
|
some sub template, but still be the owner. For example, a generic dialog component
|
||||||
|
will need to render some content, some footer, but with the parent as the
|
||||||
|
rendering context.
|
||||||
|
|
||||||
|
Slots are inserted with the `t-slot` directive:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="Dialog" class="modal">
|
||||||
|
<div class="modal-title"><t t-esc="props.title"/></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<t t-slot="content"/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<t t-slot="footer"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Slots are defined by the caller, with the `t-set-slot` directive:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="SomeComponent">
|
||||||
|
<div>some component</div>
|
||||||
|
<Dialog title="'Some Dialog'">
|
||||||
|
<t t-set-slot="content">
|
||||||
|
<div>hey</div>
|
||||||
|
</t>
|
||||||
|
<t t-set-slot="footer">
|
||||||
|
<button t-on-click="doSomething">ok</button>
|
||||||
|
</t>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the component `Dialog` will render the slots `content` and `footer`
|
||||||
|
with its parent as rendering context. This means that clicking on the button
|
||||||
|
will execute the `doSomething` method on the parent, not on the dialog.
|
||||||
|
|
||||||
|
Note: Owl previously used the `t-set` directive to define the content of a slot.
|
||||||
|
This is deprecated and should no longer be used in new code.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
### Default Slot
|
||||||
|
|
||||||
|
The first element inside the component which is not a named slot will
|
||||||
|
be considered the `default` slot. For example:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child>
|
||||||
|
<span>some content</span>
|
||||||
|
</Child>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-name="Child">
|
||||||
|
<t t-slot="default"/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default content
|
||||||
|
|
||||||
|
Slots can define a default content, in case the parent did not define them:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-name="Parent">
|
||||||
|
<Child/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span t-name="Child">
|
||||||
|
<t t-slot="default">default content</t>
|
||||||
|
</span>
|
||||||
|
<!-- will be rendered as: <div><span>default content</span></div> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
Rendering context: the content of the slots is actually rendered with the
|
||||||
|
rendering context corresponding to where it was defined, not where it is
|
||||||
|
positioned. This allows the user to define event handlers that will be bound
|
||||||
|
to the correct component (usually, the grandparent of the slot content).
|
||||||
|
|
||||||
|
### Dynamic Slots
|
||||||
|
|
||||||
|
The `t-slot` directive is actually able to use any expressions, using string
|
||||||
|
interplolation:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<t t-slot="{{current}}" />
|
||||||
|
```
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# 🦉 Tags 🦉
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [`xml` tag](#xml-tag)
|
||||||
|
- [`css` tag](#css-tag)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Tags are very small helpers intended to make it easy to write inline templates
|
||||||
|
or styles. There are currently two tags: `css` and `xml`. With these functions,
|
||||||
|
it is possible to write [single file components](../learning/how_to_write_sfc.md).
|
||||||
|
|
||||||
|
## XML tag
|
||||||
|
|
||||||
|
The `xml` tag is certainly the most useful tag. It is used to define an inline
|
||||||
|
QWeb template for a component. Without tags, creating a standalone component
|
||||||
|
would look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Component } from 'owl'
|
||||||
|
|
||||||
|
const name = 'some-unique-name';
|
||||||
|
const template = `
|
||||||
|
<div>
|
||||||
|
<span t-if="somecondition">text</span>
|
||||||
|
<button t-on-click="someMethod">Click</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
QWeb.registerTemplate(name, template);
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = name;
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With tags, this process is slightly simplified. The name is uniquely generated,
|
||||||
|
and the template is automatically registered:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { Component } = owl;
|
||||||
|
const { xml } = owl.tags;
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<span t-if="somecondition">text</span>
|
||||||
|
<button t-on-click="someMethod">Click</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CSS tag
|
||||||
|
|
||||||
|
The CSS tag is useful to define a css stylesheet in the javascript file:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div class="my-component">some template</div>
|
||||||
|
`;
|
||||||
|
static style = css`
|
||||||
|
.my-component {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `css` tag registers internally the css information. Then, whenever the first
|
||||||
|
instance of the component is created, will add a `<style>` tag to the document
|
||||||
|
`<head>`.
|
||||||
|
|
||||||
|
Note that to make it more useful, like other css preprocessors, the `css` tag
|
||||||
|
accepts a small extension of the css specification: css scopes can be nested,
|
||||||
|
and the rules will then be expanded by the `css` helper:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.my-component {
|
||||||
|
display: block;
|
||||||
|
.sub-component h {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
will be formatted as:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.my-component {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.my-component .sub-component h {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This extension brings another useful feature: the `&` selector which refers to
|
||||||
|
the parent selector. For example, we want our component to be red when hovered.
|
||||||
|
We would like to write something like:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.my-component {
|
||||||
|
display: block;
|
||||||
|
:hover {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
but it will be formatted as:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.my-component {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.my-component :hover {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `&` selector can be used to solve this problem:
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.my-component {
|
||||||
|
display: block;
|
||||||
|
&:hover {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
will be formatted as:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.my-component {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.my-component:hover {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, there is no additional processing done by the `css` tag. However, since it
|
||||||
|
is done in javascript at runtime, we actually have more power. For example:
|
||||||
|
|
||||||
|
1. sharing values between javascript and css:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { theme } from "./theme";
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`<div class="my-component">...</div>`;
|
||||||
|
static style = css`
|
||||||
|
.my-component {
|
||||||
|
color: ${theme.MAIN_COLOR};
|
||||||
|
background-color: ${theme.SECONDARY_color};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. scoping rules to the current component:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { generateUUID } from "./utils";
|
||||||
|
|
||||||
|
const uuid = generateUUID();
|
||||||
|
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static template = xml`<div data-o-${uuid}="">...</div>`;
|
||||||
|
static style = css`
|
||||||
|
[data-o-${uuid}] {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# 🦉 Utils 🦉
|
||||||
|
|
||||||
|
Owl export a few useful utility functions, to help with common issues. Those
|
||||||
|
functions are all available in the `owl.utils` namespace.
|
||||||
|
|
||||||
|
## Content
|
||||||
|
|
||||||
|
- [`whenReady`](#whenready): executing code when DOM is ready
|
||||||
|
- [`loadJS`](#loadjs): loading script files
|
||||||
|
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||||
|
- [`escape`](#escape): sanitizing strings
|
||||||
|
- [`debounce`](#debounce): limiting rate of function calls
|
||||||
|
- [`shallowEqual`](#shallowequal): shallow object comparison
|
||||||
|
|
||||||
|
## `whenReady`
|
||||||
|
|
||||||
|
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
|
||||||
|
not ready yet, resolved directly otherwise). If called with a callback as
|
||||||
|
argument, it executes it as soon as the DOM ready (or directly).
|
||||||
|
|
||||||
|
```js
|
||||||
|
Promise.all([loadFile("templates.xml"), owl.utils.whenReady()]).then(function ([templates]) {
|
||||||
|
const qweb = new owl.QWeb({ templates });
|
||||||
|
const env = { qweb };
|
||||||
|
await mount(App, { env, target: document.body });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
or alternatively:
|
||||||
|
|
||||||
|
```js
|
||||||
|
owl.utils.whenReady(function () {
|
||||||
|
const qweb = new owl.QWeb();
|
||||||
|
const env = { qweb };
|
||||||
|
await mount(App, { env, target: document.body });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## `loadJS`
|
||||||
|
|
||||||
|
`loadJS` takes a url (string) for a javascript resource, and loads it (by adding
|
||||||
|
a script tag in the document head). It returns a promise, so the caller can
|
||||||
|
properly reacts when it is ready. Also, it is smart: it maintains a list of urls
|
||||||
|
previously loaded (or currently being loaded), and prevent doing twice the work.
|
||||||
|
|
||||||
|
For example, it is useful for lazy loading external libraries:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends owl.Component {
|
||||||
|
willStart() {
|
||||||
|
return owl.utils.loadJS("/static/libs/someLib.js");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `loadFile`
|
||||||
|
|
||||||
|
`loadFile` is a helper function to fetch a file. It simply
|
||||||
|
performs a `GET` request and returns the resulting string in a promise. The
|
||||||
|
initial usecase for this function is to load a template file. For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function makeEnv() {
|
||||||
|
const templates = await owl.utils.loadFile("templates.xml");
|
||||||
|
const qweb = new owl.QWeb({ templates });
|
||||||
|
return { qweb };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that unlike `loadJS`, this function returns the content of the file as a
|
||||||
|
string. It does not add a `script` tag or any other side effect.
|
||||||
|
|
||||||
|
## `escape`
|
||||||
|
|
||||||
|
Sometimes, we need to display dynamic data (for example user-generated data) in
|
||||||
|
the user interface. If this is done by a `QWeb` template, it is not an issue:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div><t t-esc="user.data"/></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `QWeb` engine will create a `div` node and add the content of the `user.data`
|
||||||
|
string as a text node, so the web browser will not parse it as html. However,
|
||||||
|
it may be a problem if this is done with some javascript code like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class BadComponent extends Component {
|
||||||
|
// some template with a ref to a div
|
||||||
|
// some code ...
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.divRef.el.innerHTML = this.state.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, the content of the `div` will be parsed as html, which may inject
|
||||||
|
unwanted behaviour. To fix this, the `escape` function will simply transform a
|
||||||
|
string into an escaped version of the same string, which will be properly displayed
|
||||||
|
by the browser, but which will not be parsed as html (for example, `"<ok>"` is
|
||||||
|
escaped to the string: `"<ok>"`). So, the bad example above can be fixed
|
||||||
|
with the following change:
|
||||||
|
|
||||||
|
```js
|
||||||
|
this.divRef.el.innerHTML = owl.utils.escape(this.state.value);
|
||||||
|
```
|
||||||
|
|
||||||
|
## `debounce`
|
||||||
|
|
||||||
|
The `debounce` function is useful when we want to limit the number of times some
|
||||||
|
function/action is perfomed. For example, this may be useful to prevent issue
|
||||||
|
with people double clicking on a button.
|
||||||
|
|
||||||
|
It takes three arguments:
|
||||||
|
|
||||||
|
- `func` (function): this is the function that will be rate limited
|
||||||
|
- `wait` (number): this is the number of milliseconds that we want to use to
|
||||||
|
rate limit the function `func`
|
||||||
|
- `immediate` (optional, boolean, default=false): if `immediate` is true, the
|
||||||
|
function will be triggered immediately (leading edge of the interval). If false,
|
||||||
|
the function will be triggered at the end (trailing edge).
|
||||||
|
|
||||||
|
It returns a function. For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const debounce = owl.utils.debounce;
|
||||||
|
window.addEventListener("mousemove", debounce(doSomething, 100));
|
||||||
|
```
|
||||||
|
|
||||||
|
As this example shows, it is usualy useful for event handlers which are triggered
|
||||||
|
very quickly, such as `scroll` or `mousemove` events.
|
||||||
|
|
||||||
|
## `shallowEqual`
|
||||||
|
|
||||||
|
This function checks if two objects have the same values assigned to each keys:
|
||||||
|
|
||||||
|
```js
|
||||||
|
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); // true
|
||||||
|
shallowEqual({ a: 1, b: 2 }, { a: 1, b: 3 }); // false
|
||||||
|
```
|
||||||
|
|
||||||
|
However, for performance reasons, it assumes that the two objects have the same
|
||||||
|
keys. If we are in a situation where this is not guaranteed, the following code
|
||||||
|
will work:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const completeShallowEqual = (a, b) => shallowEqual(a, b) && shallowEqual(b, a);
|
||||||
|
```
|
||||||
-149
@@ -1,149 +0,0 @@
|
|||||||
# 🦉 Store 🦉
|
|
||||||
|
|
||||||
## Content
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Example](#example)
|
|
||||||
- [Reference](#reference)
|
|
||||||
- [Public API](#public-api)
|
|
||||||
- [Mutations](#mutations)
|
|
||||||
- [Actions](#actions)
|
|
||||||
- [Getters](#getters)
|
|
||||||
- [Connecting a component](#connecting-a-component)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Managing the state in an application is not an easy task. In some cases, the
|
|
||||||
state of an application can be part of the component tree, in a natural way.
|
|
||||||
However, there are situations where some part of the state need to be displayed
|
|
||||||
in various parts of the user interface, and then, it is not obvious which
|
|
||||||
component should own which part of the state.
|
|
||||||
|
|
||||||
Owl's solution to this issue is a centralized store. It is a class that owns
|
|
||||||
some state, and let the developer update it in a structured way (through
|
|
||||||
mutations and actions). Owl components can then connect to the store, and will
|
|
||||||
be updated if necessary.
|
|
||||||
|
|
||||||
Note: Owl's store is inspired by React Redux and VueX.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
Here is what a simple store look like:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const actions = {
|
|
||||||
addTodo({commit}, message) {
|
|
||||||
commit('addTodo', message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mutations = {
|
|
||||||
addTodo({state}, message) {
|
|
||||||
const todo = {
|
|
||||||
id: state.nextId++,
|
|
||||||
message,
|
|
||||||
isCompleted: false,
|
|
||||||
};
|
|
||||||
state.todos.push(todo);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
todos: [],
|
|
||||||
nextId: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = new owl.Store({state, actions, mutations});
|
|
||||||
store.on('update', () => console.log(store.state));
|
|
||||||
|
|
||||||
// updating the state
|
|
||||||
store.dispatch('addTodo', 'fix all bugs');
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
The store is a simple `owl.EventBus` that triggers `update` events whenever its
|
|
||||||
state is changed. Note that these events are triggered only after a microtask
|
|
||||||
tick, so only one event will be triggered for any number of state changes in a
|
|
||||||
call stack.
|
|
||||||
|
|
||||||
Also, it is important to mention that the state is observed (with a `owl.Observer`),
|
|
||||||
which is the reason why it is able to know if it was changed. This implies that
|
|
||||||
state changes need to be done carefully in some cases (adding a new key to an
|
|
||||||
object, or modifying an array with the `arr[i] = newValue` syntax). See the
|
|
||||||
[Observer](observer.md)'s documentation for more details.
|
|
||||||
|
|
||||||
### Public API
|
|
||||||
|
|
||||||
1. `constructor`
|
|
||||||
2. `commit`
|
|
||||||
3. `dispatch`
|
|
||||||
|
|
||||||
### Mutations
|
|
||||||
|
|
||||||
Mutations are the only way to modify the state. Changing the state outside a
|
|
||||||
mutation is not allowed (and should throw an error). Mutations are synchronous.
|
|
||||||
|
|
||||||
### Actions
|
|
||||||
|
|
||||||
Actions are used to coordinate state changes. It is also useful whenever some
|
|
||||||
asynchronous logic is necessary. For example, fetching data should be done
|
|
||||||
in an action.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const actions = {
|
|
||||||
async login({commit}) {
|
|
||||||
commit('setLoginState', 'pending');
|
|
||||||
try {
|
|
||||||
const loginInfo = await doSomeRPC('/login/', 'someinfo');
|
|
||||||
commit('setLoginState', loginInfo);
|
|
||||||
} catch {
|
|
||||||
commit('setLoginState', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Getters
|
|
||||||
|
|
||||||
Usually, data contained in the store will be stored in a normalized way. For
|
|
||||||
example,
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
posts: [{id: 11, authorId: 4, content: 'Greetings'}],
|
|
||||||
authors: [{id: 4, name: 'John'}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
However, the user interface will probably need some denormalized data like
|
|
||||||
|
|
||||||
```js
|
|
||||||
{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is what `getters` are for: they give a centralized way to process and
|
|
||||||
transform the data contained in the store.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const getters = {
|
|
||||||
getPost({state}, id) {
|
|
||||||
const post = state.posts.find(p => p.id === id);
|
|
||||||
const author = state.authors.find(a => a.id = post.id);
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
author,
|
|
||||||
content: post.content
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// somewhere else
|
|
||||||
const post = store.getters.getPost(id);
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connecting a component
|
|
||||||
|
|
||||||
Todo
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 🦉 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)
|
|
||||||
- [`loadJS`](#loadjs)
|
|
||||||
- [`loadTemplates`](#loadtemplates)
|
|
||||||
- [`escape`](#escape)
|
|
||||||
- [`debounce`](#debounce)
|
|
||||||
- [`patch` and `unpatch`](#patchandunpatch)
|
|
||||||
|
|
||||||
## `whenReady`
|
|
||||||
|
|
||||||
The function `whenReady` is useful to register some code that need to be executed
|
|
||||||
as soon as the document (page) is ready:
|
|
||||||
|
|
||||||
```js
|
|
||||||
owl.utils.whenReady(function () {
|
|
||||||
const qweb = new owl.QWeb();
|
|
||||||
const app = new App({ qweb });
|
|
||||||
app.mount(document.body);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## `loadJS`
|
|
||||||
|
|
||||||
## `loadTemplates`
|
|
||||||
|
|
||||||
## `escape`
|
|
||||||
|
|
||||||
## `debounce`
|
|
||||||
|
|
||||||
## `patch` and `unpatch`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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,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.10.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,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 0.9.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,81 +0,0 @@
|
|||||||
import { buildData, startMeasure, stopMeasure } from "../shared/utils.js";
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Likes Counter Widget
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
class Counter extends owl.Component {
|
|
||||||
state = { counter: 0 };
|
|
||||||
|
|
||||||
increment() {
|
|
||||||
this.state.counter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Message Widget
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
class Message extends owl.Component {
|
|
||||||
widgets = { Counter };
|
|
||||||
|
|
||||||
shouldUpdate(nextProps) {
|
|
||||||
return nextProps !== this.props;
|
|
||||||
}
|
|
||||||
removeMessage() {
|
|
||||||
this.trigger("remove_message", {
|
|
||||||
id: this.props.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Root Widget
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
class App extends owl.Component {
|
|
||||||
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="App" 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,29 +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-0.9.0">OWL 0.9.0</a></li>
|
|
||||||
<li><a href="benchmarks/owl-0.10.0">OWL 0.10.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,359 +0,0 @@
|
|||||||
import { SAMPLES } from "./samples.js";
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Constants, helpers, utils
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
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)
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an iframe, with all the js, css and xml properly injected.
|
|
||||||
*/
|
|
||||||
function makeCodeIframe(js, css, xml, errorHandler) {
|
|
||||||
// check templates
|
|
||||||
var qweb = new owl.QWeb();
|
|
||||||
const sanitizedXML = xml.replace(/<!--[\s\S]*?-->/g, "");
|
|
||||||
|
|
||||||
// will throw error if there is something wrong with xml
|
|
||||||
qweb.addTemplates(sanitizedXML);
|
|
||||||
|
|
||||||
// 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${js}`;
|
|
||||||
script.innerHTML = content;
|
|
||||||
iframe.contentWindow.addEventListener("error", errorHandler);
|
|
||||||
iframe.contentWindow.addEventListener("unhandledrejection", errorHandler);
|
|
||||||
setTimeout(function() {
|
|
||||||
if (iframe.contentWindow) {
|
|
||||||
iframe.contentWindow.removeEventListener("error", errorHandler);
|
|
||||||
iframe.contentWindow.removeEventListener(
|
|
||||||
"unhandledrejection",
|
|
||||||
errorHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
doc.body.appendChild(script);
|
|
||||||
});
|
|
||||||
doc.head.appendChild(owlScript);
|
|
||||||
|
|
||||||
// inject css
|
|
||||||
const style = document.createElement("style");
|
|
||||||
style.innerHTML = css;
|
|
||||||
doc.head.appendChild(style);
|
|
||||||
};
|
|
||||||
return iframe;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a zip file containing a functioning application
|
|
||||||
*/
|
|
||||||
async function makeApp(js, css, xml) {
|
|
||||||
await owl.utils.loadJS("libs/jszip.min.js");
|
|
||||||
|
|
||||||
const zip = new JSZip();
|
|
||||||
const processedJS = js
|
|
||||||
.split("\n")
|
|
||||||
.map(l => (l === "" ? "" : " " + l))
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
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
|
|
||||||
${processedJS}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for DOM ready before starting
|
|
||||||
owl.utils.whenReady(startApp);`;
|
|
||||||
|
|
||||||
zip.file("app.js", JS);
|
|
||||||
zip.file("app.css", css);
|
|
||||||
zip.file("app.py", APP_PY);
|
|
||||||
zip.file("app.xml", xml);
|
|
||||||
zip.file("index.html", DEFAULT_HTML);
|
|
||||||
zip.file("owl.js", owlSourceCode());
|
|
||||||
return zip.generateAsync({ type: "blob" });
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// MAIN APP
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
class App extends owl.Component {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
this.toggleLayout = owl.utils.debounce(this.toggleLayout, 250, true);
|
|
||||||
this.runCode = owl.utils.debounce(this.runCode, 250, true);
|
|
||||||
this.downloadCode = owl.utils.debounce(this.downloadCode, 250, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
displayError(error) {
|
|
||||||
this.state.error = error;
|
|
||||||
if (error) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.refs.content.innerHTML = "";
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runCode() {
|
|
||||||
this.state.displayWelcome = false;
|
|
||||||
let subiframe;
|
|
||||||
let error = false;
|
|
||||||
const errorHandler = e => this.displayError(e.message || e.reason.message);
|
|
||||||
try {
|
|
||||||
const { js, css, xml } = this.state;
|
|
||||||
subiframe = makeCodeIframe(js, css, xml, errorHandler);
|
|
||||||
} catch (e) {
|
|
||||||
//probably problem with the templates
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
this.displayError(error.message);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.state.error = false;
|
|
||||||
}
|
|
||||||
this.refs.content.innerHTML = "";
|
|
||||||
this.refs.content.appendChild(subiframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
const { js, css, xml } = this.state;
|
|
||||||
const content = await makeApp(js, css, xml);
|
|
||||||
await owl.utils.loadJS("libs/FileSaver.min.js");
|
|
||||||
saveAs(content, "app.zip");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Tabbed editor
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
class TabbedEditor extends owl.Component {
|
|
||||||
constructor(parent, props) {
|
|
||||||
super(parent, props);
|
|
||||||
this.state = {
|
|
||||||
currentTab: props.js ? "js" : props.xml ? "xml" : "css"
|
|
||||||
};
|
|
||||||
this.setTab = owl.utils.debounce(this.setTab, 250, true);
|
|
||||||
|
|
||||||
this.sessions = {};
|
|
||||||
for (let tab of ["js", "xml", "css"]) {
|
|
||||||
if (props[tab]) {
|
|
||||||
this.sessions[tab] = new ace.EditSession(props[tab], MODES[tab]);
|
|
||||||
this.sessions[tab].setOption("useWorker", false);
|
|
||||||
const tabSize = tab === "xml" ? 2 : 4;
|
|
||||||
this.sessions[tab].setOption("tabSize", tabSize);
|
|
||||||
this.sessions[tab].setUndoManager(new ace.UndoManager());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.editor = ace.edit(this.refs.editor);
|
|
||||||
|
|
||||||
this.editor.setValue(this.props[this.state.currentTab], -1);
|
|
||||||
this.editor.setFontSize("12px");
|
|
||||||
this.editor.setTheme("ace/theme/monokai");
|
|
||||||
this.editor.setSession(this.sessions[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() {
|
|
||||||
const session = this.sessions[this.state.currentTab];
|
|
||||||
session.setValue(this.props[this.state.currentTab], -1);
|
|
||||||
this.editor.setSession(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
willUnmount() {
|
|
||||||
this.editor.destroy();
|
|
||||||
delete this.editor;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTab(tab) {
|
|
||||||
if (this.state.currentTab !== tab) {
|
|
||||||
this.state.currentTab = tab;
|
|
||||||
const session = this.sessions[this.state.currentTab];
|
|
||||||
session.doc.setValue(this.props[tab], -1);
|
|
||||||
this.editor.setSession(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// 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,21 +0,0 @@
|
|||||||
# 🦉 Extra Stuff 🦉
|
|
||||||
|
|
||||||
To help work/improve/learn with OWL, we have here:
|
|
||||||
|
|
||||||
- a benchmarks application
|
|
||||||
- a playground application
|
|
||||||
|
|
||||||
Both of them can be accessed by using a static http server. A simple python
|
|
||||||
server is available in `server.py`. There is also a npm script to start it:
|
|
||||||
`npm run extras` (and its version with a watcher: `npm run extras:watch`).
|
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
|
|
||||||
The benchmarks application is a very small application, implemented in different
|
|
||||||
frameworks, and in different versions of Owl. This is a simple internal tool,
|
|
||||||
useful to compare various performance metrics on some tasks.
|
|
||||||
|
|
||||||
## Playground
|
|
||||||
|
|
||||||
The playground is an important application designed to help learning and
|
|
||||||
experimenting with Owl. It is available [online](https://odoo.github.io/owl/playground/).
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import sys
|
|
||||||
import thread
|
|
||||||
import webbrowser
|
|
||||||
import time
|
|
||||||
|
|
||||||
import BaseHTTPServer
|
|
||||||
import SimpleHTTPServer
|
|
||||||
|
|
||||||
HOST = '127.0.0.1'
|
|
||||||
PORT = 8000
|
|
||||||
URL = 'http://{0}:{1}/extras'.format(HOST, PORT)
|
|
||||||
|
|
||||||
|
|
||||||
# We define our own handler here to remap owl.js GET requests to the Owl build
|
|
||||||
# in dist/. This is useful for the benchmarks and playground applications.
|
|
||||||
# With this, we can simply copy the playground folder as is in the gh-page when
|
|
||||||
# we want to update the playground.
|
|
||||||
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, ())
|
|
||||||
webbrowser.open_new(URL)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
time.sleep(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit(0)
|
|
||||||
+44
-21
@@ -1,54 +1,73 @@
|
|||||||
{
|
{
|
||||||
"name": "owl",
|
"name": "@odoo/owl",
|
||||||
"version": "0.11.0",
|
"version": "2.0.0-alpha1",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"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/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.18.3"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:js": "tsc --target esnext --module es6 --outDir dist/owl src/*",
|
|
||||||
"build:bundle": "rollup -c",
|
"build:bundle": "rollup -c",
|
||||||
"build": "npm run build:js && npm run build:bundle",
|
"build": "npm run build:bundle",
|
||||||
"minify": "uglifyjs dist/owl.js -o dist/owl.min.js --compress --mangle",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"extras:serve": "python extras/server.py",
|
"playground:serve": "python3 tools/server.py || python tools/server.py",
|
||||||
"extras": "npm run build && npm run extras:serve",
|
"playground": "npm run build && npm run playground:serve",
|
||||||
"extras:watch": "npm-run-all --parallel extras:serve \"build:* -- --watch\""
|
"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"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/odoo/owl.git"
|
"url": "git+https://github.com/odoo/owl.git"
|
||||||
},
|
},
|
||||||
"author": "Odoo",
|
"author": "Odoo",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "LGPL-3.0-only",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/odoo/owl/issues"
|
"url": "https://github.com/odoo/owl/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/odoo/owl#readme",
|
"homepage": "https://github.com/odoo/owl#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^23.3.12",
|
"@types/jest": "^27.0.1",
|
||||||
|
"@types/node": "^14.11.8",
|
||||||
|
"chalk": "^3.0.0",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
|
"current-git-branch": "^1.1.0",
|
||||||
"git-rev-sync": "^1.12.0",
|
"git-rev-sync": "^1.12.0",
|
||||||
"jest": "^23.6.0",
|
"github-api": "^3.3.0",
|
||||||
|
"jest": "^27.1.0",
|
||||||
|
"jest-environment-jsdom": "^27.1.0",
|
||||||
"live-server": "^1.2.1",
|
"live-server": "^1.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"rollup": "^1.6.0",
|
"prettier": "2.4.1",
|
||||||
"rollup-plugin-typescript2": "^0.20.1",
|
"rollup": "^2.56.3",
|
||||||
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
"rollup-plugin-typescript2": "^0.31.1",
|
||||||
"sass": "^1.16.1",
|
"sass": "^1.16.1",
|
||||||
"snabbdom-to-html": "^5.1.1",
|
|
||||||
"source-map-support": "^0.5.10",
|
"source-map-support": "^0.5.10",
|
||||||
"ts-jest": "^23.10.5",
|
"ts-jest": "^27.0.5",
|
||||||
"typescript": "^3.2.2",
|
"typescript": "4.5.2",
|
||||||
"uglify-es": "^3.3.9"
|
"uglify-es": "^3.3.9"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"jest-environment-jsdom": "^24.7.1",
|
|
||||||
"snabbdom": "^0.7.3"
|
|
||||||
},
|
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
"roots": [
|
"roots": [
|
||||||
"<rootDir>/src",
|
"<rootDir>/src",
|
||||||
"<rootDir>/tests"
|
"<rootDir>/tests"
|
||||||
],
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"./tests/mocks/mockEventTarget.js"
|
||||||
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.ts?$": "ts-jest"
|
"^.+\\.ts?$": "ts-jest"
|
||||||
},
|
},
|
||||||
@@ -62,5 +81,9 @@
|
|||||||
"json",
|
"json",
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 100,
|
||||||
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# 🦉 OWL Roadmap 🦉
|
||||||
|
|
||||||
|
- Current version: 1.4.7
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
|
||||||
+67
-10
@@ -1,14 +1,71 @@
|
|||||||
import { version } from "./package.json";
|
import pkg from "./package.json";
|
||||||
import git from "git-rev-sync";
|
import git from "git-rev-sync";
|
||||||
|
import typescript from 'rollup-plugin-typescript2';
|
||||||
|
import { terser } from "rollup-plugin-terser";
|
||||||
|
|
||||||
|
const name = "owl";
|
||||||
|
const extend = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta data to be added on the __info__ object.
|
||||||
|
* Used to let external tools know the current owl version.
|
||||||
|
*/
|
||||||
|
const outro = `
|
||||||
|
__info__.version = '${pkg.version}';
|
||||||
|
__info__.date = '${new Date().toISOString()}';
|
||||||
|
__info__.hash = '${git.short()}';
|
||||||
|
__info__.url = 'https://github.com/odoo/owl';
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate from a string depicting a path a new path for the minified version.
|
||||||
|
* @param {string} pkgFileName file name
|
||||||
|
*/
|
||||||
|
function generateMinifiedNameFromPkgName(pkgFileName) {
|
||||||
|
const parts = pkgFileName.split('.');
|
||||||
|
parts.splice(parts.length - 1, 0, "min");
|
||||||
|
return parts.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rollup config based on the arguments
|
||||||
|
* @param {string} format format of the bundle
|
||||||
|
* @param {string} generatedFileName generated file name
|
||||||
|
* @param {boolean} minified should it be minified
|
||||||
|
*/
|
||||||
|
function getConfigForFormat(format, generatedFileName, minified = false) {
|
||||||
|
return {
|
||||||
|
file: minified ? generateMinifiedNameFromPkgName(generatedFileName) : generatedFileName,
|
||||||
|
format: format,
|
||||||
|
name: name,
|
||||||
|
extend: extend,
|
||||||
|
outro: outro,
|
||||||
|
freeze: false,
|
||||||
|
plugins: minified ? [terser()] : [],
|
||||||
|
indent: ' ', // indent with 4 spaces
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// rollup.config.js
|
|
||||||
export default {
|
export default {
|
||||||
input: "dist/owl/index.js",
|
input: "src/index.ts",
|
||||||
output: {
|
output: [
|
||||||
file: "dist/owl.js",
|
|
||||||
format: "iife",
|
/**
|
||||||
name: "owl",
|
* Read about module formats:
|
||||||
extend: true,
|
* https://auth0.com/blog/javascript-module-systems-showdown/
|
||||||
outro: `exports._version = '${version}';\nexports._date = '${new Date().toISOString()}';\nexports._hash = '${git.short()}';\nexports._url = 'https://github.com/odoo/owl';`
|
* https://medium.com/@kelin2025/so-you-wanna-use-es6-modules-714f48b3a953
|
||||||
}
|
*/
|
||||||
|
|
||||||
|
getConfigForFormat('esm', pkg.module),
|
||||||
|
getConfigForFormat('esm', pkg.module, true),
|
||||||
|
getConfigForFormat('cjs', pkg.main),
|
||||||
|
getConfigForFormat('cjs', pkg.main, true),
|
||||||
|
getConfigForFormat('iife', pkg.browser),
|
||||||
|
getConfigForFormat('iife', pkg.browser, true),
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
useTsconfigDeclarationDir: true
|
||||||
|
}),
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Component } from "../component/component";
|
||||||
|
import { ComponentNode } from "../component/component_node";
|
||||||
|
import { MountOptions } from "../component/fibers";
|
||||||
|
import { Scheduler } from "../component/scheduler";
|
||||||
|
import { TemplateSet } from "./template_set";
|
||||||
|
|
||||||
|
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||||
|
|
||||||
|
export interface Env {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
dev?: boolean;
|
||||||
|
env?: Env;
|
||||||
|
translatableAttributes?: string[];
|
||||||
|
translateFn?: (s: string) => string;
|
||||||
|
templates?: string | Document;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEV_MSG = `Owl is running in 'dev' mode.
|
||||||
|
|
||||||
|
This is not suitable for production use.
|
||||||
|
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
|
||||||
|
|
||||||
|
export class App<T extends typeof Component = any> extends TemplateSet {
|
||||||
|
Root: T;
|
||||||
|
props: any;
|
||||||
|
env: Env = Object.freeze({});
|
||||||
|
scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
|
||||||
|
root: ComponentNode | null = null;
|
||||||
|
|
||||||
|
constructor(Root: T, props?: any) {
|
||||||
|
super();
|
||||||
|
this.Root = Root;
|
||||||
|
this.props = props;
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(config: AppConfig): App<T> {
|
||||||
|
if (config.dev) {
|
||||||
|
this.dev = config.dev;
|
||||||
|
console.info(DEV_MSG);
|
||||||
|
}
|
||||||
|
if (config.env) {
|
||||||
|
this.env = Object.freeze(Object.assign({}, config.env));
|
||||||
|
}
|
||||||
|
if (config.translateFn) {
|
||||||
|
this.translateFn = config.translateFn;
|
||||||
|
}
|
||||||
|
if (config.translatableAttributes) {
|
||||||
|
this.translatableAttributes = config.translatableAttributes;
|
||||||
|
}
|
||||||
|
if (config.templates) {
|
||||||
|
this.addTemplates(config.templates);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(target: HTMLElement, options?: MountOptions): Promise<InstanceType<T>> {
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
throw new Error("Cannot mount component: the target is not a valid DOM element");
|
||||||
|
}
|
||||||
|
if (!document.body.contains(target)) {
|
||||||
|
throw new Error("Cannot mount a component on a detached dom node");
|
||||||
|
}
|
||||||
|
const node = new ComponentNode(this.Root, this.props, this);
|
||||||
|
this.root = node;
|
||||||
|
return node.mountComponent(target, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.root) {
|
||||||
|
this.root.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { BDom, multi, text, toggler } from "../blockdom";
|
||||||
|
import { validateProps } from "../component/props_validation";
|
||||||
|
import { Markup } from "../utils";
|
||||||
|
import { html } from "../blockdom/index";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file contains utility functions that will be injected in each template,
|
||||||
|
* to perform various useful tasks in the compiled code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function withDefault(value: any, defaultValue: any): any {
|
||||||
|
return value === undefined || value === null || value === false ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function callSlot(
|
||||||
|
ctx: any,
|
||||||
|
parent: any,
|
||||||
|
key: string,
|
||||||
|
name: string,
|
||||||
|
dynamic: boolean,
|
||||||
|
extra: any,
|
||||||
|
defaultContent?: (ctx: any, node: any, key: string) => BDom
|
||||||
|
): BDom {
|
||||||
|
const slots = (ctx.props && ctx.props.slots) || {};
|
||||||
|
const { __render, __ctx, __scope } = slots[name] || {};
|
||||||
|
const slotScope = Object.create(__ctx || {});
|
||||||
|
if (__scope) {
|
||||||
|
slotScope[__scope] = extra || {};
|
||||||
|
}
|
||||||
|
const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
|
||||||
|
if (defaultContent) {
|
||||||
|
let child1: BDom | undefined = undefined;
|
||||||
|
let child2: BDom | undefined = undefined;
|
||||||
|
if (slotBDom) {
|
||||||
|
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
|
||||||
|
} else {
|
||||||
|
child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
|
||||||
|
}
|
||||||
|
return multi([child1, child2]);
|
||||||
|
}
|
||||||
|
return slotBDom || text("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function capture(ctx: any): any {
|
||||||
|
const component = ctx.__owl__.component;
|
||||||
|
const result = Object.create(component);
|
||||||
|
for (let k in ctx) {
|
||||||
|
result[k] = ctx[k];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withKey(elem: any, k: string) {
|
||||||
|
elem.key = k;
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareList(collection: any): [any[], any[], number, any[]] {
|
||||||
|
let keys: any[];
|
||||||
|
let values: any[];
|
||||||
|
|
||||||
|
if (Array.isArray(collection)) {
|
||||||
|
keys = collection;
|
||||||
|
values = collection;
|
||||||
|
} else if (collection) {
|
||||||
|
values = Object.keys(collection);
|
||||||
|
keys = Object.values(collection);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid loop expression");
|
||||||
|
}
|
||||||
|
const n = values.length;
|
||||||
|
return [keys, values, n, new Array(n)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBoundary = Symbol("isBoundary");
|
||||||
|
|
||||||
|
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
|
||||||
|
const ctx0 = ctx;
|
||||||
|
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
|
||||||
|
const newCtx = ctx.__proto__;
|
||||||
|
if (!newCtx) {
|
||||||
|
ctx = ctx0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ctx = newCtx;
|
||||||
|
}
|
||||||
|
ctx[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber(val: string): number | string {
|
||||||
|
const n = parseFloat(val);
|
||||||
|
return isNaN(n) ? val : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shallowEqual(l1: any[], l2: any[]): boolean {
|
||||||
|
for (let i = 0, l = l1.length; i < l; i++) {
|
||||||
|
if (l1[i] !== l2[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Safely outputs `value` as a block depending on the nature of `value`
|
||||||
|
*/
|
||||||
|
export function safeOutput(value: any): ReturnType<typeof toggler> {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
let safeKey;
|
||||||
|
let block;
|
||||||
|
if (value instanceof Markup) {
|
||||||
|
safeKey = `string_safe`;
|
||||||
|
block = html(value as string);
|
||||||
|
} else if (typeof value === "string") {
|
||||||
|
safeKey = "string_unsafe";
|
||||||
|
block = text(value);
|
||||||
|
} else {
|
||||||
|
// Assuming it is a block
|
||||||
|
safeKey = "block_safe";
|
||||||
|
block = value;
|
||||||
|
}
|
||||||
|
return toggler(safeKey, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UTILS = {
|
||||||
|
withDefault,
|
||||||
|
zero: Symbol("zero"),
|
||||||
|
isBoundary,
|
||||||
|
callSlot,
|
||||||
|
capture,
|
||||||
|
withKey,
|
||||||
|
prepareList,
|
||||||
|
setContextValue,
|
||||||
|
shallowEqual,
|
||||||
|
toNumber,
|
||||||
|
validateProps,
|
||||||
|
safeOutput,
|
||||||
|
};
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { createBlock, html, list, multi, text, toggler } from "../blockdom";
|
||||||
|
import { compile, Template } from "../compiler";
|
||||||
|
import { component } from "../component/component_node";
|
||||||
|
import { UTILS } from "./template_helpers";
|
||||||
|
|
||||||
|
const bdom = { text, createBlock, list, multi, html, toggler, component };
|
||||||
|
|
||||||
|
export const globalTemplates: { [key: string]: string | Node } = {};
|
||||||
|
|
||||||
|
function parseXML(xml: string): Document {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
const doc = parser.parseFromString(xml, "text/xml");
|
||||||
|
if (doc.getElementsByTagName("parsererror").length) {
|
||||||
|
let msg = "Invalid XML in template.";
|
||||||
|
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||||
|
if (parsererrorText) {
|
||||||
|
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||||
|
const re = /\d+/g;
|
||||||
|
const firstMatch = re.exec(parsererrorText);
|
||||||
|
if (firstMatch) {
|
||||||
|
const lineNumber = Number(firstMatch[0]);
|
||||||
|
const line = xml.split("\n")[lineNumber - 1];
|
||||||
|
const secondMatch = re.exec(parsererrorText);
|
||||||
|
if (line && secondMatch) {
|
||||||
|
const columnIndex = Number(secondMatch[0]) - 1;
|
||||||
|
if (line[columnIndex]) {
|
||||||
|
msg +=
|
||||||
|
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||||
|
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TemplateSet {
|
||||||
|
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||||
|
templates: { [name: string]: Template } = {};
|
||||||
|
translateFn?: (s: string) => string;
|
||||||
|
translatableAttributes?: string[];
|
||||||
|
utils: typeof UTILS;
|
||||||
|
dev?: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const call = (subTemplate: string, ctx: any, parent: any) => {
|
||||||
|
const template = this.getTemplate(subTemplate);
|
||||||
|
return toggler(subTemplate, template(ctx, parent));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTemplate = (name: string) => this.getTemplate(name);
|
||||||
|
this.utils = Object.assign({}, UTILS, { getTemplate, call });
|
||||||
|
}
|
||||||
|
|
||||||
|
addTemplate(name: string, template: string | Node, options: { allowDuplicate?: boolean } = {}) {
|
||||||
|
if (name in this.rawTemplates && !options.allowDuplicate) {
|
||||||
|
throw new Error(`Template ${name} already defined`);
|
||||||
|
}
|
||||||
|
this.rawTemplates[name] = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTemplates(xml: string | Document, options: { allowDuplicate?: boolean } = {}) {
|
||||||
|
if (!xml) {
|
||||||
|
// empty string
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xml = xml instanceof Document ? xml : parseXML(xml);
|
||||||
|
for (const template of xml.querySelectorAll("[t-name]")) {
|
||||||
|
const name = template.getAttribute("t-name")!;
|
||||||
|
template.removeAttribute("t-name");
|
||||||
|
this.addTemplate(name, template, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTemplate(name: string): Template {
|
||||||
|
if (!(name in this.templates)) {
|
||||||
|
const rawTemplate = this.rawTemplates[name];
|
||||||
|
if (rawTemplate === undefined) {
|
||||||
|
throw new Error(`Missing template: "${name}"`);
|
||||||
|
}
|
||||||
|
const templateFn = compile(rawTemplate, {
|
||||||
|
name,
|
||||||
|
dev: this.dev,
|
||||||
|
translateFn: this.translateFn,
|
||||||
|
translatableAttributes: this.translatableAttributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// first add a function to lazily get the template, in case there is a
|
||||||
|
// recursive call to the template name
|
||||||
|
this.templates[name] = (context, parent) => this.templates[name](context, parent);
|
||||||
|
const template = templateFn(bdom, this.utils);
|
||||||
|
this.templates[name] = template;
|
||||||
|
}
|
||||||
|
return this.templates[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import type { Setter } from "./block_compiler";
|
||||||
|
|
||||||
|
const { setAttribute, 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
||||||
|
return function (this: HTMLElement, value: any) {
|
||||||
|
if (value !== false) {
|
||||||
|
setAttribute.call(this, attr, value === true ? "" : 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) {
|
||||||
|
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) {
|
||||||
|
(this as any)[name] = 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,613 @@
|
|||||||
|
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[];
|
||||||
|
forceRef?: 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 isActive = false;
|
||||||
|
let currentNS = parent && parent.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("");
|
||||||
|
isActive = true;
|
||||||
|
}
|
||||||
|
if (tagName.startsWith("block-child-")) {
|
||||||
|
domParentTree!.forceRef = true;
|
||||||
|
const index = parseInt(tagName.slice(12), 10);
|
||||||
|
info.push({ type: "child", idx: index });
|
||||||
|
el = document.createTextNode("");
|
||||||
|
isActive = true;
|
||||||
|
}
|
||||||
|
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-")) {
|
||||||
|
isActive = true;
|
||||||
|
const idx = parseInt(attrName.slice(14), 10);
|
||||||
|
info.push({
|
||||||
|
type: "handler",
|
||||||
|
idx,
|
||||||
|
event: attrValue,
|
||||||
|
});
|
||||||
|
} else if (attrName.startsWith("block-attribute-")) {
|
||||||
|
isActive = true;
|
||||||
|
const idx = parseInt(attrName.slice(16), 10);
|
||||||
|
info.push({
|
||||||
|
type: "attribute",
|
||||||
|
idx,
|
||||||
|
name: attrValue,
|
||||||
|
tag: tagName,
|
||||||
|
});
|
||||||
|
} else if (attrName === "block-attributes") {
|
||||||
|
isActive = true;
|
||||||
|
info.push({
|
||||||
|
type: "attributes",
|
||||||
|
idx: parseInt(attrValue, 10),
|
||||||
|
});
|
||||||
|
} else if (attrName === "block-ref") {
|
||||||
|
isActive = true;
|
||||||
|
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: isActive ? 1 : 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 });
|
||||||
|
isActive = true;
|
||||||
|
tree.refN = 1;
|
||||||
|
} 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 (isActive) {
|
||||||
|
let cur: IntermediateTree | null = tree;
|
||||||
|
while ((cur = cur.parent)) {
|
||||||
|
cur.refN++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildContext(
|
||||||
|
tree: IntermediateTree,
|
||||||
|
ctx?: BlockCtx,
|
||||||
|
fromIdx?: number,
|
||||||
|
toIdx?: number
|
||||||
|
): BlockCtx {
|
||||||
|
if (!ctx) {
|
||||||
|
const children = new Array(tree.info.filter((v) => v.type === "child").length);
|
||||||
|
ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN };
|
||||||
|
fromIdx = 0;
|
||||||
|
toIdx = tree.refN - 1;
|
||||||
|
}
|
||||||
|
if (tree.refN) {
|
||||||
|
const initialIdx = fromIdx!;
|
||||||
|
const isRef = tree.forceRef || tree.info.length > 0;
|
||||||
|
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, toIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// left
|
||||||
|
if (firstChild) {
|
||||||
|
ctx.collectors.push({ idx: fromIdx!, prevIdx: initialIdx, getVal: nodeGetFirstChild });
|
||||||
|
buildContext(tree.firstChild!, ctx, fromIdx!, toIdx! - nextSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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":
|
||||||
|
ctx.cbRefs.push(info.idx);
|
||||||
|
ctx.locations.push({
|
||||||
|
idx: info.idx,
|
||||||
|
refIdx: info.refIdx!,
|
||||||
|
setData: setRef,
|
||||||
|
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 refs = ctx.cbRefs;
|
||||||
|
B = class extends B {
|
||||||
|
remove() {
|
||||||
|
super.remove();
|
||||||
|
for (let ref of refs) {
|
||||||
|
let fn = (this as any).data[ref];
|
||||||
|
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;
|
||||||
|
|
||||||
|
return class Block {
|
||||||
|
el: HTMLElement | undefined;
|
||||||
|
refs: Node[] | undefined;
|
||||||
|
data: any[] | undefined;
|
||||||
|
parentEl?: HTMLElement | undefined;
|
||||||
|
children?: (VNode | 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(parent: HTMLElement, afterNode: Node | null) {
|
||||||
|
const el = nodeCloneNode.call(template, true);
|
||||||
|
nodeInsertBefore.call(parent, el, afterNode);
|
||||||
|
if (isDynamic) {
|
||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toString() {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
this.mount(div, null);
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setText(this: Text, value: any) {
|
||||||
|
characterDataSetData.call(this, toText(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRef(this: HTMLElement, fn: any) {
|
||||||
|
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,90 @@
|
|||||||
|
import { config } from "./config";
|
||||||
|
|
||||||
|
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
||||||
|
|
||||||
|
interface EventHandlerCreator {
|
||||||
|
setup: EventHandlerSetter;
|
||||||
|
update: EventHandlerSetter;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 update(this: HTMLElement, data: any) {
|
||||||
|
(this as any)[eventKey] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { setup, update };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
return { setup, update: setup };
|
||||||
|
}
|
||||||
|
|
||||||
|
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,87 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,44 @@
|
|||||||
|
export { config } from "./config";
|
||||||
|
|
||||||
|
export { toggler } from "./toggler";
|
||||||
|
export { createBlock } from "./block_compiler";
|
||||||
|
export { list } from "./list";
|
||||||
|
export { multi } from "./multi";
|
||||||
|
export { text } from "./text";
|
||||||
|
export { html } from "./html";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user