mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 316eb06279 | |||
| df59ec49ae | |||
| 2a008a8679 | |||
| 3d40533de1 | |||
| 39329f80b2 | |||
| 203ac7ac66 | |||
| 530c2f9e4c | |||
| ef8baa23d7 | |||
| acfcc5677a | |||
| f6e8aff725 | |||
| 4a5316ced5 | |||
| 9a5edb4590 | |||
| 3d49daedcd | |||
| 620e41daa1 | |||
| de84075c11 | |||
| 9fe8e93980 | |||
| bd199971bd | |||
| 6f23b18cab | |||
| 2c244aa31a | |||
| ba1a270c93 | |||
| d546244fc3 | |||
| a1f22829c1 | |||
| 64bad25762 | |||
| 7ab34c5ca5 | |||
| 669fd622ec | |||
| ab72cdddde | |||
| 17fb33475c | |||
| ab29b896eb | |||
| d5ed25cd19 | |||
| c4f0f17b9b | |||
| d27455e9f2 | |||
| 6ef38676c4 | |||
| b51756f356 | |||
| cfdf7caa50 | |||
| a5a6a592c1 | |||
| d0d7482b0f | |||
| 8fe4c0c76e | |||
| c1afaeb92a | |||
| 3883cec079 | |||
| 9cb74d619b | |||
| a93f015795 | |||
| 02a187d80b | |||
| d3b0d1971e | |||
| b90aa0e23a | |||
| 163366997c | |||
| 588b655c11 | |||
| f5d5273c25 | |||
| 9fd662fdce | |||
| 7786077921 | |||
| 30bc605c84 | |||
| d1118455aa | |||
| f8073cb153 | |||
| 6c72e0a143 | |||
| 382e3e4010 | |||
| 76c389a7a8 | |||
| b9ba0abf41 | |||
| 4ca37be7f3 | |||
| d6667ddf2e | |||
| 6f86beeaf3 | |||
| 7f580a4e1d | |||
| c7459ef87b | |||
| 5772b4e9e4 | |||
| e57e2ee378 | |||
| 2e03332acd | |||
| c16d7d52b1 | |||
| 9c4c3e3b83 | |||
| 55ac43c1db | |||
| d046913a01 | |||
| 0e6059467f | |||
| 51538c2fea | |||
| 83d4471048 | |||
| 8a967e5b2d | |||
| 385e118e58 | |||
| a3111eb9ca | |||
| 1fc88f626f | |||
| 5d5a530505 | |||
| c7bd0ab85c | |||
| 31b57cbb40 | |||
| 31fce0926c | |||
| b56a9c24cf | |||
| 6dcdb77eab | |||
| a7daef380a | |||
| 22a79cdedd | |||
| e4b810c027 | |||
| b10a700381 | |||
| 2b4d8874c7 | |||
| 98b58b505b | |||
| 586033fd95 | |||
| 1fe0bf08b1 | |||
| 4779707923 | |||
| d917af4614 | |||
| 4c77132ae2 | |||
| 114f21586e | |||
| 32d8b23b9d | |||
| a83731007a | |||
| 5c71744e19 | |||
| d09771b04b | |||
| 7137130c38 | |||
| 0cd66c8518 | |||
| 5d9cff0331 | |||
| 41f1262eb7 | |||
| 41344ef4ec | |||
| 7d14db7d31 | |||
| 1179e84971 | |||
| 24b1ea7604 | |||
| 3e4ebb6378 | |||
| 859748aed9 | |||
| fd13277e1d | |||
| 7fb166bd50 | |||
| decf42c742 | |||
| 989b0d6709 | |||
| 9b93521da4 | |||
| de240b1ebc | |||
| 55dbc01a1b | |||
| e3b1566943 | |||
| 828be28653 | |||
| d80fad760c | |||
| 7611ea6033 | |||
| c7d515a6b3 | |||
| d277039b14 | |||
| 77ff5ee895 | |||
| 47c6d6cc3c | |||
| 0625b5883a | |||
| 53ab54b1ec | |||
| 4770b91faa | |||
| c356351de2 | |||
| f04423da23 | |||
| b3062d29f1 | |||
| 56086242bb | |||
| 998ecbb337 | |||
| 50355e6a3d | |||
| 67f86a4ab8 | |||
| 14d2328c88 | |||
| e4fdd32f22 | |||
| 8d1d0a2244 | |||
| 0457e5d4ed | |||
| 8920b4b93a | |||
| 6908102a72 | |||
| d6348b8310 | |||
| 79738e00c7 | |||
| 2a1b99be2d | |||
| 8a472231cf | |||
| bb373e6a7a | |||
| d735213758 | |||
| 076b0d774e | |||
| f405fe9323 | |||
| 1ae9d514b9 | |||
| 592d9a458e | |||
| 0dbd2bd463 | |||
| 8ec7a6f9bf | |||
| 12b8ce963e | |||
| bb9d65e95b | |||
| 73c339fff1 | |||
| 406be446a5 | |||
| bd2aa8a72f | |||
| 3536f41f00 | |||
| 804ad3c35e | |||
| 4a922ed82d | |||
| a6f0985d43 | |||
| 6aee1355c8 | |||
| 921ced7c90 | |||
| 1da930cb25 | |||
| cc8e11c9c9 | |||
| 3196b585fd | |||
| ea2ccc5a03 | |||
| 960808aeb2 | |||
| 1fb1d37e32 | |||
| 24ce8613c5 | |||
| 2c1226d737 | |||
| 140818b5f9 | |||
| 83de53d283 | |||
| 50aac42bdc | |||
| bd98d4d0d0 | |||
| 4d68dac24d | |||
| 722abd6d5f | |||
| a7305a5cdb | |||
| ff734c706c | |||
| add5fdd737 | |||
| a221411938 | |||
| bb6479f44f | |||
| aa95149997 | |||
| e4b4ee471f | |||
| 3af5e57825 | |||
| 979712f84e | |||
| 93f2c1d766 | |||
| 0728c8333d | |||
| 6639d361c3 | |||
| 72962f1dd1 | |||
| a4d9aae9a7 | |||
| 1e8576ad40 | |||
| 753d82149e | |||
| 700030cc7d | |||
| d828f39a2d | |||
| 0a73154985 | |||
| d88eb34d4f | |||
| a8d88d4009 | |||
| 6d9ed0d62f | |||
| b33471e819 | |||
| 3fb65b3a89 | |||
| 09d192999a | |||
| 466cf50b73 | |||
| 96620d3e8e | |||
| 176c89b278 | |||
| 38941bc26f | |||
| ce8ddd1cbf | |||
| 81f44ee5d3 | |||
| 99b5e9ec55 | |||
| 4f35f03986 | |||
| eab0caa6cb | |||
| 5a2c769eab | |||
| 89d63ff29a | |||
| a2e8abc243 | |||
| dfd0dcedb8 | |||
| ad743c205c | |||
| 7c78442e43 | |||
| 0d13c362d3 | |||
| 374dbb2fd9 | |||
| 2a5f37cf4b | |||
| cfb6b9f958 | |||
| 82f6923a21 | |||
| cccb379377 | |||
| 82c7c24438 | |||
| 41ad5db2e3 | |||
| 42a140a8e3 | |||
| 7711733a23 | |||
| 281b32965e | |||
| cec451fd15 | |||
| 6a7703ea82 | |||
| 4a03a60084 | |||
| a6bdca082a | |||
| 90167c5436 | |||
| c221721d7f | |||
| 38f39b6755 | |||
| 1775467434 | |||
| 52d0526ddd | |||
| 4b170b9b45 | |||
| 3e1fe07ba7 | |||
| 5bf47500d5 | |||
| 75ad0835e9 | |||
| 3d6a5eb828 | |||
| aceaeef8cc | |||
| 06fc3a2c77 | |||
| 92cc4375f8 | |||
| 9f2e2bcc66 | |||
| c7af885f43 | |||
| bd5637c0a3 | |||
| 3c98ef8cb1 | |||
| aad6b806ba | |||
| 772c275bd4 | |||
| 416deeb865 | |||
| 7e40fa300a | |||
| cc1eea0945 | |||
| bf9cceb56f | |||
| 7eaecac0b5 | |||
| ddc358f48a | |||
| e2819323ee | |||
| 983b9f996d | |||
| fd295b3be3 | |||
| e675f7ff5b | |||
| 702fb3b253 | |||
| 63fbcf99fd | |||
| 14a6289f60 | |||
| 894deed13b | |||
| 6f435c36d8 | |||
| 5dddf8f9a3 | |||
| 463eb4bb86 | |||
| b66d5231d3 | |||
| 3c12519277 | |||
| c1a973a4d8 | |||
| e91e50a812 | |||
| 2e176f135d | |||
| 49c7585998 | |||
| f32b1deb2c | |||
| 1da3ecdbee | |||
| a1c619f094 | |||
| eceb3e6280 | |||
| 8a1ac13975 | |||
| 779003e715 | |||
| 8c600fa539 | |||
| bcc4fe2a27 | |||
| bb4948f3dc | |||
| cc4480e001 | |||
| 7143c2e39b | |||
| a8d8310b8e | |||
| 0bbea351a6 | |||
| 2601a176c4 | |||
| 05a57d6da5 | |||
| e6e6c31632 | |||
| 3f66d9fe6c | |||
| 5d8141a67c | |||
| 6459d8d289 | |||
| 2943ca3921 | |||
| 4866ed8e8a | |||
| 93b88cad8d | |||
| b90180a9e0 | |||
| 8239a5d2cd | |||
| a073568667 | |||
| 7143dd3ff5 | |||
| c0cf2c9e3d | |||
| db9658c140 | |||
| eb2c41aa91 | |||
| a45ca98dac | |||
| ced777f0be | |||
| 7df0a4e93f | |||
| f3555cfae0 | |||
| 629b379ea9 | |||
| a400fc5e69 | |||
| 5d4a38ad0f | |||
| 093218a067 | |||
| ed3e6dcbb6 | |||
| cb107cef7d | |||
| aecc320c29 | |||
| e580ec00fe | |||
| ee5f6c7569 | |||
| c627b0add8 | |||
| b902edc1be | |||
| 1761af9c24 | |||
| db93ef08ff | |||
| 03787cfb39 | |||
| 10745c52d0 | |||
| 2b90e3a688 | |||
| b2ea241270 | |||
| 81e5b24f2f | |||
| 717fd3b6ab | |||
| 3fa1bb62f6 | |||
| 201f06c187 | |||
| ae30d9db7d | |||
| af80cefa76 | |||
| 1f6e84d141 | |||
| 1658d15b87 | |||
| c1439814bf | |||
| d5fbaff9f7 | |||
| 8c16790471 | |||
| 9b8c582b32 | |||
| 1700a6fba3 | |||
| 7513b1e507 | |||
| e0c0306acd | |||
| bca6afeb90 | |||
| 756d32daa0 | |||
| 8169f05edc | |||
| 153f4379f4 | |||
| 7ffb9afbd9 | |||
| c03042b44d | |||
| df2d6b6a0e | |||
| 7c04cc425e | |||
| 16f1e2c237 | |||
| 8a84b5be56 | |||
| 219923d752 | |||
| 348b505e5f | |||
| d3745e4e5f | |||
| 80cb6b7a91 | |||
| 4cceb239dd | |||
| 2ae0149adb | |||
| 3eb63452e7 | |||
| 1296964ae2 | |||
| c71db28bc6 | |||
| a0b2551e4a | |||
| ebd2e4324f | |||
| aa3148eddf | |||
| 42811344da | |||
| a0e1af83ac | |||
| c16d8ed6de | |||
| 15e4c856da | |||
| ced5d0f69f | |||
| b6eb4d009e | |||
| 8c71d99e5f | |||
| d7f3f4defe | |||
| 0f2192604c | |||
| 10df0b5f4a | |||
| d569ea1c28 | |||
| 52fa81c510 | |||
| d6668e3439 | |||
| ee1ef20ce1 | |||
| 9d5ffe11c7 | |||
| 900a3ee501 | |||
| 0a544bd7e8 | |||
| 4415cc8932 | |||
| efa147fdab | |||
| e746574a1d | |||
| c06049076a | |||
| bc04f727ac | |||
| 0bc9573a8a | |||
| 73f94fba3f | |||
| 7a16449724 | |||
| 718c765e3b | |||
| 150d620b8e | |||
| 307b936d01 | |||
| 6950f8e628 | |||
| 4e3b7c74da | |||
| 98bb2842d8 | |||
| 6e185f987b | |||
| c0f495661d | |||
| 0f7a8289a6 | |||
| 1c3b04f6a8 | |||
| 64db7777dd | |||
| 37313c47a3 | |||
| 3f563de9c0 | |||
| 97564a7612 | |||
| f1abf7f2ea | |||
| 8464a1b04e | |||
| c0a62dfd05 | |||
| 600f1e35d4 | |||
| b3181f119d | |||
| 1e1e05350a | |||
| b242230e20 | |||
| 9fe2da704e | |||
| 21b1661d39 | |||
| ec05b1f5e3 | |||
| 8083678f03 | |||
| 61c2ec5d83 | |||
| e579a993fd | |||
| ea3c6f7bf0 | |||
| 9cfafc30b5 | |||
| ad42c583c6 | |||
| 6faaa6c361 | |||
| bff539cabf | |||
| 88fd1cf483 | |||
| f0b75b6890 | |||
| a1552117f9 | |||
| 17ae1d06c4 | |||
| 27629cedfa | |||
| 3a93370ab6 | |||
| a6bb4d8ee1 | |||
| caf842c482 | |||
| 1b513a1637 | |||
| 52a200878a | |||
| f4994a20d8 | |||
| 7419c1982a | |||
| a8c8f6affe | |||
| dabe7f9bfa | |||
| d08ea63565 | |||
| 7ebe0da962 | |||
| 3e0e8475a6 | |||
| 2e83c739b9 | |||
| 922eb7cd98 | |||
| d12115554c | |||
| 1a6b26c8c6 | |||
| d67d295eaa | |||
| 9cbcf20b33 | |||
| 3a461e1dd1 | |||
| e8387810e6 | |||
| acac9d1741 | |||
| 968a5460bb | |||
| e646eb697e | |||
| 7dfa901332 | |||
| 0290f63ba3 | |||
| 370fae4e1a | |||
| 76735410f8 | |||
| 19a47a7001 | |||
| 398f9f4e53 | |||
| af426aa902 | |||
| 490cf18079 | |||
| dfc782599b | |||
| 71f545058b | |||
| 1a20cc57de | |||
| 25738a1bf0 | |||
| 4a96eff3c6 | |||
| d043d47754 | |||
| 3a10468f7b | |||
| 144b323d2b | |||
| bb64e87634 | |||
| 9a87b9a4a0 | |||
| 2a53a9592e | |||
| f54b9a4a0c | |||
| abb9d0b364 | |||
| 8483cc805e | |||
| 142f47ac82 | |||
| c8a27aa1a3 | |||
| 323cb61d2c | |||
| ac9cc91701 | |||
| d83cfc12ee | |||
| cb07c99d40 | |||
| d615ffd81b | |||
| 8d2b250fef | |||
| 7626cc01b3 | |||
| 392185ab67 | |||
| 392dc0131a | |||
| 4e8e9dee3e | |||
| 25d4cb2aab | |||
| 4724652533 | |||
| b08cc1d084 | |||
| a68d7774c6 | |||
| ed9d820d9e | |||
| 4a2d019161 | |||
| 89eae191b1 | |||
| 3fbc02986c | |||
| a28ce440dc | |||
| 8e018fbbda | |||
| 33f7fc8e83 | |||
| 6a833c54ec | |||
| cb38d795f9 | |||
| 8da68e925b | |||
| 474ffa1cd9 | |||
| e73fb462c5 | |||
| 38c7ad9629 | |||
| d0c76c5854 | |||
| e032314739 | |||
| b2db7f21ed | |||
| 7b8ac13d3f | |||
| 06d852fcf9 | |||
| 7e4baf668a | |||
| fe34ba00a6 | |||
| e5e7790530 | |||
| 3bf91afc3f | |||
| 2529aa3ef2 | |||
| 8d25bddda4 | |||
| 81af21a025 | |||
| 9baea2c1cd | |||
| 8e03f9cd9c | |||
| c5a2f52afb | |||
| 9fd8315c53 | |||
| 85318b3ae6 | |||
| 4b961cbffe | |||
| 2af8cccd65 | |||
| b8d3618afa | |||
| 559fadb62a | |||
| c36333dbbc | |||
| b5c3422b4d | |||
| 23ce19e57a | |||
| aaf40e9bec | |||
| 211f6ebdd6 | |||
| 1707bd240d | |||
| 142b69823f | |||
| 94c8bce810 | |||
| ae172d42e7 | |||
| ddf30a8a97 | |||
| 8d0d8538ad | |||
| 2b0315c03f | |||
| c469cac315 | |||
| b4ad14edc0 | |||
| 718e5264ae | |||
| f5d019bb69 | |||
| afa36f52a0 | |||
| fd6327b068 | |||
| d5098dd138 | |||
| ce052e0992 | |||
| b00188c1ce | |||
| 2c01802b8e | |||
| 20eb848262 | |||
| 8b479749b7 | |||
| f7728b93bd | |||
| 03585d8fea | |||
| 047a9c8993 | |||
| 69d35abe4f | |||
| 1d7503913e | |||
| ee956a1977 | |||
| 3fdc7a48f3 | |||
| d212309f1b | |||
| 8866905f33 | |||
| 50b116c56d | |||
| a823373220 | |||
| e402ee62de | |||
| 5711eb065d | |||
| 6739e79aac | |||
| 1c8dc97fca | |||
| f9cac94dc7 | |||
| c62c3fea19 | |||
| 3845607feb | |||
| 3fcbb9ad16 | |||
| e04b160bff | |||
| bc234d2053 | |||
| 3d0f046d9a | |||
| 4c75bae0e8 | |||
| 54f5819ef9 | |||
| 0c9c5b877b | |||
| b7b0e38fca | |||
| 74bd119bf5 | |||
| 4a0d04e4ae | |||
| 49931104a1 | |||
| 21d1306ec2 | |||
| 21737d33fa | |||
| 23012f3e7c | |||
| 55889febd5 | |||
| 0af46bc123 | |||
| b63cd4c9d9 | |||
| 953778dc50 | |||
| 4f61d9f1e0 | |||
| bc2c7edff4 | |||
| c8d9c0b50e | |||
| 15b25fd838 | |||
| 1db0f5ac9b | |||
| 6465665550 | |||
| 9216c5c24b | |||
| d9bf4284c6 | |||
| ef6fd457f8 | |||
| 455e45c148 | |||
| 5e7f1d5e6d | |||
| 0b05ad619f | |||
| 29a80d0b28 | |||
| 0931a4dc5b | |||
| 4e22dbcad6 | |||
| b283e65ad4 | |||
| f517d44e32 | |||
| 08022b49df | |||
| b96ea79f2b | |||
| 97d8b3ed8c | |||
| 9921ae07b1 | |||
| 41cb5f1abd | |||
| 0762eeb010 | |||
| bc455be720 | |||
| 9145799ae9 | |||
| ce9c2f8613 | |||
| b890e7ceae | |||
| 124211c18b | |||
| be8c3bbf6f | |||
| e162a6fb6c | |||
| 7ec05ff8cf | |||
| a0bfbf6dd0 | |||
| b67cb71048 | |||
| f5b3ef5cf3 | |||
| ecfccf2448 | |||
| f12d3373c9 | |||
| 286090efca | |||
| 2b4ff7b2fd | |||
| c43178a45b | |||
| 033a431e18 | |||
| 42aa9d3ae1 | |||
| 85f26a0286 | |||
| ed4ab51c17 | |||
| 556a4644f0 | |||
| 821bd0b4b8 | |||
| 06a6d890d7 | |||
| c1269288f5 | |||
| 8c17bb0411 | |||
| 7f6782d009 | |||
| 6e5d6aa226 | |||
| 9400c0adad | |||
| 85d4393242 | |||
| 6c8b401092 | |||
| 12be815342 | |||
| 65344dbf1f | |||
| a6f9b26057 | |||
| 6c753bb49f | |||
| 1f079b883e | |||
| 8fb35ed969 | |||
| a3f2d07b40 | |||
| c993278c80 | |||
| 3aa586db43 | |||
| 14db513f3b | |||
| 7bb04185c7 | |||
| 48744cfa87 | |||
| ff76747a05 | |||
| 7e6b1a28a0 | |||
| 58f6724194 | |||
| 3c0f8ac76f | |||
| cb11c0118c | |||
| 27b4eece66 | |||
| 2e3e8cd603 | |||
| 28ee790b3e | |||
| 2922cee6ea | |||
| 7749fd3b96 | |||
| f44b9a38ae | |||
| c7773bfd2a | |||
| bd39797f17 | |||
| 94a595ef5b | |||
| f83846e054 | |||
| a97144366d | |||
| 79983de4ed | |||
| 54e1734f2f | |||
| 4aea093ed9 | |||
| 56087b95cd | |||
| e5940b4b6b | |||
| bbdc9d90d7 | |||
| f2b3ebd1ec | |||
| 55bb09ba1a | |||
| d249f50d09 | |||
| 749f0063ea | |||
| 263f31fea9 | |||
| b891ae7de4 | |||
| c5a80497ac | |||
| e47f604449 | |||
| 7e721a96b5 | |||
| af4f372506 | |||
| 5731358607 | |||
| 1226f015d7 | |||
| b211e75140 | |||
| 8d1dd06340 | |||
| bd3c1265d3 | |||
| 2413c98f50 | |||
| d74b5a03db | |||
| 83532db48f | |||
| 534152eff7 | |||
| 05a678c039 | |||
| 8fbf2172c5 | |||
| ea1376d0ca | |||
| ba483b6e2c | |||
| 2fc71cfb62 | |||
| 08cb83149e | |||
| 7d249d6f09 | |||
| 0addca63a0 | |||
| 5ba73cc09d | |||
| 9106c19066 | |||
| 9f93da4765 | |||
| 9e37b968e8 | |||
| fa6801b523 | |||
| 9edf29a3a1 | |||
| 6a434310ee | |||
| e7967d0779 | |||
| b0e2ef82f7 | |||
| c9c2b3fa6d | |||
| 62608cbb0d | |||
| 3035a9f009 | |||
| c2adb429bd | |||
| da9dda5eca | |||
| 5614c85b04 | |||
| 5d57a7bf13 | |||
| 4ebe419c56 | |||
| 9779cd196c | |||
| 9cee12d7b4 | |||
| 2c563ee380 | |||
| 2aa705f5c8 | |||
| 5911c8e3f6 | |||
| 2279dafbec | |||
| f07ec21a07 | |||
| b5b6b7342e | |||
| 97b914b9cd | |||
| 9dfcfda365 | |||
| e026f537ae | |||
| f0b5a55ad1 | |||
| 690d8edf67 | |||
| 3c38bbc076 | |||
| 9c5cad15c1 | |||
| 2bed1cfbd1 | |||
| caa9ac5cb2 | |||
| 67349eb0f0 | |||
| 1ca401811f | |||
| 2beb12678e | |||
| 0ea1091692 | |||
| 71827c3ba8 | |||
| c2f42d3b82 | |||
| a3317ab997 | |||
| 403935a41e | |||
| 39af9ec938 | |||
| 13128ed425 | |||
| be556a970e | |||
| da6c24bbca | |||
| f6d6da8393 | |||
| 918945c11e | |||
| 29c2c5b9c9 | |||
| 1bb4577ec1 | |||
| 0aeebd7b6e | |||
| 4bc49e7241 | |||
| 2b5783d9bd | |||
| 1f40f113aa | |||
| 9e65b0686f | |||
| 1d72164015 | |||
| 2825a5a55d | |||
| d40ff53f5a | |||
| 23246c42aa | |||
| 895fe7c60f | |||
| 19b10f7c1d | |||
| 0788d9fe75 | |||
| 4a346d0615 | |||
| 1337aa0107 | |||
| 3e8f60cefa | |||
| 41b0618c93 | |||
| 0928c189f4 | |||
| 2f27768be2 | |||
| aa1b0f502d | |||
| da1824f4dd | |||
| 07dac970f7 | |||
| 5f6f081ae2 | |||
| 1c3c2e5c51 | |||
| 17ce73e6a9 | |||
| cedb1f411f | |||
| e7516be95c | |||
| 683993dbc4 | |||
| 50d569411c | |||
| c3453d35b9 | |||
| b0dd0252a4 | |||
| e2b2e54e29 | |||
| 766c378ac9 | |||
| 022b29b6a0 | |||
| 367725d194 | |||
| e82ed7dc8c | |||
| 63c3d4f6b0 | |||
| b859fcb8f0 | |||
| 3005756d89 | |||
| f6820bb8ac | |||
| 090880f478 | |||
| 5cdaa7b473 | |||
| 91b9e78182 | |||
| 5335fb8fba | |||
| 4513e3f31c | |||
| ecf12f6eee | |||
| 82a6961b3a | |||
| 78a4550ebb | |||
| cc82b3ffcd | |||
| bbbaf95c8e | |||
| caa63628dc | |||
| 7a49b9d94e | |||
| 01eb338e69 | |||
| 3048c6f961 | |||
| 8edc637033 | |||
| c9910077a4 | |||
| 6bf70f016c | |||
| 905619f80f | |||
| 14ceb382ef | |||
| 9846b2e997 | |||
| cfc2fb2ba2 | |||
| d57017b6ed | |||
| 5bf7a5c671 | |||
| 9b589af75a | |||
| 471fd49063 | |||
| 4fc3423682 | |||
| ffb3263c79 | |||
| e1acf66143 | |||
| 2a40907e35 | |||
| 1c0dd79955 | |||
| 9fad164c79 | |||
| 53a8841914 | |||
| 19f0863aa3 | |||
| 28f7b44f9e | |||
| fe46a6ccee | |||
| 3c23a7b246 | |||
| c52799de2c | |||
| 4edb90f44b | |||
| 0c1a7aabae | |||
| cfdce29ce7 | |||
| b344d73e07 | |||
| 4cd08d25c8 | |||
| f9861040c8 | |||
| 40d8090232 | |||
| 55be6437f1 | |||
| 6ff4dd74bf | |||
| 3628b49c37 | |||
| f289cb17ec | |||
| e6a3ede4a6 | |||
| 10cfe0b740 | |||
| a3d563ebb4 | |||
| 3b1247d476 | |||
| beb6c2bc82 | |||
| f0cd6f61cd | |||
| c950b385a0 | |||
| c172b31481 | |||
| 9b3a97e732 | |||
| 670c98916c | |||
| c59abef374 | |||
| bb4c2506c0 | |||
| 9843d16585 | |||
| 34e463ad0a | |||
| 2294c1967f | |||
| 4b53e48e91 | |||
| 70a70161e7 | |||
| e690991a6a | |||
| a621a14028 | |||
| 9754cc7521 | |||
| aa268937a7 | |||
| 3db6f0a643 | |||
| 635a7fc72c | |||
| 6cd5ec10f0 | |||
| 0a276fbf1d | |||
| d593131cc1 | |||
| 3c3dcb44a9 | |||
| 6ecd1d6ea2 | |||
| 1a931a2a08 | |||
| bf705fcd99 | |||
| bb46346767 | |||
| cd0762b436 | |||
| ca920a0091 | |||
| e5b1ba24d8 | |||
| eaf912bed7 | |||
| fd0fb5be00 | |||
| f9f730ab7b | |||
| b30b72fc36 | |||
| ab08e84bff | |||
| cd9cac1246 | |||
| f6369f9750 | |||
| f18c73e59f | |||
| 93d0a7dd46 | |||
| b1d022b583 | |||
| 5382e824b1 | |||
| 15a3f59744 | |||
| 4a90ec7ff0 | |||
| 9f9b6b174e | |||
| 545ceefc3d | |||
| 591508e769 | |||
| d9cbd23cd9 | |||
| f4cd111cc8 | |||
| ea015af741 | |||
| bfc7c81c0d | |||
| e24ff8f8aa | |||
| 9f1e64d399 | |||
| 8e1aa71436 | |||
| be2fc965b1 | |||
| 7245ccf8f9 | |||
| 1c8e1af86d | |||
| 60a6cca960 | |||
| ad55b42ccb | |||
| 50c0a4b126 | |||
| 2ca42e7470 | |||
| d381e85d94 | |||
| af7520d869 | |||
| 0095bfa61f | |||
| 1b12cf9b91 | |||
| dbfc7e4acd | |||
| e838e879c0 | |||
| ee63f6bb0f | |||
| 16bbb8bc9f | |||
| 4a5db0c283 | |||
| cc50a2e3bb | |||
| 1e2b204fdd | |||
| 4a889b7b6b | |||
| 34695883c2 | |||
| 3d2e2a1873 | |||
| af6aca83a2 | |||
| 8c8ffb6a6b | |||
| 63a8fcd7e2 | |||
| e6a5934162 | |||
| 5524c2e323 | |||
| 35c1de26b8 | |||
| d1cf6b1b8d | |||
| bfa2c681cd | |||
| 609c108607 | |||
| a56939f13a | |||
| 5ff501f8b8 | |||
| 60917bb102 | |||
| b201ef8162 | |||
| 9cacc78ad7 | |||
| 0d933b8570 | |||
| 72f6daa695 | |||
| 567d78a9ac | |||
| f013c57050 | |||
| b9fae87273 | |||
| 1f0457f69b | |||
| c481a73a76 | |||
| e64e415b3b | |||
| aa406b53e2 | |||
| 57027bd0fa | |||
| 671c662fee | |||
| 0ae70b1d63 | |||
| 2701a7d861 | |||
| 3ebe985c3c | |||
| 1c0d4b5832 | |||
| 2840794fa2 | |||
| 710f42d4e4 | |||
| 45a2b0122d | |||
| 4c41f62364 | |||
| c2ab9774fb | |||
| 22e48e3be0 | |||
| 1ff3c32fe4 | |||
| 9a3e1ed99e | |||
| 438e2fbc2e | |||
| b3db085745 | |||
| b948c59c6d | |||
| 6426fe98f9 | |||
| 4dd9de349e | |||
| e1bcea77b5 | |||
| 2a19aeb6a3 | |||
| b53a34b7d1 | |||
| f881640d3d | |||
| 54c3dd76e4 | |||
| 7b887447da | |||
| 438b21df3f | |||
| d1094f647a | |||
| 49baf0de6e | |||
| b14d069ee7 | |||
| e14a4fe338 | |||
| 1f9ec46236 | |||
| 1464a3631b | |||
| 7d3c374b78 | |||
| 1dd257f33c | |||
| 1206afe6be | |||
| 5842ed51b0 | |||
| d22219f084 | |||
| 0be1a54e03 | |||
| 32921ecc20 | |||
| 1c290c1172 | |||
| 2f9d7ea58f | |||
| fe23e76341 | |||
| 73023d4869 | |||
| ff51443b8e | |||
| f8e07f6d30 | |||
| 00da778753 | |||
| 187eb922c0 | |||
| 466c12a0e6 | |||
| 8dc3ec94bf | |||
| a554436d03 | |||
| 34b5049dee | |||
| 0eccfd08fe | |||
| 98b063fd70 | |||
| e9ba94ae55 | |||
| 4d72a4f240 | |||
| a08deb895d | |||
| 60cea14b01 | |||
| f60904b740 | |||
| 376f801541 | |||
| 26f14180e2 | |||
| bced9f3d04 | |||
| 94208d9cbb | |||
| 681983142d | |||
| a525b42537 | |||
| a4cc085b6d | |||
| 837dacf1cb | |||
| 7cdd1aa9a1 | |||
| 24c569e6d5 | |||
| 050694414c | |||
| 639908366a | |||
| 59f401303a | |||
| 649e20b956 | |||
| c2280ff1bb | |||
| 8a32d823cd | |||
| 13b31458da | |||
| 92b3ab32e2 | |||
| 4ca0a157e3 | |||
| 1dec8df715 | |||
| 8f29cfc996 | |||
| da275e74fd | |||
| 3d9f4e028c | |||
| 13577997bc | |||
| 671965026d | |||
| bc7fe9023d | |||
| c2c7997df3 | |||
| a1d7e6c987 | |||
| a1e982aba5 | |||
| 2ce027a0a6 | |||
| 61f21511a5 | |||
| 5682577870 | |||
| 25b633cc63 | |||
| f16b267931 | |||
| 30ec952588 | |||
| 46ba77d0cc | |||
| 042e376c2b | |||
| b9f1af2ad5 | |||
| 7df0fdcc0a | |||
| 73d743a1c3 | |||
| 8758dfcedf | |||
| 0a10bbc023 | |||
| 9c1d369e94 | |||
| 9556696fa8 | |||
| 04eb720619 | |||
| 8e098e8ed6 | |||
| c836cead58 | |||
| 361ecbe90b | |||
| ae8d7d3be2 | |||
| ed4f92211c | |||
| f8804d6501 | |||
| de7c680387 | |||
| 6dd7391cdc | |||
| dfa7d5932d | |||
| 53911de00a | |||
| 1760d0d336 | |||
| 0aba9748c9 | |||
| 8ee0973dab | |||
| e04c5b1c6b | |||
| f52a15d7f6 | |||
| 147f6fced7 | |||
| d928927da8 | |||
| 29cc94d567 | |||
| 7f33f9b68e | |||
| 80ffa37773 | |||
| 2f70a7a352 | |||
| f7f8c34e1c | |||
| edc7d2b635 | |||
| 7f0b236747 | |||
| b7860a95b0 | |||
| f7495dc897 | |||
| ba506be58d | |||
| 91896942dd | |||
| dfab8c7333 | |||
| bd29e4f761 | |||
| b9591cfecd | |||
| 51320204f9 | |||
| ec0bafaa8e | |||
| eb508b7660 | |||
| 7c8fc323e1 | |||
| ff2393f5df | |||
| ab2a98426e | |||
| 84b3cc42cf | |||
| d28b854e8d | |||
| 6c8c69affa | |||
| faec7d4ad9 | |||
| 5b9abb6785 | |||
| eb4ca0a8d5 | |||
| 641adc6e5c | |||
| 10ab97e3c6 | |||
| e060130655 | |||
| 4e982f5460 | |||
| d3197e9865 | |||
| 0da44033ac | |||
| 6350d9598d | |||
| 9df6c0aede | |||
| 896efca43f | |||
| efe82b6cd0 | |||
| eb910193d7 | |||
| 40e486a9eb | |||
| e56580efb2 | |||
| 46e05f1c96 | |||
| 4a748d2ea4 | |||
| 8f388e712e | |||
| 0a5b6aa0ec | |||
| fc1ea10243 | |||
| d52177560b | |||
| 1701d5aa86 | |||
| b08e876136 | |||
| 005b727328 | |||
| b44f274e37 | |||
| b6bd0f92a7 | |||
| 78108e8d2c | |||
| c405dadc1b | |||
| 2bd0719b9a | |||
| 9544034e6f | |||
| cf47f8459a | |||
| 8f7d06a5bd | |||
| d1708fc252 | |||
| f23a26426d | |||
| 40613e0a76 | |||
| aa11229641 | |||
| 754272b436 | |||
| d509373228 | |||
| 379cd2a90f | |||
| 4c2115d365 | |||
| 94b48cb389 | |||
| 3ecb772a56 | |||
| b14d264718 | |||
| 1a778922af | |||
| 79b97c6d24 | |||
| a8e919f382 | |||
| 20b33f50fa | |||
| 9170882010 | |||
| ae36cca2ef | |||
| f5ce05c81e | |||
| 2fbf4f2c22 | |||
| 8a4fd5015a | |||
| de04ed2339 | |||
| ae263730d9 | |||
| 85418043f4 | |||
| 4fb2733321 | |||
| d9d109c9a2 | |||
| 4ee39282ca | |||
| 3b3fd8a6c9 | |||
| 29111a5c10 | |||
| 4b30d0b412 | |||
| d07f396578 | |||
| 2c218c4463 | |||
| af83a8249e |
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2022": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"root": true,
|
||||
"rules": {
|
||||
"no-restricted-globals": ["error", "event", "self"],
|
||||
"no-const-assign": ["error"],
|
||||
"no-debugger": ["error"],
|
||||
"no-dupe-class-members": ["error"],
|
||||
"no-dupe-keys": ["error"],
|
||||
"no-dupe-args": ["error"],
|
||||
"no-dupe-else-if": ["error"],
|
||||
"no-unsafe-negation": ["error"],
|
||||
"no-duplicate-imports": ["error"],
|
||||
"valid-typeof": ["error"],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false, "caughtErrors": "all" }],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "MemberExpression[object.name='test'][property.name='only']",
|
||||
"message": "test.only(...) is forbidden",
|
||||
},
|
||||
{
|
||||
"selector": "MemberExpression[object.name='describe'][property.name='only']",
|
||||
"message": "describe.only(...) is forbidden",
|
||||
}
|
||||
],
|
||||
},
|
||||
"globals": {
|
||||
"describe": true,
|
||||
"expect": true,
|
||||
"test": true,
|
||||
"beforeEach": true,
|
||||
"beforeAll": true,
|
||||
"afterEach": true,
|
||||
"afterAll": true,
|
||||
"jest": true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run test
|
||||
- run: npm run check-formatting
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
+16
-2
@@ -1,5 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
|
||||
npm-debug.log
|
||||
|
||||
# misc
|
||||
@@ -13,5 +14,18 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
package-lock.json
|
||||
.vscode
|
||||
#ide's
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
node_modules
|
||||
|
||||
# Extras temp file
|
||||
/tools/owl.js
|
||||
|
||||
release-notes.md
|
||||
|
||||
.rpt2_cache
|
||||
|
||||
# useful in some cases
|
||||
/temp
|
||||
+766
@@ -0,0 +1,766 @@
|
||||
# Changelog
|
||||
|
||||
This document contains an overview of all changes between Owl 1.x and
|
||||
Owl 2.x, with some pointers on how to update the code.
|
||||
|
||||
Note that some of these changes can be magically implemented (for example, by
|
||||
patching the `setup` method of `Component` to auto register all the lifecycle
|
||||
methods as hooks). This will be done for the transition period, but will be
|
||||
removed after.
|
||||
|
||||
## From Owl 1.x to Owl 2.0
|
||||
|
||||
All changes are documented here in no particular order.
|
||||
|
||||
**Components**
|
||||
|
||||
- components can now have empty content or multiple root nodes (htmlelement or text) ([details](#31-components-can-now-have-arbitrary-content))
|
||||
- breaking: component.el is removed ([details](#9-componentel-is-removed))
|
||||
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||
- new `onWillDestroy`, `onWillRender` and `onRendered` hooks ([doc](doc/reference/component.md#lifecycle))
|
||||
- breaking: lifecycle methods are removed ([details](#1-component-lifecycle-methods-are-removed))
|
||||
- breaking: can no longer be mounted on detached DOM ([details](#2-components-can-no-longer-be-mounted-in-a-detached-dom-element))
|
||||
- breaking: standalone `mount` method API is simpler ([details](#4-mount-method-api-is-simpler))
|
||||
- breaking: components can no longer be instantiated and mounted by hand ([details](#5-components-can-no-longer-be-instantiated-and-mounted-by-hand))
|
||||
- breaking: components can no longer be unmounted/remounted ([details](#6-components-can-no-longer-be-unmountedremounted))
|
||||
- breaking: template name is no longer inferred from the class name ([details](#7-template-name-is-no-longer-inferred-from-the-class-name))
|
||||
- breaking: components no longer have a `shouldUpdate` method ([details](#8-components-no-longer-have-a-shouldupdate-method))
|
||||
- breaking: components can no longer be mounted with position=self ([details](#11-components-can-no-longer-be-mounted-with-positionself))
|
||||
- breaking: `render` method does not return a promise anymore ([details](#35-render-method-does-not-return-a-promise-anymore))
|
||||
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
|
||||
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
|
||||
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
|
||||
- breaking: prop validation system does not allow default prop on a mandatory (not optional) prop ([doc](doc/reference/props.md#props-validation))
|
||||
- breaking: rendering a component does not necessarily render child components ([details](#40-rendering-a-component-does-not-necessarily-render-child-components))
|
||||
|
||||
|
||||
|
||||
**Templates**
|
||||
|
||||
- breaking: `t-foreach` should always have a corresponding `t-key` ([details](#20-t-foreach-should-always-have-a-corresponding-t-key))
|
||||
- breaking: `t-ref` does not work on components ([details](#29-t-ref-does-not-work-on-component))
|
||||
- breaking: `t-raw` directive has been removed (replaced by `t-out`) ([details](#38-t-raw-directive-has-been-removed-replaced-by-t-out))
|
||||
- new: add support for synthetic events ([doc](doc/reference/event_handling.md#synthetic-events))
|
||||
- breaking: style/class on components are now regular props ([details](#10-styleclass-on-components-are-now-regular-props))
|
||||
- new: components can use the `.bind` suffix to bind function props ([doc](doc/reference/props.md#binding-function-props))
|
||||
- breaking: `t-on` does not accept expressions, only functions ([details](#30-t-on-does-not-accept-expressions-only-functions))
|
||||
- new: an error is thrown if an handler defined in a `t-on-` directive is not a function (failed silently previously in some cases)
|
||||
- breaking: `t-component` no longer accepts strings ([details](#17-t-component-no-longer-accepts-strings))
|
||||
- new: the `this` variable in template expressions is now bound to the component
|
||||
|
||||
|
||||
**Reactivity**
|
||||
|
||||
- finer grained reactivity: owl 2 tracks change per key/component
|
||||
- finer grained reactivity: sub components can reobserve state ([doc](doc/reference/reactivity.md))
|
||||
- new: `reactive` function: create reactive state (without being linked to a component) ([doc](doc/reference/reactivity.md#reactive))
|
||||
- new: `markRaw` function: mark an object or array so that it is ignored by the reactivity system ([doc](doc/reference/reactivity.md#markraw))
|
||||
- new: `toRaw` function: given a reactive objet, return the raw (non reactive) underlying object ([doc](doc/reference/reactivity.md#toraw))
|
||||
|
||||
|
||||
**Slots**
|
||||
|
||||
- breaking: `t-set` does not define a slot any more ([details](#3-t-set-will-no-longer-work-to-define-a-slot))
|
||||
- slots capabilities have been improved ([doc](doc/reference/slots.md))
|
||||
- params can be give to slot content (to pass information from slot owner to slot user)
|
||||
- slots are given as a `prop` (and can be manipulated/propagated to sub components )
|
||||
- slots can define scopes (to pass information from slot user to slot owner)
|
||||
|
||||
|
||||
**Portal**
|
||||
|
||||
- Portal are now defined with `t-portal` ([details](#33-portal-are-now-defined-with-t-portal))
|
||||
- portals can now have arbitrary content (no longer restricted to one single child)
|
||||
- breaking: does no longer transfer dom events ([details](#13-portal-does-no-longer-transfer-dom-events))
|
||||
- breaking: does render as an empty text node instead of `<portal/>` ([details](#14-portal-does-render-as-an-empty-text-node-instead-of-portal))
|
||||
|
||||
|
||||
**Miscellaneous**
|
||||
|
||||
- improved performance
|
||||
- much simpler code
|
||||
- new App class to encapsulate a root Owl component (with the config for that application) ([doc](doc/reference/app.md))
|
||||
- new `useEffect` hook ([doc](doc/reference/hooks.md#useeffect))
|
||||
- breaking: `Context` is removed ([details](#15-context-is-removed))
|
||||
- breaking: `env` is now totally empty ([details](#16-env-is-now-totally-empty))
|
||||
- breaking: `env` is now frozen ([details](#28-env-is-now-frozen))
|
||||
- new hook: `useChildSubEnv` (only applies to child components) ([details](#27-usechildsubenv-only-applies-to-child-components))
|
||||
- breaking: most exports are exported at top level ([details](#18-most-exports-are-exported-at-top-level))
|
||||
- breaking: properties are no longer set as attributes ([details](#19-properties-are-no-longer-set-as-attributes))
|
||||
- breaking: `EventBus` api changed: it is now an `EventTarget` ([details](#21-eventbus-api-changed-it-is-now-an-eventtarget))
|
||||
- breaking: `Store` is removed ([details](#22-store-is-removed))
|
||||
- breaking: `Router` is removed ([details](#23-router-is-removed))
|
||||
- breaking: transition system is removed ([details](#24-transition-system-is-removed))
|
||||
- breaking: no more global components or templates ([details](#25-no-more-global-components-or-templates))
|
||||
- breaking: `AsyncRoot` utility component is removed ([details](#26-asyncroot-utility-component-is-removed))
|
||||
- breaking: `renderToString` function on qweb has been removed ([details](#32-rendertostring-on-qweb-has-been-removed))
|
||||
- breaking: `debounce` utility function has been removed ([details](#34-debounce-utility-function-has-been-removed))
|
||||
- breaking: `browser` object has been removed ([details](#39-browser-object-has-been-removed))
|
||||
|
||||
## Details/Rationale/Migration
|
||||
|
||||
All changes are listed in no particular order.
|
||||
|
||||
### 1. component lifecycle methods are removed
|
||||
|
||||
There was two ways to define hooks: the component methods (`willStart`, `mounted`, ...) and the hooks (`onWillStart`, `onMounted`, ...). In Owl 2, the component methods have been removed.
|
||||
|
||||
Rationale: it makes the implementation simpler and slightly faster. Hooks are more composable
|
||||
than component methods. It enforces a single entry point to check all the useful lifecycle
|
||||
calls (instead of it being scattered in the component definition). It feels more "modern".
|
||||
|
||||
Migration: lifecycle methods should be defined in the `setup`:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
mounted() {
|
||||
// do something
|
||||
}
|
||||
}
|
||||
```
|
||||
should become:
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Documentation: [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||
|
||||
### 2. components can no longer be mounted in a detached dom element
|
||||
|
||||
Nor document fragment.
|
||||
|
||||
Rationale: it is actually very difficult to do it: this implies that a component
|
||||
can be mounted more than once, that we need to check every time different status,
|
||||
that some elements is in the dom, and was a cause for bugs. Also, we don't use it
|
||||
in practice. Removing this means that we have a much simpler mental model of what
|
||||
happens.
|
||||
|
||||
Migration: well, not really easy. The code needs to be refactored in a different way.
|
||||
|
||||
|
||||
### 3. **`t-set` will no longer work to define a slot**
|
||||
|
||||
The `t-set` directive cannot define a slot anymore. Only the `t-set-slot` directive
|
||||
can do it.
|
||||
|
||||
Rationale: it was left for compatibility reason, but was deprecated anyway.
|
||||
|
||||
Migration: `t-set` should be changed to `t-set-slot` (when defining a slot)
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
<SideBar><t t-set="content">content</t></SideBar>
|
||||
```
|
||||
should become:
|
||||
```xml
|
||||
<SideBar><t t-set-slot="content">content</t></SideBar>
|
||||
```
|
||||
|
||||
### 4. `mount` method API is simpler
|
||||
|
||||
Before, the `mount` method was used like this:
|
||||
|
||||
```js
|
||||
await mount(Root, { target: document.body });
|
||||
```
|
||||
|
||||
It is now simpler and takes the root component and a target argument:
|
||||
|
||||
```js
|
||||
await mount(Root, document.body);
|
||||
```
|
||||
|
||||
Rationale: the `mount` method is only useful anyway for small toy examples,
|
||||
because real applications will need to configure the templates, the translations,
|
||||
and other stuff. All complex usecases need to go through the new `App` class,
|
||||
that encapsulates the root of an owl application.
|
||||
|
||||
Documentation: [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
|
||||
### 5. components can no longer be instantiated and mounted by hand
|
||||
|
||||
In Owl 1, it was possible to instantiate a component by hand:
|
||||
|
||||
```js
|
||||
const root = new Root();
|
||||
await root.mount(document.body);
|
||||
```
|
||||
|
||||
Now, it is no longer possible. All component instantiations should be done by
|
||||
the owl framework itself.
|
||||
|
||||
Rationale: the `mount` method does not make sense for all non root components.
|
||||
Also, the fact that it was possible for a component to be sometimes root,
|
||||
sometimes a child made for a weird constructor signature. This changes makes it
|
||||
simpler.
|
||||
|
||||
Migration: all code doing that should use either the `mount` method (if the use
|
||||
case is simple enough, or the `App` class):
|
||||
|
||||
```js
|
||||
const app = new App(Root);
|
||||
app.configure({ templates: ..., ...});
|
||||
await app.mount(document.body);
|
||||
```
|
||||
|
||||
### 6. components can no longer be unmounted/remounted
|
||||
|
||||
Rationale: this is a very difficult feature to implement (it adds a lot of possible
|
||||
state transitions), compared to its benefit.
|
||||
|
||||
Migration: all code using it should find a way to export and reimport the state
|
||||
|
||||
### 7. template name is no longer inferred from the class name
|
||||
|
||||
Before, it was possible to define a component without specifying its template:
|
||||
|
||||
```js
|
||||
class Blabla extends Component {
|
||||
// no static template here!
|
||||
}
|
||||
```
|
||||
with the `Blabla` template. It also worked with subclasses. But then, this means
|
||||
that the code had to look up all the super classes names to find the correct
|
||||
template.
|
||||
|
||||
Rationale: in practice, it is not really useful, since all templates are usually
|
||||
namespaced: `web.SomeComponent` anyway. All the trouble to do that was just not
|
||||
worth it.
|
||||
|
||||
Migration: simply explicitely defines the template key everytime:
|
||||
|
||||
```js
|
||||
class Blabla extends Component {}
|
||||
Blabla.template = "Blabla";
|
||||
```
|
||||
|
||||
### 8. components no longer have a `shouldUpdate` method
|
||||
|
||||
Rationale: `shouldUpdate` is a dangerous method to use, that may cause a lot of
|
||||
issues. Vue does not have such a mechanism (see https://github.com/vuejs/vue/issues/4255),
|
||||
because the reactivity system in Vue is smart enough to only rerender the minimal
|
||||
subset of components that is subscribed to a piece of state. Now, Owl 2 features
|
||||
a much more powerful reactivity system, so the same rationale applies: in a way,
|
||||
it's like each Owl 2 component has a `shouldUpdate` method that precisely tracks
|
||||
every value used by the component.
|
||||
|
||||
Migration code: remove the `shouldUpdate` methods, and it should work as well
|
||||
as before.
|
||||
|
||||
### 9. component.el is removed
|
||||
|
||||
This comes from the fact that Owl 2 supports fragments (arbitrary content).
|
||||
|
||||
Migration: if one need a reference to the root htmlelement of a template, it is
|
||||
suggested to simply add a `ref` on it, and access the reference as needed.
|
||||
|
||||
Documentation: [Refs](doc/reference/refs.md)
|
||||
|
||||
### 10. style/class on components are now regular props
|
||||
|
||||
Before, it was possible to do this in a template:
|
||||
|
||||
```xml
|
||||
<Child style="..." class="..."/>
|
||||
```
|
||||
(or with `t-att-style` and `t-att-class`). This does no longer work, as they are
|
||||
now considered normal props.
|
||||
|
||||
Rationale: with the move to fragments, the semantics of where the style/class
|
||||
attribute should be set is unclear. Also, it is actually very hard to implement
|
||||
properly, in particular with higher order components. And another issue is that
|
||||
it (slightly) breaks the encapsulation of behaviour from the `Child` component
|
||||
perspective.
|
||||
|
||||
Migration: each component that wishes to be customized should explicitely add
|
||||
the `class` and `style` attributes in its template. Also, the parent component
|
||||
should be aware that since we are talking about props, it should be a javascript expression:
|
||||
|
||||
In parent:
|
||||
|
||||
```xml
|
||||
<Child class="'o_my_god'"/>
|
||||
```
|
||||
and in child:
|
||||
|
||||
```xml
|
||||
<div t-att-class="props.class">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### 11. components can no longer be mounted with position=self
|
||||
|
||||
Rationale: this is due to the implementation of owl 2 virtual dom. The hack
|
||||
necessary to support position=self does not work. This position also is not
|
||||
compatible with the fact that a component can have a root `<div>` then later,
|
||||
change it to something else, or even a text node.
|
||||
|
||||
Migration: no real way to do the same. Owl application needs to be appended or
|
||||
prepended in something, maybe a `div`. Remember that you the root component
|
||||
can have multiple roots
|
||||
|
||||
Documentation:
|
||||
- [Fragments](doc/reference/templates.md#fragments)
|
||||
- [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
|
||||
|
||||
### 13. Portal does no longer transfer DOM events
|
||||
|
||||
In Owl 1, a Portal component would listen to events emitted on its portalled
|
||||
child, and redispatch them on itself. It no longer works.
|
||||
|
||||
Rationale: Portal now supports an arbitrary content (so, more than one child,
|
||||
and potentially no html element), so it is already unclear what it should listen
|
||||
to. Also, redispatching events was an hack. And this changes allows the portal
|
||||
to render itself as a text node, which is nice. This is also in line with the
|
||||
fact that modern Owl moves toward using callback instead of `t-on` for communication.
|
||||
|
||||
Migration: use callback if possible to communicate. Otherwise, use a sub env.
|
||||
|
||||
### 14. Portal does render as an empty text node instead of `<portal/>`
|
||||
|
||||
That is pretty nice. No real migration needed.
|
||||
|
||||
### 15. Context is removed
|
||||
|
||||
Context was an abstraction in Owl that was used to define some reactive state
|
||||
and to let some components subscribe to it, then only them would be rerendered
|
||||
if the context was updated. This has been removed.
|
||||
|
||||
Rationale: first, the Context api and code was kind of awkward, which is a sign
|
||||
that the abstraction is not well thought. But the good news is that it is actually
|
||||
completely replaced by the new reactivity system, which is even more powerful,
|
||||
since it can tracks changes key by key.
|
||||
|
||||
Migration: replace all uses of Context with the new reactivity system.
|
||||
|
||||
```js
|
||||
// somewhere, maybe in a service, or in the global env
|
||||
const context = observe({some: "state"})
|
||||
|
||||
// in a component that would previously get a reference to the context:
|
||||
|
||||
setup() {
|
||||
this.context = useState(context);
|
||||
// now the component is subscribed to the context and will react to any
|
||||
// change for any key read by the component, and only those changes
|
||||
}
|
||||
```
|
||||
|
||||
### 16. `env` is now totally empty
|
||||
|
||||
In Owl 1, the `env` object had to contain a QWeb instance. This was the way
|
||||
components would get a reference to their template function. It no longer works
|
||||
that way: the `env` object is now totally empty (from the perspective of Owl).
|
||||
It is now a user space concept, useful for the application.
|
||||
|
||||
Rationale: first, there is no longer a QWeb class. Also, this changes simplifies
|
||||
the way components works internally.
|
||||
|
||||
Migration: there is no proper way to get an equivalent. The closest is to get
|
||||
a reference to the root App using `this.__owl__.app`. If you need to do this,
|
||||
let us know. If this is a legitimate usecase, we may add a `useApp` hook.
|
||||
|
||||
Documentation: [Environment](doc/reference/environment.md)
|
||||
|
||||
### 17. `t-component` no longer accepts strings
|
||||
|
||||
In owl 1, we could write this:
|
||||
|
||||
```xml
|
||||
<t t-component="Coucou"/>
|
||||
```
|
||||
|
||||
This meant that Owl would look for the component class like this: `components["Coucou"]`,
|
||||
so, essentially equivalent to `<Coucou/>`. In Owl 2, the `t-component` directive
|
||||
is assumed to be an expression evaluating to a component class:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
static template = xml`<t t-component="Child"/>`;
|
||||
Child = Child;
|
||||
}
|
||||
```
|
||||
|
||||
Rationale: it simply seems more consistent with the way directive works. Also,
|
||||
the implementation is slightly simpler.
|
||||
|
||||
Migration: simply using `constructor.components.Coucou` instead of `Coucou` will
|
||||
do the trick.
|
||||
|
||||
Documentation: [Component](doc/reference/component.md#dynamic-sub-components)
|
||||
|
||||
### 18. most exports are exported at top level
|
||||
|
||||
Most exports are flattened: for ex, `onMounted` is in owl, not in `owl.hooks`.
|
||||
|
||||
Rationale: this makes it easier to work with, instead of importing stuff from
|
||||
`owl`, then `owl.hooks` and `owl.tags` for example.
|
||||
|
||||
Migration: all import code simply need to be slightly adapted.
|
||||
|
||||
### 19. Properties are no longer set as attributes
|
||||
|
||||
Formerly, html properties `<input type="checkbox" t-att-checked="blah"/>` were
|
||||
set as property and as attribute, so, they would be visible in the DOM:
|
||||
`<input type="checkbox" checked="blah"/>`. Now, they are treated as property only:
|
||||
`<input type="checkbox"/>`.
|
||||
|
||||
Rationale: this is actually simple to do, is faster, and makes more sense to me.
|
||||
|
||||
### 20. `t-foreach` should always have a corresponding `t-key`
|
||||
|
||||
It was possible in Owl 1 to write a `t-foreach` without a `t-key`. In that case,
|
||||
the index was used as key. Since it was clearly a possible bug, Owl 1 had a
|
||||
warning in some cases, when it could detect that there was definitely not a `t-key`.
|
||||
However, this was imperfect, and in some cases no warning was displayed. In Owl 2,
|
||||
the tag with a `t-foreach` has to have a corresponding `t-key`.
|
||||
|
||||
Rationale: this makes it easier to avoid bugs.
|
||||
|
||||
Migration: simply move the `t-key` to the tag with the `t-foreach`. If this is
|
||||
a situation where there is really not a need for a `t-key`, you can still add
|
||||
it with the `_index` suffix:
|
||||
|
||||
```xml
|
||||
<div t-foreach="items" t-as="item" t-key="item_index">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### 21. `EventBus` api changed: it is now an `EventTarget`
|
||||
|
||||
In Owl 1, the `EventBus` class was done manually, with a custom API. In Owl 2,
|
||||
it simply extends `EventTarget` (the native Dom class), so its implementation
|
||||
is basically only 5 lines long. This means that it has now the usual DOM interface:
|
||||
|
||||
```js
|
||||
bus.addEventListener('event-name', callback);
|
||||
```
|
||||
|
||||
Rationale: it makes it easier to have just one interface to remember, it makes
|
||||
the code simpler
|
||||
|
||||
Migration: most bus methods need to be adapted. So, `bus.on("event-type", owner, (info) => {...})` has to be
|
||||
rewritten like this: `bus.addEventListener("event-type", (({detail: info}) => {...}).bind(owner))`.
|
||||
|
||||
Do not forget to similarly replace `bus.off(...)` by `bus.removeEventListener(...)`
|
||||
|
||||
Documentation: [EventBus](doc/reference/utils.md#eventbus)
|
||||
|
||||
### 22. `Store` is removed
|
||||
|
||||
The Store system had been abandoned in owl 2.
|
||||
|
||||
Rationale: first, it was complicated to maintain. Second, it was not really
|
||||
used in Odoo. Finally, the new reactivity system seems to be a pretty good basis
|
||||
to write a store, and it should not take much work. Also, this can be done in
|
||||
user space (so, not necessarily at the framework level). Another point is that
|
||||
the store API was invented before the hooks, then was still a little awkward.
|
||||
|
||||
Migration:
|
||||
- rewrite the code not to use a store
|
||||
- probably use the reactivity system instead and build a store class and a few
|
||||
hooks on top of it.
|
||||
|
||||
### 23. `Router` is removed
|
||||
|
||||
Rationale: Router was not used that much, and it felt like it did not fit in Owl 2.
|
||||
Its API needs to be reworked, and we are not confident that it is a good
|
||||
experience to use it. Also, it can be done in userspace (it does not need specific
|
||||
integration at the framework level)
|
||||
|
||||
Migration: reimport all missing piece from the code in Owl 1.
|
||||
|
||||
### 24. transition system is removed
|
||||
|
||||
Rationale: this was a high ratio cost/value, with a lot of potential for bugs.
|
||||
We feel like there should be a way to reimplement in userspace the simple cases.
|
||||
|
||||
Maybe something like: add a `t-ref` in the template, and define a hook `useFadeOut`
|
||||
that takes the ref, and add a fadeout class at initial render, then in mounted,
|
||||
wait for a micro tick and remove it.
|
||||
|
||||
Migration: try to reimplement it manually.
|
||||
|
||||
### 25. no more global components or templates
|
||||
|
||||
It was possible in Owl 1 to register globally a component or a template. This is
|
||||
no longer the case in Owl 2.
|
||||
|
||||
Rationale: first, this was a tradeoff: ease of use was gained, but at the cost
|
||||
of a higher complexity. Users had to know that there was a magic mechanism. Also,
|
||||
it was not used much in practice, and the cost of having to import manually components
|
||||
is low. Finally, this can be mostly done in user space (for example, by subclassing
|
||||
`Component`).
|
||||
|
||||
Migration: import manually all required global components, or find a way to organize
|
||||
the code to do it.
|
||||
|
||||
### 26. `AsyncRoot` utility component is removed
|
||||
|
||||
Rationale: it was difficult to understand, never used, and not really useful.
|
||||
It seems better to control the asynchrony of an application by simply controlling
|
||||
how/when the state is updated, and how each component is loading/updating itself.
|
||||
|
||||
Migration: remove the `AsyncRoot` component, then possibly, reorganize the code
|
||||
to fetch data in a higher order component, and using a `t-if/t-else` to display
|
||||
either a fallback when the data is not ready, or the actual component with data
|
||||
as props. If there is no escape, and `AsyncRoot` is needed, please reach out to
|
||||
us so we can study this usecase.
|
||||
|
||||
### 27. `useChildSubEnv` (only applies to child components)
|
||||
|
||||
In Owl, a call to `useSubEnv` would define a new environment for the children
|
||||
AND the component. It is very useful, but in some cases, one only need to update
|
||||
the children component environment. This can now be done with a new hook:
|
||||
[`useChildSubEnv`](doc/reference/hooks.md#usesubenv-and-usechildsubenv)
|
||||
|
||||
### 28. `env` is now frozen
|
||||
|
||||
In Owl 2, the `env` object is frozen. It can no longer be modified (structurally)
|
||||
arbitrarily.
|
||||
|
||||
Rationale: it seems like the `env` object purpose is to have a global channel of
|
||||
communication between components. It is however scary if anyone can add something
|
||||
to it. The usual use case is to add something to the environment for some child
|
||||
components. This use case still works with `useSubEnv`.
|
||||
|
||||
Migration: use `useSubEnv` instead of writing directly to the env. Also, note
|
||||
that the environment given to the App can initially contain anything.
|
||||
|
||||
Documentation: [Environment](doc/reference/environment.md)
|
||||
|
||||
### 29. `t-ref` does not work on component
|
||||
|
||||
Before, `t-ref` could be used to get a reference to a child component. It no
|
||||
longer works.
|
||||
|
||||
Rationale: the possibility of having a ref to a child component breaks the
|
||||
encapsulation provided by Owl components: a child component now has a private
|
||||
and a public interface. Another issue is that it may be unclear when the ref
|
||||
should be set: is the component active on setup, or on mounted? Also, it is
|
||||
kind of awkward to implement.
|
||||
|
||||
Migration: the `env` and `props` should provide a communication channel wide enough:
|
||||
the sub component can expose its public API by calling a callback at the proper
|
||||
timing, or by triggering an event.
|
||||
|
||||
### 30. `t-on` does not accept expressions, only functions
|
||||
|
||||
In Owl 1, it was possible to define simple expressions inline, in a template:
|
||||
|
||||
```xml
|
||||
<button t-on-click="state.value = state.value + 1">blabla</button>
|
||||
<button t-on-click="someFunction(someVar)">blabla</button>
|
||||
```
|
||||
|
||||
This does not work anymore. Now, the `t-on` directive assumes that what it get is
|
||||
a function.
|
||||
|
||||
Rationale: the fact that owl 1 had to support expressions meant that it was not
|
||||
possible to properly inject the event in general. With this restriction, Owl 2
|
||||
can support more general use cases. Also, the examples above can simply be
|
||||
wrapped in a lambda function.
|
||||
|
||||
Migration: use lambda functions. For example, the two examples above can be
|
||||
adapted like this:
|
||||
|
||||
```xml
|
||||
<button t-on-click="() => state.value = state.value + 1">blabla</button>
|
||||
<button t-on-click="() => this.someFunction(someVar)">blabla</button>
|
||||
```
|
||||
|
||||
Documentation: [Event Handling](doc/reference/event_handling.md)
|
||||
|
||||
### 31. components can now have arbitrary content
|
||||
|
||||
Before Owl 2, components had to limit themselves to one single htmlelement as
|
||||
root. Now, the content is arbitrary: it can be empty, or multiple html elements.
|
||||
So, the following template works for components:
|
||||
|
||||
```xml
|
||||
<div>1</div>
|
||||
<div>2</div>
|
||||
hello
|
||||
```
|
||||
|
||||
Documentation: [Fragments](doc/reference/templates.md#fragments)
|
||||
|
||||
### 32. `renderToString` on QWeb has been removed
|
||||
|
||||
Rationale: the `renderToString` function was a qweb method, which made sense because
|
||||
the qweb instance knew all templates. But now, the closest analogy is the `App`
|
||||
class, but it is not as convenient, since the `app` instance is no longer visible
|
||||
to components (while before, `qweb` was in the environment).
|
||||
|
||||
Also, this can easily be done in userspace, by mounting a component in a div. For example:
|
||||
|
||||
```js
|
||||
export async function renderToString(template, context) {
|
||||
class C extends Component {
|
||||
static template = template;
|
||||
setup () {
|
||||
Object.assign(this, context);
|
||||
}
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
const app = new App(C);
|
||||
await app.mount(div);
|
||||
const result = div.innerHTML;
|
||||
app.destroy();
|
||||
div.remove();
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
The function above works for most cases, but is asynchronous. An alternative
|
||||
function could look like this:
|
||||
|
||||
```js
|
||||
const { App, blockDom } = owl;
|
||||
const app = new App(Component); // act as a template repository
|
||||
|
||||
function renderToString(template, context = {}) {
|
||||
app.addTemplate(template, template, { allowDuplicate: true });
|
||||
const templateFn = app.getTemplate(template);
|
||||
const bdom = templateFn(context, {});
|
||||
const div = document.createElement('div')
|
||||
blockDom.mount(bdom, div);
|
||||
return div.innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
This is a synchronous function, so it will not work with components, but it should
|
||||
be useful for most simple templates.
|
||||
|
||||
Also note that these two examples do not translate their templates. To do that,
|
||||
they need to be modified to pass the proper translate function to the `App`
|
||||
configuration.
|
||||
|
||||
### 33. Portal are now defined with `t-portal`
|
||||
|
||||
Before Owl 2, one could use the `Portal` component by importing it and using it.
|
||||
Now, it is no longer available. Instead, we can simply use the `t-portal` directive:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
some content
|
||||
<span t-portal="'body'">
|
||||
portalled content
|
||||
</span>
|
||||
<div>
|
||||
```
|
||||
|
||||
Rationale: it makes it slightly simpler to use (just need the directive, instead
|
||||
of having to import and use a sub component), it makes the implementation slightly
|
||||
simpler as well. Also, it prevents subclassing the Portal component, which could
|
||||
be dangerous, since it is really doing weird stuff under the hood, and could
|
||||
easily be broken inadvertendly.
|
||||
|
||||
### 34. `debounce` utility function has been removed
|
||||
|
||||
Rationale: it did not really help that much, is available as utility function
|
||||
elsewhere, so, we decided to have a smaller footprint by focusing Owl on what
|
||||
it does best.
|
||||
|
||||
### 35. `render` method does not return a promise anymore
|
||||
|
||||
Rationale: using the `render` method directly and waiting for it to complete
|
||||
was slightly un-declarative. Also, it can be done using the lifecycle hooks
|
||||
any way.
|
||||
|
||||
Migration: if necessary, one can use the lifecycle hooks to execute code after
|
||||
the next mounted/patched operation.
|
||||
|
||||
### 36. `catchError` method is replaced by `onError` hook
|
||||
|
||||
The `catchError` method was used to provide a way to components to handle errors
|
||||
occurring during the component lifecycle. This has been replaced by a `onError`
|
||||
hook, with a similar API.
|
||||
|
||||
Rationale: `catchError` felt a little big awkward, when most of the way we
|
||||
interact with componentss is via hooks. Using hooks felt more natural and
|
||||
consistent.
|
||||
|
||||
Migration: mostly replace all `catchError` methods by `onError` hooks in the
|
||||
`setup` method.
|
||||
|
||||
Documentation: [Error Handling](doc/reference/error_handling.md)
|
||||
|
||||
|
||||
## 37. Support for inline css (`css` tag and static `style`) has been removed
|
||||
|
||||
Rationale: Owl tries to focus on what it does best, and supporting inline css
|
||||
was not a priority. It used to support some simplified scss language, but it
|
||||
was feared that it would cause more trouble than it was worth. Also, it seems
|
||||
like it can be done in userspace.
|
||||
|
||||
Migration: it seems possible to implement an equivalent solution using hooks. A
|
||||
simple implementation could look like this:
|
||||
|
||||
```js
|
||||
let cache = {};
|
||||
|
||||
function useStyle(css) {
|
||||
if (!css in cache) {
|
||||
const sheet = document.createElement("style");
|
||||
sheet.innerHTML = css;
|
||||
cache[css] = sheet;
|
||||
document.head.appendChild(sheet);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 38. `t-raw` directive has been removed (replaced by `t-out`)
|
||||
|
||||
To match the Odoo qweb server implementation, Owl does no longer implement `t-raw`.
|
||||
It is replaced by the `t-out` directive, which is safer: it requires the data
|
||||
to be marked explicitely as markup if it is to be inserted without escaping.
|
||||
Otherwise, it will be escaped (just like `t-esc`).
|
||||
|
||||
Migration: replace all `t-raw` uses by `t-out`, and uses the `markup` function
|
||||
to mark all the js values.
|
||||
|
||||
Documentation: [Outputting data](doc/reference/templates.md#outputting-data)
|
||||
|
||||
## 39. `browser` object has been removed
|
||||
|
||||
Rationale: the `browser` object caused more trouble than it was worth. Also, it
|
||||
seems like this should be done in user space, not at the framework level.
|
||||
|
||||
Migration: code should just be adapted to either use another browser object,
|
||||
or to use native browser function (and then, just mock them directly).
|
||||
|
||||
## 40. Rendering a component does not necessarily render child components
|
||||
|
||||
Before, if one had the following component tree:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
```
|
||||
|
||||
when `A` would render, it would also render `B` and `C`. Now, in Owl 2, it will
|
||||
(shallow) compare the before and after props, and `B` or `C` will only be rerendered
|
||||
if their props have changed.
|
||||
|
||||
Now, the question is what happens if the props have changed, but in a deeper way?
|
||||
In that case, Owl will know, because each props are now reactive. So, if some
|
||||
inner value read by `B` was changed, then only `B` will be updated.
|
||||
|
||||
Rationale: This was just not possible in Owl 1, but it now possible. This is
|
||||
due to the rewriteof the underlying rendering engine and the reactivity
|
||||
system. The goal is to have a big performance boost in large screen with many
|
||||
components: now Owl only rerender what is strictly useful.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
Most of the files are
|
||||
|
||||
Copyright (c) 2004-2015 Odoo S.A.
|
||||
|
||||
Many files also contain contributions from third
|
||||
parties. In this case the original copyright of
|
||||
the contributions can be traced through the
|
||||
history of the source version control system.
|
||||
|
||||
When that is not the case, the files contain a prominent
|
||||
notice stating the original copyright and applicable
|
||||
license, or come with their own dedicated COPYRIGHT
|
||||
and/or LICENSE file.
|
||||
@@ -1,12 +1,9 @@
|
||||
|
||||
For copyright information, please see the COPYRIGHT file.
|
||||
|
||||
Odoo is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
|
||||
OWL is published under the GNU LESSER GENERAL PUBLIC LICENSE, Version 3
|
||||
(LGPLv3), as included below. Since the LGPL is a set of additional
|
||||
permissions on top of the GPL, the text of the GPL is included at the
|
||||
bottom as well.
|
||||
|
||||
Some external libraries and contributions bundled with Odoo may be published
|
||||
Some external libraries and contributions bundled with OWL may be published
|
||||
under other GPL-compatible licenses. For these, please refer to the relevant
|
||||
source files and/or license files, in the source code tree.
|
||||
|
||||
|
||||
@@ -1,124 +1,128 @@
|
||||
<h1 align="center">🦉 Odoo Web Lab 🦉</h1>
|
||||
<h1 align="center">🦉 <a href="https://odoo.github.io/owl/">Owl Framework</a> 🦉</h1>
|
||||
|
||||
[](https://www.gnu.org/licenses/lgpl-3.0)
|
||||
[](https://badge.fury.io/js/@odoo%2Fowl)
|
||||
[](https://www.npmjs.com/package/@odoo/owl)
|
||||
|
||||
_Class based components with hooks, reactive state and concurrent mode_
|
||||
|
||||
**Try it online!** you can experiment with the Owl framework in an online [playground](https://odoo.github.io/owl/playground).
|
||||
|
||||
## Project Overview
|
||||
|
||||
Odoo Web Lab (OWL) is a project to collect some useful, reusable, (hopefully)
|
||||
well designed building blocks for building web applications. However, since this is the basis for the Odoo web client, we will not hesitate
|
||||
to design the code here to better match the Odoo architecture/design principles.
|
||||
The Odoo Web Library (Owl) is a smallish (~<20kb gzipped) UI framework built by
|
||||
[Odoo](https://www.odoo.com/) for its products. Owl is a modern
|
||||
framework, written in Typescript, taking the best ideas from React and Vue in a
|
||||
simple and consistent way. Owl's main features are:
|
||||
|
||||
The most important element of this repository is certainly the component system.
|
||||
It is designed to be:
|
||||
- a declarative component system,
|
||||
- a fine grained reactivity system similar to Vue,
|
||||
- hooks
|
||||
- fragments
|
||||
- asynchronous rendering
|
||||
|
||||
1. **declarative:** the user interface should be described in term of the state
|
||||
of the application, not as a sequence of imperative steps.
|
||||
Owl components are defined with ES6 classes and xml templates, uses an
|
||||
underlying virtual DOM, integrates beautifully with hooks, and the rendering is
|
||||
asynchronous.
|
||||
|
||||
2. **composable:** each widget can seamlessly be created in a parent widget by
|
||||
a simple directive in its template.
|
||||
Quick links:
|
||||
|
||||
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.
|
||||
- [documentation](#documentation),
|
||||
- [changelog](CHANGELOG.md) (from Owl 1.x to 2.x),
|
||||
- [playground](https://odoo.github.io/owl/playground)
|
||||
|
||||
4. **uses QWeb as a template system:** the templates are described in XML
|
||||
and follow the QWeb specification. This is a requirement for Odoo.
|
||||
## Example
|
||||
|
||||
5. **with an imperative escape hatch:** if necessary, sub widgets can easily be
|
||||
manually created/destroyed.
|
||||
Here is a short example to illustrate interactive components:
|
||||
|
||||
Note: the code is written in typescript. This does not mean that the main web
|
||||
client will ever be converted to typescript (even though I would really like it).
|
||||
```javascript
|
||||
const { Component, useState, mount, xml } = owl;
|
||||
|
||||
## Try it online
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="() => state.value = state.value + props.increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
You can experiment with the OWL project online: [https://odoo.github.io/owl/](https://odoo.github.io/owl/)
|
||||
state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
## Installing/Building
|
||||
class Root extends Component {
|
||||
static template = xml`
|
||||
<span>Hello Owl</span>
|
||||
<Counter increment="2"/>`;
|
||||
|
||||
Some npm scripts are available:
|
||||
static components = { Counter };
|
||||
}
|
||||
|
||||
| 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 build:es5 | build a bundle of _owl_ in the _/dist/_ folder (ES5 code) |
|
||||
| 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 |
|
||||
mount(Root, 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/templates.md#inline-templates) 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.
|
||||
|
||||
## Documentation
|
||||
|
||||
The complete documentation can be found [here](doc/readme.md). The most important sections are:
|
||||
### Learning Owl
|
||||
|
||||
- [Quick Start](doc/quick_start.md)
|
||||
- [Tutorial](doc/tutorial.md)
|
||||
- [Component](doc/component.md)
|
||||
- [QWeb](doc/qweb.md)
|
||||
Are you new to Owl? This is the place to start!
|
||||
|
||||
# Examples
|
||||
- [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md)
|
||||
- [How to start an Owl project](doc/learning/quick_start.md)
|
||||
- [How to test Components](doc/learning/how_to_test.md)
|
||||
|
||||
Here is a minimal Hello World example:
|
||||
### Reference
|
||||
|
||||
```javascript
|
||||
class HelloWorld extends owl.core.Component {
|
||||
inlineTemplate = `<div>Hello <t t-esc="props.name"/></div>`;
|
||||
}
|
||||
- [Overview](doc/readme.md)
|
||||
- [App](doc/reference/app.md)
|
||||
- [Component](doc/reference/component.md)
|
||||
- [Component Lifecycle](doc/reference/component.md#lifecycle)
|
||||
- [Concurrency Model](doc/reference/concurrency_model.md)
|
||||
- [Dev mode](doc/reference/app.md#dev-mode)
|
||||
- [Dynamic sub components](doc/reference/component.md#dynamic-sub-components)
|
||||
- [Environment](doc/reference/environment.md)
|
||||
- [Error Handling](doc/reference/error_handling.md)
|
||||
- [Event Handling](doc/reference/event_handling.md)
|
||||
- [Form Input Bindings](doc/reference/input_bindings.md)
|
||||
- [Fragments](doc/reference/templates.md#fragments)
|
||||
- [Hooks](doc/reference/hooks.md)
|
||||
- [Loading Templates](doc/reference/app.md#loading-templates)
|
||||
- [Mounting a component](doc/reference/app.md#mount-helper)
|
||||
- [Portal](doc/reference/portal.md)
|
||||
- [Precompiling templates](doc/reference/precompiling_templates.md)
|
||||
- [Props](doc/reference/props.md)
|
||||
- [Props Validation](doc/reference/props.md#props-validation)
|
||||
- [Reactivity](doc/reference/reactivity.md)
|
||||
- [Rendering SVG](doc/reference/templates.md#rendering-svg)
|
||||
- [Refs](doc/reference/refs.md)
|
||||
- [Slots](doc/reference/slots.md)
|
||||
- [Sub components](doc/reference/component.md#sub-components)
|
||||
- [Sub templates](doc/reference/templates.md#sub-templates)
|
||||
- [Templates (Qweb)](doc/reference/templates.md)
|
||||
- [Translations](doc/reference/translations.md)
|
||||
- [Utils](doc/reference/utils.md)
|
||||
|
||||
const env = {
|
||||
qweb: new owl.core.QWeb()
|
||||
};
|
||||
### Other Topics
|
||||
|
||||
const hello = new HelloWorld(env, { name: "World" });
|
||||
hello.mount(document.body);
|
||||
- [Notes On Owl Architecture](doc/miscellaneous/architecture.md)
|
||||
- [Comparison with React/Vue](doc/miscellaneous/comparison.md)
|
||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
|
||||
|
||||
## Installing Owl
|
||||
|
||||
Owl is available on `npm` and can be installed with the following command:
|
||||
|
||||
```
|
||||
npm install @odoo/owl
|
||||
```
|
||||
|
||||
The next example show how interactive widgets can be created and how widget
|
||||
composition works:
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
```javascript
|
||||
class Counter extends owl.core.Component {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.value"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>`;
|
||||
- [owl](https://github.com/odoo/owl/releases/latest)
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = {
|
||||
value: props.initialState || 0
|
||||
};
|
||||
}
|
||||
|
||||
increment(delta) {
|
||||
this.updateState({ value: this.state.value + delta });
|
||||
}
|
||||
}
|
||||
|
||||
class App extends owl.core.Component {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<t t-widget="Counter" t-props="{initialState: 1}"/>
|
||||
<t t-widget="Counter" t-props="{initialState: 42}"/>
|
||||
</div>`;
|
||||
|
||||
widgets = { Counter };
|
||||
}
|
||||
|
||||
const env = {
|
||||
qweb: new owl.core.QWeb()
|
||||
};
|
||||
|
||||
const app = new App(env);
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
More interesting examples on how to work with this web framework can be found in the _examples/_ folder:
|
||||
|
||||
- [Todo Application](examples/readme.md#todo-app)
|
||||
- [Web Client](examples/readme.md#web-client-example)
|
||||
- [Benchmarks](examples/readme.md#benchmarks)
|
||||
|
||||
## License
|
||||
|
||||
OWL is [GPL licensed](./LICENSE).
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
# Component
|
||||
|
||||
Components are the reusable, composable widgets. They are designed to be low
|
||||
level, to be declarative, and with asynchronous rendering.
|
||||
|
||||
For example:
|
||||
|
||||
```javascript
|
||||
export class Counter extends Component {
|
||||
template = "counter";
|
||||
state = { counter: 0 };
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state.counter = props.initialState || 0;
|
||||
}
|
||||
|
||||
increment(delta) {
|
||||
this.state.counter += delta;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.counter"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>`;
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
A solid and robust component system needs useful hooks/methods to help
|
||||
developers write components. Here is a description of the lifecycle of a owl
|
||||
component:
|
||||
|
||||
- **[constructor](#constructor)**
|
||||
- **[willStart](#willStart)**
|
||||
- **[mounted](#mounted)**
|
||||
- **[willPatch](#willPatch)**
|
||||
- **[updated](#updated)**
|
||||
- **[willUnmount](#willUnmount)**
|
||||
|
||||
Note: no hook method should ever be called manually. They are supposed to be
|
||||
called by the owl framework whenever it is required.
|
||||
|
||||
### constructor
|
||||
|
||||
The constructor is not exactly a hook, it is the constructor of the component.
|
||||
|
||||
### 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.
|
||||
|
||||
Note that a slow willStart method will slow down the rendering of the user
|
||||
interface. Therefore, some effort should be made to make this method as
|
||||
fast as possible.
|
||||
|
||||
### mounted
|
||||
|
||||
mounted is a hook that is called each time a component is attached to the
|
||||
DOM. This is a good place to add some listeners, or to interact with the
|
||||
DOM, if the component needs to perform some measure for example.
|
||||
|
||||
It is the opposite of _willUnmount_. If a component has been mounted, it will
|
||||
be unmounted at some point.
|
||||
|
||||
### willUpdateProps
|
||||
|
||||
The willUpdateProps is an asynchronous hook, called just before new props
|
||||
are set. This is useful if the component needs some asynchronous task
|
||||
performed, depending on the props (for example, assuming that the props are
|
||||
some record Id, fetching the record data).
|
||||
|
||||
This hook is not called during the first render (but willStart is called
|
||||
and performs a similar job).
|
||||
|
||||
### 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 get some
|
||||
information which are in the DOM. For example, the current position of the
|
||||
scrollbar
|
||||
|
||||
### patched
|
||||
|
||||
This hook is called whenever a component did actually update its props,
|
||||
state or env.
|
||||
|
||||
This method is not called on the initial render. It is useful to interact
|
||||
with the DOM (for example, through an external library) whenever the
|
||||
component was patched.
|
||||
|
||||
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.
|
||||
|
||||
This is the opposite method of _mounted_. The _willUnmount_ method will be
|
||||
called in reverse order: first the children, then the parents.
|
||||
@@ -0,0 +1,95 @@
|
||||
# 🦉 How to test Components 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Unit Tests](#unit-tests)
|
||||
|
||||
## Overview
|
||||
|
||||
It is a good practice to test applications and components to ensure that they
|
||||
behave as expected. There are many ways to test a user interface: manual
|
||||
testing, integration testing, unit testing, ...
|
||||
|
||||
In this section, we will discuss how to write unit tests for components.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
Writing unit tests for Owl components really depends on the testing framework
|
||||
used in a project. But usually, it involves the following steps:
|
||||
|
||||
- create a test file: for example `SomeComponent.test.js`,
|
||||
- in that file, import the code for `SomeComponent`,
|
||||
- add a test case:
|
||||
- create a real DOM element to use as test fixture,
|
||||
- create a test environment
|
||||
- create an instance of `SomeComponent`, mount it to the fixture
|
||||
- interact with the component and assert some properties.
|
||||
|
||||
To help with this, it is useful to have a `helper.js` file that contains some
|
||||
common utility functions:
|
||||
|
||||
```js
|
||||
let lastFixture = null;
|
||||
|
||||
export function makeTestFixture() {
|
||||
let fixture = document.createElement("div");
|
||||
document.body.appendChild(fixture);
|
||||
if (lastFixture) {
|
||||
lastFixture.remove();
|
||||
}
|
||||
lastFixture = fixture;
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export async function nextTick() {
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
```
|
||||
|
||||
With such a file, a typical test suite for Jest will look like this:
|
||||
|
||||
```js
|
||||
// in SomeComponent.test.js
|
||||
import { SomeComponent } from "../../src/ui/SomeComponent";
|
||||
import { nextTick, makeTestFixture } from '../helpers';
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Setup
|
||||
//------------------------------------------------------------------------------
|
||||
let fixture: HTMLElement;
|
||||
let env: Env;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//------------------------------------------------------------------------------
|
||||
describe("SomeComponent", () => {
|
||||
test("component behaves as expected", async () => {
|
||||
const props = {...}; // depends on the component
|
||||
const comp = await mount(SomeComponent, fixture, { props });
|
||||
|
||||
// do some assertions
|
||||
expect(...).toBe(...);
|
||||
|
||||
fixture.querySelector('button').click();
|
||||
await nextTick();
|
||||
|
||||
// some other assertions
|
||||
expect(...).toBe(...);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note that Owl does wait for the next animation frame to actually update the DOM.
|
||||
This is why it is necessary to wait with the `nextTick` (or other methods) to
|
||||
make sure that the DOM is up-to-date.
|
||||
@@ -0,0 +1,399 @@
|
||||
# 🦉 How to start an Owl project 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Simple html file](#simple-html-file)
|
||||
- [With a static server](#with-a-static-server)
|
||||
- [Standard Javascript project](#standard-javascript-project)
|
||||
|
||||
## Overview
|
||||
|
||||
Each software project has its specific needs. Many of these needs can be solved
|
||||
with some tooling: `webpack`, `gulp`, css preprocessor, bundlers, transpilers, ...
|
||||
|
||||
Because of that, it is usually not simple to just start a project. Some
|
||||
frameworks provide their own tooling to help with that. But then, you have to
|
||||
integrate and learn how these applications work.
|
||||
|
||||
Owl is designed to be used with no tooling at all. Because of that, Owl can
|
||||
"easily" be integrated in a modern build toolchain. In this section, we will
|
||||
discuss a few different setups to start a project. Each of these setups has
|
||||
advantages and disadvantages in different situations.
|
||||
|
||||
## Simple html file
|
||||
|
||||
The simplest possible setup is the following: a simple javascript file with your
|
||||
code. To do that, let us create the following file structure:
|
||||
|
||||
```
|
||||
hello_owl/
|
||||
index.html
|
||||
owl.js
|
||||
app.js
|
||||
```
|
||||
|
||||
The file `owl.js` can be downloaded from the last release published at
|
||||
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases). It
|
||||
is a single javascript file which export all Owl into the global `owl` object.
|
||||
Note that there are multiple files, and in this case, we need one of the two
|
||||
files suffixed with `.iife`: they are built to be directly used in a browser.
|
||||
|
||||
Now, `index.html` should contain the following:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
<script src="owl.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
And `app.js` should look like this:
|
||||
|
||||
```js
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
// Owl Components
|
||||
class Root extends Component {
|
||||
static template = xml`<div>Hello Owl</div>`;
|
||||
}
|
||||
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
Now, simply loading this html file in a browser should display a welcome message.
|
||||
This setup is not fancy, but it is extremely simple. There are no tooling at
|
||||
all required. It can be slightly optimized by using the minified build of Owl.
|
||||
|
||||
## With a static server
|
||||
|
||||
The previous setup has a big disadvantage: the application code is located in a
|
||||
single file. Obviously, we could split it in several files and add multiple
|
||||
`<script>` tags in the html page, but then we need to make sure the script are
|
||||
inserted in the proper order, we need to export each file content in global
|
||||
variables and we lose autocompletion across files.
|
||||
|
||||
There is a low tech solution to this issue: using native javascript modules.
|
||||
This however has a requirement: for security reasons, browsers will not accept
|
||||
modules on content served through the `file` protocol. This means that we need
|
||||
to use a static server.
|
||||
|
||||
Let us start a new project with the following file structure:
|
||||
|
||||
```
|
||||
hello_owl/
|
||||
src/
|
||||
index.html
|
||||
main.js
|
||||
owl.js
|
||||
root.js
|
||||
```
|
||||
|
||||
As previously, the file `owl.js` can be downloaded from the last release published at
|
||||
[https://github.com/odoo/owl/releases](https://github.com/odoo/owl/releases).
|
||||
Note that there are multiple files, and in this case, we need one of the two
|
||||
files suffixed with `.iife`: they are built to be directly used in a browser.
|
||||
|
||||
Now, `index.html` should contain the following:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
<script src="owl.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Not that the `main.js` script tag has the `type="module"` attribute. This means
|
||||
that the browser will parse the script as a module, and load all its dependencies.
|
||||
|
||||
Here is the content of `root.js` and `main.js`:
|
||||
|
||||
```js
|
||||
// root.js ----------------------------------------------------------------------
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
export class Root extends Component {
|
||||
static template = xml`<div>Hello Owl</div>`;
|
||||
}
|
||||
|
||||
// main.js ---------------------------------------------------------------------
|
||||
import { Root } from "./root.js";
|
||||
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
The `main.js` file imports the `root.js` file. Note that the import statement has
|
||||
a `.js` suffix, which is important. Most text editor can understand this syntax
|
||||
and will provide autocompletion.
|
||||
|
||||
Now, to execute this code, we need to serve the `src` folder statically. A low
|
||||
tech way to do that is to use for example the python `SimpleHTTPServer` feature:
|
||||
|
||||
```
|
||||
$ cd src
|
||||
$ python -m SimpleHTTPServer 8022 # now content is available at localhost:8022
|
||||
```
|
||||
|
||||
Another more "javascripty" way to do it is to create a `npm` application. To do
|
||||
that, we can add the following `package.json` file at the root of the project:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "hello_owl",
|
||||
"version": "0.1.0",
|
||||
"description": "Starting Owl app",
|
||||
"main": "src/index.html",
|
||||
"scripts": {
|
||||
"serve": "serve src"
|
||||
},
|
||||
"author": "John",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"serve": "^11.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can now install the `serve` tool with the command `npm install`, and then,
|
||||
start a static server with the simple `npm run serve` command.
|
||||
|
||||
## Standard Javascript project
|
||||
|
||||
The previous setup works, and is certainly good for some usecases, including
|
||||
quick prototyping. However, it lacks some useful features, such as livereload,
|
||||
a test suite, or bundling the code in a single file.
|
||||
|
||||
Each of these features, and many others, can be done in many different ways.
|
||||
Since it is really not trivial to configure such a project, we provide here an
|
||||
example that can be used as a starting point.
|
||||
|
||||
Our standard Owl project has the following file structure:
|
||||
|
||||
```
|
||||
hello_owl/
|
||||
public/
|
||||
index.html
|
||||
src/
|
||||
components/
|
||||
Root.js
|
||||
main.js
|
||||
tests/
|
||||
components/
|
||||
Root.test.js
|
||||
helpers.js
|
||||
.gitignore
|
||||
package.json
|
||||
webpack.config.js
|
||||
```
|
||||
|
||||
This project as a `public` folder, meant to contain all static assets, such as
|
||||
images and styles. The `src` folder has the javascript source code, and finally,
|
||||
`tests` contains the test suite.
|
||||
|
||||
Here is the content of `index.html`:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello Owl</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Note that there are no `<script>` tag here. They will be injected by webpack.
|
||||
Now, let's have a look at the javascript files:
|
||||
|
||||
```js
|
||||
// src/components/Root.js -------------------------------------------------------
|
||||
import { Component, xml, useState } from "@odoo/owl";
|
||||
|
||||
export class Root extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="update">
|
||||
Hello <t t-esc="state.text"/>
|
||||
</div>`;
|
||||
|
||||
state = useState({ text: "Owl" });
|
||||
update() {
|
||||
this.state.text = this.state.text === "Owl" ? "World" : "Owl";
|
||||
}
|
||||
}
|
||||
|
||||
// src/main.js -----------------------------------------------------------------
|
||||
import { utils, mount } from "@odoo/owl";
|
||||
import { Root } from "./components/Root";
|
||||
|
||||
mount(Root, document.body);
|
||||
|
||||
// tests/components/Root.test.js ------------------------------------------------
|
||||
import { Root } from "../../src/components/Root";
|
||||
import { makeTestFixture, nextTick, click } from "../helpers";
|
||||
import { mount } from "@odoo/owl";
|
||||
|
||||
let fixture;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeTestFixture();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
describe("Root", () => {
|
||||
test("Works as expected...", async () => {
|
||||
await mount(Root, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>Hello Owl</div>");
|
||||
|
||||
click(fixture, "div");
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div>Hello World</div>");
|
||||
});
|
||||
});
|
||||
|
||||
// tests/helpers.js ------------------------------------------------------------
|
||||
import { Component } from "@odoo/owl";
|
||||
import "regenerator-runtime/runtime";
|
||||
|
||||
export async function nextTick() {
|
||||
await new Promise((resolve) => setTimeout(resolve));
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
export function makeTestFixture() {
|
||||
let fixture = document.createElement("div");
|
||||
document.body.appendChild(fixture);
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export function click(elem, selector) {
|
||||
elem.querySelector(selector).dispatchEvent(new Event("click"));
|
||||
}
|
||||
```
|
||||
|
||||
Finally, here is the configuration files `.gitignore`, `package.json` and
|
||||
`webpack.config.js`:
|
||||
|
||||
```
|
||||
node_modules/
|
||||
package-lock.json
|
||||
dist/
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "hello_owl",
|
||||
"version": "0.1.0",
|
||||
"description": "Demo app",
|
||||
"main": "src/index.html",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack-dev-server --mode development"
|
||||
},
|
||||
"author": "Someone",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"babel-jest": "^25.1.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "^25.1.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"serve": "^11.3.0",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.10.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@odoo/owl": "^1.0.4"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["transform-es2015-modules-commonjs"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"verbose": false,
|
||||
"testRegex": "(/tests/.*(test|spec))\\.js?$",
|
||||
"moduleFileExtensions": ["js"],
|
||||
"transform": {
|
||||
"^.+\\.[t|j]sx?$": "babel-jest"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
const host = process.env.HOST || "localhost";
|
||||
|
||||
module.exports = function (env, argv) {
|
||||
const mode = argv.mode || "development";
|
||||
return {
|
||||
mode: mode,
|
||||
entry: "./src/main.js",
|
||||
output: {
|
||||
filename: "main.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
loader: "babel-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".jsx"],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.resolve(__dirname, "public/index.html"),
|
||||
compress: true,
|
||||
hot: true,
|
||||
host,
|
||||
port: 3000,
|
||||
publicPath: "/",
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: path.resolve(__dirname, "public/index.html"),
|
||||
}),
|
||||
],
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
With this setup, we can now use the following script commands:
|
||||
|
||||
```
|
||||
npm run build # build the full application in prod mode in dist/
|
||||
|
||||
npm run dev # start a dev server with livereload
|
||||
|
||||
npm run test # run the jest test suite
|
||||
```
|
||||
@@ -0,0 +1,997 @@
|
||||
# 🦉 OWL Tutorial: TodoApp 🦉
|
||||
|
||||
For this tutorial, we will build a very simple Todo list application. The app
|
||||
should satisfy the following requirements:
|
||||
|
||||
- let the user create and remove tasks
|
||||
- tasks can be marked as completed
|
||||
- tasks can be filtered to display active/completed tasks
|
||||
|
||||
This project will be an opportunity to discover and learn some important Owl
|
||||
concepts, such as components, store, and how to organize an application.
|
||||
|
||||
## Content
|
||||
|
||||
1. [Setting up the project](#1-setting-up-the-project)
|
||||
2. [Adding a first component](#2-adding-a-first-component)
|
||||
3. [Displaying a list of tasks](#3-displaying-a-list-of-tasks)
|
||||
4. [Layout: some basic css](#4-layout-some-basic-css)
|
||||
5. [Extracting Task as a subcomponent](#5-extracting-task-as-a-subcomponent)
|
||||
6. [Adding tasks (part 1)](#6-adding-tasks-part-1)
|
||||
7. [Adding tasks (part 2)](#7-adding-tasks-part-2)
|
||||
8. [Toggling tasks](#8-toggling-tasks)
|
||||
9. [Deleting tasks](#9-deleting-tasks)
|
||||
10. [Using a store](#10-using-a-store)
|
||||
11. [Saving tasks in local storage](#11-saving-tasks-in-local-storage)
|
||||
12. [Filtering tasks](#12-filtering-tasks)
|
||||
13. [The Final Touch](#13-the-final-touch)
|
||||
14. [Final Code](#final-code)
|
||||
|
||||
## 1. Setting up the project
|
||||
|
||||
For this tutorial, we will do a very simple project, with static files and
|
||||
no additional tooling. The first step is to create the following file structure:
|
||||
|
||||
```
|
||||
todoapp/
|
||||
index.html
|
||||
app.css
|
||||
app.js
|
||||
owl.js
|
||||
```
|
||||
|
||||
The entry point for this application is the file `index.html`, which should have
|
||||
the following content:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OWL Todo App</title>
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="owl.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Then, `app.css` can be left empty for now. It will be useful later on to style
|
||||
our application. `app.js` is where we will write all our code. For now, let's
|
||||
just put the following code:
|
||||
|
||||
```js
|
||||
(function () {
|
||||
console.log("hello owl", owl.__info__.version);
|
||||
})();
|
||||
```
|
||||
|
||||
Note that we put everything inside an immediately executed function to avoid leaking
|
||||
anything to the global scope.
|
||||
|
||||
Finally, `owl.js` should be the last version downloaded from the Owl repository (you can use `owl.min.js` if you prefer). Be aware that you should download the `owl.iife.js` or `owl.iife.min.js`, because these files
|
||||
are built to run directly on the browser, and rename it `owl.js` (other files such as `owl.cjs.js` are
|
||||
built to be bundled by other tools).
|
||||
|
||||
Now, the project should be ready. Loading the `index.html` file into a browser
|
||||
should show an empty page, with the title `Owl Todo App`, and it should log a
|
||||
message such as `hello owl 2.x.y` in the console.
|
||||
|
||||
## 2. Adding a first component
|
||||
|
||||
An Owl application is made out of [components](../reference/component.md), with
|
||||
a single root component. Let us start by defining a `Root` component. Replace the
|
||||
content of the function in `app.js` by the following code:
|
||||
|
||||
```js
|
||||
const { Component, mount, xml } = owl;
|
||||
|
||||
// Owl Components
|
||||
class Root extends Component {
|
||||
static template = xml`<div>todo app</div>`;
|
||||
}
|
||||
|
||||
mount(Root, document.body);
|
||||
```
|
||||
|
||||
Now, reloading the page in a browser should display a message.
|
||||
|
||||
The code is pretty simple: we define a component with an inline template, then
|
||||
mount it in the document body.
|
||||
|
||||
Note 1: in a larger project, we would split the code in multiple files, with
|
||||
components in a sub folder, and a main file that would initialize the application.
|
||||
However, this is a very small project, and we want to keep it as simple as possible.
|
||||
|
||||
Note 2: this tutorial uses the static class field syntax. This is not yet
|
||||
supported by all browsers. Most real projects will transpile their code, so this
|
||||
is not a problem, but for this tutorial, if you need the code to work on every
|
||||
browser, you will need to translate each `static` keyword to an assignation to
|
||||
the class:
|
||||
|
||||
```js
|
||||
class App extends Component {}
|
||||
App.template = xml`<div>todo app</div>`;
|
||||
```
|
||||
|
||||
Note 3: writing inline templates with the [`xml` helper](../reference/templates.md#inline-templates)
|
||||
is nice, but there is no syntax highlighting, and this makes it very easy to
|
||||
have malformed xml. Some editors support syntax highlighting for this situation.
|
||||
For example, VS Code has an addon `Comment tagged template`, which, if installed,
|
||||
will properly display tagged templates:
|
||||
|
||||
```js
|
||||
static template = xml /* xml */`<div>todo app</div>`;
|
||||
```
|
||||
|
||||
Note 4: Large applications will probably want to be able to translate templates.
|
||||
Using inline templates makes it slightly harder, since we need additional tooling
|
||||
to extract the xml from the code, and to replace it with the translated values.
|
||||
|
||||
## 3. Displaying a list of tasks
|
||||
|
||||
Now that the basics are done, it is time to start thinking about tasks. To
|
||||
accomplish what we need, we will keep track of the tasks as an array of objects
|
||||
with the following keys:
|
||||
|
||||
- `id`: a number. It is extremely useful to have a way to uniquely identify
|
||||
tasks. Since the title is something created/edited by the user, it offers
|
||||
no guarantee that it is unique. So, we will generate a unique `id` number for
|
||||
each task.
|
||||
- `text`: a string, to explain what the task is about.
|
||||
- `isCompleted`: a boolean, to keep track of the status of the task
|
||||
|
||||
Now that we decided on the internal format of the state, let us add some demo
|
||||
data and a template to the `App` component:
|
||||
|
||||
```js
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<div class="task">
|
||||
<input type="checkbox" t-att-checked="task.isCompleted"/>
|
||||
<span><t t-esc="task.text"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</div>`;
|
||||
|
||||
tasks = [
|
||||
{
|
||||
id: 1,
|
||||
text: "buy milk",
|
||||
isCompleted: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: "clean house",
|
||||
isCompleted: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
The template contains a [`t-foreach`](../reference/templates.md#loops) loop to iterate
|
||||
through the tasks. It can find the `tasks` list from the component, since the rendering
|
||||
context contains the properties of the component. Note that we use the `id` of each task
|
||||
as a `t-key`, which is very common. There are two css classes: `task-list` and `task`,
|
||||
that we will use in the next section.
|
||||
|
||||
Finally, notice the use of the `t-att-checked` attribute:
|
||||
prefixing an attribute by [`t-att`](../reference/templates.md#dynamic-attributes) makes
|
||||
it dynamic. Owl will evaluate the expression and set it as the value of the
|
||||
attribute.
|
||||
|
||||
## 4. Layout: some basic css
|
||||
|
||||
So far, our task list looks quite bad. Let us add the following to `app.css`:
|
||||
|
||||
```css
|
||||
.task-list {
|
||||
width: 300px;
|
||||
margin: 50px auto;
|
||||
background: aliceblue;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.task {
|
||||
font-size: 18px;
|
||||
color: #111111;
|
||||
}
|
||||
```
|
||||
|
||||
This is better. Now, let us add an extra feature: completed tasks should be
|
||||
styled a little differently, to make it clearer that they are not as important.
|
||||
To do that, we will add a dynamic css class on each task:
|
||||
|
||||
```xml
|
||||
<div class="task" t-att-class="task.isCompleted ? 'done' : ''">
|
||||
```
|
||||
|
||||
```css
|
||||
.task.done {
|
||||
opacity: 0.7;
|
||||
}
|
||||
```
|
||||
|
||||
Notice that we have here another use of a dynamic attribute.
|
||||
|
||||
## 5. Extracting Task as a subcomponent
|
||||
|
||||
It is now clear that there should be a `Task` component to encapsulate the look
|
||||
and behavior of a task.
|
||||
|
||||
This `Task` component will display a task, but it cannot _own_ the state of the
|
||||
task: a piece of data should only have one owner. Doing otherwise is asking for
|
||||
trouble. So, the `Task` component will get its data as a `prop`. This means that
|
||||
the data is still owned by the `App` component, but can be used by the `Task`
|
||||
component (without modifying it).
|
||||
|
||||
Since we are moving code around, it is a good opportunity to refactor the code
|
||||
a little bit:
|
||||
|
||||
```js
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Task extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"/>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
</div>`;
|
||||
static props = ["task"];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Root extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>`;
|
||||
static components = { Task };
|
||||
|
||||
tasks = [
|
||||
...
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
mount(Root, document.body, {dev: true});
|
||||
```
|
||||
|
||||
A lot of stuff happened here:
|
||||
|
||||
- first, we have now a sub component `Task`, defined on top of the file,
|
||||
- whenever we define a sub component, it needs to be added to the static
|
||||
[`components`](../reference/component.md#static-properties)
|
||||
key of its parent, so Owl can get a reference to it,
|
||||
- the `Task` component has a `props` key: this is only useful for validation
|
||||
purpose. It says that each `Task` should be given exactly one prop, named
|
||||
`task`. If this is not the case, Owl will throw an
|
||||
[error](../reference/props.md#props-validation). This is extremely
|
||||
useful when refactoring components
|
||||
- finally, to activate the props validation, we need to set Owl's
|
||||
[mode](../reference/app.md#configuration) to `dev`. This is done in the last argument
|
||||
of the `mount` function. Note that this should be removed when an app is used in a real
|
||||
production environment, since `dev` mode is slightly slower, due to extra
|
||||
checks and validations.
|
||||
|
||||
## 6. Adding tasks (part 1)
|
||||
|
||||
We still use a list of hardcoded tasks. It's really time to give the user a way
|
||||
to add tasks himself. The first step is to add an input to the `Root` component.
|
||||
But this input will be outside of the task list, so we need to adapt `Root`
|
||||
template, js, and css:
|
||||
|
||||
```xml
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
const text = ev.target.value.trim();
|
||||
ev.target.value = "";
|
||||
console.log('adding task', text);
|
||||
// todo
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
.todo-app {
|
||||
width: 300px;
|
||||
margin: 50px auto;
|
||||
background: aliceblue;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.todo-app > input {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
We now have a working input, which log to the console whenever the user adds a
|
||||
task. Notice that when you load the page, the input is not focused. But adding
|
||||
tasks is a core feature of a task list, so let us make it as fast as possible by
|
||||
focusing the input.
|
||||
|
||||
We need to execute code when the `Root` component is ready (mounted). Let's do
|
||||
that using the `onMounted` hook. We will also need to get a reference to the
|
||||
input, by using the `t-ref` directive with the [`useRef`](../reference/hooks.md#useref) hook:
|
||||
|
||||
```xml
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
```
|
||||
|
||||
```js
|
||||
// on top of file:
|
||||
const { Component, mount, xml, useRef, onMounted } = owl;
|
||||
```
|
||||
|
||||
```js
|
||||
// in App
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
}
|
||||
```
|
||||
|
||||
This is a very common situation: whenever we need to perform some actions depending
|
||||
on the lifecycle of a component, we need to do it in the `setup` method, by using
|
||||
one of the lifecycle hook. Here, we first get a reference to the `inputRef`,
|
||||
then in the `onMounted` hook, we simply focus the html element.
|
||||
|
||||
## 7. Adding tasks (part 2)
|
||||
|
||||
In the previous section, we did everything except implement the code that actually
|
||||
create tasks! So, let us do that now.
|
||||
|
||||
We need a way to generate unique `id` numbers. To do that, we will simply add a
|
||||
`nextId` number in `App`. At the same time, let us remove the demo tasks in `App`:
|
||||
|
||||
```js
|
||||
nextId = 1;
|
||||
tasks = [];
|
||||
```
|
||||
|
||||
Now, the `addTask` method can be implemented:
|
||||
|
||||
```js
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
const text = ev.target.value.trim();
|
||||
ev.target.value = "";
|
||||
if (text) {
|
||||
const newTask = {
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(newTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This almost works, but if you test it, you will notice that no new task is ever
|
||||
displayed when the user press `Enter`. But if you add a `debugger` or a
|
||||
`console.log` statement, you will see that the code is actually running as
|
||||
expected. The problem is that Owl has no way of knowing that it needs to rerender
|
||||
the user interface. We can fix the issue by making `tasks` reactive, with the
|
||||
[`useState`](../reference/hooks.md#usestate) hook:
|
||||
|
||||
```js
|
||||
// on top of the file
|
||||
const { Component, mount, xml, useRef, onMounted, useState } = owl;
|
||||
|
||||
// replace the task definition in App with the following:
|
||||
tasks = useState([]);
|
||||
```
|
||||
|
||||
It now works as expected!
|
||||
|
||||
## 8. Toggling tasks
|
||||
|
||||
If you tried to mark a task as completed, you may have noticed that the text
|
||||
did not change in opacity. This is because there is no code to modify the
|
||||
`isCompleted` flag.
|
||||
|
||||
Now, this is an interesting situation: the task is displayed by the `Task`
|
||||
component, but it is not the owner of its state, so ideally, it should not modify it.
|
||||
However, for now, that's what we will do (this will be improved in a later step).
|
||||
In `Task`, change the `input` to:
|
||||
|
||||
```xml
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
|
||||
```
|
||||
|
||||
and add the `toggleTask` method:
|
||||
|
||||
```js
|
||||
toggleTask() {
|
||||
this.props.task.isCompleted = !this.props.task.isCompleted;
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Deleting tasks
|
||||
|
||||
Let us now add the possibility do delete tasks. This is different from the previous
|
||||
feature: deleting task has to be done on the task itself, but the actual operation
|
||||
need to be done on the task list. So, we need to communicate the request to the
|
||||
`Root` component. This is usually done by providing a callback in a prop.
|
||||
|
||||
First, let us update the `Task` template, css and js:
|
||||
|
||||
```xml
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="toggleTask"/>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
<span class="delete" t-on-click="deleteTask">🗑</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.task {
|
||||
font-size: 18px;
|
||||
color: #111111;
|
||||
display: grid;
|
||||
grid-template-columns: 30px auto 30px;
|
||||
}
|
||||
|
||||
.task > input {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.delete {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task:hover .delete {
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
static props = ["task", "onDelete"];
|
||||
|
||||
deleteTask() {
|
||||
this.props.onDelete(this.props.task);
|
||||
}
|
||||
```
|
||||
|
||||
And now, we need to provide the `onDelete` callback to each tasks in the `Root`
|
||||
component:
|
||||
|
||||
```xml
|
||||
<Task task="task" onDelete.bind="deleteTask"/>
|
||||
```
|
||||
|
||||
```js
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex(t => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
```
|
||||
|
||||
Notice that the `onDelete` prop is defined with a `.bind` suffix: this is a special
|
||||
suffix that makes sure the function callback is bound to the component.
|
||||
|
||||
Notice also that we have two functions named `deleteTask`. The one in the Task
|
||||
component just delegates the work to the Root component that owns the task list
|
||||
via the `onDelete` property.
|
||||
|
||||
## 10. Using a store
|
||||
|
||||
Looking at the code, it is apparent that all the code handling tasks is scattered
|
||||
all around the application. Also, it mixes UI code and business logic
|
||||
code. Owl does not provide any high level abstraction to manage business logic,
|
||||
but it is easy to do it with the basic reactivity primitives (`useState` and `reactive`).
|
||||
|
||||
Let us use it in our application to implement a central store. This is a pretty
|
||||
large refactoring (for our application), since it involves extracting all task
|
||||
related code out of the components. Here is the new content of the `app.js` file:
|
||||
|
||||
```js
|
||||
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Store
|
||||
// -------------------------------------------------------------------------
|
||||
function useStore() {
|
||||
const env = useEnv();
|
||||
return useState(env.store);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TaskList
|
||||
// -------------------------------------------------------------------------
|
||||
class TaskList {
|
||||
nextId = 1;
|
||||
tasks = [];
|
||||
|
||||
addTask(text) {
|
||||
text = text.trim();
|
||||
if (text) {
|
||||
const task = {
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTask(task) {
|
||||
task.isCompleted = !task.isCompleted;
|
||||
}
|
||||
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex((t) => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
return reactive(new TaskList());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Task extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted" t-on-click="() => store.toggleTask(props.task)"/>
|
||||
<span><t t-esc="props.task.text"/></span>
|
||||
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
|
||||
</div>`;
|
||||
|
||||
static props = ["task"];
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="store.tasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>`;
|
||||
static components = { Task };
|
||||
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
this.store.addTask(ev.target.value);
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
const env = {
|
||||
store: createTaskStore(),
|
||||
};
|
||||
mount(Root, document.body, { dev: true, env });
|
||||
```
|
||||
|
||||
## 11. Saving tasks in local storage
|
||||
|
||||
Now, our TodoApp works great, except if the user closes or refresh the browser!
|
||||
It is really inconvenient to only keep the state of the application in memory.
|
||||
To fix this, we will save the tasks in the local storage. With our current
|
||||
codebase, it is a simple change: we need to save tasks to local storage and
|
||||
listen to any change.
|
||||
|
||||
```js
|
||||
class TaskList {
|
||||
constructor(tasks) {
|
||||
this.tasks = tasks || [];
|
||||
const taskIds = this.tasks.map((t) => t.id);
|
||||
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||
saveTasks();
|
||||
return taskStore;
|
||||
}
|
||||
```
|
||||
|
||||
The key point is that the `reactive` function takes a callback that will be called
|
||||
every time an observed value is changed. Note that we need to call the `saveTasks`
|
||||
method initially to make sure we observe all current values.
|
||||
|
||||
## 12. Filtering tasks
|
||||
|
||||
We are almost done, we can add/update/delete tasks. The only missing feature is
|
||||
the possibility to display the task according to their completed status. We will
|
||||
need to keep track of the state of the filter in `Root`, then filter the visible
|
||||
tasks according to its value.
|
||||
|
||||
```js
|
||||
class Root extends Component {
|
||||
static template = xml /* xml */`
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="task-panel" t-if="store.tasks.length">
|
||||
<div class="task-counter">
|
||||
<t t-esc="displayedTasks.length"/>
|
||||
<t t-if="displayedTasks.length lt store.tasks.length">
|
||||
/ <t t-esc="store.tasks.length"/>
|
||||
</t>
|
||||
task(s)
|
||||
</div>
|
||||
<div>
|
||||
<span t-foreach="['all', 'active', 'completed']"
|
||||
t-as="f" t-key="f"
|
||||
t-att-class="{active: filter.value===f}"
|
||||
t-on-click="() => this.setFilter(f)"
|
||||
t-esc="f"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
...
|
||||
this.filter = useState({ value: "all" });
|
||||
}
|
||||
|
||||
get displayedTasks() {
|
||||
const tasks = this.store.tasks;
|
||||
switch (this.filter.value) {
|
||||
case "active": return tasks.filter(t => !t.isCompleted);
|
||||
case "completed": return tasks.filter(t => t.isCompleted);
|
||||
case "all": return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.filter.value = filter;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
.task-panel {
|
||||
color: #0088ff;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.task-panel .task-counter {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.task-panel span {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-panel span.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
|
||||
Notice here that we set dynamically the css class of the filter with the object
|
||||
syntax.
|
||||
|
||||
## 13. The Final Touch
|
||||
|
||||
Our list is feature complete. We can still add a few extra details to improve
|
||||
the user experience.
|
||||
|
||||
1. Add a visual feedback when the user mouse is over a task:
|
||||
|
||||
```css
|
||||
.task:hover {
|
||||
background-color: #def0ff;
|
||||
}
|
||||
```
|
||||
|
||||
2. Make the text of a task clickable, to toggle its checkbox:
|
||||
|
||||
```xml
|
||||
<input type="checkbox" t-att-checked="props.task.isCompleted"
|
||||
t-att-id="props.task.id"
|
||||
t-on-click="() => store.toggleTask(props.task)"/>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
||||
```
|
||||
|
||||
3. Strike the text of completed task:
|
||||
|
||||
```css
|
||||
.task.done label {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
```
|
||||
|
||||
## Final code
|
||||
|
||||
Our application is now complete. It works, the UI code is well separated from
|
||||
the business logic code, it is testable, all under 150 lines of code (template
|
||||
included!).
|
||||
|
||||
For reference, here is the final code:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OWL Todo App</title>
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="owl.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```js
|
||||
(function () {
|
||||
const { Component, mount, xml, useRef, onMounted, useState, reactive, useEnv } = owl;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Store
|
||||
// -------------------------------------------------------------------------
|
||||
function useStore() {
|
||||
const env = useEnv();
|
||||
return useState(env.store);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TaskList
|
||||
// -------------------------------------------------------------------------
|
||||
class TaskList {
|
||||
constructor(tasks) {
|
||||
this.tasks = tasks || [];
|
||||
const taskIds = this.tasks.map((t) => t.id);
|
||||
this.nextId = taskIds.length ? Math.max(...taskIds) + 1 : 1;
|
||||
}
|
||||
|
||||
addTask(text) {
|
||||
text = text.trim();
|
||||
if (text) {
|
||||
const task = {
|
||||
id: this.nextId++,
|
||||
text: text,
|
||||
isCompleted: false,
|
||||
};
|
||||
this.tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTask(task) {
|
||||
task.isCompleted = !task.isCompleted;
|
||||
}
|
||||
|
||||
deleteTask(task) {
|
||||
const index = this.tasks.findIndex((t) => t.id === task.id);
|
||||
this.tasks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function createTaskStore() {
|
||||
const saveTasks = () => localStorage.setItem("todoapp", JSON.stringify(taskStore.tasks));
|
||||
const initialTasks = JSON.parse(localStorage.getItem("todoapp") || "[]");
|
||||
const taskStore = reactive(new TaskList(initialTasks), saveTasks);
|
||||
saveTasks();
|
||||
return taskStore;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Task Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Task extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="task" t-att-class="props.task.isCompleted ? 'done' : ''">
|
||||
<input type="checkbox"
|
||||
t-att-id="props.task.id"
|
||||
t-att-checked="props.task.isCompleted"
|
||||
t-on-click="() => store.toggleTask(props.task)"/>
|
||||
<label t-att-for="props.task.id"><t t-esc="props.task.text"/></label>
|
||||
<span class="delete" t-on-click="() => store.deleteTask(props.task)">🗑</span>
|
||||
</div>`;
|
||||
|
||||
static props = ["task"];
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Root Component
|
||||
// -------------------------------------------------------------------------
|
||||
class Root extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="todo-app">
|
||||
<input placeholder="Enter a new task" t-on-keyup="addTask" t-ref="add-input"/>
|
||||
<div class="task-list">
|
||||
<t t-foreach="displayedTasks" t-as="task" t-key="task.id">
|
||||
<Task task="task"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="task-panel" t-if="store.tasks.length">
|
||||
<div class="task-counter">
|
||||
<t t-esc="displayedTasks.length"/>
|
||||
<t t-if="displayedTasks.length lt store.tasks.length">
|
||||
/ <t t-esc="store.tasks.length"/>
|
||||
</t>
|
||||
task(s)
|
||||
</div>
|
||||
<div>
|
||||
<span t-foreach="['all', 'active', 'completed']"
|
||||
t-as="f" t-key="f"
|
||||
t-att-class="{active: filter.value===f}"
|
||||
t-on-click="() => this.setFilter(f)"
|
||||
t-esc="f"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
static components = { Task };
|
||||
|
||||
setup() {
|
||||
const inputRef = useRef("add-input");
|
||||
onMounted(() => inputRef.el.focus());
|
||||
this.store = useStore();
|
||||
this.filter = useState({ value: "all" });
|
||||
}
|
||||
|
||||
addTask(ev) {
|
||||
// 13 is keycode for ENTER
|
||||
if (ev.keyCode === 13) {
|
||||
this.store.addTask(ev.target.value);
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
get displayedTasks() {
|
||||
const tasks = this.store.tasks;
|
||||
switch (this.filter.value) {
|
||||
case "active":
|
||||
return tasks.filter((t) => !t.isCompleted);
|
||||
case "completed":
|
||||
return tasks.filter((t) => t.isCompleted);
|
||||
case "all":
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.filter.value = filter;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup
|
||||
// -------------------------------------------------------------------------
|
||||
const env = { store: createTaskStore() };
|
||||
mount(Root, document.body, { dev: true, env });
|
||||
})();
|
||||
```
|
||||
|
||||
```css
|
||||
.todo-app {
|
||||
width: 300px;
|
||||
margin: 50px auto;
|
||||
background: aliceblue;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.todo-app > input {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.task {
|
||||
font-size: 18px;
|
||||
color: #111111;
|
||||
display: grid;
|
||||
grid-template-columns: 30px auto 30px;
|
||||
}
|
||||
|
||||
.task:hover {
|
||||
background-color: #def0ff;
|
||||
}
|
||||
|
||||
.task > input {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.delete {
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task:hover .delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.task.done {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.task.done label {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.task-panel {
|
||||
color: #0088ff;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.task-panel .task-counter {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.task-panel span {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-panel span.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,68 @@
|
||||
# 🦉 Notes On Owl Architecture 🦉
|
||||
|
||||
We explain here how Owl is designed
|
||||
|
||||
Warning: these notes are technical by nature, and intended for people working
|
||||
on Owl (or interested in understanding its design).
|
||||
|
||||
## Overview
|
||||
|
||||
Roughly speaking, Owl has 5 main parts:
|
||||
|
||||
- a virtual dom system (in `src/blockdom`)
|
||||
- a component system (in `src/component`)
|
||||
- a template compiler (located in the `src/compiler` folder)
|
||||
- a small runtime code to tie them together (in `src/app`)
|
||||
- a reactivity system (in `src/reactivity.ts`)
|
||||
|
||||
There are some other files, but the core of Owl can be understood with these
|
||||
five main parts.
|
||||
|
||||
The virtual dom is an optimized virtual dom based on blocks, which supports
|
||||
multi blocks (for fragments). Everything that owl renders is internally
|
||||
represented by a virtual node. The job of the virtual dom is to efficiently
|
||||
represent the current state of the application, and to build an actual DOM
|
||||
representation when needed, or update the DOM whenever it is needed.
|
||||
|
||||
- some other helpers/smaller scale stuff
|
||||
A rendering occurs in two phases:
|
||||
|
||||
- virtual rendering: this generates the virtual dom in memory, asynchronously
|
||||
- patch: applies a virtual tree to the screen (synchronously)
|
||||
|
||||
There are several classes involved in a rendering:
|
||||
|
||||
- components
|
||||
- a scheduler
|
||||
- fibers: small objects containing some metadata, associated with a rendering of
|
||||
a specific component
|
||||
|
||||
Components are organized in a dynamic component tree, visible in the user
|
||||
interface. Whenever a rendering is initiated in a component `C`:
|
||||
|
||||
- a fiber is created on `C` with the rendering props information
|
||||
- the virtual rendering phase starts on C (will asynchronously render all the
|
||||
child components)
|
||||
- the fiber is added to the scheduler, which will poll continuously, every
|
||||
animation frame, if the fiber is done
|
||||
- once it is done, the scheduler will call the task callback, which will apply
|
||||
the patch (if it was not cancelled in the meantime).
|
||||
|
||||
# 🦉 VDom 🦉
|
||||
|
||||
Owl is a declarative component system: we declare the structure of the component
|
||||
tree, and Owl will translate that to a list of imperative operations. This
|
||||
translation is done by a virtual dom. This is the low level layer of Owl, most
|
||||
developer will not need to call directly the virtual dom functions.
|
||||
|
||||
The main idea behind a virtual dom is to keep a in-memory representation of the
|
||||
DOM (called a virtual node), and whenever some change is needed, to regenerate
|
||||
a new representation, compute the difference between the old and the new, then
|
||||
apply the changes.
|
||||
|
||||
`vdom` exports two functions:
|
||||
|
||||
- `h`: create a new virtual node
|
||||
- `patch`: compare two virtual nodes, and apply the difference.
|
||||
|
||||
Note: Owl's virtual dom is a fork of [snabbdom](https://github.com/snabbdom/snabbdom).
|
||||
@@ -0,0 +1,255 @@
|
||||
# Comparison with Vue/React
|
||||
|
||||
OWL, React and Vue have the same main feature: they allow developers to build
|
||||
declarative user interfaces. To do that, all these frameworks uses a virtual dom. However, there are still obviously many differences.
|
||||
|
||||
In this page, we try to highlight some of these differences. Obviously, a lot of
|
||||
effort was put to be fair. However, if you disagree with some of the points
|
||||
discussed, feel free to open an issue/submit a PR to correct this text.
|
||||
|
||||
## Content
|
||||
|
||||
- [Size](#size)
|
||||
- [Class Based](#class-based)
|
||||
- [Tooling/Build Step](#toolingbuild-step)
|
||||
- [Templating](#templating)
|
||||
- [Asynchronous rendering](#asynchronous-rendering)
|
||||
- [Reactivity](#reactivity)
|
||||
- [Hooks](#hooks)
|
||||
|
||||
## Size
|
||||
|
||||
OWL is intended to be small and to work at a slightly lower level of abstraction
|
||||
than React and Vue. Also, jQuery is not the same kind of framework, but it is interesting to compare.
|
||||
|
||||
| Framework | Size (minified, gzipped) |
|
||||
| ------------------------ | ------------------------ |
|
||||
| OWL | 18kb |
|
||||
| Vue + VueX | 30kb |
|
||||
| Vue + VueX + Vue Router | 39kb |
|
||||
| React + ReactDOM + Redux | 40kb |
|
||||
| jQuery | 30kb |
|
||||
|
||||
Note that those comparisons are not entirely fair, because we do not compare
|
||||
the same exact set of features. For example, VueX and Vue Router support more
|
||||
advanced use cases.
|
||||
|
||||
## Class Based
|
||||
|
||||
Both React and Vue moved away from defining components with classes. They prefer
|
||||
a more functional approach, in particular, with the new `hooks` mechanisms.
|
||||
|
||||
This has some advantages and disadvantages. But the end result is that React
|
||||
and Vue both offers multiple different ways of defining new components. In
|
||||
contrast, Owl has only one mechanism: class-based components. We believe that Owl
|
||||
components are fast enough for all our usecases, and making it as simple as
|
||||
possible for developers is more valuable (for us).
|
||||
|
||||
Also, functions or class based components are more than just syntax. Functions
|
||||
come with a mindset of composition and class are about inheritance. Clearly,
|
||||
both of these are important mechanisms for reusing code. Also, one does not
|
||||
exclude the other.
|
||||
|
||||
It certainly looks like the world of UI frameworks is moving toward composition,
|
||||
for many very good reasons. Owl is still good at composition (for example,
|
||||
Owl supports slots, which is the primary mechanism to make generic reusable
|
||||
components). But it can also use inheritance (and this is very important since
|
||||
templates can also be inherited with `xpaths` transformations).
|
||||
|
||||
## Tooling/Build step
|
||||
|
||||
OWL is designed to be easy to use in a standalone way. For various reasons,
|
||||
Odoo does not want to rely on standard web tools (such as webpack), and OWL can
|
||||
be used by simply adding a script tag to a page.
|
||||
|
||||
```html
|
||||
<script src="owl.min.js" />
|
||||
```
|
||||
|
||||
In comparison, React encourages using JSX, which necessitate a build step, and
|
||||
most Vue applications uses single file components, which also necessitate a build step.
|
||||
|
||||
On the flipside, external tooling may make it harder to use in some case, but it
|
||||
also brings a lot of benefits. And React/Vue have both a large ecosystem.
|
||||
|
||||
Note that since Owl is not dependant on any external tool nor libraries, it is
|
||||
very easy to integrate into any build toolchain. Also, since we cannot rely on
|
||||
additional tools, we made a lot of effort to make the most of the web platform.
|
||||
|
||||
For example, Owl uses the standard `xml` parser that comes with every browser.
|
||||
Because of that, Owl did not have to write its own template parser. Another
|
||||
example is the [`xml`](../reference/templates.md#inline-templates) tag helper function, which makes use of
|
||||
native template literals to allow in a natural way to write `xml` templates
|
||||
directly in the javascript code. This can be easily integrated with editor
|
||||
plugins to have autocompletion inside the template.
|
||||
|
||||
## Templating
|
||||
|
||||
OWL uses its own QWeb engine, which compiles templates on the
|
||||
frontend, as they are needed. This is extremely convenient for our use case, in
|
||||
particular because templates are described in XML files, and can be modified by
|
||||
XPaths. Since Odoo is at its heart a modular application, this is an important
|
||||
feature for us.
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<button t-on-click="increment">Click Me! [<t t-esc="state.value"/>]</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
Vue is actually kind of similar. Its template language is kind of close to QWeb,
|
||||
with the `v` replaced by the `t`. However, it is also more fully featured. For
|
||||
example, Vue templates have slots, or event modifiers. A large difference is that
|
||||
most Vue applications will need to be built ahead of time, to compile the templates
|
||||
into javascript functions. Note that Vue has a separate build which includes the
|
||||
template compiler.
|
||||
|
||||
In contrast, most React applications do not use a templating language, but write
|
||||
some JSX code, which is precompiled into plain JavaScript by a build step. This
|
||||
example is done with the (kind of outdated) React class system:
|
||||
|
||||
```jsx
|
||||
class Clock extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello, world!</h1>
|
||||
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This has the advantage of having the full power of Javascript, but is less
|
||||
structured than a template language. Note that the tooling is quite impressive:
|
||||
there is a syntax highlighter for jsx here on github!
|
||||
|
||||
By comparison, here is the equivalent Owl component, written with the
|
||||
[`xml`](../reference/templates.md#inline-templates) tag helper:
|
||||
|
||||
```js
|
||||
class Clock extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<h1>Hello, world!</h1>
|
||||
<h2>It is {props.date.toLocaleTimeString()}.</h2>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## Asynchronous Rendering
|
||||
|
||||
This is actually a big difference between OWL and React/Vue: components in OWL
|
||||
are totally asynchronous. They have two asynchronous hooks in their lifecycle:
|
||||
|
||||
- `willStart` (before the component starts rendering)
|
||||
- `willUpdateProps` (before new props are set)
|
||||
|
||||
Both these methods can be implemented and return a promise. The rendering will
|
||||
then wait for these promises to be completed before patching the DOM. This is
|
||||
useful for some use cases: for example, a component may want to fetch an external
|
||||
library (a calendar component may need a specialized calendar rendering library),
|
||||
in its willStart hook.
|
||||
|
||||
```javascript
|
||||
class MyCalendarComponent extends owl.Component {
|
||||
...
|
||||
|
||||
willStart() {
|
||||
return utils.lazyLoad('static/libs/fullcalendar/fullcalendar.js');
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This may be dangerous (to stop the rendering waiting for the network), but it is
|
||||
extremely powerful as well, as demonstrated by the Odoo Web Client.
|
||||
|
||||
Lazy loading static libraries can obviously be done with React/Vue, but it is
|
||||
more convoluted. For example, in Vue, you need to use a dynamic import keyword
|
||||
that needs to be transpiled at build time in order for the component to be loaded
|
||||
asynchronously (see [the documentation](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components)).
|
||||
|
||||
## Reactivity
|
||||
|
||||
React has a simple model: whenever the state changes, it is
|
||||
replaced with a new state (via the `setState` method). Then, the DOM is patched.
|
||||
This is simple, efficient, and a little bit awkward to write.
|
||||
|
||||
Vue is a little bit different: it replace magically the properties in the state
|
||||
by getters/setters. With that, it can notify components whenever the state that
|
||||
they read was changed.
|
||||
|
||||
Owl is closer to vue: it also tracks magically the state properties, but it does
|
||||
only increment an internal counter whenever it changes. Note that it is done
|
||||
with a `Proxy`, which means that it is totally transparent to the developers.
|
||||
Adding new keys is supported. Once any part of the state has been changed, a
|
||||
rendering is scheduled in the next microtask tick (promise queue).
|
||||
|
||||
## Hooks
|
||||
|
||||
[Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over
|
||||
the React world. They solve a lot of seemingly unconnected problems: attach
|
||||
reusable behavior to a component, in a composable way, extract stateful logic
|
||||
from a component or reuse stateful logic between component, without changing your
|
||||
component hierarchy.
|
||||
|
||||
Here is an example of the React `useState` hook:
|
||||
|
||||
```js
|
||||
import React, { useState } from "react";
|
||||
|
||||
function Example() {
|
||||
// Declare a new state variable, which we'll call "count"
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>You clicked {count} times</p>
|
||||
<button onClick={() => setCount(count + 1)}>Click me</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Because of the way React designed the hooks API, they only work for functional
|
||||
components. But in that case, they really are powerful. Every major React library
|
||||
is in the process of redesigning their API with hooks (for example,
|
||||
[Redux](https://react-redux.js.org/next/api/hooks)).
|
||||
|
||||
Vue 2 does not have hooks, but the Vue project is working on its next version,
|
||||
which will feature its new [composition API](https://vue-composition-api-rfc.netlify.com/).
|
||||
This work is based on the new ideas introduced by React hooks.
|
||||
|
||||
From the way React and Vue introduce their hooks, it may look like hooks are not
|
||||
compatible with class components. However, this is not the case, as shown by
|
||||
Owl [hooks](../reference/hooks.md). They are inspired by both React and Vue. For example,
|
||||
the `useState` hook is named after React, but its API is closer to the `reactive`
|
||||
Vue hook.
|
||||
|
||||
Here is what the `Counter` example above look like in Owl:
|
||||
|
||||
```js
|
||||
import { Component, Owl } from "owl";
|
||||
import { xml } from "owl/tags";
|
||||
|
||||
class Example extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<p>You clicked {count.value} times</p>
|
||||
<button t-on-click="increment">Click me</button>
|
||||
</div>`;
|
||||
|
||||
count = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since the Owl framework had hooks from early in its life, its main APIs
|
||||
are designed to be interacted with hooks from the start. For example, the
|
||||
`Context` abstraction.
|
||||
@@ -0,0 +1,98 @@
|
||||
# 🦉 Notes On Owl Compiled Templates 🦉
|
||||
|
||||
This page will explain what an Owl compiled template look like. This is a
|
||||
technical document intended for developers interested in understanding how Owl
|
||||
works internally.
|
||||
|
||||
Broadly speaking, Owl compiles templates into a javascript function (a closure)
|
||||
that returns a function (the "render" function). The point of the closure is to
|
||||
have a place to store all values specific to the template (in particular, "blocks").
|
||||
Once a template is compiled, its closure function is called once to get the
|
||||
render function, and from then on, only the render function is used.
|
||||
|
||||
The render function takes some context (and some additional information) and
|
||||
return a virtual dom representation of the rendered template, as a block tree.
|
||||
A block tree is a very light weight representation that only contains the dynamic
|
||||
part of the template, and its structure. It is actually independant of the
|
||||
static part of the templates (which are contained in the blocks captured by the
|
||||
closure). This means that the work performed at render time is only to collect
|
||||
dynamic data, and to describe the block structure of the result.
|
||||
|
||||
It looks like this, in pseudo code:
|
||||
|
||||
```js
|
||||
function closure(bdom, helpers) {
|
||||
// here is some place to put stuff specific to the template, such as
|
||||
// blocks
|
||||
...
|
||||
|
||||
return function render(context, node, key) {
|
||||
// only build here all dynamic parts of the template
|
||||
// build a block tree
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, let us see an example. Consider the following template:
|
||||
|
||||
```xml
|
||||
<div class="some-class">
|
||||
<div class="blabla">
|
||||
<span><t t-esc="state.value"/></span>
|
||||
</div>
|
||||
<t t-if="state.info">
|
||||
<p class="info" t-att-class="someAttribute">
|
||||
<t t-esc="state.info"/>
|
||||
</p>
|
||||
</t>
|
||||
<SomeComponent value="value"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
If you look carefully, there are 5 dynamic things:
|
||||
|
||||
- a text value (the first `t-esc`),
|
||||
- a sub block (the `t-if`),
|
||||
- a dynamic attribute (the `t-att-class` attribute),
|
||||
- another text value (the second `t-esc`),
|
||||
- and finally, a sub component
|
||||
|
||||
Here is the compiled code for this template:
|
||||
|
||||
```js
|
||||
function closure(bdom, helpers) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(
|
||||
`<div class="some-class"><div class="blabla"><span><block-text-0/></span></div><block-child-0/><block-child-1/></div>`
|
||||
);
|
||||
let block2 = createBlock(`<p class="info" block-attribute-0="class"><block-text-1/></p>`);
|
||||
|
||||
return function render(ctx, node, key = "") {
|
||||
let b2, b3;
|
||||
let txt1 = ctx["state"].value;
|
||||
if (ctx["state"].info) {
|
||||
let attr1 = ctx["someAttribute"];
|
||||
let txt2 = ctx["state"].info;
|
||||
b2 = block2([attr1, txt2]);
|
||||
}
|
||||
b3 = component(`SomeComponent`, { value: ctx["value"] }, key + `__1`, node, ctx);
|
||||
return block1([txt1], [b2, b3]);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The values captured in the closure capture the static part of the template: we
|
||||
define here two blocks (which contains a template node, that can be deep cloned
|
||||
whenever a block is mounted). Then the render function only describes the block
|
||||
tree structure of the result, depending on the context. This means that we
|
||||
minimize the amount of work done at render time.
|
||||
|
||||
Then, when we want to patch the dom, Owl will uses the `patch` function from
|
||||
blockdom, which then will diff the block tree, and deep clone new blocks whenever
|
||||
a new block is inserted, keep track of dynamic parts of each block, and update
|
||||
them accordingly.
|
||||
|
||||
With this design, the cost of rendering a template is proportional to the number
|
||||
of dynamic values, and not to the size of the template.
|
||||
@@ -0,0 +1,180 @@
|
||||
# 🦉 Why Owl ? 🦉
|
||||
|
||||
The common wisdom is that one should not reinvent the wheel, because that would
|
||||
waste effort and resources. It is certainly true in many cases. A javascript
|
||||
framework is a considerable investment, so it is quite logical to ask the question:
|
||||
why did Odoo decide to make OWL instead of using a standard/well known framework,
|
||||
such as React or Vue?
|
||||
|
||||
As you might expect, the answer to that question is not simple. But most of the
|
||||
reasons discussed in this page are a consequence from a single fact: Odoo is
|
||||
extremely modular.
|
||||
|
||||
This means, for example, that the core parts of Odoo are not aware, before runtime,
|
||||
of what files will be loaded/executed, or what will be the state of the UI. Because
|
||||
of that, Odoo cannot rely on a standard build toolchain. Also, this implies that
|
||||
the core parts of Odoo need to be extremely generic. In other words, Odoo is not
|
||||
really an application with a user interface. It is an application which generates
|
||||
a dynamic user interface. And most frameworks are not up to the task.
|
||||
|
||||
Betting on Owl was not an easy choice to make, because there certainly are a lot
|
||||
of conflicting needs that we want to carefully balance. Choosing anything other
|
||||
than a well known framework is bound to be controversial. This page will explain
|
||||
some of the reason why we still believe that building Owl is a worthwile
|
||||
endeavour.
|
||||
|
||||
## Strategy
|
||||
|
||||
It is true that we want to keep control of our technology, in the sense that we
|
||||
do not want to depend on Facebook or Google, or any other large (or small)
|
||||
company. If they decide to change their license, or to go in a direction that
|
||||
will not work for us, this may be a problem. This is even more true because
|
||||
Odoo is not a conventional javascript application, and our needs are probably
|
||||
quite different as most other applications.
|
||||
|
||||
## Class components
|
||||
|
||||
It is clear that the biggest frameworks are moving away from class components.
|
||||
There is an implicit assumption that class components are terrible, and that
|
||||
functional programming is the way to go. React even goes as far as to say that
|
||||
classes are confusing for developers.
|
||||
|
||||
While there is some truth to that, and to the fact that composition is certainly
|
||||
a good mechanism for code reuse, we believe that classes and inheritance are
|
||||
important tools.
|
||||
|
||||
Sharing code between generic components with inheritance is the way Odoo built
|
||||
its web client. And it is clear that inheritance is not the root of all evils.
|
||||
It is often a perfectly simple and appropriate solution. What matter most is
|
||||
the architectural decisions.
|
||||
|
||||
Also, Odoo has another specific use out of class components: each method of a
|
||||
class provides an extension point for addons. This may not be a clean architecture
|
||||
pattern, but it is a pragmatic decision that served Odoo well: classes are
|
||||
sometimes monkey-patched to add behaviour from the outside. A little bit like
|
||||
mixins, but from the outside.
|
||||
|
||||
Using React or Vue would make it significantly harder to monkey patch components,
|
||||
because a lot of the state is hidden in their internals.
|
||||
|
||||
## Tooling
|
||||
|
||||
React or Vue have a huge community, and a lot of effort have been made into their
|
||||
tooling. This is wonderful, but at the same time, a pretty big issue for Odoo:
|
||||
since the assets are totally dynamic (and could change whenever the user installs
|
||||
or removes an addon), we need to have all that kind of tooling on the production
|
||||
servers. This is certainly not ideal.
|
||||
|
||||
Also, this makes it very complicated to setup Vue or React tools: Odoo code is
|
||||
not a simple file that import other files. It changes all the time, assets
|
||||
are bundled differently in different contexts. This is the reason why Odoo has
|
||||
its own module system, which are resolved at runtime, by the browser. The
|
||||
dynamic nature of Odoo means that we often need to delay work as late as possible
|
||||
(in other word, we want a JIT user interface!)
|
||||
|
||||
Our ideal framework has minimal (mandatory) tooling, which makes it easier to
|
||||
deploy. Using React without JSX, or Vue without vue file is not very appealing.
|
||||
|
||||
At the same time, Owl is designed to solve this issue: it compiles templates
|
||||
by the browser, it doesn't need much code for that, since we use the XML parser
|
||||
built into each browser. Owl works with or without any additional tooling. It
|
||||
can use template strings to write single file components, and is easy to integrate
|
||||
in any html page, with a simple `<script>` tag.
|
||||
|
||||
## Template based
|
||||
|
||||
Odoo stores templates as XML documents in a database. This is very powerful, since
|
||||
this allow the use of xpaths to customize other templates. This is a very
|
||||
important feature of odoo, and one of the key to Odoo modularity.
|
||||
|
||||
Because of that, we still expect to write our templates in an XML document.
|
||||
Weirdly enough, no major framework uses XML to store templates, even though it
|
||||
is extremely convenient.
|
||||
|
||||
So, using React or Vue means that we need to make a template compiler. For React,
|
||||
that would be a compiler that would take a QWeb template, and convert it to a
|
||||
React render function. For Vue, it would convert it to a Vue template. Then
|
||||
we need to bundle the vue template compiler as well.
|
||||
|
||||
Not only this would be complex (compiling a templating language into another is
|
||||
not an easy task), but it would negatively impact the developer experience as
|
||||
well. Writing Vue or React components in a QWeb template would certainly be
|
||||
awkward, and very confusing.
|
||||
|
||||
## Developer Experience
|
||||
|
||||
This brings us to the following point: developer experience. We see this choice
|
||||
as an investment for the future, and we want to make onboarding developers as
|
||||
easy as possible.
|
||||
|
||||
While many javascript professionals clearly think that react/vue is not difficult
|
||||
(which is true to some extent), it is alsy true that many non js specialists are
|
||||
overwhelmed with the frontend world: functional components, hooks, and many other
|
||||
fancy words. Also, what is available in the compilation context may be difficult,
|
||||
there is a lot of black magic going on in pretty much every framework. Vue
|
||||
somehow join various namespaces into one, under the hood, and add various internal
|
||||
keys. Svelte transform the code. React require that state transformations are
|
||||
deep, and not shallow.
|
||||
|
||||
Owl is trying very hard to have a simple and familiar API. It uses classes. Its
|
||||
reactivity system is explicit, not implicit. The scoping rules are obvious. In
|
||||
case of doubt, we err on the side of not implementing a feature.
|
||||
|
||||
It is certainly different from React or Vue, but at the same time, kind of
|
||||
familiar for experienced developers.
|
||||
|
||||
## JIT compilation
|
||||
|
||||
There is also a clear trend in the frontend world to compile code
|
||||
as much as possible ahead of time. Most frameworks will compile templates ahead
|
||||
of time. And now Svelte is trying to compile the JS code away, so it can remove
|
||||
itself from the bundle.
|
||||
|
||||
This is certainly reasonable for many usecases. However, this is not what Odoo
|
||||
needs: Odoo will fetch templates from the database and need to compile them only
|
||||
at the last possible moment, so we can apply all necessary xpaths.
|
||||
|
||||
Even more: Odoo needs to be able to generate (and compile) templates at runtime.
|
||||
Currently, Odoo form views interpret an xml description. But the form view code
|
||||
then needs to do a lot of complicated operations. With Owl, we will be able to
|
||||
transform a view description into a QWeb template, then compile that and use it
|
||||
immediately.
|
||||
|
||||
## Reactivity
|
||||
|
||||
There are other design choices that we feel are not optimal in other frameworks.
|
||||
For example, the reactivity system. We like the way Vue did it, but it has a
|
||||
flaw: it is not really optional. There is actually a way to opt out of the reactivity
|
||||
system by freezing the state, but then, it is freezed.
|
||||
|
||||
And there certainly are situations where we need a state, which is not read-only,
|
||||
and not observed. For example, imagine a spreadsheet component. It may have a
|
||||
very large internal state, and it knows exactly when it needs to be rendered
|
||||
(basically, whenever the user performs some action). Then, observing its state
|
||||
is a net performance loss, both for the CPU and the memory.
|
||||
|
||||
## Concurrency
|
||||
|
||||
Many applications are happy to simply display a spinner whenever a new asynchronous
|
||||
action is performed, but Odoo wants a different user experience: most asynchronous
|
||||
state changes are not displayed until ready. This is sometimes called a concurrent
|
||||
mode: the UI is rendered in memory, and displayed only when it is ready (and
|
||||
only if it has not been cancelled by subsequent user actions).
|
||||
|
||||
React has now an experimental concurrent mode, but it was not ready when Owl
|
||||
started. Vue has not really an equivalent API (suspense is not what we need).
|
||||
|
||||
Also, React concurrent mode is complex to use. Concurrency was one of the rare
|
||||
strong point of the former Odoo js framework (widgets), and we feel that Owl has
|
||||
now a very strong concurrent mode, which is simple and powerful at the same time.
|
||||
|
||||
## Conclusion
|
||||
|
||||
This lengthy discussion showed that there are many small and not so small reasons
|
||||
that current standard frameworks are not tailored to our needs. It is perfectly
|
||||
fine, because they each chose a different set of tradeoffs.
|
||||
|
||||
However, we feel that there is still room in the framework world for something
|
||||
that is different. For a framework that makes choices compatible with Odoo.
|
||||
|
||||
And that is why we built Owl 🦉.
|
||||
@@ -1,61 +0,0 @@
|
||||
# Quick Start
|
||||
|
||||
To build an application (or a sub-part of an application), we need two things:
|
||||
|
||||
- an environment
|
||||
- a root widget
|
||||
|
||||
Here are a few steps that may be useful 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
|
||||
|
||||
## Simple example
|
||||
|
||||
In a oversimplified example, here is what it could look like:
|
||||
|
||||
```javascript
|
||||
const templates = await loadTemplates();
|
||||
|
||||
const qweb = new QWeb();
|
||||
qweb.loadTemplates(templates);
|
||||
|
||||
const env = {
|
||||
qweb: qweb
|
||||
};
|
||||
|
||||
const root = new RootWidget(env, { initialState: 17 });
|
||||
|
||||
const target = document.getElementById("app");
|
||||
root.mount(target);
|
||||
```
|
||||
|
||||
## Managing state
|
||||
|
||||
The environment is propagated to each subchildren. So, it is convenient to use
|
||||
to give a reference to some global object. This is potentially very useful
|
||||
in a redux-like architecture:
|
||||
|
||||
```javascript
|
||||
...
|
||||
|
||||
const store = new Store();
|
||||
|
||||
const env = {
|
||||
qweb: qweb,
|
||||
store: store,
|
||||
};
|
||||
|
||||
const state = store.getState()
|
||||
const root = new RootWidget(env, state);
|
||||
|
||||
store.on('state_change', nextState => {
|
||||
root.updateProps(nextState);
|
||||
});
|
||||
|
||||
const target = document.getElementById('app');
|
||||
root.mount(target);
|
||||
```
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
# QWeb
|
||||
|
||||
QWeb is a template specification. The QWeb class in this repository is:
|
||||
|
||||
- an implementation of the QWeb specification
|
||||
- which outputs a virtual dom instead of a string
|
||||
- and extended with a few extra directives
|
||||
|
||||
## QWeb Specification
|
||||
|
||||
add here a full description of what QWeb is supposed to be, with some examples
|
||||
|
||||
## QWeb Implementation
|
||||
|
||||
```javascript
|
||||
var qweb = new QWeb();
|
||||
qweb.addTemplate("sometemplate", '<div>hello <t t-esc="name"/></div>');
|
||||
|
||||
// result is a vnode which represent <div>hello world</div>
|
||||
result = qweb.render("sometemplate", { name: "world" });
|
||||
```
|
||||
|
||||
## QWeb extensions
|
||||
|
||||
- t-on directive
|
||||
- t-widget, t-props, t-key
|
||||
- t-ref
|
||||
|
||||
## Note on 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
|
||||
+46
-14
@@ -1,17 +1,49 @@
|
||||
# Odoo Web Lab Documentation
|
||||
# 🦉 Owl overview 🦉
|
||||
|
||||
Currently, this repository contains:
|
||||
Here is a list of everything exported by the Owl library:
|
||||
|
||||
- an implementation/extension of the QWeb template engine that outputs a virtual
|
||||
dom (using the snabbdom library)
|
||||
- a Component class, which uses the QWeb engine as its underlying rendering
|
||||
mechanism. The component class is designed to be declarative, with
|
||||
asynchronous rendering. Also, it uses snabbdom as the virtual dom library.
|
||||
- some utility functions/classes
|
||||
- a Store class and a connect function, to help manage the state of an application (like react-redux)
|
||||
Main entities:
|
||||
|
||||
- [Quick Start](quick_start.md)
|
||||
- [Tutorial](tutorial.md)
|
||||
- [Component](component.md)
|
||||
- [QWeb](qweb.md)
|
||||
- [State Management](state_management.md)
|
||||
- [`App`](reference/app.md): represent an Owl application (mainly a root component,a set of templates, and a config)
|
||||
- [`Component`](reference/component.md): the main class to define a concrete Owl component
|
||||
- [`mount`](reference/app.md#mount-helper): main entry point for most application: mount a component to a target
|
||||
- [`xml`](reference/templates.md#inline-templates): helper to define an inline template
|
||||
|
||||
Reactivity
|
||||
|
||||
- [`useState`](reference/reactivity.md#usestate): create a reactive object (hook, linked to a specific component)
|
||||
- [`reactive`](reference/reactivity.md#reactive): create a reactive object (not linked to any component)
|
||||
- [`markRaw`](reference/reactivity.md#markraw): mark an object or array so that it is ignored by the reactivity system
|
||||
- [`toRaw`](reference/reactivity.md#toraw): given a reactive objet, return the raw (non reactive) underlying object
|
||||
|
||||
Lifecycle hooks:
|
||||
|
||||
- [`onWillStart`](reference/component.md#willstart): hook to define asynchronous code that should be executed before component is rendered
|
||||
- [`onMounted`](reference/component.md#mounted): hook to define code that should be executed when component is mounted
|
||||
- [`onWillPatch`](reference/component.md#willpatch): hook to define code that should be executed before component is patched
|
||||
- [`onWillUpdateProps`](reference/component.md#willupdateprops): hook to define code that should be executed before component is updated
|
||||
- [`onPatched`](reference/component.md#patched): hook to define code that should be executed when component is patched
|
||||
- [`onWillRender`](reference/component.md#willrender): hook to define code that should be executed before component is rendered
|
||||
- [`onRendered`](reference/component.md#rendered): hook to define code that should be executed after component is rendered
|
||||
- [`onWillUnmount`](reference/component.md#willunmount): hook to define code that should be executed before component is unmounted
|
||||
- [`onWillDestroy`](reference/component.md#willdestroy): hook to define code that should be executed before component is destroyed
|
||||
- [`onError`](reference/component.md#onerror): hook to define a Owl error handler
|
||||
|
||||
Other hooks:
|
||||
|
||||
- [`useComponent`](reference/hooks.md#usecomponent): return a reference to the current component (useful to create derived hooks)
|
||||
- [`useEffect`](reference/hooks.md#useeffect): define an effect with its dependencies
|
||||
- [`useEnv`](reference/hooks.md#useenv): return a reference to the current env
|
||||
- [`useExternalListener`](reference/hooks.md#useexternallistener): add a listener outside of a component DOM
|
||||
- [`useRef`](reference/hooks.md#useref): get an object representing a reference (`t-ref`)
|
||||
- [`useChildSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for child components)
|
||||
- [`useSubEnv`](reference/hooks.md#usesubenv-and-usechildsubenv): extend the current env with additional information (for current component and child components)
|
||||
|
||||
Utility/helpers:
|
||||
|
||||
- [`EventBus`](reference/utils.md#eventbus): a simple event bus
|
||||
- [`loadFile`](reference/utils.md#loadfile): an helper to load a file from the server
|
||||
- [`markup`](reference/templates.md#outputting-data): utility function to define strings that represent html (should not be escaped)
|
||||
- [`status`](reference/component.md#status-helper): utility function to get the status of a component (new, mounted or destroyed)
|
||||
- [`validate`](reference/utils.md#validate): validates if an object satisfies a specified schema
|
||||
- [`whenReady`](reference/utils.md#whenready): utility function to execute code when DOM is ready
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# 🦉 App 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [API](#api)
|
||||
- [Configuration](#configuration)
|
||||
- [`mount` helper](#mount-helper)
|
||||
- [Loading templates](#loading-templates)
|
||||
|
||||
## Overview
|
||||
|
||||
Every Owl application has a root element, a set of templates, an environment and
|
||||
possibly a few other settings. The `App` class is a simple class that represents
|
||||
all of these elements. Here is an example:
|
||||
|
||||
```js
|
||||
const {Component, App } = owl;
|
||||
|
||||
class MyComponent extends Component { ... }
|
||||
|
||||
const app = new App(MyComponent, { props: {...}, templates: "..."});
|
||||
app.mount(document.body);
|
||||
```
|
||||
|
||||
The basic workflow is: create an `App` instance configured with the root
|
||||
component, the templates, and possibly other settings. Then, we mount that
|
||||
instance somewhere in the DOM.
|
||||
|
||||
## API
|
||||
|
||||
- **`constructor(Root[, config])`**: first argument should be a component class (not
|
||||
an instance), and the optional second argument is a configuration object (see below).
|
||||
|
||||
- **`mount(target, options)`**: first argument is an html element, and the optional
|
||||
second argument is an object with mounting options (see below). Mount the app
|
||||
to a target in the DOM. Note that this is an asynchronous operation: the `mount`
|
||||
method returns a promise that resolves to the component instance whenever it
|
||||
is complete.
|
||||
|
||||
The `option` object is an object with the following keys:
|
||||
|
||||
- **`position (string)`**: either `first-child` or `last-child`. This option determines
|
||||
the position of the application in the target: either first or last child.
|
||||
|
||||
- **`destroy()`**: destroys the application
|
||||
|
||||
## Configuration
|
||||
|
||||
The `config` object is an object with some of the following keys:
|
||||
|
||||
- **`env (object)`**: if given, this will be the shared `env` given to each component
|
||||
- **`props (object)`**: the props given to the root component
|
||||
- **`dev (boolean, default=false)`**: if `true`, the application is rendered in
|
||||
[`dev` mode](#dev-mode);
|
||||
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
|
||||
that Owl will not log a message to warn that Owl is in `dev` mode.
|
||||
- **`translatableAttributes (string[])`**: a list of additional attributes that should
|
||||
be translated (see [translations](translations.md))
|
||||
- **`translateFn (function)`**: a function that will be called by owl to translate
|
||||
templates (see [translations](translations.md))
|
||||
- **`templates (string | xml document)`**: all the templates that will be used by
|
||||
the components created by the application.
|
||||
- **`warnIfNoStaticProps (boolean, default=false)`**: if true, Owl will log a warning
|
||||
whenever it encounters a component that does not provide a [static props description](props.md#props-validation).
|
||||
|
||||
## `mount` helper
|
||||
|
||||
Note that there is a `mount` helper to do that in just a line:
|
||||
|
||||
```js
|
||||
const { mount, Component } = owl;
|
||||
|
||||
class MyComponent extends Component {
|
||||
...
|
||||
}
|
||||
|
||||
mount(MyComponent, document.body, { props: {...}, templates: "..."});
|
||||
```
|
||||
|
||||
Here is the `mount` function signature:
|
||||
|
||||
**`mount(Component, target, config)`** with the following arguments:
|
||||
|
||||
- **`Component`**: a component class (Root component of the app)
|
||||
- **`target`**: an html element, where the component will be mounted as last child
|
||||
- **`config (optional)`**: a config object (the same as the App config object)
|
||||
|
||||
Most of the time, the `mount` helper is more convenient, but whenever one needs
|
||||
a reference to the actual Owl App, then using the `App` class directly is
|
||||
possible.
|
||||
|
||||
## Loading templates
|
||||
|
||||
Most applications will need to load templates whenever they start. Here is
|
||||
what it could look like in practice:
|
||||
|
||||
```js
|
||||
// in the main js file:
|
||||
const { loadFile, mount } = owl;
|
||||
|
||||
// async, so we can use async/await
|
||||
(async function setup() {
|
||||
const templates = await loadFile(`/some/endpoint/that/return/templates`);
|
||||
const env = {
|
||||
_t: someTranslateFn,
|
||||
templates,
|
||||
// possibly other stuff
|
||||
};
|
||||
|
||||
mount(Root, document.body, { env });
|
||||
})();
|
||||
```
|
||||
|
||||
## Dev mode
|
||||
|
||||
Dev mode activates some additional checks and developer amenities:
|
||||
|
||||
- [Props validation](./props.md#props-validation) is performed
|
||||
- [t-foreach](./templates.md#loops) loops check for key unicity
|
||||
- Lifecycle hooks are wrapped to report their errors in a more developer-friendly way
|
||||
- onWillStart and onWillUpdateProps will emit a warning in the console when they
|
||||
take longer than 3 seconds in an effort to ease debugging the presence of deadlocks
|
||||
@@ -0,0 +1,455 @@
|
||||
# 🦉 Owl Component 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Properties and methods](#properties-and-methods)
|
||||
- [Static Properties](#static-properties)
|
||||
- [Lifecycle](#lifecycle)
|
||||
- [`setup`](#setup)
|
||||
- [`willStart`](#willstart)
|
||||
- [`willRender`](#willrender)
|
||||
- [`rendered`](#rendered)
|
||||
- [`mounted`](#mounted)
|
||||
- [`willUpdateProps`](#willupdateprops)
|
||||
- [`willPatch`](#willpatch)
|
||||
- [`patched`](#patched)
|
||||
- [`willUnmount`](#willunmount)
|
||||
- [`willDestroy`](#willdestroy)
|
||||
- [`onError`](#onerror)
|
||||
- [Sub components](#sub-components)
|
||||
- [Dynamic Sub components](#dynamic-sub-components)
|
||||
- [`status` helper](#status-helper)
|
||||
|
||||
## Overview
|
||||
|
||||
An Owl component is a small class which represents some part of the user interface.
|
||||
It is part of a component tree, and has an [environment](environment.md) (`env`),
|
||||
which is propagated from a parent to its children.
|
||||
|
||||
OWL components are defined by subclassing the `Component` class. For example,
|
||||
here is how a `Counter` component could be implemented:
|
||||
|
||||
```javascript
|
||||
const { Component, xml, useState } = owl;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, we use the `xml` helper to define inline templates, and the
|
||||
`useState` hook, which returns a reactive version of its argument (see the page
|
||||
on reactivity).
|
||||
|
||||
## Properties and methods
|
||||
|
||||
The `Component` class has a very small API.
|
||||
|
||||
- **`env (object)`**: the component [environment](environment.md)
|
||||
|
||||
- **`props (object)`**: this is an object containing all the [props](props.md) given by
|
||||
the parent to a child component
|
||||
|
||||
Note that `props` are owned by the parent, not by the component.
|
||||
As such, it should not ever be modified by the component (otherwise you risk
|
||||
unintended effects, since the parent may not be aware of the change)!!
|
||||
|
||||
The `props` can be modified dynamically by the parent. In that case, the
|
||||
component will go through the following lifecycle methods: `willUpdateProps`,
|
||||
`willPatch` and `patched`.
|
||||
|
||||
* **`render(deep[=false])`**: calling this method directly will cause a rerender. Note
|
||||
that with the reactivity system, this should be rare to have to do it manually.
|
||||
Also, the rendering operation is asynchronous, so the DOM will only be updated
|
||||
slightly later (at the next animation frame, if no component delays the
|
||||
rendering)
|
||||
|
||||
By default, the render initiated by this method will stop at each child
|
||||
component if their props are (shallow) equal. To force a render to update
|
||||
all child components, one can use the optional `deep` argument. Note that the
|
||||
value of the `deep` argument needs to be a boolean, not a truthy value.
|
||||
|
||||
## Static Properties
|
||||
|
||||
- **`template (string)`**: this is the name of the template that
|
||||
will render the component. Note that there is a helper `xml` to
|
||||
make it easy to define an inline template.
|
||||
|
||||
* **`components (object, optional)`**: if given, this is an object that contains
|
||||
the classes of any sub components needed by the template.
|
||||
|
||||
```js
|
||||
class ParentComponent extends owl.Component {
|
||||
static components = { SubComponent };
|
||||
}
|
||||
```
|
||||
|
||||
* **`props (object, optional)`**: if given, this is an object that describes the
|
||||
type and shape of the (actual) props given to the component. If Owl mode is
|
||||
`dev`, this will be used to validate the props each time the component is
|
||||
created/updated. See [Props Validation](props.md#props-validation) for more information.
|
||||
|
||||
```js
|
||||
class Counter extends owl.Component {
|
||||
static props = {
|
||||
initialValue: Number,
|
||||
optional: true,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- **`defaultProps (object, optional)`**: if given, this object define default
|
||||
values for (top-level) props. Whenever `props` are given to the object, they
|
||||
will be altered to add default value (if missing). Note that it does not
|
||||
change the initial object, a new object will be created instead. See
|
||||
[default props](props.md#default-props) for more information
|
||||
|
||||
```js
|
||||
class Counter extends owl.Component {
|
||||
static defaultProps = {
|
||||
initialValue: 0,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
A solid and robust component system needs a complete lifecycle system to help
|
||||
developers write components. Here is a complete description of the lifecycle of
|
||||
a Owl component:
|
||||
|
||||
| Method | Hook | Description |
|
||||
| --------------------------------------- | ------------------- | ---------------------------------------------------------------------- |
|
||||
| **[setup](#setup)** | none | setup |
|
||||
| **[willStart](#willstart)** | `onWillStart` | async, before first rendering |
|
||||
| **[willRender](#willrender)** | `onWillRender` | just before component is rendered |
|
||||
| **[rendered](#rendered)** | `onRendered` | just after component is rendered |
|
||||
| **[mounted](#mounted)** | `onMounted` | just after component is rendered and added to the DOM |
|
||||
| **[willUpdateProps](#willupdateprops)** | `onWillUpdateProps` | async, before props update |
|
||||
| **[willPatch](#willpatch)** | `onWillPatch` | just before the DOM is patched |
|
||||
| **[patched](#patched)** | `onPatched` | just after the DOM is patched |
|
||||
| **[willUnmount](#willunmount)** | `onWillUnmount` | just before removing component from DOM |
|
||||
| **[willDestroy](#willdestroy)** | `onWillDestroy` | just before component is destroyed |
|
||||
| **[error](#onerror)** | `onError` | catch and handle errors (see [error handling page](error_handling.md)) |
|
||||
|
||||
### `setup`
|
||||
|
||||
_setup_ is run just after the component is constructed. It is a lifecycle method,
|
||||
very similar to the _constructor_, except that it does not receive any argument.
|
||||
|
||||
It is the proper place to call hook functions. Note that one of the main reason to
|
||||
have the `setup` hook in the component lifecycle is to make it possible to
|
||||
monkey patch it. It is a common need in the Odoo ecosystem.
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
useSetupAutofocus();
|
||||
}
|
||||
```
|
||||
|
||||
### `willStart`
|
||||
|
||||
`willStart` is an asynchronous hook that can be implemented to
|
||||
perform some (most of the time asynchronous) action before the initial rendering of a component.
|
||||
|
||||
It will be called exactly once before the initial rendering. It is useful
|
||||
in some cases, for example, to load external assets (such as a JS library)
|
||||
before the component is rendered. Another use case is to load data from a server.
|
||||
|
||||
The `onWillStart` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
this.data = await this.loadData()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
At this point, the component is not yet rendered. Note that slow `willStart`
|
||||
code will slow down the rendering of the user interface. Therefore, some care
|
||||
should be made to make this method as fast as possible.
|
||||
|
||||
Note that if there are more than one `onWillStart` registered callback, then they
|
||||
will all be run in parallel.
|
||||
|
||||
### `willRender`
|
||||
|
||||
It is uncommon but it may happen that one need to execute code just before a
|
||||
component is rendered (more precisely, when its compiled template function is executed).
|
||||
To do that, one can use the `onWillRender` hook:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillRender(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`willRender` hooks are called just before rendering templates, parent first,
|
||||
then children.
|
||||
|
||||
### `rendered`
|
||||
|
||||
It is uncommon but it may happen that one need to execute code just after a
|
||||
component is rendered (more precisely, when its compiled template function is executed).
|
||||
To do that, one can use the `onRendered` hook:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onRendered(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`rendered` hooks are called just after rendering templates, parent first,
|
||||
then children. Note that at this moment, the actual DOM may not exist yet (if
|
||||
it is the first rendering), or is not updated yet. This will be dom in the next
|
||||
animation frame as soon as all the components are ready.
|
||||
|
||||
### `mounted`
|
||||
|
||||
The `mounted` hook is called each time a component is attached to the
|
||||
DOM, after the initial rendering. At this point, the component is considered
|
||||
_active_. This is a good place to add some listeners, or to interact with the
|
||||
DOM, if the component needs to perform some measure for example.
|
||||
|
||||
It is the opposite of `willUnmount`. If a component has been mounted, it will
|
||||
always be unmounted at some point in the future.
|
||||
|
||||
The mounted method will be called recursively on each of its children. First,
|
||||
children, then parents.
|
||||
|
||||
It is allowed (but not encouraged) to modify the state in the `mounted` hook.
|
||||
Doing so will cause a rerender, which will not be perceptible by the user, but
|
||||
will slightly slow down the component.
|
||||
|
||||
The `onMounted` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
// do something here
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### `willUpdateProps`
|
||||
|
||||
The `willUpdateProps` is an asynchronous hook, called just before new props
|
||||
are set. This is useful if the component needs to perform an asynchronous task,
|
||||
depending on the props (for example, assuming that the props are
|
||||
some record Id, fetching the record data).
|
||||
|
||||
The `onWillUpdateProps` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillUpdateProps(nextProps => {
|
||||
return this.loadData({id: nextProps.id});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Notice that it receives the next props for the component.
|
||||
|
||||
This hook is not called during the first render (but `willStart` is called
|
||||
and performs a similar job). Also, as most of the hooks, it is called in the
|
||||
usual order: parents first, then children.
|
||||
|
||||
### `willPatch`
|
||||
|
||||
The willPatch hook is called just before the DOM patching process starts.
|
||||
It is not called on the initial render. This is useful to read
|
||||
information from the DOM. For example, the current position of the
|
||||
scrollbar.
|
||||
|
||||
Note that modifying the state is not allowed here. This method is called just
|
||||
before an actual DOM patch, and is only intended to be used to save some local
|
||||
DOM state. Also, it will not be called if the component is not in the DOM.
|
||||
|
||||
The `onWillPatch` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillPatch(() => {
|
||||
this.scrollState = this.getScrollSTate();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `willPatch` is called in the usual parent->children order.
|
||||
|
||||
### `patched`
|
||||
|
||||
This hook is called whenever a component did actually update its DOM (most
|
||||
likely via a change in its state/props or environment).
|
||||
|
||||
This method is not called on the initial render. It is useful to interact
|
||||
with the DOM (for example, through an external library) whenever the
|
||||
component was patched. Note that this hook will not be called if the component is
|
||||
not in the DOM.
|
||||
|
||||
The `onPatched` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onPatched(() => {
|
||||
this.scrollState = this.getScrollSTate();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Updating the component state in this hook is possible, but not encouraged.
|
||||
One needs to be careful, because updates here will create an additional rendering, which in
|
||||
turn will cause other calls to the `patched` method. So, we need to be particularly
|
||||
careful at avoiding endless cycles.
|
||||
|
||||
Like `mounted`, the `patched` hook is called in the order: children first, then
|
||||
parent.
|
||||
|
||||
### `willUnmount`
|
||||
|
||||
`willUnmount` is a hook that is called each time just before a component is
|
||||
unmounted from the DOM. This is a good place to remove listeners, for example.
|
||||
|
||||
The `onWillUnmount` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
// add some listener
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
// remove listener
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
This is the opposite method of `mounted`. Note that if a component is destroyed
|
||||
before being mounted, the `willUnmount` method may not be called.
|
||||
|
||||
Parent `willUnmount` hooks will be called before children.
|
||||
|
||||
### `willDestroy`
|
||||
|
||||
Sometimes, components need to do some action in the `setup` and clean it up when
|
||||
they are inactive. However, the `willUnmount` hook is not appropriate for the
|
||||
cleaning operation, since the component may be destroyed before it has even been
|
||||
mounted. The `willDestroy` hook is useful in that situation, since it is always
|
||||
called.
|
||||
|
||||
The `onWillUnmount` hook is used to register a function that will be executed at
|
||||
this moment:
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onWillDestroy(() => {
|
||||
// do some cleanup
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The `willDestroy` hooks are first called on children, then on parents.
|
||||
|
||||
### `onError`
|
||||
|
||||
Sadly, it may happen that components crashes at runtime. This is an unfortunate
|
||||
reality, and this is why Owl needs to provide a way to handle these errors.
|
||||
|
||||
The `onError` hook is useful when we need to intercept and properly react
|
||||
to errors that occur in some sub components. See the page on
|
||||
[error handling](error_handling.md) for more detail.
|
||||
|
||||
```javascript
|
||||
setup() {
|
||||
onError(() => {
|
||||
// do something
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Sub components
|
||||
|
||||
It is convenient to define a component using other (sub) components. This is
|
||||
called composition, and is very powerful in practice. To do that in Owl, one
|
||||
can just use a tag starting with a capital letter in its template, and register
|
||||
the sub component class in its static `components` object:
|
||||
|
||||
```js
|
||||
class Child extends Component {
|
||||
static template = xml`<div>child component <t t-esc="props.value"/></div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child value="1"/>
|
||||
<Child value="2"/>
|
||||
</div>`;
|
||||
|
||||
static components = { Child };
|
||||
}
|
||||
```
|
||||
|
||||
This example also shows how one can pass information from the parent component
|
||||
to the child component, as props. See the [props section](props.md)
|
||||
for more information.
|
||||
|
||||
## Dynamic sub components
|
||||
|
||||
It is not common, but sometimes we need a dynamic component name. In this case,
|
||||
the `t-component` directive can also be used to accept dynamic values. This should
|
||||
be an expression that evaluates to a component class. For example:
|
||||
|
||||
```js
|
||||
class A extends Component {
|
||||
static template = xml`<div>child a</div>`;
|
||||
}
|
||||
class B extends Component {
|
||||
static template = xml`<span>child b</span>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<t t-component="myComponent"/>`;
|
||||
|
||||
state = useState({ child: "a" });
|
||||
|
||||
get myComponent() {
|
||||
return this.state.child === "a" ? A : B;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `status` helper
|
||||
|
||||
It is sometimes convenient to have a way to find out in which state a component
|
||||
is currently. To do that, one can use the `status` helper:
|
||||
|
||||
```js
|
||||
const { status } = owl;
|
||||
// assume component is an instance of a Component
|
||||
|
||||
console.log(status(component));
|
||||
// logs either:
|
||||
// - 'new', if the component is new and has not been mounted yet
|
||||
// - 'mounted', if the component is currently mounted
|
||||
// - 'destroyed' if the component is currently destroyed
|
||||
```
|
||||
@@ -0,0 +1,163 @@
|
||||
# 🦉 Concurrency Model 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Rendering Components](#rendering-components)
|
||||
- [Semantics](#semantics)
|
||||
- [Asynchronous Rendering](#asynchronous-rendering)
|
||||
|
||||
## Overview
|
||||
|
||||
Owl was designed from the very beginning with asynchronous components. This comes
|
||||
from the `willStart` and the `willUpdateProps` lifecycle hooks. With these
|
||||
asynchronous hooks, it is possible to build complex highly concurrent applications.
|
||||
|
||||
Owl concurrent mode has several benefits: it makes it possible to delay the
|
||||
rendering until some asynchronous operation is complete, it makes it possible
|
||||
to lazy load libraries, while keeping the previous screen completely functional.
|
||||
It is also good for performance reasons: Owl uses it to only apply the result of
|
||||
many different renderings only once in an animation frame. Owl can cancel
|
||||
a rendering that is no longer relevant, restart it, reuse it in some cases.
|
||||
|
||||
But even though using concurrency is quite simple (and is the default behaviour),
|
||||
asynchrony is difficult, because it introduces an additional dimension that
|
||||
vastly increase the complexity of an application. This section will explain
|
||||
how Owl manages this complexity, how concurrent rendering works in a general way.
|
||||
|
||||
## Rendering Components
|
||||
|
||||
The word _rendering_ is a little vague, so, let us explain more precisely the
|
||||
process by which Owl components are displayed on a screen.
|
||||
|
||||
When a component is mounted or updated, a new rendering is started. It has
|
||||
two phases: _virtual rendering_ and _patching_.
|
||||
|
||||
### Virtual rendering
|
||||
|
||||
This phase represent the process of rendering a template, in memory, which creates
|
||||
a virtual representation of the desired component html). The output of this phase is a
|
||||
virtual DOM.
|
||||
|
||||
It is asynchronous: each subcomponents needs to either be created (so, `willStart`
|
||||
will need to be called), or updated (which is done with the `willUpdateProps`
|
||||
method). This is completely a recursive process: a component is the root of a
|
||||
component tree, and each sub component needs to be (virtually) rendered.
|
||||
|
||||
### Patching
|
||||
|
||||
Once a rendering is complete, it will be applied on the next animation frame.
|
||||
This is done synchronously: the whole component tree is patched to the real
|
||||
DOM.
|
||||
|
||||
## Semantics
|
||||
|
||||
We give here an informal description of the way components are created/updated
|
||||
in an application. Here, ordered lists describe actions that are executed
|
||||
sequentially, bullet lists describe actions that are executed in parallel.
|
||||
|
||||
**Scenario 1: initial rendering** Imagine we want to render the following component tree:
|
||||
|
||||
```
|
||||
A
|
||||
/ \
|
||||
B C
|
||||
/ \
|
||||
D E
|
||||
```
|
||||
|
||||
Here is what happen whenever we mount the root
|
||||
component (with some code like `app.mount(document.body)`).
|
||||
|
||||
1. `willStart` is called on `A`
|
||||
|
||||
2. when it is done, template `A` is rendered.
|
||||
|
||||
- component `B` is created
|
||||
1. `willStart` is called on `B`
|
||||
2. template `B` is rendered
|
||||
- component `C` is created
|
||||
1. `willStart` is called on `C`
|
||||
2. template `C` is rendered
|
||||
- component `D` is created
|
||||
1. `willStart` is called on `D`
|
||||
2. template `D` is rendered
|
||||
- component `E` is created
|
||||
1. `willStart` is called on `E`
|
||||
2. template `E` is rendered
|
||||
|
||||
3. each components are patched into a detached DOM element, in the following order:
|
||||
`E`, `D`, `C`, `B`, `A`. (so the actual full DOM tree is created
|
||||
in one pass)
|
||||
|
||||
4. the component `A` root element is actually appended to `document.body`
|
||||
|
||||
5. The method `mounted` is called recursively on all components in the following
|
||||
order: `E`, `D`, `C`, `B`, `A`.
|
||||
|
||||
**Scenario 2: updating a component**. Now, let's assume that the user clicked on some
|
||||
button in `C`, and this results in a state update, which is supposed to:
|
||||
|
||||
- update `D`,
|
||||
- remove `E`,
|
||||
- add new component `F`.
|
||||
|
||||
So, the component tree should look like this:
|
||||
|
||||
```
|
||||
A
|
||||
/ \
|
||||
B C
|
||||
/ \
|
||||
D F
|
||||
```
|
||||
|
||||
Here is what Owl will do:
|
||||
|
||||
1. because of a state change, the method `render` is called on `C`
|
||||
2. template `C` is rendered again
|
||||
|
||||
- component `D` is updated:
|
||||
1. hook `willUpdateProps` is called on `D` (async)
|
||||
2. template `D` is rerendered
|
||||
- component `F` is created:
|
||||
1. hook `willStart` is called on `F` (async)
|
||||
2. template `F` is rendered
|
||||
|
||||
3. `willPatch` hooks are called recursively on components `C`, `D` (not on `F`,
|
||||
because it is not mounted yet)
|
||||
|
||||
4. components `F`, `D` are patched in that order
|
||||
|
||||
5. component `C` is patched, which will cause recursively:
|
||||
|
||||
1. `willUnmount` hook on `E`
|
||||
2. destruction of `E`,
|
||||
|
||||
6. `mounted` hook is called on `F`, `patched` hooks are called on `D`, `C`
|
||||
|
||||
Tags are very small helpers to make it easy to write inline templates. There is
|
||||
only one currently available tag: `xml`.
|
||||
|
||||
### Asynchronous Rendering
|
||||
|
||||
Working with asynchronous code always adds a lot of complexity to a system. Whenever
|
||||
different parts of a system are active at the same time, one needs to think
|
||||
carefully about all possible interactions. Clearly, this is also true for Owl
|
||||
components.
|
||||
|
||||
There are two different common problems with Owl asynchronous rendering model:
|
||||
|
||||
- any component can delay the rendering (initial and subsequent) of the whole
|
||||
application
|
||||
- for a given component, there are two independant situations that will trigger an
|
||||
asynchronous rerendering: a change in the state, or a change in the props.
|
||||
These changes may be done at different times, and Owl has no way of knowing
|
||||
how to reconcile the resulting renderings.
|
||||
|
||||
Here are a few tips on how to work with asynchronous components:
|
||||
|
||||
1. Minimize the use of asynchronous components!
|
||||
2. Lazy loading external libraries is a good use case for async rendering. This
|
||||
is mostly fine, because we can assume that it will only takes a fraction of a
|
||||
second, and only once.
|
||||
@@ -0,0 +1,76 @@
|
||||
# 🦉 Environment 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Setting an Environment](#setting-an-environment)
|
||||
- [Using a sub environment](#using-a-sub-environment)
|
||||
- [Content of an Environment](#content-of-an-environment)
|
||||
|
||||
## Overview
|
||||
|
||||
An environment is a shared object given to all components in a tree. It is not
|
||||
used by Owl itself, but it is useful for application developers to provide a
|
||||
simple communication channel between components (in addition to the props).
|
||||
|
||||
The `env` given to the [`App`](app.md) is assigned to the `env` component
|
||||
property.
|
||||
|
||||
```
|
||||
Root
|
||||
/ \
|
||||
A B
|
||||
```
|
||||
|
||||
Also, the `env` object is frozen when the application is started. This is done
|
||||
to ensure a simpler mental model of what's happening in runtime. Note that it
|
||||
is only shallowly frozen, so sub objects can be modified.
|
||||
|
||||
## Setting an environment
|
||||
|
||||
The correct way to customize an environment is to simply give it to the `App`,
|
||||
whenever it is created.
|
||||
|
||||
```js
|
||||
const env = {
|
||||
_t: myTranslateFunction,
|
||||
user: {...},
|
||||
services: {
|
||||
...
|
||||
},
|
||||
};
|
||||
|
||||
new App(Root, { env }).mount(document.body);
|
||||
|
||||
// or alternatively
|
||||
mount(App, document.body, { env });
|
||||
```
|
||||
|
||||
## Using a sub environment
|
||||
|
||||
It is sometimes useful to add one (or more) specific keys to the environment,
|
||||
from the perspective of a specific component and its children. In that case, the
|
||||
solution presented above will not work, since it sets the global environment.
|
||||
|
||||
There are two hooks for this situation: [`useSubEnv` and `useChildSubEnv`](hooks.md#usesubenv-and-usechildsubenv).
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
setup() {
|
||||
useSubEnv({ myKey: someValue }); // myKey is now available for all child components
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Content of an Environment
|
||||
|
||||
The `env` object content is totally up to the application developer. However,
|
||||
some good use cases for additional keys in the environment are:
|
||||
|
||||
- some configuration keys,
|
||||
- session information,
|
||||
- generic services (such as doing rpcs).
|
||||
- other utility functions that one want to inject, such as a translation function.
|
||||
|
||||
Doing it this way means that components are easily testable: we can simply
|
||||
create a test environment with mock services.
|
||||
@@ -0,0 +1,75 @@
|
||||
# 🦉 Error Handling 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Managing Errors](#managing-errors)
|
||||
- [Example](#example)
|
||||
|
||||
## Overview
|
||||
|
||||
By default, whenever an error occurs in the rendering of an Owl application, we
|
||||
destroy the whole application. Otherwise, we cannot offer any guarantee on the
|
||||
state of the resulting component tree. It might be hopelessly corrupted, but
|
||||
without any user-visible feedback.
|
||||
|
||||
Clearly, it is usually a little bit extreme to destroy the application. This
|
||||
is why we need a mechanism to handle rendering errors (and errors coming
|
||||
from lifecycle hooks): the `onError` hook.
|
||||
|
||||
The main idea is that the `onError` hook register a function that will be called
|
||||
with the error. This function need to handle the situation, most of the time by
|
||||
updating some state and rerendering itself, so the application can return to a
|
||||
normal state.
|
||||
|
||||
## Managing Errors
|
||||
|
||||
Whenever the `onError` lifecycle hook is used, all errors coming from
|
||||
sub components rendering and/or lifecycle method calls will be caught and given
|
||||
to the `onError` method. This allows us to properly handle the error, and to
|
||||
not break the application.
|
||||
|
||||
There are important things to know:
|
||||
|
||||
- If an error that occured in the internal rendering cycle is not caught, then
|
||||
Owl will destroy the full application. This is done on purpose, because Owl
|
||||
cannot guarantee that the state is not corrupted from this point on.
|
||||
|
||||
- errors coming from event handlers are NOT managed by `onError` or any other
|
||||
owl mechanism. This is up to the application developer to properly recover
|
||||
from an error
|
||||
|
||||
- if an error handler is unable to properly handle an error, it can just rethrow
|
||||
an error, and Owl will try looking for another error handler up the component
|
||||
tree.
|
||||
|
||||
## Example
|
||||
|
||||
For example, here is how we could implement a generic component `ErrorBoundary`
|
||||
that render its content, and a fallback if an error happened.
|
||||
|
||||
```js
|
||||
class ErrorBoundary extends Component {
|
||||
static template = xml`
|
||||
<t t-if="error" t-slot="fallback">An error occurred</t>
|
||||
<t t-else="" t-slot="content"`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ error: false });
|
||||
onError(() => (this.state.error = true));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using the `ErrorBoundary` is then simple simple:
|
||||
|
||||
```xml
|
||||
<ErrorBoundary>
|
||||
<SomeOtherComponent/>
|
||||
<t t-set-slot="fallback">Some specific error message</t>
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
Note that we need to be careful here: the fallback UI should not throw any
|
||||
error, otherwise we risk going into an infinite loop (also, see the page on
|
||||
[slots](slots.md) for more information on the `t-slot` directive).
|
||||
@@ -0,0 +1,119 @@
|
||||
# 🦉 Event Handling 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Event Handling](#event-handling)
|
||||
- [Modifiers](#modifiers)
|
||||
- [Synthetic Events](#synthetic-events)
|
||||
- [On Components](#on-components)
|
||||
|
||||
## Event Handling
|
||||
|
||||
In a component's template, it is useful to be able to register handlers on DOM
|
||||
elements to some specific events. This is what makes a template _alive_. This
|
||||
is done with the `t-on` directive. For example:
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
This will be roughly translated in javascript like this:
|
||||
|
||||
```js
|
||||
button.addEventListener("click", component.someMethod.bind(component));
|
||||
```
|
||||
|
||||
The suffix (`click` in this example) is simply the name of the actual DOM
|
||||
event. The value of the `t-on` expression should be a valid javascript expression
|
||||
that evaluates to a function in the context of the current component. So, one
|
||||
can get a reference to the event, or pass some additional arguments. For example,
|
||||
all the following expressions are valid:
|
||||
|
||||
```xml
|
||||
<button t-on-click="someMethod">Do something</button>
|
||||
<button t-on-click="() => this.increment(3)">Add 3</button>
|
||||
<button t-on-click="ev => this.doStuff(ev, 'value')">Do something</button>
|
||||
```
|
||||
|
||||
Notice the use of the `this` keyword in the lambda function: this is the
|
||||
correct way to call a method on the component in a lambda function.
|
||||
|
||||
One could use the following expression:
|
||||
|
||||
```xml
|
||||
<button t-on-click="() => increment(3)">Add 3</button>
|
||||
```
|
||||
|
||||
But then, the increment function may be unbound (unless the component binds it
|
||||
in its setup function, for example).
|
||||
|
||||
## Modifiers
|
||||
|
||||
In order to remove the DOM event details from the event handlers (like calls to
|
||||
`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
|
||||
specified as additional suffixes of the `t-on` directive.
|
||||
|
||||
| Modifier | Description |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `.stop` | calls `event.stopPropagation()` before calling the method |
|
||||
| `.prevent` | calls `event.preventDefault()` before calling the method |
|
||||
| `.self` | calls the method only if the `event.target` is the element itself |
|
||||
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
|
||||
| `.synthetic` | define a synthetic event handler (see below) |
|
||||
|
||||
```xml
|
||||
<button t-on-click.stop="someMethod">Do something</button>
|
||||
```
|
||||
|
||||
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
|
||||
the order may matter. For instance `t-on-click.prevent.self` will prevent all
|
||||
clicks while `t-on-click.self.prevent` will only prevent clicks on the element
|
||||
itself.
|
||||
|
||||
Finally, empty handlers are tolerated as they could be defined only to apply
|
||||
modifiers. For example,
|
||||
|
||||
```xml
|
||||
<button t-on-click.stop="">Do something</button>
|
||||
```
|
||||
|
||||
This will simply stop the propagation of the event.
|
||||
|
||||
## Synthetic Events
|
||||
|
||||
In some cases, attaching an event handler for each element of large lists has
|
||||
a non trivial cost. Owl provides a way to efficiently improve the performance:
|
||||
with synthetic event, it actually adds only one handler on the document body,
|
||||
and will properly call the handler, just as expected.
|
||||
|
||||
The only difference with regular events is that the event is caught at the document
|
||||
body, so it cannot be stopped before it actually gets there. Since it may be
|
||||
surprising in some cases, it is not enabled by default.
|
||||
|
||||
To enable it, one can just use the `.synthetic` suffix:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<t t-foreach="largeList" t-as="elem" t-key="elem.id">
|
||||
<button t-on-click.synthetic="doSomething" ...>
|
||||
<!-- some content -->
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
## On Components
|
||||
|
||||
The `t-on` directive also works on a child component:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
in some template
|
||||
<Child t-on-click="dosomething"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
This will catch all click events on any html element contained in the `Child`
|
||||
sub component. Note that if the child component is reduced to one (or more) text
|
||||
nodes, then clicking on it will not call the handler, since the event will be
|
||||
dispatched by the browser on the parent element (a `div` in this case).
|
||||
@@ -0,0 +1,325 @@
|
||||
# 🦉 Hooks 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [The Hook Rule](#the-hook-rule)
|
||||
- [Lifecycle hooks](#lifecycle-hooks)
|
||||
- [Other hooks](#other-hooks)
|
||||
- [`useState`](#usestate)
|
||||
- [`useRef`](#useref)
|
||||
- [`useSubEnv` and `useChildSubEnv`](#usesubenv-and-usechildsubenv)
|
||||
- [`useExternalListener`](#useexternallistener)
|
||||
- [`useComponent`](#usecomponent)
|
||||
- [`useEnv`](#useenv)
|
||||
- [`useEffect`](#useeffect)
|
||||
- [Example: Mouse Position](#example-mouse-position)
|
||||
|
||||
## Overview
|
||||
|
||||
Hooks were popularised by React as a way to solve the following issues:
|
||||
|
||||
- help reusing stateful logic between components
|
||||
- help organizing code by feature in complex components
|
||||
- use state in functional components, without writing a class.
|
||||
|
||||
Owl hooks serve the same purpose, except that they work for class components
|
||||
(note: React hooks do not work on class components, and maybe because of that,
|
||||
there seems to be the misconception that hooks are in opposition to class. This
|
||||
is clearly not true, as shown by Owl hooks).
|
||||
|
||||
Hooks work beautifully with Owl components: they solve the problems mentioned
|
||||
above, and in particular, they are the perfect way to make your component
|
||||
reactive.
|
||||
|
||||
## The Hook Rule
|
||||
|
||||
There is only one rule: every hook for a component has to be called in the _setup_ method, or in class fields:
|
||||
|
||||
```js
|
||||
// ok
|
||||
class SomeComponent extends Component {
|
||||
state = useState({ value: 0 });
|
||||
}
|
||||
|
||||
// also ok
|
||||
class SomeComponent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// not ok: this is executed after the constructor is called
|
||||
class SomeComponent extends Component {
|
||||
async willStart() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
All lifecycle hooks are documented in detail in their specific [section](component.md#lifecycle).
|
||||
|
||||
| Hook | Description |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| **[onWillStart](component.md#willstart)** | async, before first rendering |
|
||||
| **[onWillRender](component.md#willrender)** | just before component is rendered |
|
||||
| **[onRendered](component.md#rendered)** | just after component is rendered |
|
||||
| **[onMounted](component.md#mounted)** | just after component is rendered and added to the DOM |
|
||||
| **[onWillUpdateProps](component.md#willupdateprops)** | async, before props update |
|
||||
| **[onWillPatch](component.md#willpatch)** | just before the DOM is patched |
|
||||
| **[onPatched](component.md#patched)** | just after the DOM is patched |
|
||||
| **[onWillUnmount](component.md#willunmount)** | just before removing component from DOM |
|
||||
| **[onWillDestroy](component.md#willdestroy)** | just before component is destroyed |
|
||||
| **[onError](component.md#onerror)** | catch and handle errors (see [error handling page](error_handling.md)) |
|
||||
|
||||
## Other Hooks
|
||||
|
||||
### `useState`
|
||||
|
||||
The `useState` hook is certainly the most important hook for Owl components:
|
||||
this is what allows a component to be reactive, to react to state change.
|
||||
|
||||
The `useState` hook has to be given an object or an array, and will return
|
||||
an observed version of it (using a `Proxy`).
|
||||
|
||||
```javascript
|
||||
const { useState, Component } = owl;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<button t-on-click="increment">
|
||||
Click Me! [<t t-esc="state.value"/>]
|
||||
</button>`;
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It is important to remember that `useState` only works with objects or arrays. It
|
||||
is necessary, since Owl needs to react to a change in state.
|
||||
|
||||
### `useRef`
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
of a component, rendered by Owl. It only work on a html element tagged by the
|
||||
`t-ref` directive:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-ref="someDiv"/>
|
||||
<span>hello</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, the component will be able to access the `div` and the component
|
||||
`SubComponent` with the `useRef` hook:
|
||||
|
||||
```js
|
||||
class Parent extends Component {
|
||||
inputRef = useRef("someComponent");
|
||||
|
||||
someMethod() {
|
||||
// here, if component is mounted, refs are active:
|
||||
// - this.inputRef.el is the input HTMLElement
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As shown by the example above, the actual HTMLElement instance is accessed with
|
||||
the `el` key.
|
||||
|
||||
The `t-ref` directive also accepts dynamic values with string interpolation
|
||||
(like the [`t-attf-`](templates.md#dynamic-attributes) and
|
||||
`t-component` directives). For example,
|
||||
|
||||
```xml
|
||||
<div t-ref="component_{{someCondition ? '1' : '2'}}"/>
|
||||
```
|
||||
|
||||
Here, the references need to be set like this:
|
||||
|
||||
```js
|
||||
this.ref1 = useRef("component_1");
|
||||
this.ref2 = useRef("component_2");
|
||||
```
|
||||
|
||||
References are only guaranteed to be active while the parent component is mounted.
|
||||
If this is not the case, accessing `el` or `comp` on it will return `null`.
|
||||
|
||||
### `useSubEnv` and `useChildSubEnv`
|
||||
|
||||
The environment is sometimes useful to share some common information between
|
||||
all components. But sometimes, we want to _scope_ that knowledge to a subtree.
|
||||
|
||||
For example, if we have a form view component, maybe we would like to make some
|
||||
`model` object available to all sub components, but not to the whole application.
|
||||
This is where the `useChildSubEnv` hook may be useful: it lets a component add some
|
||||
information to the environment in a way that only its children
|
||||
can access it:
|
||||
|
||||
```js
|
||||
class FormComponent extends Component {
|
||||
setup() {
|
||||
const model = makeModel();
|
||||
// model will be available on this.env for this component and all children
|
||||
useSubEnv({ model });
|
||||
// someKey will be available on this.env for all children
|
||||
useChildSubEnv({ someKey: "value" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `useSubEnv` and `useChildSubEnv` hooks take one argument: an object which
|
||||
contains some key/value that will be added to the current environment. These hooks
|
||||
will create a new env object with the new information:
|
||||
|
||||
- `useSubEnv` will assign this new `env` to itself and to all children components
|
||||
- `useChildSubEnv` will only assign this new `env` to all children components.
|
||||
|
||||
As usual in Owl, [environments](environment.md) created with these two hooks are
|
||||
frozen, to prevent unwanted modifications.
|
||||
|
||||
Note that both these hooks can be called an arbitrary number of times. The `env`
|
||||
will then be updated accordingly.
|
||||
|
||||
### `useExternalListener`
|
||||
|
||||
The `useExternalListener` hook helps solve a very common problem: adding and removing
|
||||
a listener on some target whenever a component is mounted/unmounted. For example,
|
||||
a dropdown menu (or its parent) may need to listen to a `click` event on `window`
|
||||
to be closed:
|
||||
|
||||
```js
|
||||
useExternalListener(window, "click", this.closeMenu);
|
||||
```
|
||||
|
||||
### `useComponent`
|
||||
|
||||
The `useComponent` hook is useful as a building block for some customized hooks,
|
||||
that may need a reference to the component calling them.
|
||||
|
||||
```js
|
||||
function useSomething() {
|
||||
const component = useComponent();
|
||||
// now, component is bound to the instance of the current component
|
||||
}
|
||||
```
|
||||
|
||||
### `useEnv`
|
||||
|
||||
The `useEnv` hook is useful as a building block for some customized hooks,
|
||||
that may need a reference to the env of the component calling them.
|
||||
|
||||
```js
|
||||
function useSomething() {
|
||||
const env = useEnv();
|
||||
// now, env is bound to the env of the current component
|
||||
}
|
||||
```
|
||||
|
||||
### `useEffect`
|
||||
|
||||
This hook will run a callback when a component is mounted and patched, and
|
||||
will run a cleanup function before patching and before unmounting the
|
||||
the component (only if some dependencies have changed).
|
||||
|
||||
It has almost the same API as the React `useEffect` hook, except that the dependencies
|
||||
are defined by a function instead of just the dependencies.
|
||||
|
||||
The `useEffect` hook takes two function: the effect function and the dependency
|
||||
function. The effect function perform some task and return (optionally) a cleanup
|
||||
function. The dependency function returns a list of dependencies, these dependencies
|
||||
are passed as parameters in the effect function . If any of these
|
||||
dependencies changes, then the current effect will be cleaned up and reexecuted.
|
||||
|
||||
Here is an example without any dependencies:
|
||||
|
||||
```js
|
||||
useEffect(
|
||||
() => {
|
||||
window.addEventListener("mousemove", someHandler);
|
||||
return () => window.removeEventListener("mousemove", someHandler);
|
||||
},
|
||||
() => []
|
||||
);
|
||||
```
|
||||
|
||||
In the example above, the dependency list is empty, so the effect is only cleaned
|
||||
up when the component is unmounted.
|
||||
|
||||
If the dependency function is skipped, then the effect will be cleaned up and
|
||||
rerun at every patch.
|
||||
|
||||
Here is another example, of how one could implement a `useAutofocus` hook with
|
||||
the `useEffect` hook:
|
||||
|
||||
```js
|
||||
function useAutofocus(name) {
|
||||
let ref = useRef(name);
|
||||
useEffect(
|
||||
(el) => el && el.focus(),
|
||||
() => [ref.el]
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This hook takes the name of a valid `t-ref` directive, which should be present
|
||||
in the template. It then checks whenever the component is mounted or patched if
|
||||
the reference is not valid, and in this case, it will focus the node element.
|
||||
This hook can be used like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<input />
|
||||
<input t-ref="myinput"/>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
useAutofocus("myinput");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: mouse position
|
||||
|
||||
Here is the classical example of a non trivial hook to track the mouse position.
|
||||
|
||||
```js
|
||||
const { useState, onWillDestroy, Component } = owl;
|
||||
|
||||
// We define here a custom behaviour: this hook tracks the state of the mouse
|
||||
// position
|
||||
function useMouse() {
|
||||
const position = useState({ x: 0, y: 0 });
|
||||
|
||||
function update(e) {
|
||||
position.x = e.clientX;
|
||||
position.y = e.clientY;
|
||||
}
|
||||
window.addEventListener("mousemove", update);
|
||||
onWillDestroy(() => {
|
||||
window.removeEventListener("mousemove", update);
|
||||
});
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
// Main root component
|
||||
class Root extends Component {
|
||||
static template = xml`<div>Mouse: <t t-esc="mouse.x"/>, <t t-esc="mouse.y"/></div>`;
|
||||
|
||||
// this hooks is bound to the 'mouse' property.
|
||||
mouse = useMouse();
|
||||
}
|
||||
```
|
||||
|
||||
Note that we use the prefix `use` for hooks, just like in React. This is just
|
||||
a convention.
|
||||
@@ -0,0 +1,92 @@
|
||||
# 🦉 Form Input Bindings 🦉
|
||||
|
||||
It is very common to need to be able to read the value out of an html `input` (or
|
||||
`textarea`, or `select`) in order to use it (note: it does not need to be in a
|
||||
form!). A possible way to do this is to do it by hand:
|
||||
|
||||
```js
|
||||
class Form extends owl.Component {
|
||||
state = useState({ text: "" });
|
||||
|
||||
_updateInputValue(event) {
|
||||
this.state.text = event.target.value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-on-input="_updateInputValue" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
|
||||
This works. However, this requires a little bit of _plumbing_ code. Also, the
|
||||
plumbing code is slightly different if you need to interact with a checkbox,
|
||||
or with radio buttons, or with select tags.
|
||||
|
||||
To help with this situation, Owl has a builtin directive `t-model`: its value
|
||||
should be an observed value in the component (usually `state.someValue`). With
|
||||
the `t-model` directive, we can write a shorter code, equivalent to the previous
|
||||
example:
|
||||
|
||||
```js
|
||||
class Form extends owl.Component {
|
||||
state = { text: "" };
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-model="state.text" />
|
||||
<span t-esc="state.text" />
|
||||
</div>
|
||||
```
|
||||
|
||||
The `t-model` directive works with `<input>`, `<input type="checkbox">`,
|
||||
`<input type="radio">`, `<textarea>` and `<select>`:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<div>Text in an input: <input t-model="state.someVal"/></div>
|
||||
<div>Textarea: <textarea t-model="state.otherVal"/></div>
|
||||
<div>Boolean value: <input type="checkbox" t-model="state.someFlag"/></div>
|
||||
<div>Selection:
|
||||
<select t-model="state.color">
|
||||
<option value="">Select a color</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
Selection with radio buttons:
|
||||
<span>
|
||||
<input type="radio" name="color" id="red" value="red" t-model="state.color"/>
|
||||
<label for="red">Red</label>
|
||||
</span>
|
||||
<span>
|
||||
<input type="radio" name="color" id="blue" value="blue" t-model="state.color" />
|
||||
<label for="blue">Blue</label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Like event handling, the `t-model` directive accepts the following modifiers:
|
||||
|
||||
| Modifier | Description |
|
||||
| --------- | -------------------------------------------------------------------- |
|
||||
| `.lazy` | update the value on the `change` event (default is on `input` event) |
|
||||
| `.number` | try to parse the value to a number (using `parseFloat`) |
|
||||
| `.trim` | trim the resulting value |
|
||||
|
||||
For example:
|
||||
|
||||
```xml
|
||||
<input t-model.lazy="state.someVal" />
|
||||
```
|
||||
|
||||
These modifiers can be combined. For instance, `t-model.lazy.number` will only
|
||||
update a number whenever the change is done.
|
||||
|
||||
Note: the online playground has an example to show how it works.
|
||||
@@ -0,0 +1,17 @@
|
||||
# 🦉 Portal 🦉
|
||||
|
||||
It is sometimes useful to be able to render some content outside the boundaries
|
||||
of a component. To do that, Owl provides a special directive: `t-portal`:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>this is inside the component</div>
|
||||
<div t-portal="'body'">and this is outside</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
The `t-portal` directive takes a valid css selector as argument. The content of
|
||||
the portalled template will be mounted at the corresponding location. Note that
|
||||
Owl need to insert an empty text node at the location of the portalled content.
|
||||
@@ -0,0 +1,30 @@
|
||||
# 🦉 Precompiling templates 🦉
|
||||
|
||||
Owl is designed to be used by the Odoo javascript framework. Since Odoo handles
|
||||
its assets in its own non standard way, it was decided/assumed that Owl would
|
||||
compile templates at runtime.
|
||||
|
||||
However, in some cases, it is not optimal, or even worse, not possible to do that.
|
||||
For example, browser extensions do not allow javascript code to create a new
|
||||
function (using the `new Function(...)` syntax).
|
||||
|
||||
Therefore, in these cases, it is required to compile templates ahead of time. It
|
||||
is possible to do that in Owl, but the tooling is still rough. For now, the
|
||||
process is the following:
|
||||
|
||||
1. write your templates in xml files (with a `t-name` directive to declare the name
|
||||
of the template)
|
||||
2. Compile them in a `templates.js` file
|
||||
3. get the `owl.iife.runtime.js` file (which is a owl build without the compiler)
|
||||
4. bundle `owl.iife.runtime.js` and `template.js` with your assets (owl needs to
|
||||
be positioned before the templates)
|
||||
|
||||
Here is a more detailed explanation on how to compile xml files into a js file:
|
||||
|
||||
1. clone the owl repository locally
|
||||
2. `npm install` to install all the required tooling
|
||||
3. `npm run build:runtime` to build the `owl.iife.runtime.js` file
|
||||
4. `npm run build:compiler` to build the template compiler
|
||||
5. `npm run compile_templates -- path/to/your/templates` will scan your target
|
||||
folder, find all xml files, get all templates, compile them, and generate a
|
||||
`templates.js` file.
|
||||
@@ -0,0 +1,270 @@
|
||||
# 🦉 Props 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Definition](#definition)
|
||||
- [Binding function props](#binding-function-props)
|
||||
- [Dynamic Props](#dynamic-props)
|
||||
- [Default Props](#default-props)
|
||||
- [Props validation](#props-validation)
|
||||
- [Good Practices](#good-practices)
|
||||
|
||||
## Overview
|
||||
|
||||
In Owl, `props` (short for _properties_) is an object which contains every piece
|
||||
of data given to a component by its parent.
|
||||
|
||||
```js
|
||||
class Child extends Component {
|
||||
static template = xml`<div><t t-esc="props.a"/><t t-esc="props.b"/></div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><Child a="state.a" b="'string'"/></div>`;
|
||||
static components = { Child };
|
||||
state = useState({ a: "fromparent" });
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the `Child` component receives two props from its parent: `a`
|
||||
and `b`. They are collected into a `props` object by Owl, with each value being
|
||||
evaluated in the context of the parent. So, `props.a` is equal to `'fromparent'` and
|
||||
`props.b` is equal to `'string'`.
|
||||
|
||||
Note that `props` is an object that only makes sense from the perspective of the
|
||||
child component.
|
||||
|
||||
## Definition
|
||||
|
||||
The `props` object is made of every attributes defined on the template, with the
|
||||
following exceptions:
|
||||
|
||||
- every attribute starting with `t-` are not props (they are QWeb directives),
|
||||
|
||||
In the following example:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<ComponentA a="state.a" b="'string'"/>
|
||||
<ComponentB t-if="state.flag" model="model"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
the `props` object contains the following keys:
|
||||
|
||||
- for `ComponentA`: `a` and `b`,
|
||||
- for `ComponentB`: `model`,
|
||||
|
||||
## Binding function props
|
||||
|
||||
It is common to have the need to pass a callback as a prop. Since Owl components
|
||||
are class based, the callback frequently needs to be bound to its owner component.
|
||||
So, one can do this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child callback="doSomething"/>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.doSomething = this.doSomething.bind(this);
|
||||
}
|
||||
|
||||
doSomething() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, this is such a common use case that Owl provides a special suffix to do
|
||||
just that: `.bind`. This looks like this:
|
||||
|
||||
```js
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Child callback.bind="doSomething"/>
|
||||
</div>`;
|
||||
|
||||
doSomething() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Props
|
||||
|
||||
The `t-props` directive can be used to specify totally dynamic props:
|
||||
|
||||
```xml
|
||||
<div t-name="ParentComponent">
|
||||
<Child t-props="some.obj"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
class ParentComponent {
|
||||
static components = { Child };
|
||||
some = { obj: { a: 1, b: 2 } };
|
||||
}
|
||||
```
|
||||
|
||||
## Default Props
|
||||
|
||||
If the static `defaultProps` property is defined, it will be used to complete
|
||||
props received by the parent, if missing.
|
||||
|
||||
```js
|
||||
class Counter extends owl.Component {
|
||||
static defaultProps = {
|
||||
initialValue: 0,
|
||||
};
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, the `initialValue` props is now by default set to 0.
|
||||
|
||||
## Props Validation
|
||||
|
||||
As an application becomes complex, it may be quite unsafe to define props in an informal way. This leads to two issues:
|
||||
|
||||
- hard to tell how a component should be used, by looking at its code.
|
||||
- unsafe, it is easy to send wrong props into a component, either by refactoring a component, or one of its parents.
|
||||
|
||||
A props type system solves both issues, by describing the types and shapes
|
||||
of the props. Here is how it works in Owl:
|
||||
|
||||
- `props` key is a static key (so, different from `this.props` in a component instance)
|
||||
- it is optional: it is ok for a component to not define a `props` key.
|
||||
- props are validated whenever a component is created/updated
|
||||
- props are only validated in `dev` mode (see [how to configure an app](app.md#configuration))
|
||||
- if a key does not match the description, an error is thrown
|
||||
- it validates keys defined in (static) `props`. Additional keys given by the
|
||||
parent will cause an error (unless the special prop `*` is present).
|
||||
- it is an object or a list of strings
|
||||
- a list of strings is a simplified props definition, which only lists the name
|
||||
of the props. Also, if the name ends with `?`, it is considered optional.
|
||||
- all props are by default required, unless they are defined with `optional: true`
|
||||
(in that case, it is only done if there is a value)
|
||||
- valid types are: `Number, String, Boolean, Object, Array, Date, Function`, and all
|
||||
constructor functions (so, if you have a `Person` class, it can be used as a type)
|
||||
- arrays are homogeneous (all elements have the same type/shape)
|
||||
|
||||
For each key, a `prop` definition is either a boolean, a constructor, a list of constructors, or an object:
|
||||
|
||||
- a boolean: indicate that the props exists, and is mandatory.
|
||||
- a constructor: this should describe the type, for example: `id: Number` describe
|
||||
the props `id` as a number
|
||||
- an object describing a value as type. This is done by using the `value` key. For example, `{value: false}` specifies that the corresponding value should be equal to false.
|
||||
- a list of constructors. In that case, this means that we allow more than one
|
||||
type. For example, `id: [Number, String]` means that `id` can be either a string
|
||||
or a number.
|
||||
- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory):
|
||||
- `type`: the main type of the prop being validated
|
||||
- `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements,
|
||||
- `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements,
|
||||
- `validate`: this is a function which should return a boolean to determine if
|
||||
the value is valid or not. Useful for custom validation logic.
|
||||
- `optional`: if true, the prop is not mandatory
|
||||
|
||||
There is a special `*` prop that means that additional prop are allowed. This is
|
||||
sometimes useful for generic components that will propagate some or all their
|
||||
props to their child components.
|
||||
|
||||
Note that default values cannot be defined for a mandatory props. Doing so will
|
||||
result in a prop validation error.
|
||||
|
||||
Examples:
|
||||
|
||||
```js
|
||||
class ComponentA extends owl.Component {
|
||||
static props = ['id', 'url'];
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
class ComponentB extends owl.Component {
|
||||
static props = {
|
||||
count: {type: Number},
|
||||
messages: {
|
||||
type: Array,
|
||||
element: {type: Object, shape: {id: Boolean, text: String }
|
||||
},
|
||||
date: Date,
|
||||
combinedVal: [Number, Boolean],
|
||||
optionalProp: { type: Number, optional: true }
|
||||
};
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented
|
||||
static props = ['message', 'id', 'date'];
|
||||
```
|
||||
|
||||
```js
|
||||
// only the existence of those 3 keys is documented. any other key is allowed.
|
||||
static props = ['message', 'id', 'date', '*'];
|
||||
```
|
||||
|
||||
```js
|
||||
// size is optional
|
||||
static props = ['message', 'size?'];
|
||||
```
|
||||
|
||||
```js
|
||||
static props = {
|
||||
messageIds: {type: Array, element: Number}, // list of number
|
||||
otherArr: {type: Array}, // just array. no validation is made on sub elements
|
||||
otherArr2: Array, // same as otherArr
|
||||
someObj: {type: Object}, // just an object, no internal validation
|
||||
someObj2: {
|
||||
type: Object,
|
||||
shape: {
|
||||
id: Number,
|
||||
name: {type: String, optional: true},
|
||||
url: String
|
||||
]}, // object, with keys id (number), name (string, optional) and url (string)
|
||||
someFlag: Boolean, // a boolean, mandatory (even if `false`)
|
||||
someVal: [Boolean, Date], // either a boolean or a date
|
||||
otherValue: true, // indicates that it is a prop
|
||||
kindofsmallnumber: {
|
||||
type: Number,
|
||||
validate: n => (0 <= n && n <= 10)
|
||||
},
|
||||
size: {
|
||||
validate: e => ["small", "medium", "large"].includes(e)
|
||||
},
|
||||
someId: [Number, {value: false}], // either a number or false
|
||||
};
|
||||
```
|
||||
|
||||
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
||||
|
||||
## Good Practices
|
||||
|
||||
A `props` object is a collection of values that come from the parent. As such,
|
||||
they are owned by the parent, and should never be modified by the child:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
props.a.b = 43; // Never do that!!!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Props should be considered readonly, from the perspective of the child component.
|
||||
If there is a need to modify them, then the request to update them should be
|
||||
sent to the parent (for example, with an event).
|
||||
|
||||
Any value can go in a props. Strings, objects, classes, or even callbacks could
|
||||
be given to a child component (but then, in the case of callbacks, communicating
|
||||
with events seems more appropriate).
|
||||
@@ -0,0 +1,395 @@
|
||||
# 🦉 Reactivity 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [`useState`](#usestate)
|
||||
- [`reactive`](#reactive)
|
||||
- [`Escape hatches`](#escape-hatches)
|
||||
- [`Advanced usage`](#advanced-usage)
|
||||
|
||||
## Introduction
|
||||
|
||||
Reactivity is a big topic in javascript frameworks. The goal is to provide a
|
||||
simple way to manipulate state, in such a way that the interface updates automatically
|
||||
according to state changes, and to do so in a performant manner.
|
||||
|
||||
To this end, Owl provides a proxy-based reactivity system, based on the `reactive` primitive.
|
||||
The `reactive` function takes an object as a first argument, and an optional callback as its second
|
||||
argument, it returns a proxy of the object. This proxy tracks what properties are read
|
||||
through the proxy, and calls the provided callback whenever one of these properties is changed
|
||||
through any reactive version of the same object. It does so in depth, by returning reactive versions
|
||||
of the subobjects when they are read.
|
||||
|
||||
## `useState`
|
||||
|
||||
While the `reactive` primitive is very powerful, its usage in components follow a very standard pattern:
|
||||
components want to be rerendered when part of the state which they depend on for rendering changes. To
|
||||
this end, owl provides a standard hook: `useState`. To put it simply, this hook simply calls reactive
|
||||
with the provided object, and the current component's render function as its callback. This will cause
|
||||
it to rerender whenever any part of the state object that has been read by this component is modified.
|
||||
|
||||
Here is a simple example of how `useState` can be used:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="() => this.state.value++">
|
||||
<t t-esc="state.value"/>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This component reads `state.value` when it renders, subscribing it to changes to that key. Whenever
|
||||
the value changes, Owl will update the component. Note that there is nothing special about the
|
||||
`state` property, you can name your state variables whatever you want, and you can have multiple of
|
||||
them on the same component if it makes sense to do so. This also allows `useState` to be used in custom
|
||||
hooks that may require state that is specific to that hook.
|
||||
|
||||
### Reactive props
|
||||
|
||||
Since version 2.0, Owl renders are no longer "deep" by default: a component is only rerendered by its
|
||||
parent if its props have changed (using a simple equality test). What if the contents of a props have
|
||||
changed in a deeper property? If that prop is reactive, owl will rerender the child components that
|
||||
need to be updated automatically, and only those components, it does so by reobserving reactive
|
||||
objects passed as props to components. Consider the following example:
|
||||
|
||||
```js
|
||||
class Counter extends Component {
|
||||
static template = xml`
|
||||
<div t-on-click="() => props.state.value++">
|
||||
<t t-esc="props.state.value"/>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Counter state="this.state"/>
|
||||
<button t-on-click="() => this.state.value = 0">Reset counter</button>
|
||||
<button t-on-click="() => this.state.test++" t-esc="this.state.test"/>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ value: 0, test: 1 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When clicking on the counter button, only the Counter rerenders, because the Parent has never read
|
||||
the "value" key in the state. When clicking on the "Reset Counter" button, the same thing happens:
|
||||
only the Counter component rerenders. What matters is not _where_ the state is updated, but which
|
||||
parts of the state are updated, and which components depend on them. This is achieved by Owl by
|
||||
automatically calling `useState` on reactive objects passed as props to a child component.
|
||||
|
||||
When clicking on the last button, the parent is rerendered, but the child does not care about the
|
||||
`test` key: it has not read it. The props that we give it (`this.state`) have also not changed,
|
||||
as such, the parent updates but the child doesn't.
|
||||
|
||||
For most day-to-day operations, `useState` should cover all of your needs. If
|
||||
you are curious about more advanced use cases and technical details, read on.
|
||||
|
||||
### Debugging subscriptions
|
||||
|
||||
Owl provides a way to show which reactive objects and keys a component is subscribed to: you can
|
||||
look at `component.__owl__.subscriptions`. Note that this is on the internal `__owl__` field, and
|
||||
should not be used in any type of production code as the name of this property or any of its properties
|
||||
or methods are subject to change at any point, even in stable versions of Owl, and may become available
|
||||
only in debug mode in the future.
|
||||
|
||||
## `reactive`
|
||||
|
||||
The `reactive` function is the basic reactivity primitive. It takes an object
|
||||
or an array as first argument, and optionally, a function as the second argument.
|
||||
The function is called whenever any tracked value is updated.
|
||||
|
||||
```js
|
||||
const obj = reactive({ a: 1 }, () => console.log("changed"));
|
||||
|
||||
obj.a = 2; // does not log anything: the 'a' key has not been read yet
|
||||
console.log(obj.a); // logs 2 and reads the 'a' key => it is now tracked
|
||||
obj.a = 3; // logs 'changed' because we updated a tracked value
|
||||
```
|
||||
|
||||
An important property of reactive objects is that they can be reobserved: this
|
||||
will create an independent proxy that tracks another set of keys:
|
||||
|
||||
```js
|
||||
const obj1 = reactive({ a: 1, b: 2 }, () => console.log("observer 1"));
|
||||
const obj2 = reactive(obj1, () => console.log("observer 2"));
|
||||
|
||||
console.log(obj1.a); // logs 1, and reads the 'a' key => it is now tracked by observer 1
|
||||
console.log(obj2.b); // logs 2, and 'b' is now tracked by observer 2
|
||||
obj2.a = 3; // only logs 'observer1', because observer2 does not track a
|
||||
obj2.b = 3; // only logs 'observer2', because observer1 does not track b
|
||||
console.log(obj2.a, obj1.b); // logs 3 and 3, while the object is observed independently, it is still a single object
|
||||
```
|
||||
|
||||
Because `useState` returns a normal reactive object, it is possible to call `reactive` on the result
|
||||
of a `useState` to observe changes to that object while outside the context of a component, or to
|
||||
call `useState` on reactive objects created outside of components. In those cases, one needs to be
|
||||
careful with regards to the lifetime of those reactive objects, as holding references to these
|
||||
objects may prevent garbage collection of the component and its data even if Owl has destroyed it.
|
||||
|
||||
### Subscriptions are ephemereal
|
||||
|
||||
Subscription to state changes are ephemereal, whenever an observer is notified that a state object
|
||||
has changed, all of its subscriptions are cleared, meaning that if it still cares about it, it
|
||||
should read the properties it cares about again. For example:
|
||||
|
||||
```js
|
||||
const obj = reactive({ a: 1 }, () => console.log("observer called"));
|
||||
|
||||
console.log(obj.a); // logs 1, and reads the 'a' key => it is now tracked by the observer
|
||||
obj.a = 3; // logs 'observer1' and clears the subscriptions of the observer
|
||||
obj.a = 4; // doesn't log anything, the key is no longer observed
|
||||
```
|
||||
|
||||
This may seem counter-intuitive, but it makes perfect sense in the context of components:
|
||||
|
||||
```js
|
||||
class DoubleCounter extends Component {
|
||||
static template = xml`
|
||||
<t t-esc="state.selected + ': ' + state[state.selected].value"/>
|
||||
<button t-on-click="() => this.state.count1++">increment count 1</button>
|
||||
<button t-on-click="() => this.state.count2++">increment count 2</button>
|
||||
<button t-on-click="changeCounter">Switch counter</button>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ selected: "count1", count1: 0, count2: 0 });
|
||||
}
|
||||
|
||||
changeCounter() {
|
||||
this.state.selected = this.state.selected === "count1" ? "count2" : "count1";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this component, if we increment the value of the second counter, the component will not rerender,
|
||||
which makes sense as rerendering will have no effect, as the second counter is not displayed. If we
|
||||
toggle the component to display the second counter, we now no longer want the component to rerender
|
||||
when the value of the first counter changes, and this is what happens: a component only rerenders
|
||||
when there are changes to pieces of state that have been read during or after the previous render.
|
||||
If a piece of state has not been read in the last render, we know that its value won't influence the
|
||||
rendered output, and so we can ignore it.
|
||||
|
||||
### reactive `Map` and `Set`
|
||||
|
||||
The reactivity system has special support built-in for the standard container types `Map` and `Set`.
|
||||
They behave like one would expect: reading a key subscribes the observer to that key, adding or
|
||||
removing an item to them notifies observers that have used any of the iterators on that reactive
|
||||
object, such as `.entries()` or `.keys()`, likewise with clearing them.
|
||||
|
||||
## Escape hatches
|
||||
|
||||
Sometimes, it is desirable to bypass the reactivity system. Creating proxies when interacting with
|
||||
reactive objects is expensive, and while on the whole, the performance benefit that we get by
|
||||
rerendering only the parts of the interface that need it outweighs that cost, in some cases, we want
|
||||
to be able to opt out of creating them in the first place. This is the purpose of `markRaw`:
|
||||
|
||||
### `markRaw`
|
||||
|
||||
Marks an object so that it is ignored by the reactivity system, meaning that if this object is ever
|
||||
part of a of a reactive object, it will be returned as is, and no keys in that object will be
|
||||
observed.
|
||||
|
||||
```js
|
||||
const someObject = markRaw({ b: 1 });
|
||||
const state = useState({
|
||||
a: 1,
|
||||
obj: someObject,
|
||||
});
|
||||
console.log(state.obj.b); // attempt to subscribe to the "b" key in someObject
|
||||
state.obj.b = 2; // No rerender will occur here
|
||||
console.log(someObject === state.obj); // true
|
||||
```
|
||||
|
||||
This is useful in some rare cases. One such example would be if you want to use an array of objects
|
||||
that is potentially large to render a list, but those objects are known to be immutable:
|
||||
|
||||
```js
|
||||
this.items = useState([
|
||||
{ label: "some text", value: 42 },
|
||||
// ... 1000 total objects
|
||||
]);
|
||||
```
|
||||
|
||||
in the template:
|
||||
|
||||
```xml
|
||||
<t t-foreach="items" t-as="item" t-key="item.label" t-esc="item.label + item.value"/>
|
||||
```
|
||||
|
||||
Here, on every render, we go and read one thousand keys from a reactive object, which causes
|
||||
one thousand reactive objects to be created. If we know that the content of these objects
|
||||
cannot change, this is wasted work. If instead all of these objects are marked as raw, we avoid
|
||||
all of this work while keeping the ability to lean on the reactivity to track the presence and
|
||||
identity of these objects:
|
||||
|
||||
```js
|
||||
this.items = useState([
|
||||
markRaw({ label: "some text", value: 42 }),
|
||||
// ... 1000 total objects
|
||||
]);
|
||||
```
|
||||
|
||||
However, use this function with caution: this is an escape hatch from the reactivity
|
||||
system, and as such, using it may cause subtle and unintended issues! For example:
|
||||
|
||||
```js
|
||||
// This will cause a rerender
|
||||
this.items.push(markRaw({ label: "another label", value: 1337 }));
|
||||
|
||||
// THIS WILL NOT CAUSE A RENDER!
|
||||
this.items[17].value = 3;
|
||||
// The UI is now desynced from component's state until the next render caused by something else
|
||||
```
|
||||
|
||||
In short: only use `markRaw` if your application is slowing down noticeably and profiling reveals
|
||||
that a lot of time is spent creating useless reactive objects.
|
||||
|
||||
### `toRaw`
|
||||
|
||||
While `markRaw` marks an object so that it is never made reactive, `toRaw` takes an object and
|
||||
returns the underlying non-reactive object. It can be useful in some niche cases. In particular,
|
||||
because the reactivity system returns a proxy, the returned object does not compare equal to the
|
||||
original object:
|
||||
|
||||
```js
|
||||
const obj = {};
|
||||
const reactiveObj = reactive(obj);
|
||||
console.log(obj === reactiveObj); // false
|
||||
console.log(obj === toRaw(reactiveObj)); // true
|
||||
```
|
||||
|
||||
It can also be useful during debugging, as unfolding proxies recursively in debuggers can be confusing.
|
||||
|
||||
## Advanced usage
|
||||
|
||||
The following is a collection of small snippets that leverage the reactivity system in
|
||||
"non-standard" ways to help you understand its power and where using it might make your code simpler.
|
||||
|
||||
### Notification manager
|
||||
|
||||
Showing notifications is a pretty common need in web applications, you may want to show a
|
||||
notification from any other component within the application, and the notifications should stack on
|
||||
top of one another regardless of which component spawned them, here is how we can leverage the
|
||||
reactivity to accomplish this:
|
||||
|
||||
```js
|
||||
let notificationId = 1;
|
||||
const notifications = reactive({});
|
||||
class NotificationContainer extends Component {
|
||||
static template = xml`
|
||||
<t t-foreach="notifications" t-as="notification" t-key="notification_key" t-esc="notification"/>
|
||||
`;
|
||||
setup() {
|
||||
this.notifications = useState(notifications);
|
||||
}
|
||||
}
|
||||
|
||||
export function addNotification(label) {
|
||||
const id = notificationId++;
|
||||
notifications[id] = label;
|
||||
return () => {
|
||||
delete notifications[id];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Here, the `notifications` variable is a reactive object. Notice how we didn't give `reactive` a
|
||||
callback: this is because in this case, all we care about is that adding or removing notifications
|
||||
in the `addNotification` function goes through the reactivity system. The `NotificationContainer`
|
||||
component reobserves this object with `useState`, and is updated whenever notifications are
|
||||
added or removed.
|
||||
|
||||
### Store
|
||||
|
||||
Centralizing application state is a pretty common want/need in web applications. Because of the way
|
||||
the reactivity system works, you can treat any reactive object as a store, and if you call `useState`
|
||||
on it, components automatically observe only the part of the store that they're interested in:
|
||||
|
||||
```js
|
||||
export const store = reactive({
|
||||
list: [],
|
||||
add(item) {
|
||||
this.list.push(item);
|
||||
},
|
||||
});
|
||||
|
||||
export function useStore() {
|
||||
return useState(store);
|
||||
}
|
||||
```
|
||||
|
||||
In any component:
|
||||
|
||||
```js
|
||||
import { useStore } from "./store";
|
||||
|
||||
class List extends Component {
|
||||
static template = xml`
|
||||
<t t-foreach="store.list" t-as="item" t-key="item" t-esc="item"/>
|
||||
`;
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Anywhere in the application:
|
||||
|
||||
```js
|
||||
import { store } from "./store";
|
||||
// Will cause any instance of the List component in the app to update
|
||||
store.add("New list item!");
|
||||
```
|
||||
|
||||
Notice how we can make objects with methods into reactive objects, and when these methods are used
|
||||
to mutate the store contents, it works as expected. And while stores are generally one-off objects,
|
||||
it is entirely possible to make class instances reactive:
|
||||
|
||||
```js
|
||||
class Store {
|
||||
list = [];
|
||||
add(item) {
|
||||
this.list.push(item);
|
||||
}
|
||||
}
|
||||
// Essentially equivalent to the previous code
|
||||
export const store = reactive(new Store());
|
||||
```
|
||||
|
||||
Which can be useful to unit test the class separately.
|
||||
|
||||
### Local storage synchronization
|
||||
|
||||
Sometimes, you want to persist some state accross reloads, you can do this by storing it in the
|
||||
`localStorage`, but what if you want to update the `localStorage` item every time the state changes,
|
||||
so that you don't have to manually synchronize the states? Well, you can use the reactivity system
|
||||
to write a custom hook that will do that for you:
|
||||
|
||||
```js
|
||||
function useStoredState(key, initialState) {
|
||||
const state = JSON.parse(localStorage.getItem(key)) || initialState;
|
||||
const store = (obj) => localStorage.setItem(key, JSON.stringify(obj));
|
||||
const reactiveState = reactive(state, () => store(reactiveState));
|
||||
store(reactiveState);
|
||||
return useState(state);
|
||||
}
|
||||
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.state = useStoredState("MyComponent.state", { value: 1 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
One important thing to notice is that both times we call `store`, we call it with `reactiveState`,
|
||||
not `state`: we need `store` to read the keys through a reactive object for it to correctly
|
||||
subscribe to state changes. Notice also that we call `store` the first time by hand, as otherwise it
|
||||
will not be subscribed to anything, and no amount of change in the object will cause the reactive
|
||||
callback to be invoked.
|
||||
@@ -0,0 +1,37 @@
|
||||
# 🦉 References 🦉
|
||||
|
||||
The `useRef` hook is useful when we need a way to interact with some inside part
|
||||
of a component, rendered by Owl. It can work either on a DOM node, or on a component,
|
||||
targeted by the `t-ref` directive. See the [hooks section](hooks.md#useref) for
|
||||
more detail.
|
||||
|
||||
As a short example, here is how we could set the focus on a given input:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<input t-ref="input"/>
|
||||
<button t-on-click="focusInput">Click</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
import { useRef } from "owl/hooks";
|
||||
|
||||
class SomeComponent extends Component {
|
||||
inputRef = useRef("input");
|
||||
|
||||
focusInput() {
|
||||
this.inputRef.el.focus();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Be aware that the `el` property will only be set when the target of the `t-ref`
|
||||
directive is mounted in the DOM. Otherwise, it will be set to `null`.
|
||||
|
||||
The `useRef` hook cannot be used to get a reference to an instance of a sub
|
||||
component.
|
||||
|
||||
Note that this example uses the suffix `ref` to name the reference. This
|
||||
is not mandatory, but it is a useful convention, so we do not forget that it is
|
||||
a reference object.
|
||||
@@ -0,0 +1,260 @@
|
||||
# 🦉 Slots 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Named slots](#named-slots)
|
||||
- [Rendering Context](#rendering-context)
|
||||
- [Default Slot](#default-slot)
|
||||
- [Default Content](#default-content)
|
||||
- [Dynamic slots](#dynamic-slots)
|
||||
- [Slots and props](#slots-and-props)
|
||||
- [Slot params](#slot-params)
|
||||
- [Slot scopes](#slot-scopes)
|
||||
|
||||
## Overview
|
||||
|
||||
Owl is a template based component system. There is therefore a need to be able
|
||||
to make generic components. For example, imagine a generic `Navbar`
|
||||
component, which displays a navbar, but with some customizable content. Since
|
||||
the specific content is only known to the user of the `Navbar`, it would be nice
|
||||
to specify it in the template where `Navbar` is used:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<Navbar>
|
||||
<span>Hello Owl</span>
|
||||
</Navbar>
|
||||
</div>
|
||||
```
|
||||
|
||||
This is exactly the way slots work! In the example above, the user of the `Navbar`
|
||||
component specify some content (here, in the default slot). The `Navbar`
|
||||
component can insert that content in its own template at the appropriate location.
|
||||
An important information to notice is that the content of the slot is rendered in
|
||||
the parent context, not in the navbar. As such, it can access values and methods
|
||||
from the parent component.
|
||||
|
||||
Here is how the `Navbar` component could be defined, with the `t-slot` directive:
|
||||
|
||||
```xml
|
||||
<div class="navbar">
|
||||
<t t-slot="default"/>
|
||||
<ul>
|
||||
<!-- rest of the navbar here -->
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Named slots
|
||||
|
||||
Default slots are very useful, but sometimes, we may need more than one slot.
|
||||
This is what named slots are for! For example, suppose we implement a component
|
||||
`InfoBox` that display a title and some specific content. Its template could look
|
||||
like this:
|
||||
|
||||
```xml
|
||||
<div class="info-box">
|
||||
<div class="info-box-title">
|
||||
<t t-slot="title"/>
|
||||
<span class="info-box-close-button" t-on-click="close">X</span>
|
||||
</div>
|
||||
<div class="info-box-content">
|
||||
<t t-slot="content"/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
And one could use it with the `t-set-slot` directive:
|
||||
|
||||
```xml
|
||||
<InfoBox>
|
||||
<t t-set-slot="title">
|
||||
Specific Title. It could be html also.
|
||||
</t>
|
||||
<t t-set-slot="content">
|
||||
<!-- some template here, with html, events, whatever -->
|
||||
</t>
|
||||
</InfoBox>
|
||||
```
|
||||
|
||||
## Rendering context
|
||||
|
||||
The content of the slots is actually rendered with the rendering context corresponding
|
||||
to where it was defined, not where it is positioned. This allows the user to define
|
||||
event handlers that will be bound to the correct component (usually, the
|
||||
grandparent of the slot content).
|
||||
|
||||
## Default Slot
|
||||
|
||||
All elements inside the component which are not a named slot will be treated as
|
||||
part of the content of the `default` slot. For example:
|
||||
|
||||
```xml
|
||||
<div t-name="Parent">
|
||||
<Child>
|
||||
<span>some content</span>
|
||||
</Child>
|
||||
</div>
|
||||
|
||||
<div t-name="Child">
|
||||
<t t-slot="default"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
One can mix default slot and named slots:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<Child>
|
||||
default content
|
||||
<t t-set-slot="footer">
|
||||
content for footer slot here
|
||||
</t>
|
||||
</Child>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Default content
|
||||
|
||||
Slots can define a default content, in case the parent did not define them:
|
||||
|
||||
```xml
|
||||
<div t-name="Parent">
|
||||
<Child/>
|
||||
</div>
|
||||
|
||||
<span t-name="Child">
|
||||
<t t-slot="default">default content</t>
|
||||
</span>
|
||||
<!-- will be rendered as: <div><span>default content</span></div> -->
|
||||
```
|
||||
|
||||
## Dynamic Slots
|
||||
|
||||
The `t-slot` directive is actually able to use any expressions, using string
|
||||
interplolation:
|
||||
|
||||
```xml
|
||||
<t t-slot="{{current}}" />
|
||||
```
|
||||
|
||||
This will evaluate the `current` expression, and insert the corresponding slot
|
||||
at the place of the `t-slot` directive.
|
||||
|
||||
## Slots and props
|
||||
|
||||
In a sense, slots are almost the same as a prop: they define some information
|
||||
to pass to the child component. To make it possible to use it, and to pass it
|
||||
down to sub component, Owl actually define a special prop `slots` that contains
|
||||
all slot information given to the component. It looks like this:
|
||||
|
||||
```js
|
||||
{ slotName_1: slotInfo_1, ..., slotName_m: slotInfo_m }
|
||||
```
|
||||
|
||||
So, a component can pass its slots to a subcomponent like this:
|
||||
|
||||
```xml
|
||||
<Child slots="props.slots"/>
|
||||
```
|
||||
|
||||
## Slot params
|
||||
|
||||
For advanced usecases, it may be necessary to pass additional information to a
|
||||
slot. This can be done by providing extra key/value pairs to the `t-set-slot`
|
||||
directive. Then, the generic component can read them in its prop `slots`.
|
||||
|
||||
For example, here is how a Notebook component could be implemented (a component
|
||||
with multiple page, and a tab bar, which only render the current active page,
|
||||
and each page has a title).
|
||||
|
||||
```js
|
||||
class Notebook extends Component {
|
||||
static template = xml`
|
||||
<div class="notebook">
|
||||
<div class="tabs">
|
||||
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
|
||||
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
|
||||
<t t-esc="props.slots[tab].title"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="page">
|
||||
<t t-slot="{{currentSlot}}"/>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ activeTab: 0 });
|
||||
this.tabNames = Object.keys(this.props.slots);
|
||||
}
|
||||
|
||||
get currentSlot() {
|
||||
return this.tabNames[this.state.activeTab];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice how one can read the `title` value for each slots. Here is how one could
|
||||
use this `Notebook` component:
|
||||
|
||||
```xml
|
||||
<Notebook>
|
||||
<t t-set-slot="page1" title="'Page 1'">
|
||||
<div>this is in the page 1</div>
|
||||
</t>
|
||||
<t t-set-slot="page2" title="'Page 2'" hidden="somevalue">
|
||||
<div>this is in the page 2</div>
|
||||
</t>
|
||||
</Notebook>
|
||||
```
|
||||
|
||||
Slot params works like normal props, so one can use the `.bind` suffix to
|
||||
bind a function if needed.
|
||||
|
||||
## Slot scopes
|
||||
|
||||
For other kinds of advanced use cases, the content of a slot may depends on some
|
||||
information specific to the generic component. This is the opposite of the slot
|
||||
params.
|
||||
|
||||
To solve this kind of problems, one can use the `t-slot-scope` directive along
|
||||
with the `t-set-slot`. This defines the name of a variable that can access
|
||||
everything given by the child component:
|
||||
|
||||
```xml
|
||||
<MyComponent>
|
||||
<t t-set-slot="foo" t-slot-scope="scope">
|
||||
content
|
||||
<t t-esc="scope.bool"/>
|
||||
<t t-esc="scope.num"/>
|
||||
</t>
|
||||
</MyComponent>
|
||||
```
|
||||
|
||||
And the child component that includes the slot can provide values like this:
|
||||
|
||||
```xml
|
||||
<t t-slot="foo" bool="other_var" num="5">
|
||||
```
|
||||
|
||||
or this:
|
||||
|
||||
```xml
|
||||
<t t-slot="foo" t-props="someObject">
|
||||
```
|
||||
|
||||
In the case of the default slot, you may declare the slot scope directly on the
|
||||
component itself:
|
||||
|
||||
```xml
|
||||
<MyComponent t-slot-scope="scope">
|
||||
content
|
||||
<t t-esc="scope.bool"/>
|
||||
<t t-esc="scope.num"/>
|
||||
</MyComponent>
|
||||
```
|
||||
|
||||
Slot values works like normal props, so one can use the `.bind` suffix to
|
||||
bind a function if needed.
|
||||
@@ -0,0 +1,702 @@
|
||||
# 🦉 Templates 🦉
|
||||
|
||||
## Content
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Directives](#directives)
|
||||
- [QWeb Template reference](#qweb-template-reference)
|
||||
- [White Spaces](#white-spaces)
|
||||
- [Expression Evaluation](#expression-evaluation)
|
||||
- [Static html Nodes](#static-html-nodes)
|
||||
- [Outputting Data](#outputting-data)
|
||||
- [Setting Variables](#setting-variables)
|
||||
- [Conditionals](#conditionals)
|
||||
- [Dynamic Attributes](#dynamic-attributes)
|
||||
- [Dynamic Class Attribute](#dynamic-class-attribute)
|
||||
- [Dynamic Tag Names](#dynamic-tag-names)
|
||||
- [Loops](#loops)
|
||||
- [Sub Templates](#sub-templates)
|
||||
- [Dynamic Sub Templates](#dynamic-sub-templates)
|
||||
- [Debugging](#debugging)
|
||||
- [Fragments](#fragments)
|
||||
- [Inline templates](#inline-templates)
|
||||
- [Rendering svg](#rendering-svg)
|
||||
- [Restrictions](#restrictions)
|
||||
|
||||
## Overview
|
||||
|
||||
Owl templates are describe using the [QWeb](https://www.odoo.com/documentation/13.0/reference/qweb.html) specification. It is based on the XML format, and used
|
||||
mostly to generate HTML. In OWL, QWeb templates are compiled into functions that
|
||||
generate a virtual dom representation of the HTML. Also, since Owl is a live
|
||||
component system, there are additional directives specific to Owl (such as `t-on`).
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<span t-if="somecondition">Some string</span>
|
||||
<ul t-else="">
|
||||
<li t-foreach="messages" t-as="message">
|
||||
<t t-esc="message"/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
Template directives are specified as XML attributes prefixed with `t-`, for
|
||||
instance `t-if` for conditionals, with elements and other attributes being
|
||||
rendered directly.
|
||||
|
||||
To avoid element rendering, a placeholder element `<t>` is also available, which
|
||||
executes its directive but doesn’t generate any output in and of itself.
|
||||
|
||||
We present in this section the templating language, including its Owl specific
|
||||
extensions.
|
||||
|
||||
## Directives
|
||||
|
||||
For reference, here is a list of all standard QWeb directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------ | --------------------------------------------------------------- |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
||||
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------- | --------------------------------------------------------------- |
|
||||
| `t-component`, `t-props` | [Defining a sub component](component.md#sub-components) |
|
||||
| `t-ref` | [Setting a reference to a dom node or a sub component](refs.md) |
|
||||
| `t-key` | [Defining a key (to help virtual dom reconciliation)](#loops) |
|
||||
| `t-on-*` | [Event handling](event_handling.md) |
|
||||
| `t-portal` | [Portal](portal.md) |
|
||||
| `t-slot`, `t-set-slot`, `t-slot-scope` | [Rendering a slot](slots.md) |
|
||||
| `t-model` | [Form input bindings](input_bindings.md) |
|
||||
| `t-tag` | [Rendering nodes with dynamic tag name](#dynamic-tag-names) |
|
||||
|
||||
## QWeb Template Reference
|
||||
|
||||
### White Spaces
|
||||
|
||||
White spaces in a template are handled in a special way:
|
||||
|
||||
- consecutive whitespaces are always condensed to a single whitespace
|
||||
- if a whitespace-only text node contains a linebreak, it is ignored
|
||||
- the previous rules do not apply if we are in a `<pre>` tag
|
||||
|
||||
### Expression Evaluation
|
||||
|
||||
QWeb expressions are strings that will be processed at compile time. Each variable in
|
||||
the javascript expression will be replaced with a lookup in the context (so, the
|
||||
component). For example, `a + b.c(d)` will be converted into:
|
||||
|
||||
```js
|
||||
context["a"] + context["b"].c(context["d"]);
|
||||
```
|
||||
|
||||
It is useful to explain the various rules that apply on these expressions:
|
||||
|
||||
1. it should be a simple expression which returns a value. It cannot be a statement.
|
||||
|
||||
```xml
|
||||
<div><p t-if="1 + 2 === 3">ok</p></div>
|
||||
```
|
||||
|
||||
is valid, but the following is not valid:
|
||||
|
||||
```xml
|
||||
<div><p t-if="console.log(1)">NOT valid</p></div>
|
||||
```
|
||||
|
||||
2. it can use anything in the rendering context (which typically contains the properties of the component):
|
||||
|
||||
```xml
|
||||
<p t-if="user.birthday === today()">Happy bithday!</p>
|
||||
```
|
||||
|
||||
is valid, and will read the `user` object from the context, and call the
|
||||
`today` function.
|
||||
|
||||
3. it can use a few special operators to avoid using symbols such as `<`, `>`,
|
||||
`&` or `|`. This is useful to make sure that we still write valid XML.
|
||||
|
||||
| Word | replaced with |
|
||||
| ----- | ------------- |
|
||||
| `and` | `&&` |
|
||||
| `or` | `\|\|` |
|
||||
| `gt` | `>` |
|
||||
| `gte` | `>=` |
|
||||
| `lt` | `<` |
|
||||
| `lte` | `<=` |
|
||||
|
||||
So, one can write this:
|
||||
|
||||
```xml
|
||||
<div><p t-if="10 + 2 gt 5">ok</p></div>
|
||||
```
|
||||
|
||||
### Static Html Nodes
|
||||
|
||||
Normal, regular html nodes are rendered into themselves:
|
||||
|
||||
```xml
|
||||
<div>hello</div> <!–– rendered as itself ––>
|
||||
```
|
||||
|
||||
### Outputting Data
|
||||
|
||||
The `t-esc` directive is necessary whenever you want to add a dynamic text
|
||||
expression in a template. The text is escaped to avoid security issues.
|
||||
|
||||
```xml
|
||||
<p><t t-esc="value"/></p>
|
||||
```
|
||||
|
||||
rendered with the value `value` set to `42` in the rendering context yields:
|
||||
|
||||
```html
|
||||
<p>42</p>
|
||||
```
|
||||
|
||||
The `t-out` directive is almost the same as `t-esc`, but possibly without the
|
||||
escaping. The difference is that the value received by the `t-out` directive
|
||||
will only be not-escaped if it has been marked as such, using the `markup`
|
||||
utility function:
|
||||
|
||||
For example, in the following component:
|
||||
|
||||
```js
|
||||
const { markup, Component, xml } = owl;
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-out="value1"/>
|
||||
<t t-out="value2"/>`;
|
||||
|
||||
value1 = "<div>some text 1</div>";
|
||||
value2 = markup("<div>some text 2</div>");
|
||||
}
|
||||
```
|
||||
|
||||
The first `t-out` will act as a `t-esc` directive, which means that the content
|
||||
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||
this will be injected as html.
|
||||
|
||||
### Setting Variables
|
||||
|
||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||
|
||||
This is done via the `t-set` directive, which takes the name of the variable to create. The value to set can be provided in two ways:
|
||||
|
||||
1. a `t-value` attribute containing an expression, and the result of its
|
||||
evaluation will be set:
|
||||
|
||||
```xml
|
||||
<t t-set="foo" t-value="2 + 1"/>
|
||||
<t t-esc="foo"/>
|
||||
```
|
||||
|
||||
will print `3`. Note that the evaluation is done at rendering time, not at
|
||||
compilte time.
|
||||
|
||||
2. if there is no `t-value` attribute, the node’s body is saved and its value is
|
||||
set as the variable’s value:
|
||||
|
||||
```xml
|
||||
<t t-set="foo">
|
||||
<li>ok</li>
|
||||
</t>
|
||||
<t t-esc="foo"/>
|
||||
```
|
||||
|
||||
will generate `<li>ok</li>` (the content is escaped as we used the `t-esc` directive)
|
||||
|
||||
The `t-set` directive acts like a regular variable in most programming language.
|
||||
It is lexically scoped (inner nodes are sub scopes), can be shadowed, ...
|
||||
|
||||
### Conditionals
|
||||
|
||||
The `t-if` directive is useful to conditionally render something. It evaluates
|
||||
the expression given as attribute value, and then acts accordingly.
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<t t-if="condition">
|
||||
<p>ok</p>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
The element is rendered if the condition (evaluated with the current rendering
|
||||
context) is true:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p>ok</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
but if the condition is false it is removed from the result:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
</div>
|
||||
```
|
||||
|
||||
The conditional rendering applies to the bearer of the directive, which does not
|
||||
have to be `<t>`:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p t-if="condition">ok</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
will give the same results as the previous example.
|
||||
|
||||
Extra conditional branching directives `t-elif` and `t-else` are also available:
|
||||
|
||||
```xml
|
||||
<div>
|
||||
<p t-if="user.birthday == today()">Happy bithday!</p>
|
||||
<p t-elif="user.login == 'root'">Welcome master!</p>
|
||||
<p t-else="">Welcome!</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Dynamic Attributes
|
||||
|
||||
One can use the `t-att-` directive to add dynamic attributes. Its main use is to
|
||||
evaluate an expression (at rendering time) and bind an attribute to its result:
|
||||
|
||||
For example, if we have `id` set to 32 in the rendering context,
|
||||
|
||||
```xml
|
||||
<div t-att-data-action-id="id"/> <!-- result: <div data-action-id="32"></div> -->
|
||||
```
|
||||
|
||||
If an expression evaluates to a falsy value, it will not be set at all:
|
||||
|
||||
```xml
|
||||
<div t-att-foo="false"/> <!-- result: <div></div> -->
|
||||
```
|
||||
|
||||
It is sometimes convenient to format an attribute with string interpolation. In
|
||||
that case, the `t-attf-` directive can be used. It is useful when we need to mix
|
||||
literal and dynamic elements, such as css classes. The dynamic elements can be
|
||||
specified with either `{{...}}` or `#{...}`:
|
||||
|
||||
```xml
|
||||
<div t-attf-foo="a {{value1}} is #{value2} of {{value3}} ]"/>
|
||||
<!-- result if values are set to 1,2 and 3: <div foo="a 0 is 1 of 2 ]"></div> -->
|
||||
```
|
||||
|
||||
If we need completely dynamic attribute names, then there is an additional
|
||||
directive: `t-att`, which takes either an object (with keys mapping to their
|
||||
values) or a pair `[key, value]`. For example:
|
||||
|
||||
```xml
|
||||
<div t-att="{'a': 1, 'b': 2}"/> <!-- result: <div a="1" b="2"></div> -->
|
||||
|
||||
<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->
|
||||
```
|
||||
|
||||
### Dynamic class attribute
|
||||
|
||||
For convenience, Owl supports a special case for the `t-att-class` case: one can
|
||||
use an object with keys describing the classes, and values boolean value denoting
|
||||
if the class is or is not present:
|
||||
|
||||
```xml
|
||||
<div t-att-class="{'a': true, 'b': true}"/> <!-- result: <div class="a b"></div> -->
|
||||
|
||||
<div t-att-class="{'a b': true, 'c': true}"/> <!-- result: <div class="a b c"></div> -->
|
||||
```
|
||||
|
||||
Note that it can be combined with normal class attribute:
|
||||
|
||||
```xml
|
||||
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
|
||||
```
|
||||
|
||||
### Dynamic tag names
|
||||
|
||||
When writing generic components or templates, the specific concrete tag for an
|
||||
HTML element is not known yet. In those situations, the `t-tag` directive is
|
||||
useful. It simply evaluates dynamically an expression to use as a tag name. The
|
||||
template:
|
||||
|
||||
```xml
|
||||
<t t-tag="tag">
|
||||
<span>content</span>
|
||||
</t>
|
||||
```
|
||||
|
||||
will be rendered as `<div><span>content</span></div>` if the `tag` context key
|
||||
is set to `div`.
|
||||
|
||||
### Loops
|
||||
|
||||
QWeb has an iteration directive `t-foreach` which take an expression returning the
|
||||
collection to iterate on, and a second parameter `t-as` providing the name to use
|
||||
for the current item of the iteration:
|
||||
|
||||
```xml
|
||||
<t t-foreach="[1, 2, 3]" t-as="i" t-key="i">
|
||||
<p><t t-esc="i"/></p>
|
||||
</t>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<p>1</p>
|
||||
<p>2</p>
|
||||
<p>3</p>
|
||||
```
|
||||
|
||||
Like conditions, `t-foreach` applies to the element bearing the directive’s attribute, and
|
||||
|
||||
```xml
|
||||
<p t-foreach="[1, 2, 3]" t-as="i" t-key="i">
|
||||
<t t-esc="i"/>
|
||||
</p>
|
||||
```
|
||||
|
||||
is equivalent to the previous example.
|
||||
|
||||
An important difference should be made with the usual `QWeb` behaviour: Owl
|
||||
requires the presence of a `t-key` directive, to be able to properly reconcile
|
||||
renderings.
|
||||
|
||||
`t-foreach` can iterate on an array (the current item will be the current value)
|
||||
or an object (the current item will be the current key).
|
||||
|
||||
In addition to the name passed via t-as, `t-foreach` provides a few other
|
||||
variables for various data points (note: `$as` will be replaced with the name
|
||||
passed to `t-as`):
|
||||
|
||||
- `$as_value`: the current iteration value, identical to `$as` for lists and
|
||||
integers, but for objects, it provides the value (where `$as` provides the key)
|
||||
- `$as_index`: the current iteration index (the first item of the iteration has index 0)
|
||||
- `$as_first`: whether the current item is the first of the iteration
|
||||
(equivalent to `$as_index == 0`)
|
||||
- `$as_last`: whether the current item is the last of the iteration
|
||||
(equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be
|
||||
available
|
||||
|
||||
These extra variables provided and all new variables created into the `t-foreach`
|
||||
are only available in the scope of the `t-foreach`. If the variable exists outside
|
||||
the context of the `t-foreach`, the value is copied at the end of the foreach
|
||||
into the global context.
|
||||
|
||||
```xml
|
||||
<t t-set="existing_variable" t-value="false"/>
|
||||
<!-- existing_variable now False -->
|
||||
|
||||
<p t-foreach="Array(3)" t-as="i" t-key="i">
|
||||
<t t-set="existing_variable" t-value="true"/>
|
||||
<t t-set="new_variable" t-value="true"/>
|
||||
<!-- existing_variable and new_variable now true -->
|
||||
</p>
|
||||
|
||||
<!-- existing_variable always true -->
|
||||
<!-- new_variable undefined -->
|
||||
```
|
||||
|
||||
Even though Owl tries to be as declarative as possible, the DOM does not fully
|
||||
expose its state declaratively in the DOM tree. For example, the scrolling state,
|
||||
the current user selection, the focused element or the state of an input are not
|
||||
set as attribute in the DOM tree. This is why we use a virtual dom
|
||||
algorithm to make sure we keep the actual DOM node instead of replacing it with
|
||||
a new one.
|
||||
|
||||
Consider the following situation: we have a list of two items `[{text: "a"}, {text: "b"}]`
|
||||
and we render them in this template:
|
||||
|
||||
```xml
|
||||
<p t-foreach="items" t-as="item" t-key="item_index"><t t-esc="item.text"/></p>
|
||||
```
|
||||
|
||||
The result will be two `<p>` tags with text `a` and `b`. Now, if we swap them,
|
||||
and rerender the template, Owl needs to know what the intent is:
|
||||
|
||||
- should Owl actually swap the DOM nodes,
|
||||
- or should it keep the DOM nodes, but with an updated text content?
|
||||
|
||||
This might look trivial, but it actually matters. These two possibilities lead
|
||||
to different results in some cases. For example, if the user selected the text
|
||||
of the first `p`, swapping them will keep the selection while updating the
|
||||
text content will not.
|
||||
|
||||
There are many other cases where this is important: `input` tags with their
|
||||
value, css classes and animations, scroll position...
|
||||
|
||||
So, the `t-key` directive is used to give an identity to an element. It allows
|
||||
Owl to understand if different elements of a list are actually different or not.
|
||||
|
||||
The above example could be modified by adding an ID: `[{id: 1, text: "a"}, {id: 2, text: "b"}]`.
|
||||
Then, the template could look like this:
|
||||
|
||||
```xml
|
||||
<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>
|
||||
```
|
||||
|
||||
The `t-key` directive is useful for lists (`t-foreach`). A key should be
|
||||
a unique number or string (objects will not work: they will be cast to the
|
||||
`"[object Object]"` string, which is obviously not unique).
|
||||
|
||||
Also, the key can be set on a `t` tag or on its children. The following variations
|
||||
are all equivalent:
|
||||
|
||||
```xml
|
||||
<p t-foreach="items" t-as="item" t-key="item.id">
|
||||
<t t-esc="item.text"/>
|
||||
</p>
|
||||
|
||||
<t t-foreach="items" t-as="item" t-key="item.id">
|
||||
<p t-esc="item.text"/>
|
||||
</t>
|
||||
|
||||
<t t-foreach="items" t-as="item">
|
||||
<p t-key="item.id" t-esc="item.text"/>
|
||||
</t>
|
||||
```
|
||||
|
||||
If there is no `t-key` directive, Owl will use the index as a default key.
|
||||
|
||||
Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
|
||||
not work with other iterables, such as `Set`. However, it is only a matter of
|
||||
using the `...` javascript operator. For example:
|
||||
|
||||
```xml
|
||||
<t t-foreach="...items" t-as="item">...</t>
|
||||
```
|
||||
|
||||
The `...` operator will convert the `Set` (or any other iterables) into a list,
|
||||
which will work with Owl QWeb.
|
||||
|
||||
### Sub Templates
|
||||
|
||||
QWeb templates can be used for top level rendering, but they can also be used
|
||||
from within another template (to avoid duplication or give names to parts of
|
||||
templates), using the `t-call` directive:
|
||||
|
||||
```xml
|
||||
<div t-name="other-template">
|
||||
<p><t t-value="var"/></p>
|
||||
</div>
|
||||
|
||||
<div t-name="main-template">
|
||||
<t t-set="var" t-value="owl"/>
|
||||
<t t-call="other-template"/>
|
||||
</div>
|
||||
```
|
||||
|
||||
will be rendered as `<div><p>owl</p></div>`. This example shows that the sub
|
||||
template is rendered with the execution context of the parent. The sub template
|
||||
is actually inlined in the main template, but in a sub scope: variables defined
|
||||
in the sub template do not escape.
|
||||
|
||||
Sometimes, one might want to pass information to the sub template. In that case,
|
||||
the content of the body of the `t-call` directive is available as a special
|
||||
magic variable `0`:
|
||||
|
||||
```xml
|
||||
<t t-name="other-template">
|
||||
This template was called with content:
|
||||
<t t-raw="0"/>
|
||||
</t>
|
||||
|
||||
<div t-name="main-template">
|
||||
<t t-call="other-template">
|
||||
<em>content</em>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
will result in :
|
||||
|
||||
```xml
|
||||
<div>
|
||||
This template was called with content:
|
||||
<em>content</em>
|
||||
</div>
|
||||
```
|
||||
|
||||
This can be used to define variables scoped to a sub template:
|
||||
|
||||
```xml
|
||||
<t t-call="other-template">
|
||||
<t t-set="var" t-value="1"/>
|
||||
</t>
|
||||
<!-- "var" does not exist here -->
|
||||
```
|
||||
|
||||
Note: by default, the rendering context for a sub template is simply the current
|
||||
rendering context. However, it may be useful to be able to specify a specific
|
||||
object as context. This can be done by using the `t-call-context` directive:
|
||||
|
||||
```xml
|
||||
<t t-call="other-template" t-call-context="obj"/>
|
||||
```
|
||||
|
||||
### Dynamic sub templates
|
||||
|
||||
The `t-call` directive can also be used to dynamically call a sub template,
|
||||
using string interpolation. For example:
|
||||
|
||||
```xml
|
||||
<div t-name="main-template">
|
||||
<t t-call="{{template}}">
|
||||
<em>content</em>
|
||||
</t>
|
||||
</div>
|
||||
```
|
||||
|
||||
Here, the name of the template is obtained from the `template` value in the
|
||||
template rendering context.
|
||||
|
||||
### Debugging
|
||||
|
||||
The javascript QWeb implementation provides two useful debugging directives:
|
||||
|
||||
`t-debug` adds a debugger statement during template rendering:
|
||||
|
||||
```xml
|
||||
<t t-if="a_test">
|
||||
<t t-debug=""/>
|
||||
</t>
|
||||
```
|
||||
|
||||
will stop execution if the browser dev tools are open.
|
||||
|
||||
`t-log` takes an expression parameter, evaluates the expression during rendering and logs its result with console.log:
|
||||
|
||||
```xml
|
||||
<t t-set="foo" t-value="42"/>
|
||||
<t t-log="foo"/>
|
||||
```
|
||||
|
||||
will print 42 to the console.
|
||||
|
||||
## Fragments
|
||||
|
||||
Owl 2 supports templates with an arbitrary number of root elements, or even just
|
||||
a text node. So, the following templates are all valid:
|
||||
|
||||
```xml
|
||||
hello owl. This is just a text node!
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
<div>ola</div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<div t-if="someCondition"><SomeChildComponent/></div>
|
||||
```
|
||||
|
||||
```xml
|
||||
<t t-if="someCondition"><SomeChildComponent/></t>
|
||||
```
|
||||
|
||||
## Inline templates
|
||||
|
||||
Most real applications will define their templates in a XML file, to benefit
|
||||
from the XML ecosystem, and to do some additional processing, such as translating
|
||||
them. However, in some cases, it is convenient to be able to define a template
|
||||
inline. To do so, one can use the `xml` helper function:
|
||||
|
||||
```js
|
||||
const { Component, xml } = owl;
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<span t-if="somecondition">text</span>
|
||||
<button t-on-click="someMethod">Click</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
mount(MyComponent, document.body);
|
||||
```
|
||||
|
||||
This function simply generates an unique string id, and register the template
|
||||
under that id in the internals of Owl, then return the id.
|
||||
|
||||
## Rendering svg
|
||||
|
||||
Owl components can be used to generate dynamic SVG graphs:
|
||||
|
||||
```js
|
||||
class Node extends Component {
|
||||
static template = xml`
|
||||
<g>
|
||||
<circle t-att-cx="props.x" t-att-cy="props.y" r="4" fill="black"/>
|
||||
<text t-att-x="props.x - 5" t-att-y="props.y + 18"><t t-esc="props.node.label"/></text>
|
||||
<t t-set="childx" t-value="props.x + 100"/>
|
||||
<t t-set="height" t-value="props.height/(props.node.children || []).length"/>
|
||||
<t t-foreach="props.node.children || []" t-as="child">
|
||||
<t t-set="childy" t-value="props.y + child_index*height"/>
|
||||
<line t-att-x1="props.x" t-att-y1="props.y" t-att-x2="childx" t-att-y2="childy" stroke="black" />
|
||||
<Node x="childx" y="childy" node="child" height="height"/>
|
||||
</t>
|
||||
</g>
|
||||
`;
|
||||
static components = { Node };
|
||||
}
|
||||
|
||||
class RootNode extends Component {
|
||||
static template = xml`
|
||||
<svg height="180">
|
||||
<Node node="graph" x="10" y="20" height="180"/>
|
||||
</svg>
|
||||
`;
|
||||
static components = { Node };
|
||||
graph = {
|
||||
label: "a",
|
||||
children: [
|
||||
{ label: "b" },
|
||||
{ label: "c", children: [{ label: "d" }, { label: "e" }] },
|
||||
{ label: "f", children: [{ label: "g" }] },
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This `RootNode` component will then display a live SVG representation of the
|
||||
graph described by the `graph` property. Note that there is a recursive structure
|
||||
here: the `Node` component uses itself as a subcomponent.
|
||||
|
||||
**Important note:** Owl needs to properly set the namespace for each svg elements.
|
||||
Since Owl compile each template separately, it is not able to determine easily
|
||||
if a template is supposed to be included in a svg namespace or not. Therefore,
|
||||
Owl depends on a heuristic: if a tag is either `svg`, `g` or `path`, then it will
|
||||
be considered as svg. In practice, this means that each component or each sub
|
||||
templates (included with `t-call`) should have one of these tag as root tag.
|
||||
|
||||
## Restrictions
|
||||
|
||||
Note that Owl templates forbid the use of tag and or attributes starting with
|
||||
the `block-` string. This restriction prevents name collision with the internal
|
||||
code of Owl.
|
||||
|
||||
```xml
|
||||
<div><block-1>this will not be accepted by Owl</block-1></div>
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
# 🦉 Translations 🦉
|
||||
|
||||
If properly setup, Owl can translate all rendered templates. To do
|
||||
so, it needs a translate function, which takes a string and returns a string.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const translations = {
|
||||
hello: "bonjour",
|
||||
yes: "oui",
|
||||
no: "non",
|
||||
};
|
||||
const translateFn = (str) => translations[str] || str;
|
||||
|
||||
const app = new App(Root, { templates, tranaslateFn });
|
||||
// ...
|
||||
```
|
||||
|
||||
See the [app configuration page](app.md#configuration) for more info on how to
|
||||
configure an Owl application.
|
||||
|
||||
Once setup, all rendered templates will be translated using `translateFn`:
|
||||
|
||||
- each text node will be replaced with its translation,
|
||||
- each of the following attribute values will be translated as well: `title`,
|
||||
`placeholder`, `label` and `alt`,
|
||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||
if its value is `off`.
|
||||
|
||||
So, with the above `translateFn`, the following templates:
|
||||
|
||||
```xml
|
||||
<div>hello</div>
|
||||
<div t-translation="off">hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="hello" other="yes"/>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<div>bonjour</div>
|
||||
<div>hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="bonjour" other="yes"/>
|
||||
```
|
||||
|
||||
Note that the translation is done during the compilation of the template, not
|
||||
when it is rendered.
|
||||
|
||||
In some case, it is useful to be able to extend the list of translatable attributes.
|
||||
For example, one may want to also translate `data-title` attributes. To do that,
|
||||
we can define additional attributes with the `translatableAttributes` option:
|
||||
|
||||
```js
|
||||
const app = new App(Root, { templates, tranaslateFn, translatableAttributes: ["data-title"] });
|
||||
// ...
|
||||
```
|
||||
|
||||
It is also possible to remove an attribute from the default list by prefixing it with `-`:
|
||||
|
||||
```js
|
||||
const app = new App(Root, {
|
||||
templates,
|
||||
tranaslateFn,
|
||||
translatableAttributes: ["data-title", "-title"],
|
||||
});
|
||||
// data-title attribute will be translated, but not title attribute...
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
# 🦉 Utils 🦉
|
||||
|
||||
Owl export a few useful utility functions, to help with common issues. Those
|
||||
functions are all available in the `owl.utils` namespace.
|
||||
|
||||
## Content
|
||||
|
||||
- [`whenReady`](#whenready): executing code when DOM is ready
|
||||
- [`loadFile`](#loadfile): loading a file (useful for templates)
|
||||
- [`EventBus`](#eventbus): a simple EventBus
|
||||
- [`validate`](#validate): a validation function
|
||||
|
||||
## `whenReady`
|
||||
|
||||
The function `whenReady` returns a `Promise` resolved when the DOM is ready (if
|
||||
not ready yet, resolved directly otherwise). If called with a callback as
|
||||
argument, it executes it as soon as the DOM ready (or directly).
|
||||
|
||||
```js
|
||||
const { whenReady } = owl;
|
||||
|
||||
await whenReady();
|
||||
// do something
|
||||
```
|
||||
|
||||
or alternatively:
|
||||
|
||||
```js
|
||||
whenReady(function () {
|
||||
// do something
|
||||
});
|
||||
```
|
||||
|
||||
## `loadFile`
|
||||
|
||||
`loadFile` is a helper function to fetch a file. It simply
|
||||
performs a `GET` request and returns the resulting string in a promise. The
|
||||
initial usecase for this function is to load a template file. For example:
|
||||
|
||||
```js
|
||||
const { loadFile } = owl;
|
||||
|
||||
async function makeEnv() {
|
||||
const templates = await loadFile("templates.xml");
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
## `EventBus`
|
||||
|
||||
It is a simple `EventBus`, with the same API as usual DOM elements, and an
|
||||
additional `trigger` method to dispatch events:
|
||||
|
||||
```js
|
||||
const bus = new EventBus();
|
||||
bus.addEventListener("event", () => console.log("something happened"));
|
||||
|
||||
bus.trigger("event"); // 'something happened' is logged
|
||||
```
|
||||
|
||||
## `validate`
|
||||
|
||||
The `validate` function is a function that validates if a given object satisfies a
|
||||
specified schema. It is actually used by Owl itself to perform
|
||||
[props validation](props.md#props-validation). For example:
|
||||
|
||||
```js
|
||||
validate(
|
||||
{ a: "hey" },
|
||||
{
|
||||
id: Number,
|
||||
url: [Boolean, { type: Array, element: Number }],
|
||||
}
|
||||
);
|
||||
|
||||
// throws an error with the following information:
|
||||
// - unknown key 'a',
|
||||
// - 'id' is missing (should be a number),
|
||||
// - 'url' is missing (should be a boolean or list of numbers),
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
# OWL: State Management
|
||||
|
||||
Managing the state in an application is not an easy task. Many different
|
||||
architectures/designs/systems/... have been created. We propose here to use
|
||||
the idea of a central store.
|
||||
|
||||
- Store
|
||||
- connect
|
||||
@@ -1,3 +0,0 @@
|
||||
# Tutorial
|
||||
|
||||
todo...
|
||||
@@ -1,45 +0,0 @@
|
||||
.main {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
}
|
||||
|
||||
.left-thing {
|
||||
background-color: gray;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.left-thing button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left-thing .counter span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.left-thing .counter button {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Message } from "./message.js";
|
||||
import { messages } from "./data.js";
|
||||
|
||||
const template = `
|
||||
<div class="main">
|
||||
<div class="left-thing">
|
||||
<div class="counter">
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.messages.length"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>
|
||||
<button t-on-click="setMessageCount(10)">10 messages</button>
|
||||
<button t-on-click="setMessageCount(20)">20 messages</button>
|
||||
<button t-on-click="setMessageCount(500)">500 messages</button>
|
||||
<button t-on-click="setMessageCount(1000)">1000 messages</button>
|
||||
<button t-on-click="setMessageCount(5000)">5000 messages</button>
|
||||
<button t-on-click="setMessageCount(15000)">15000 messages</button>
|
||||
</div>
|
||||
<div class="right-thing">
|
||||
<div class="content">
|
||||
<t t-foreach="state.messages" t-as="message">
|
||||
<t t-widget="Message" t-att-key="message.id" t-props="message" t-on-remove_message="removeMessage"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export class App extends owl.Component {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.inlineTemplate = template;
|
||||
this.widgets = { Message };
|
||||
this.state = {
|
||||
messages: messages.slice(0, 10)
|
||||
};
|
||||
}
|
||||
|
||||
setMessageCount(n) {
|
||||
this.state.messages = messages.slice(0, n);
|
||||
}
|
||||
|
||||
removeMessage(data) {
|
||||
const index = messages.findIndex(m => m.id === data.id);
|
||||
this.state.messages.splice(index, 1);
|
||||
}
|
||||
|
||||
increment(delta) {
|
||||
const n = this.state.messages.length + delta;
|
||||
this.setMessageCount(n);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export class Counter extends owl.Component {
|
||||
inlineTemplate = `
|
||||
<div>
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.counter"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>`;
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.state = {
|
||||
counter: props.initialState || 0
|
||||
};
|
||||
}
|
||||
|
||||
increment(delta) {
|
||||
this.state.counter += delta;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export const messages = [];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
for (let i = 1; i < 16000; i++) {
|
||||
messages.push({
|
||||
id: i,
|
||||
author: chooseRandomly(authors),
|
||||
msg: `${i}: ${chooseRandomly(content)}`,
|
||||
likes: 0
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Odoo WebCore Benchmarks App</title>
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<!-- Application JS/CSS -->
|
||||
<script src="/owl.js"></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<script type="module" src="/main.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +0,0 @@
|
||||
import { App } from "./app.js";
|
||||
|
||||
function createApp(el) {
|
||||
const env = {
|
||||
qweb: new owl.QWeb()
|
||||
};
|
||||
const app = new App(env, { initialState: 13 });
|
||||
app.mount(el);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
createApp(document.body);
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Counter } from "./counter.js";
|
||||
|
||||
export class Message extends owl.Component {
|
||||
inlineTemplate = `
|
||||
<div 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" t-props="{initialState: props.id}"/>
|
||||
</div>`;
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
this.widgets = { Counter };
|
||||
}
|
||||
|
||||
removeMessage() {
|
||||
this.trigger("remove_message", {
|
||||
id: this.props.id
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
# Examples
|
||||
|
||||
This project features three example to illustrate how to work with the web-core framework:
|
||||
|
||||
- _benchmarks_ is a small application to test large number of widgets,
|
||||
- _todoapp_ is the classical todo application (from the todomvc project),
|
||||
|
||||
## Benchmarks
|
||||
|
||||
This example is just a playground to experiment/showcase some features of the framework, with large number of widgets.
|
||||
|
||||
```
|
||||
npm run example:benchmarks:build # make a build in dist/examples/
|
||||
npm run example:benchmarks:dev # make a build in dist/examples/, and make a live server to access it
|
||||
```
|
||||
|
||||
The benchmarks application generates a large number of demo messages, and display them in a list, with a few buttons that can
|
||||
be used to alter the number of visible widgets.
|
||||
|
||||
Note that each message is itself a widget, with a sub widget. This
|
||||
example could be made faster (by not using subwidgets), but the point is to observe/measure the overhead of the Component class.
|
||||
|
||||
## Todo App
|
||||
|
||||
The Todo App is the classical todo application from _http://todomvc.com/_. It is a good mini application with non trivial data structures and interface updates.
|
||||
|
||||
```
|
||||
npm run example:todoapp:build # make a build in dist/examples/
|
||||
npm run example:todoapp:dev # make a build in dist/examples/, and make a live server to access it
|
||||
```
|
||||
|
||||
It is implemented with the Store class (as in redux/vuex).
|
||||
@@ -1,63 +0,0 @@
|
||||
import { TodoItem } from "./TodoItem.js";
|
||||
|
||||
const { Component, connect } = owl;
|
||||
|
||||
const ENTER_KEY = 13;
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return { todos: state.todos };
|
||||
}
|
||||
|
||||
class TodoApp extends Component {
|
||||
template = "todoapp";
|
||||
widgets = { TodoItem };
|
||||
state = { filter: "all" };
|
||||
|
||||
get visibleTodos() {
|
||||
let todos = this.props.todos;
|
||||
if (this.state.filter === "active") {
|
||||
todos = todos.filter(t => !t.completed);
|
||||
}
|
||||
if (this.state.filter === "completed") {
|
||||
todos = todos.filter(t => t.completed);
|
||||
}
|
||||
return todos;
|
||||
}
|
||||
|
||||
get allChecked() {
|
||||
return this.props.todos.every(todo => todo.completed);
|
||||
}
|
||||
|
||||
get remaining() {
|
||||
return this.props.todos.filter(todo => !todo.completed).length;
|
||||
}
|
||||
|
||||
get remainingText() {
|
||||
const items = this.remaining < 2 ? "item" : "items";
|
||||
return ` ${items} left`;
|
||||
}
|
||||
|
||||
addTodo(ev) {
|
||||
if (ev.keyCode === ENTER_KEY) {
|
||||
const title = ev.target.value;
|
||||
if (title.trim()) {
|
||||
this.env.store.dispatch("addTodo", title);
|
||||
}
|
||||
ev.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
clearCompleted() {
|
||||
this.env.store.dispatch("clearCompleted");
|
||||
}
|
||||
|
||||
toggleAll() {
|
||||
this.env.store.dispatch("toggleAll", !this.allChecked);
|
||||
}
|
||||
|
||||
setFilter(filter) {
|
||||
this.state.filter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TodoApp);
|
||||
@@ -1,51 +0,0 @@
|
||||
const ENTER_KEY = 13;
|
||||
const ESC_KEY = 27;
|
||||
|
||||
export class TodoItem extends owl.Component {
|
||||
template = "todoitem";
|
||||
|
||||
state = { isEditing: false };
|
||||
|
||||
removeTodo() {
|
||||
this.env.store.dispatch("removeTodo", this.props.id);
|
||||
}
|
||||
|
||||
toggleTodo() {
|
||||
this.env.store.dispatch("toggleTodo", this.props.id);
|
||||
}
|
||||
|
||||
async editTodo() {
|
||||
this.state.isEditing = true;
|
||||
setTimeout(() => {
|
||||
this.refs.input.value = "";
|
||||
this.refs.input.focus();
|
||||
this.refs.input.value = this.props.title;
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyup(ev) {
|
||||
if (ev.keyCode === ENTER_KEY) {
|
||||
this.updateTitle(ev.target.value);
|
||||
}
|
||||
if (ev.keyCode === ESC_KEY) {
|
||||
ev.target.value = this.props.title;
|
||||
this.state.isEditing = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur(ev) {
|
||||
this.updateTitle(ev.target.value);
|
||||
}
|
||||
updateTitle(title) {
|
||||
const value = title.trim();
|
||||
if (!value) {
|
||||
this.removeTodo(this.props.id);
|
||||
} else {
|
||||
this.env.store.dispatch("editTodo", {
|
||||
id: this.props.id,
|
||||
title: value
|
||||
});
|
||||
this.state.isEditing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
background: #f5f5f5;
|
||||
color: #4d4d4d;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::-moz-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp input::input-placeholder {
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -155px;
|
||||
width: 100%;
|
||||
font-size: 100px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: rgba(175, 47, 47, 0.15);
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.new-todo,
|
||||
.edit {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-size: 24px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 1.4em;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
padding: 6px;
|
||||
border: 1px solid #999;
|
||||
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.new-todo {
|
||||
padding: 16px 16px 16px 60px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.toggle-all {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
border: none; /* Mobile Safari */
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.toggle-all + label {
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
font-size: 0;
|
||||
position: absolute;
|
||||
top: -52px;
|
||||
left: -13px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.toggle-all + label:before {
|
||||
content: "❯";
|
||||
font-size: 22px;
|
||||
color: #e6e6e6;
|
||||
padding: 10px 27px 10px 27px;
|
||||
}
|
||||
|
||||
.toggle-all:checked + label:before {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing {
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.todo-list li.editing .edit {
|
||||
display: block;
|
||||
width: calc(100% - 43px);
|
||||
padding: 12px 16px;
|
||||
margin: 0 0 0 43px;
|
||||
}
|
||||
|
||||
.todo-list li.editing .view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.todo-list li .toggle + label {
|
||||
/*
|
||||
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
|
||||
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
|
||||
*/
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.todo-list li .toggle:checked + label {
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.todo-list li label {
|
||||
word-break: break-all;
|
||||
padding: 15px 15px 15px 60px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
}
|
||||
|
||||
.todo-list li.completed label {
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-list li .destroy {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: auto 0;
|
||||
font-size: 30px;
|
||||
color: #cc9a9a;
|
||||
margin-bottom: 11px;
|
||||
transition: color 0.2s ease-out;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:hover {
|
||||
color: #af5b5e;
|
||||
}
|
||||
|
||||
.todo-list li .destroy:after {
|
||||
content: "×";
|
||||
}
|
||||
|
||||
.todo-list li:hover .destroy {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.todo-list li .edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-list li.editing:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #777;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.footer:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo-count strong {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
.clear-completed,
|
||||
html .clear-completed:active {
|
||||
float: right;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-completed:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 65px auto 0;
|
||||
color: #bfbfbf;
|
||||
font-size: 10px;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info p {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.info a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
.toggle-all,
|
||||
.todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Odoo Demo App</title>
|
||||
<link rel="icon" href="data:,">
|
||||
|
||||
<!-- Application JS/CSS -->
|
||||
<script src="/owl.js"></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<script type="module" src="/main.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,26 +0,0 @@
|
||||
import TodoApp from "./TodoApp.js";
|
||||
import { makeStore } from "./store.js";
|
||||
|
||||
async function makeEnv() {
|
||||
const result = await fetch("templates.xml");
|
||||
if (!result.ok) {
|
||||
throw new Error("Error while fetching xml templates");
|
||||
}
|
||||
let templates = await result.text();
|
||||
templates = templates.replace(/<!--[\s\S]*?-->/g, "");
|
||||
const qweb = new owl.QWeb();
|
||||
qweb.loadTemplates(templates);
|
||||
return {
|
||||
qweb,
|
||||
store: makeStore()
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const env = await makeEnv();
|
||||
const app = new TodoApp(env);
|
||||
|
||||
// for debugging purpose
|
||||
window.app = app;
|
||||
await app.mount(document.body);
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// ACTIONS
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const actions = {
|
||||
addTodo({ commit }, title) {
|
||||
commit("addTodo", title);
|
||||
},
|
||||
removeTodo({ commit }, id) {
|
||||
commit("removeTodo", id);
|
||||
},
|
||||
toggleTodo({ state, commit }, id) {
|
||||
const todo = state.todos.find(t => t.id === id);
|
||||
commit("editTodo", { id, completed: !todo.completed });
|
||||
},
|
||||
clearCompleted({ state, commit }) {
|
||||
state.todos
|
||||
.filter(todo => todo.completed)
|
||||
.forEach(todo => {
|
||||
commit("removeTodo", todo.id);
|
||||
});
|
||||
},
|
||||
toggleAll({ state, commit }, completed) {
|
||||
state.todos.forEach(todo => {
|
||||
commit("editTodo", { id: todo.id, completed });
|
||||
});
|
||||
},
|
||||
editTodo({ commit }, { id, title }) {
|
||||
commit("editTodo", { id, title });
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// MUTATIONS
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const mutations = {
|
||||
addTodo({ state }, title) {
|
||||
const id = state.nextId++;
|
||||
const todo = { id, title, completed: false };
|
||||
state.todos.push(todo);
|
||||
},
|
||||
removeTodo({ state }, id) {
|
||||
const index = state.todos.findIndex(t => t.id === id);
|
||||
state.todos.splice(index, 1);
|
||||
},
|
||||
editTodo({ state }, { id, title, completed }) {
|
||||
const todo = state.todos.find(t => t.id === id);
|
||||
if (title !== undefined) {
|
||||
todo.title = title;
|
||||
}
|
||||
if (completed !== undefined) {
|
||||
todo.completed = completed;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// STORE
|
||||
//------------------------------------------------------------------------------
|
||||
const LOCALSTORAGE_KEY = "todos-odoo";
|
||||
|
||||
export function makeStore() {
|
||||
const todos = JSON.parse(
|
||||
window.localStorage.getItem(LOCALSTORAGE_KEY) || "[]"
|
||||
);
|
||||
const nextId = Math.max(0, ...todos.map(t => t.id || 0)) + 1;
|
||||
const state = { todos, nextId };
|
||||
const store = new owl.Store({ state, actions, mutations });
|
||||
store.on("update", null, () => {
|
||||
const state = JSON.stringify(store.state.todos);
|
||||
window.localStorage.setItem(LOCALSTORAGE_KEY, state);
|
||||
});
|
||||
return store;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<section t-name="todoapp" class="todoapp">
|
||||
<!-- header -->
|
||||
<header class="header">
|
||||
<h1>todos</h1>
|
||||
<input class="new-todo" autofocus="true" autocomplete="off" placeholder="What needs to be done?" t-on-keyup="addTodo"/>
|
||||
</header>
|
||||
<!-- main section -->
|
||||
<section class="main" t-if="props.todos.length">
|
||||
<input class="toggle-all" id="toggle-all" type="checkbox" t-att-checked="allChecked" t-on-click="toggleAll"/>
|
||||
<label for="toggle-all"></label>
|
||||
<ul class="todo-list">
|
||||
<t t-foreach="visibleTodos" t-as="todo">
|
||||
<t t-widget="TodoItem" t-key="todo.id" t-props="todo"/>
|
||||
</t>
|
||||
</ul>
|
||||
</section>
|
||||
<!-- footer -->
|
||||
<footer class="footer" t-if="props.todos.length">
|
||||
<span class="todo-count">
|
||||
<strong>
|
||||
<t t-esc="remaining"/>
|
||||
</strong>
|
||||
<t t-esc="remainingText"/>
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a href="#/all" t-on-click="setFilter('all')" t-att-class="{selected: state.filter === 'all'}">All</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/active" t-on-click="setFilter('active')" t-att-class="{selected: state.filter === 'active'}">Active</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/completed" t-on-click="setFilter('completed')" t-att-class="{selected: state.filter === 'completed'}">Completed</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="clear-completed" t-if="props.todos.length gt remaining" t-on-click="clearCompleted">
|
||||
Clear completed
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<li t-name="todoitem" class="todo" t-att-class="{completed: props.completed, editing: state.isEditing}">
|
||||
<div class="view">
|
||||
<input class="toggle" type="checkbox" t-on-change="toggleTodo" t-att-checked="props.completed"/>
|
||||
<label t-on-dblclick="editTodo">
|
||||
<t t-esc="props.title"/>
|
||||
</label>
|
||||
<button class="destroy" t-on-click="removeTodo"></button>
|
||||
</div>
|
||||
<input class="edit" t-ref="'input'" t-if="state.isEditing" t-att-value="props.title" t-on-keyup="handleKeyup" t-on-blur="handleBlur"/>
|
||||
</li>
|
||||
|
||||
</templates>
|
||||
@@ -1,9 +0,0 @@
|
||||
module.exports = {
|
||||
roots: ["<rootDir>/src", "<rootDir>/tests"],
|
||||
transform: {
|
||||
"^.+\\.ts?$": "ts-jest"
|
||||
},
|
||||
verbose: false,
|
||||
testRegex: "(/tests/.*(test|spec))\\.ts?$",
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"]
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
|
||||
# Generated JavaScript
|
||||
/test/browserified.js
|
||||
/browserified.js
|
||||
/h.d.ts
|
||||
/h.js
|
||||
/h.js.map
|
||||
/hooks.d.ts
|
||||
/hooks.js
|
||||
/hooks.js.map
|
||||
/htmldomapi.d.ts
|
||||
/htmldomapi.js
|
||||
/htmldomapi.js.map
|
||||
/is.d.ts
|
||||
/is.js
|
||||
/is.js.map
|
||||
/snabbdom.bundle.d.ts
|
||||
/snabbdom.bundle.js
|
||||
/snabbdom.bundle.js.map
|
||||
/snabbdom.d.ts
|
||||
/snabbdom.js
|
||||
/snabbdom.js.map
|
||||
/thunk.d.ts
|
||||
/thunk.js
|
||||
/thunk.js.map
|
||||
/tovnode.d.ts
|
||||
/tovnode.js
|
||||
/tovnode.js.map
|
||||
/vnode.d.ts
|
||||
/vnode.js
|
||||
/vnode.js.map
|
||||
/modules
|
||||
/helpers
|
||||
/es
|
||||
@@ -1,33 +0,0 @@
|
||||
/test
|
||||
/perf
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
@@ -1,17 +0,0 @@
|
||||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6.10.1'
|
||||
script:
|
||||
- export IP_ADDR=$(ip addr | grep eth -A 4 | grep 'inet ' | awk '{ print $2 }' | sed 's/\/..//')
|
||||
- npm test
|
||||
addons:
|
||||
browserstack:
|
||||
username:
|
||||
secure: <TODO>
|
||||
access_key:
|
||||
secure: <TODO>
|
||||
env:
|
||||
global:
|
||||
- secure: <TODO>
|
||||
- secure: <TODO>
|
||||
@@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Simon Friis Vindum
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,758 +0,0 @@
|
||||
# Snabbdom
|
||||
|
||||
A virtual DOM library with focus on simplicity, modularity, powerful features
|
||||
and performance.
|
||||
|
||||
[](https://opensource.org/licenses/MIT) [](https://badge.fury.io/js/snabbdom) [](https://www.npmjs.com/package/snabbdom)
|
||||
|
||||
[](https://gitter.im/paldepind/snabbdom?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Features](#features)
|
||||
* [Inline example](#inline-example)
|
||||
* [Examples](#examples)
|
||||
* [Core documentation](#core-documentation)
|
||||
* [Modules documentation](#modules-documentation)
|
||||
* [Helpers](#helpers)
|
||||
* [Virtual Node documentation](#virtual-node)
|
||||
* [Structuring applications](#structuring-applications)
|
||||
|
||||
## Why
|
||||
|
||||
Virtual DOM is awesome. It allows us to express our application's view
|
||||
as a function of its state. But existing solutions were way way too
|
||||
bloated, too slow, lacked features, had an API biased towards OOP
|
||||
and/or lacked features I needed.
|
||||
|
||||
## Introduction
|
||||
|
||||
Snabbdom consists of an extremely simple, performant and extensible
|
||||
core that is only ≈ 200 SLOC. It offers a modular architecture with
|
||||
rich functionality for extensions through custom modules. To keep the
|
||||
core simple, all non-essential functionality is delegated to modules.
|
||||
|
||||
You can mold Snabbdom into whatever you desire! Pick, choose and
|
||||
customize the functionality you want. Alternatively you can just use
|
||||
the default extensions and get a virtual DOM library with high
|
||||
performance, small size and all the features listed below.
|
||||
|
||||
## Features
|
||||
|
||||
* Core features
|
||||
* About 200 SLOC – you could easily read through the entire core and fully
|
||||
understand how it works.
|
||||
* Extendable through modules.
|
||||
* A rich set of hooks available, both per vnode and globally for modules,
|
||||
to hook into any part of the diff and patch process.
|
||||
* Splendid performance. Snabbdom is among the fastest virtual DOM libraries
|
||||
in the [Virtual DOM Benchmark](http://vdom-benchmark.github.io/vdom-benchmark/).
|
||||
* Patch function with a function signature equivalent to a reduce/scan
|
||||
function. Allows for easier integration with a FRP library.
|
||||
* Features in modules
|
||||
* `h` function for easily creating virtual DOM nodes.
|
||||
* [SVG _just works_ with the `h` helper](#svg).
|
||||
* Features for doing complex CSS animations.
|
||||
* Powerful event listener functionality.
|
||||
* [Thunks](#thunks) to optimize the diff and patch process even further.
|
||||
* Third party features
|
||||
* JSX support thanks to [snabbdom-pragma](https://github.com/Swizz/snabbdom-pragma).
|
||||
* Server-side HTML output provided by [snabbdom-to-html](https://github.com/acstll/snabbdom-to-html).
|
||||
* Compact virtual DOM creation with [snabbdom-helpers](https://github.com/krainboltgreene/snabbdom-helpers).
|
||||
* Template string support using [snabby](https://github.com/jamen/snabby).
|
||||
* Virtual DOM assertion with [snabbdom-looks-like](https://github.com/jvanbruegge/snabbdom-looks-like)
|
||||
|
||||
## Inline example
|
||||
|
||||
```javascript
|
||||
var snabbdom = require('snabbdom');
|
||||
var patch = snabbdom.init([ // Init patch function with chosen modules
|
||||
require('snabbdom/modules/class').default, // makes it easy to toggle classes
|
||||
require('snabbdom/modules/props').default, // for setting properties on DOM elements
|
||||
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
|
||||
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
|
||||
]);
|
||||
var h = require('snabbdom/h').default; // helper function for creating vnodes
|
||||
|
||||
var container = document.getElementById('container');
|
||||
|
||||
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
|
||||
h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
|
||||
' and this is just normal text',
|
||||
h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
|
||||
]);
|
||||
// Patch into empty DOM element – this modifies the DOM as a side effect
|
||||
patch(container, vnode);
|
||||
|
||||
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
|
||||
h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
|
||||
' and this is still just normal text',
|
||||
h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
|
||||
]);
|
||||
// Second `patch` invocation
|
||||
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
* [Animated reordering of elements](http://snabbdom.github.io/snabbdom/examples/reorder-animation/)
|
||||
* [Hero transitions](http://snabbdom.github.io/snabbdom/examples/hero/)
|
||||
* [SVG Carousel](http://snabbdom.github.io/snabbdom/examples/carousel-svg/)
|
||||
|
||||
## Core documentation
|
||||
|
||||
The core of Snabbdom provides only the most essential functionality.
|
||||
It is designed to be as simple as possible while still being fast and
|
||||
extendable.
|
||||
|
||||
### `snabbdom.init`
|
||||
|
||||
The core exposes only one single function `snabbdom.init`. This `init`
|
||||
takes a list of modules and returns a `patch` function that uses the
|
||||
specified set of modules.
|
||||
|
||||
```javascript
|
||||
var patch = snabbdom.init([
|
||||
require('snabbdom/modules/class').default,
|
||||
require('snabbdom/modules/style').default,
|
||||
]);
|
||||
```
|
||||
|
||||
### `patch`
|
||||
|
||||
The `patch` function returned by `init` takes two arguments. The first
|
||||
is a DOM element or a vnode representing the current view. The second
|
||||
is a vnode representing the new, updated view.
|
||||
|
||||
If a DOM element with a parent is passed, `newVnode` will be turned
|
||||
into a DOM node, and the passed element will be replaced by the
|
||||
created DOM node. If an old vnode is passed, Snabbdom will efficiently
|
||||
modify it to match the description in the new vnode.
|
||||
|
||||
Any old vnode passed must be the resulting vnode from a previous call
|
||||
to `patch`. This is necessary since Snabbdom stores information in the
|
||||
vnode. This makes it possible to implement a simpler and more
|
||||
performant architecture. This also avoids the creation of a new old
|
||||
vnode tree.
|
||||
|
||||
```javascript
|
||||
patch(oldVnode, newVnode);
|
||||
```
|
||||
|
||||
### `snabbdom/h`
|
||||
|
||||
It is recommended that you use `snabbdom/h` to create vnodes. `h` accepts a
|
||||
tag/selector as a string, an optional data object and an optional string or
|
||||
array of children.
|
||||
|
||||
```javascript
|
||||
var h = require('snabbdom/h').default;
|
||||
var vnode = h('div', {style: {color: '#000'}}, [
|
||||
h('h1', 'Headline'),
|
||||
h('p', 'A paragraph'),
|
||||
]);
|
||||
```
|
||||
|
||||
### `snabbdom/tovnode`
|
||||
|
||||
Converts a DOM node into a virtual node. Especially good for patching over an pre-existing,
|
||||
server-side generated content.
|
||||
|
||||
```javascript
|
||||
var snabbdom = require('snabbdom')
|
||||
var patch = snabbdom.init([ // Init patch function with chosen modules
|
||||
require('snabbdom/modules/class').default, // makes it easy to toggle classes
|
||||
require('snabbdom/modules/props').default, // for setting properties on DOM elements
|
||||
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
|
||||
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
|
||||
]);
|
||||
var h = require('snabbdom/h').default; // helper function for creating vnodes
|
||||
var toVNode = require('snabbdom/tovnode').default;
|
||||
|
||||
var newVNode = h('div', {style: {color: '#000'}}, [
|
||||
h('h1', 'Headline'),
|
||||
h('p', 'A paragraph'),
|
||||
]);
|
||||
|
||||
patch(toVNode(document.querySelector('.container')), newVNode)
|
||||
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
Hooks are a way to hook into the lifecycle of DOM nodes. Snabbdom
|
||||
offers a rich selection of hooks. Hooks are used both by modules to
|
||||
extend Snabbdom, and in normal code for executing arbitrary code at
|
||||
desired points in the life of a virtual node.
|
||||
|
||||
#### Overview
|
||||
|
||||
| Name | Triggered when | Arguments to callback |
|
||||
| ----------- | -------------- | ----------------------- |
|
||||
| `pre` | the patch process begins | none |
|
||||
| `init` | a vnode has been added | `vnode` |
|
||||
| `create` | a DOM element has been created based on a vnode | `emptyVnode, vnode` |
|
||||
| `insert` | an element has been inserted into the DOM | `vnode` |
|
||||
| `prepatch` | an element is about to be patched | `oldVnode, vnode` |
|
||||
| `update` | an element is being updated | `oldVnode, vnode` |
|
||||
| `postpatch` | an element has been patched | `oldVnode, vnode` |
|
||||
| `destroy` | an element is directly or indirectly being removed | `vnode` |
|
||||
| `remove` | an element is directly being removed from the DOM | `vnode, removeCallback` |
|
||||
| `post` | the patch process is done | none |
|
||||
|
||||
The following hooks are available for modules: `pre`, `create`,
|
||||
`update`, `destroy`, `remove`, `post`.
|
||||
|
||||
The following hooks are available in the `hook` property of individual
|
||||
elements: `init`, `create`, `insert`, `prepatch`, `update`,
|
||||
`postpatch`, `destroy`, `remove`.
|
||||
|
||||
#### Usage
|
||||
|
||||
To use hooks, pass them as an object to `hook` field of the data
|
||||
object argument.
|
||||
|
||||
```javascript
|
||||
h('div.row', {
|
||||
key: movie.rank,
|
||||
hook: {
|
||||
insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight; }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### The `init` hook
|
||||
|
||||
This hook is invoked during the patch process when a new virtual node
|
||||
has been found. The hook is called before Snabbdom has processed the
|
||||
node in any way. I.e., before it has created a DOM node based on the
|
||||
vnode.
|
||||
|
||||
#### The `insert` hook
|
||||
|
||||
This hook is invoked once the DOM element for a vnode has been
|
||||
inserted into the document _and_ the rest of the patch cycle is done.
|
||||
This means that you can do DOM measurements (like using
|
||||
[getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)
|
||||
in this hook safely, knowing that no elements will be changed
|
||||
afterwards that could affect the position of the inserted elements.
|
||||
|
||||
#### The `remove` hook
|
||||
|
||||
Allows you to hook into the removal of an element. The hook is called
|
||||
once a vnode is to be removed from the DOM. The handling function
|
||||
receives both the vnode and a callback. You can control and delay the
|
||||
removal with the callback. The callback should be invoked once the
|
||||
hook is done doing its business, and the element will only be removed
|
||||
once all `remove` hooks have invoked their callback.
|
||||
|
||||
The hook is only triggered when an element is to be removed from its
|
||||
parent – not if it is the child of an element that is removed. For
|
||||
that, see the `destroy` hook.
|
||||
|
||||
#### The `destroy` hook
|
||||
|
||||
This hook is invoked on a virtual node when its DOM element is removed
|
||||
from the DOM or if its parent is being removed from the DOM.
|
||||
|
||||
To see the difference between this hook and the `remove` hook,
|
||||
consider an example.
|
||||
|
||||
```js
|
||||
var vnode1 = h('div', [h('div', [h('span', 'Hello')])]);
|
||||
var vnode2 = h('div', []);
|
||||
patch(container, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
```
|
||||
|
||||
Here `destroy` is triggered for both the inner `div` element _and_ the
|
||||
`span` element it contains. `remove`, on the other hand, is only
|
||||
triggered on the `div` element because it is the only element being
|
||||
detached from its parent.
|
||||
|
||||
You can, for instance, use `remove` to trigger an animation when an
|
||||
element is being removed and use the `destroy` hook to additionally
|
||||
animate the disappearance of the removed element's children.
|
||||
|
||||
### Creating modules
|
||||
|
||||
Modules works by registering global listeners for [hooks](#hooks). A module is simply a dictionary mapping hook names to functions.
|
||||
|
||||
```javascript
|
||||
var myModule = {
|
||||
create: function(oldVnode, vnode) {
|
||||
// invoked whenever a new virtual node is created
|
||||
},
|
||||
update: function(oldVnode, vnode) {
|
||||
// invoked whenever a virtual node is updated
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
With this mechanism you can easily augment the behaviour of Snabbdom.
|
||||
For demonstration, take a look at the implementations of the default
|
||||
modules.
|
||||
|
||||
## Modules documentation
|
||||
|
||||
This describes the core modules. All modules are optional.
|
||||
|
||||
### The class module
|
||||
|
||||
The class module provides an easy way to dynamically toggle classes on
|
||||
elements. It expects an object in the `class` data property. The
|
||||
object should map class names to booleans that indicates whether or
|
||||
not the class should stay or go on the vnode.
|
||||
|
||||
```javascript
|
||||
h('a', {class: {active: true, selected: false}}, 'Toggle');
|
||||
```
|
||||
|
||||
### The props module
|
||||
|
||||
Allows you to set properties on DOM elements.
|
||||
|
||||
```javascript
|
||||
h('a', {props: {href: '/foo'}}, 'Go to Foo');
|
||||
```
|
||||
|
||||
### The attributes module
|
||||
|
||||
Same as props, but set attributes instead of properties on DOM elements.
|
||||
|
||||
```javascript
|
||||
h('a', {attrs: {href: '/foo'}}, 'Go to Foo');
|
||||
```
|
||||
|
||||
Attributes are added and updated using `setAttribute`. In case of an
|
||||
attribute that had been previously added/set and is no longer present
|
||||
in the `attrs` object, it is removed from the DOM element's attribute
|
||||
list using `removeAttribute`.
|
||||
|
||||
In the case of boolean attributes (e.g. `disabled`, `hidden`,
|
||||
`selected` ...), the meaning doesn't depend on the attribute value
|
||||
(`true` or `false`) but depends instead on the presence/absence of the
|
||||
attribute itself in the DOM element. Those attributes are handled
|
||||
differently by the module: if a boolean attribute is set to a
|
||||
[falsy value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)
|
||||
(`0`, `-0`, `null`, `false`,`NaN`, `undefined`, or the empty string
|
||||
(`""`)), then the attribute will be removed from the attribute list of
|
||||
the DOM element.
|
||||
|
||||
### The dataset module
|
||||
|
||||
Allows you to set custom data attributes (`data-*`) on DOM elements. These can then be accessed with the [HTMLElement.dataset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) property.
|
||||
|
||||
```javascript
|
||||
h('button', {dataset: {action: 'reset'}}, 'Reset');
|
||||
```
|
||||
|
||||
### The style module
|
||||
|
||||
The style module is for making your HTML look slick and animate smoothly. At
|
||||
its core it allows you to set CSS properties on elements.
|
||||
|
||||
```javascript
|
||||
h('span', {
|
||||
style: {border: '1px solid #bada55', color: '#c0ffee', fontWeight: 'bold'}
|
||||
}, 'Say my name, and every colour illuminates');
|
||||
```
|
||||
|
||||
Note that the style module does not remove style attributes if they
|
||||
are removed as properties from the style object. To remove a style,
|
||||
you should instead set it to the empty string.
|
||||
|
||||
```javascript
|
||||
h('div', {
|
||||
style: {position: shouldFollow ? 'fixed' : ''}
|
||||
}, 'I, I follow, I follow you');
|
||||
```
|
||||
|
||||
#### Custom properties (CSS variables)
|
||||
|
||||
CSS custom properties (aka CSS variables) are supported, they must be prefixed
|
||||
with `--`
|
||||
|
||||
```javascript
|
||||
h('div', {
|
||||
style: {'--warnColor': 'yellow'}
|
||||
}, 'Warning');
|
||||
```
|
||||
|
||||
#### Delayed properties
|
||||
|
||||
You can specify properties as being delayed. Whenever these properties
|
||||
change, the change is not applied until after the next frame.
|
||||
|
||||
```javascript
|
||||
h('span', {
|
||||
style: {opacity: '0', transition: 'opacity 1s', delayed: {opacity: '1'}}
|
||||
}, 'Imma fade right in!');
|
||||
```
|
||||
|
||||
This makes it easy to declaratively animate the entry of elements.
|
||||
|
||||
#### Set properties on `remove`
|
||||
|
||||
Styles set in the `remove` property will take effect once the element
|
||||
is about to be removed from the DOM. The applied styles should be
|
||||
animated with CSS transitions. Only once all the styles are done
|
||||
animating will the element be removed from the DOM.
|
||||
|
||||
```javascript
|
||||
h('span', {
|
||||
style: {opacity: '1', transition: 'opacity 1s',
|
||||
remove: {opacity: '0'}}
|
||||
}, 'It\'s better to fade out than to burn away');
|
||||
```
|
||||
|
||||
This makes it easy to declaratively animate the removal of elements.
|
||||
|
||||
#### Set properties on `destroy`
|
||||
|
||||
```javascript
|
||||
h('span', {
|
||||
style: {opacity: '1', transition: 'opacity 1s',
|
||||
destroy: {opacity: '0'}}
|
||||
}, 'It\'s better to fade out than to burn away');
|
||||
```
|
||||
|
||||
### Eventlisteners module
|
||||
|
||||
The event listeners module gives powerful capabilities for attaching
|
||||
event listeners.
|
||||
|
||||
You can attach a function to an event on a vnode by supplying an
|
||||
object at `on` with a property corresponding to the name of the event
|
||||
you want to listen to. The function will be called when the event
|
||||
happens and will be passed the event object that belongs to it.
|
||||
|
||||
```javascript
|
||||
function clickHandler(ev) { console.log('got clicked'); }
|
||||
h('div', {on: {click: clickHandler}});
|
||||
```
|
||||
|
||||
Very often, however, you're not really interested in the event object
|
||||
itself. Often you have some data associated with the element that
|
||||
triggers an event and you want that data passed along instead.
|
||||
|
||||
Consider a counter application with three buttons, one to increment
|
||||
the counter by 1, one to increment the counter by 2 and one to
|
||||
increment the counter by 3. You don't really care exactly which button
|
||||
was pressed. Instead you're interested in what number was associated
|
||||
with the clicked button. The event listeners module allows one to
|
||||
express that by supplying an array at the named event property. The
|
||||
first element in the array should be a function that will be invoked
|
||||
with the value in the second element once the event occurs.
|
||||
|
||||
```javascript
|
||||
function clickHandler(number) { console.log('button ' + number + ' was clicked!'); }
|
||||
h('div', [
|
||||
h('a', {on: {click: [clickHandler, 1]}}),
|
||||
h('a', {on: {click: [clickHandler, 2]}}),
|
||||
h('a', {on: {click: [clickHandler, 3]}}),
|
||||
]);
|
||||
```
|
||||
|
||||
Each handler is called not only with the given arguments but also with the current event and vnode appended to the argument list. It also supports using multiple listeners per event by specifying an array of handlers:
|
||||
```javascript
|
||||
stopPropagation = function(ev) { ev.stopPropagation() }
|
||||
sendValue = function(func, ev, vnode) { func(vnode.elm.value) }
|
||||
|
||||
h('a', { on:{ click:[[sendValue, console.log], stopPropagation] } });
|
||||
```
|
||||
|
||||
Snabbdom allows swapping event handlers between renders. This happens without
|
||||
actually touching the event handlers attached to the DOM.
|
||||
|
||||
Note, however, that **you should be careful when sharing event
|
||||
handlers between vnodes**, because of the technique this module uses
|
||||
to avoid re-binding event handlers to the DOM. (And in general,
|
||||
sharing data between vnodes is not guaranteed to work, because modules
|
||||
are allowed to mutate the given data).
|
||||
|
||||
In particular, you should **not** do something like this:
|
||||
|
||||
```javascript
|
||||
// Does not work
|
||||
var sharedHandler = {
|
||||
change: function(e){ console.log('you chose: ' + e.target.value); }
|
||||
};
|
||||
h('div', [
|
||||
h('input', {props: {type: 'radio', name: 'test', value: '0'},
|
||||
on: sharedHandler}),
|
||||
h('input', {props: {type: 'radio', name: 'test', value: '1'},
|
||||
on: sharedHandler}),
|
||||
h('input', {props: {type: 'radio', name: 'test', value: '2'},
|
||||
on: sharedHandler})
|
||||
]);
|
||||
```
|
||||
|
||||
For many such cases, you can use array-based handlers instead (described above).
|
||||
Alternatively, simply make sure each node is passed unique `on` values:
|
||||
|
||||
```javascript
|
||||
// Works
|
||||
var sharedHandler = function(e){ console.log('you chose: ' + e.target.value); };
|
||||
h('div', [
|
||||
h('input', {props: {type: 'radio', name: 'test', value: '0'},
|
||||
on: {change: sharedHandler}}),
|
||||
h('input', {props: {type: 'radio', name: 'test', value: '1'},
|
||||
on: {change: sharedHandler}}),
|
||||
h('input', {props: {type: 'radio', name: 'test', value: '2'},
|
||||
on: {change: sharedHandler}})
|
||||
]);
|
||||
```
|
||||
|
||||
## Helpers
|
||||
|
||||
### SVG
|
||||
|
||||
SVG just works when using the `h` function for creating virtual
|
||||
nodes. SVG elements are automatically created with the appropriate
|
||||
namespaces.
|
||||
|
||||
```javascript
|
||||
var vnode = h('div', [
|
||||
h('svg', {attrs: {width: 100, height: 100}}, [
|
||||
h('circle', {attrs: {cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow'}})
|
||||
])
|
||||
]);
|
||||
```
|
||||
|
||||
See also the [SVG example](./examples/svg) and the [SVG Carousel example](./examples/carousel-svg/).
|
||||
|
||||
#### Using Classes in SVG Elements
|
||||
|
||||
Certain browsers (like IE <=11) [do not support `classList` property in SVG elements](http://caniuse.com/#feat=classlist).
|
||||
Hence, the _class_ module (which uses `classList` property internally) will not work for these browsers.
|
||||
|
||||
The classes in selectors for SVG elements work fine from version 0.6.7.
|
||||
|
||||
You can add dynamic classes to SVG elements for these cases by using the _attributes_ module and an Array as shown below:
|
||||
|
||||
```js
|
||||
h('svg', [
|
||||
h('text.underline', { // 'underline' is a selector class, remain unchanged between renders.
|
||||
attrs: {
|
||||
// 'active' and 'red' are dynamic classes, they can change between renders
|
||||
// so we need to put them in the class attribute.
|
||||
// (Normally we'd use the classModule, but it doesn't work inside SVG)
|
||||
class: [isActive && "active", isColored && "red"].filter(Boolean).join(" ")
|
||||
}
|
||||
},
|
||||
'Hello World'
|
||||
)
|
||||
])
|
||||
```
|
||||
|
||||
### Thunks
|
||||
|
||||
The `thunk` function takes a selector, a key for identifying a thunk,
|
||||
a function that returns a vnode and a variable amount of state
|
||||
parameters. If invoked, the render function will receive the state
|
||||
arguments.
|
||||
|
||||
`thunk(selector, key, renderFn, [stateArguments])`
|
||||
|
||||
The `key` is optional. It should be supplied when the `selector` is
|
||||
not unique among the thunks siblings. This ensures that the thunk is
|
||||
always matched correctly when diffing.
|
||||
|
||||
Thunks are an optimization strategy that can be used when one is
|
||||
dealing with immutable data.
|
||||
|
||||
Consider a simple function for creating a virtual node based on a number.
|
||||
|
||||
```js
|
||||
function numberView(n) {
|
||||
return h('div', 'Number is: ' + n);
|
||||
}
|
||||
```
|
||||
|
||||
The view depends only on `n`. This means that if `n` is unchanged,
|
||||
then creating the virtual DOM node and patching it against the old
|
||||
vnode is wasteful. To avoid the overhead we can use the `thunk` helper
|
||||
function.
|
||||
|
||||
```js
|
||||
function render(state) {
|
||||
return thunk('num', numberView, [state.number]);
|
||||
}
|
||||
```
|
||||
|
||||
Instead of actually invoking the `numberView` function this will only
|
||||
place a dummy vnode in the virtual tree. When Snabbdom patches this
|
||||
dummy vnode against a previous vnode, it will compare the value of
|
||||
`n`. If `n` is unchanged it will simply reuse the old vnode. This
|
||||
avoids recreating the number view and the diff process altogether.
|
||||
|
||||
The view function here is only an example. In practice thunks are only
|
||||
relevant if you are rendering a complicated view that takes
|
||||
significant computational time to generate.
|
||||
|
||||
## Virtual Node
|
||||
**Properties**
|
||||
- [sel](#sel--string)
|
||||
- [data](#data--object)
|
||||
- [children](#children--array)
|
||||
- [text](#text--string)
|
||||
- [elm](#elm--element)
|
||||
- [key](#key--string--number)
|
||||
|
||||
#### sel : String
|
||||
|
||||
The `.sel` property of a virtual node is the CSS selector passed to
|
||||
[`h()`](#snabbdomh) during creation. For example: `h('div#container',
|
||||
{}, [...])` will create a a virtual node which has `div#container` as
|
||||
its `.sel` property.
|
||||
|
||||
#### data : Object
|
||||
|
||||
The `.data` property of a virtual node is the place to add information
|
||||
for [modules](#modules-documentation) to access and manipulate the
|
||||
real DOM element when it is created; Add styles, CSS classes,
|
||||
attributes, etc.
|
||||
|
||||
The data object is the (optional) second parameter to [`h()`](#snabbdomh)
|
||||
|
||||
For example `h('div', {props: {className: 'container'}}, [...])` will produce a virtual node with
|
||||
```js
|
||||
{
|
||||
"props": {
|
||||
className: "container"
|
||||
}
|
||||
}
|
||||
```
|
||||
as its `.data` object.
|
||||
|
||||
#### children : Array<vnode>
|
||||
|
||||
The `.children` property of a virtual node is the third (optional)
|
||||
parameter to [`h()`](#snabbdomh) during creation. `.children` is
|
||||
simply an Array of virtual nodes that should be added as children of
|
||||
the parent DOM node upon creation.
|
||||
|
||||
For example `h('div', {}, [ h('h1', {}, 'Hello, World') ])` will
|
||||
create a virtual node with
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
sel: 'h1',
|
||||
data: {},
|
||||
children: undefined,
|
||||
text: 'Hello, World',
|
||||
elm: Element,
|
||||
key: undefined,
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
as its `.children` property.
|
||||
|
||||
#### text : string
|
||||
|
||||
The `.text` property is created when a virtual node is created with
|
||||
only a single child that possesses text and only requires
|
||||
`document.createTextNode()` to be used.
|
||||
|
||||
For example: `h('h1', {}, 'Hello')` will create a virtual node with
|
||||
`Hello` as its `.text` property.
|
||||
|
||||
#### elm : Element
|
||||
|
||||
The `.elm` property of a virtual node is a pointer to the real DOM
|
||||
node created by snabbdom. This property is very useful to do
|
||||
calculations in [hooks](#hooks) as well as
|
||||
[modules](#modules-documentation).
|
||||
|
||||
#### key : string | number
|
||||
|
||||
The `.key` property is created when a key is provided inside of your
|
||||
[`.data`](#data--object) object. The `.key` property is used to keep
|
||||
pointers to DOM nodes that existed previously to avoid recreating them
|
||||
if it is unnecessary. This is very useful for things like list
|
||||
reordering. A key must be either a string or a number to allow for
|
||||
proper lookup as it is stored internally as a key/value pair inside of
|
||||
an object, where `.key` is the key and the value is the
|
||||
[`.elm`](#elm--element) property created.
|
||||
|
||||
For example: `h('div', {key: 1}, [])` will create a virtual node
|
||||
object with a `.key` property with the value of `1`.
|
||||
|
||||
|
||||
## Structuring applications
|
||||
|
||||
Snabbdom is a low-level virtual DOM library. It is unopinionated with
|
||||
regards to how you should structure your application.
|
||||
|
||||
Here are some approaches to building applications with Snabbdom.
|
||||
|
||||
* [functional-frontend-architecture](https://github.com/paldepind/functional-frontend-architecture) –
|
||||
a repository containing several example applications that
|
||||
demonstrates an architecture that uses Snabbdom.
|
||||
* [Cycle.js](https://cycle.js.org/) –
|
||||
"A functional and reactive JavaScript framework for cleaner code"
|
||||
uses Snabbdom
|
||||
* [Vue.js](http://vuejs.org/) use a fork of snabbdom.
|
||||
* [scheme-todomvc](https://github.com/amirouche/scheme-todomvc/) build
|
||||
redux-like architecture on top of snabbdom bindings.
|
||||
* [kaiju](https://github.com/AlexGalays/kaiju) -
|
||||
Stateful components and observables on top of snabbdom
|
||||
* [Tweed](https://tweedjs.github.io) –
|
||||
An Object Oriented approach to reactive interfaces.
|
||||
* [Cyclow](http://cyclow.js.org) -
|
||||
"A reactive frontend framework for JavaScript"
|
||||
uses Snabbdom
|
||||
* [Tung](https://github.com/Reon90/tung) –
|
||||
A JavaScript library for rendering html. Tung helps to divide html and JavaScript development.
|
||||
* [sprotty](https://github.com/theia-ide/sprotty) - "A web-based diagramming framework" uses Snabbdom.
|
||||
* [Mark Text](https://github.com/marktext/marktext) - "Realtime preview Markdown Editor" build on Snabbdom.
|
||||
* [puddles](https://github.com/flintinatux/puddles) -
|
||||
"Tiny vdom app framework. Pure Redux. No boilerplate." - Built with :heart: on Snabbdom.
|
||||
* [Backbone.VDOMView](https://github.com/jcbrand/backbone.vdomview) - A [Backbone](http://backbonejs.org/) View with VirtualDOM capability via Snabbdom.
|
||||
|
||||
Be sure to share it if you're building an application in another way
|
||||
using Snabbdom.
|
||||
|
||||
## Common errors
|
||||
|
||||
```
|
||||
Uncaught NotFoundError: Failed to execute 'insertBefore' on 'Node':
|
||||
The node before which the new node is to be inserted is not a child of this node.
|
||||
```
|
||||
The reason for this error is reusing of vnodes between patches (see code example), snabbdom stores actual dom nodes inside the virtual dom nodes passed to it as performance improvement, so reusing nodes between patches is not supported.
|
||||
```js
|
||||
var sharedNode = h('div', {}, 'Selected');
|
||||
var vnode1 = h('div', [
|
||||
h('div', {}, ['One']),
|
||||
h('div', {}, ['Two']),
|
||||
h('div', {}, [sharedNode]),
|
||||
]);
|
||||
var vnode2 = h('div', [
|
||||
h('div', {}, ['One']),
|
||||
h('div', {}, [sharedNode]),
|
||||
h('div', {}, ['Three']),
|
||||
]);
|
||||
patch(container, vnode1);
|
||||
patch(vnode1, vnode2);
|
||||
```
|
||||
You can fix this issue by creating a shallow copy of the object (here with object spread syntax):
|
||||
```js
|
||||
var vnode2 = h('div', [
|
||||
h('div', {}, ['One']),
|
||||
h('div', {}, [{ ...sharedNode }]),
|
||||
h('div', {}, ['Three']),
|
||||
]);
|
||||
```
|
||||
Another solution would be to wrap shared vnodes in a factory function:
|
||||
```js
|
||||
var sharedNode = () => h('div', {}, 'Selected');
|
||||
var vnode1 = h('div', [
|
||||
h('div', {}, ['One']),
|
||||
h('div', {}, ['Two']),
|
||||
h('div', {}, [sharedNode()]),
|
||||
]);
|
||||
```
|
||||
@@ -1,95 +0,0 @@
|
||||
module.exports = {
|
||||
// Latest mainstream
|
||||
BS_Chrome_Current: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'chrome',
|
||||
browser_version: 'latest',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
BS_Firefox_Current: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'firefox',
|
||||
browser_version: 'latest',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
BS_Safari_Current: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'safari',
|
||||
browser_version: 'latest',
|
||||
os: 'OS X',
|
||||
os_version: 'High Sierra',
|
||||
},
|
||||
BS_Android_8: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'Android',
|
||||
device: 'Google Pixel 2',
|
||||
os: 'Android',
|
||||
os_version: '8.0',
|
||||
real_mobile: true,
|
||||
},
|
||||
|
||||
// Older mainstream
|
||||
BS_Chrome_49: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'chrome',
|
||||
browser_version: '49',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
BS_Firefox_52: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'firefox',
|
||||
browser_version: '52',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
BS_Safari_9: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'safari',
|
||||
browser_version: '9.1',
|
||||
os: 'OS X',
|
||||
os_version: 'El Capitan',
|
||||
},
|
||||
|
||||
// Misc
|
||||
BS_Android_4_4: {
|
||||
base: 'BrowserStack',
|
||||
device_browser: 'ucbrowser',
|
||||
device: 'Google Nexus 5',
|
||||
os: 'Android',
|
||||
os_version: '4.4',
|
||||
real_mobile: true,
|
||||
},
|
||||
BS_iphone_10: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'Mobile Safari',
|
||||
browser_version: null,
|
||||
device: 'iPhone 7',
|
||||
real_mobile: true,
|
||||
os: 'ios',
|
||||
os_version: '10.3',
|
||||
},
|
||||
BS_MS_Edge: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'edge',
|
||||
browser_version: 'latest',
|
||||
os: 'Windows',
|
||||
os_version: '10',
|
||||
},
|
||||
BS_IE_11: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'ie',
|
||||
browser_version: '11.0',
|
||||
os: 'Windows',
|
||||
os_version: '7',
|
||||
},
|
||||
BS_IE_10: {
|
||||
base: 'BrowserStack',
|
||||
browser: 'ie',
|
||||
browser_version: '10.0',
|
||||
os: 'Windows',
|
||||
os_version: '7',
|
||||
},
|
||||
};
|
||||
Vendored
-83
@@ -1,83 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.h = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var vnode_1 = require("./vnode");
|
||||
var is = require("./is");
|
||||
function addNS(data, children, sel) {
|
||||
data.ns = 'http://www.w3.org/2000/svg';
|
||||
if (sel !== 'foreignObject' && children !== undefined) {
|
||||
for (var i = 0; i < children.length; ++i) {
|
||||
var childData = children[i].data;
|
||||
if (childData !== undefined) {
|
||||
addNS(childData, children[i].children, children[i].sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function h(sel, b, c) {
|
||||
var data = {}, children, text, i;
|
||||
if (c !== undefined) {
|
||||
data = b;
|
||||
if (is.array(c)) {
|
||||
children = c;
|
||||
}
|
||||
else if (is.primitive(c)) {
|
||||
text = c;
|
||||
}
|
||||
else if (c && c.sel) {
|
||||
children = [c];
|
||||
}
|
||||
}
|
||||
else if (b !== undefined) {
|
||||
if (is.array(b)) {
|
||||
children = b;
|
||||
}
|
||||
else if (is.primitive(b)) {
|
||||
text = b;
|
||||
}
|
||||
else if (b && b.sel) {
|
||||
children = [b];
|
||||
}
|
||||
else {
|
||||
data = b;
|
||||
}
|
||||
}
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
if (is.primitive(children[i]))
|
||||
children[i] = vnode_1.vnode(undefined, undefined, undefined, children[i]);
|
||||
}
|
||||
}
|
||||
if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
|
||||
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')) {
|
||||
addNS(data, children, sel);
|
||||
}
|
||||
return vnode_1.vnode(sel, data, children, text, undefined);
|
||||
}
|
||||
exports.h = h;
|
||||
;
|
||||
exports.default = h;
|
||||
|
||||
},{"./is":2,"./vnode":3}],2:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.array = Array.isArray;
|
||||
function primitive(s) {
|
||||
return typeof s === 'string' || typeof s === 'number';
|
||||
}
|
||||
exports.primitive = primitive;
|
||||
|
||||
},{}],3:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function vnode(sel, data, children, text, elm) {
|
||||
var key = data === undefined ? undefined : data.key;
|
||||
return { sel: sel, data: data, children: children,
|
||||
text: text, elm: elm, key: key };
|
||||
}
|
||||
exports.vnode = vnode;
|
||||
exports.default = vnode;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy8ucmVnaXN0cnkubnBtanMub3JnL2Jyb3dzZXItcGFjay82LjAuMi9ub2RlX21vZHVsZXMvYnJvd3Nlci1wYWNrL19wcmVsdWRlLmpzIiwiaC5qcyIsImlzLmpzIiwidm5vZGUuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQzFEQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ1BBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIlwidXNlIHN0cmljdFwiO1xuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFwiX19lc01vZHVsZVwiLCB7IHZhbHVlOiB0cnVlIH0pO1xudmFyIHZub2RlXzEgPSByZXF1aXJlKFwiLi92bm9kZVwiKTtcbnZhciBpcyA9IHJlcXVpcmUoXCIuL2lzXCIpO1xuZnVuY3Rpb24gYWRkTlMoZGF0YSwgY2hpbGRyZW4sIHNlbCkge1xuICAgIGRhdGEubnMgPSAnaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnO1xuICAgIGlmIChzZWwgIT09ICdmb3JlaWduT2JqZWN0JyAmJiBjaGlsZHJlbiAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGZvciAodmFyIGkgPSAwOyBpIDwgY2hpbGRyZW4ubGVuZ3RoOyArK2kpIHtcbiAgICAgICAgICAgIHZhciBjaGlsZERhdGEgPSBjaGlsZHJlbltpXS5kYXRhO1xuICAgICAgICAgICAgaWYgKGNoaWxkRGF0YSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICAgICAgYWRkTlMoY2hpbGREYXRhLCBjaGlsZHJlbltpXS5jaGlsZHJlbiwgY2hpbGRyZW5baV0uc2VsKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbn1cbmZ1bmN0aW9uIGgoc2VsLCBiLCBjKSB7XG4gICAgdmFyIGRhdGEgPSB7fSwgY2hpbGRyZW4sIHRleHQsIGk7XG4gICAgaWYgKGMgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBkYXRhID0gYjtcbiAgICAgICAgaWYgKGlzLmFycmF5KGMpKSB7XG4gICAgICAgICAgICBjaGlsZHJlbiA9IGM7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSBpZiAoaXMucHJpbWl0aXZlKGMpKSB7XG4gICAgICAgICAgICB0ZXh0ID0gYztcbiAgICAgICAgfVxuICAgICAgICBlbHNlIGlmIChjICYmIGMuc2VsKSB7XG4gICAgICAgICAgICBjaGlsZHJlbiA9IFtjXTtcbiAgICAgICAgfVxuICAgIH1cbiAgICBlbHNlIGlmIChiICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKGlzLmFycmF5KGIpKSB7XG4gICAgICAgICAgICBjaGlsZHJlbiA9IGI7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSBpZiAoaXMucHJpbWl0aXZlKGIpKSB7XG4gICAgICAgICAgICB0ZXh0ID0gYjtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIGlmIChiICYmIGIuc2VsKSB7XG4gICAgICAgICAgICBjaGlsZHJlbiA9IFtiXTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGRhdGEgPSBiO1xuICAgICAgICB9XG4gICAgfVxuICAgIGlmIChpcy5hcnJheShjaGlsZHJlbikpIHtcbiAgICAgICAgZm9yIChpID0gMDsgaSA8IGNoaWxkcmVuLmxlbmd0aDsgKytpKSB7XG4gICAgICAgICAgICBpZiAoaXMucHJpbWl0aXZlKGNoaWxkcmVuW2ldKSlcbiAgICAgICAgICAgICAgICBjaGlsZHJlbltpXSA9IHZub2RlXzEudm5vZGUodW5kZWZpbmVkLCB1bmRlZmluZWQsIHVuZGVmaW5lZCwgY2hpbGRyZW5baV0pO1xuICAgICAgICB9XG4gICAgfVxuICAgIGlmIChzZWxbMF0gPT09ICdzJyAmJiBzZWxbMV0gPT09ICd2JyAmJiBzZWxbMl0gPT09ICdnJyAmJlxuICAgICAgICAoc2VsLmxlbmd0aCA9PT0gMyB8fCBzZWxbM10gPT09ICcuJyB8fCBzZWxbM10gPT09ICcjJykpIHtcbiAgICAgICAgYWRkTlMoZGF0YSwgY2hpbGRyZW4sIHNlbCk7XG4gICAgfVxuICAgIHJldHVybiB2bm9kZV8xLnZub2RlKHNlbCwgZGF0YSwgY2hpbGRyZW4sIHRleHQsIHVuZGVmaW5lZCk7XG59XG5leHBvcnRzLmggPSBoO1xuO1xuZXhwb3J0cy5kZWZhdWx0ID0gaDtcbi8vIyBzb3VyY2VNYXBwaW5nVVJMPWguanMubWFwIiwiXCJ1c2Ugc3RyaWN0XCI7XG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgXCJfX2VzTW9kdWxlXCIsIHsgdmFsdWU6IHRydWUgfSk7XG5leHBvcnRzLmFycmF5ID0gQXJyYXkuaXNBcnJheTtcbmZ1bmN0aW9uIHByaW1pdGl2ZShzKSB7XG4gICAgcmV0dXJuIHR5cGVvZiBzID09PSAnc3RyaW5nJyB8fCB0eXBlb2YgcyA9PT0gJ251bWJlcic7XG59XG5leHBvcnRzLnByaW1pdGl2ZSA9IHByaW1pdGl2ZTtcbi8vIyBzb3VyY2VNYXBwaW5nVVJMPWlzLmpzLm1hcCIsIlwidXNlIHN0cmljdFwiO1xuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFwiX19lc01vZHVsZVwiLCB7IHZhbHVlOiB0cnVlIH0pO1xuZnVuY3Rpb24gdm5vZGUoc2VsLCBkYXRhLCBjaGlsZHJlbiwgdGV4dCwgZWxtKSB7XG4gICAgdmFyIGtleSA9IGRhdGEgPT09IHVuZGVmaW5lZCA/IHVuZGVmaW5lZCA6IGRhdGEua2V5O1xuICAgIHJldHVybiB7IHNlbDogc2VsLCBkYXRhOiBkYXRhLCBjaGlsZHJlbjogY2hpbGRyZW4sXG4gICAgICAgIHRleHQ6IHRleHQsIGVsbTogZWxtLCBrZXk6IGtleSB9O1xufVxuZXhwb3J0cy52bm9kZSA9IHZub2RlO1xuZXhwb3J0cy5kZWZhdWx0ID0gdm5vZGU7XG4vLyMgc291cmNlTWFwcGluZ1VSTD12bm9kZS5qcy5tYXAiXX0=
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var r;r="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,r.h=e()}}(function(){return function e(r,n,i){function t(f,u){if(!n[f]){if(!r[f]){var d="function"==typeof require&&require;if(!u&&d)return d(f,!0);if(o)return o(f,!0);var a=new Error("Cannot find module '"+f+"'");throw a.code="MODULE_NOT_FOUND",a}var v=n[f]={exports:{}};r[f][0].call(v.exports,function(e){var n=r[f][1][e];return t(n?n:e)},v,v.exports,e,r,n,i)}return n[f].exports}for(var o="function"==typeof require&&require,f=0;f<i.length;f++)t(i[f]);return t}({1:[function(e,r,n){"use strict";function i(e,r,n){if(e.ns="http://www.w3.org/2000/svg","foreignObject"!==n&&void 0!==r)for(var t=0;t<r.length;++t){var o=r[t].data;void 0!==o&&i(o,r[t].children,r[t].sel)}}function t(e,r,n){var t,u,d,a={};if(void 0!==n?(a=r,f.array(n)?t=n:f.primitive(n)?u=n:n&&n.sel&&(t=[n])):void 0!==r&&(f.array(r)?t=r:f.primitive(r)?u=r:r&&r.sel?t=[r]:a=r),f.array(t))for(d=0;d<t.length;++d)f.primitive(t[d])&&(t[d]=o.vnode(void 0,void 0,void 0,t[d]));return"s"!==e[0]||"v"!==e[1]||"g"!==e[2]||3!==e.length&&"."!==e[3]&&"#"!==e[3]||i(a,t,e),o.vnode(e,a,t,u,void 0)}var o=e("./vnode"),f=e("./is");n.h=t,Object.defineProperty(n,"__esModule",{value:!0}),n["default"]=t},{"./is":2,"./vnode":3}],2:[function(e,r,n){"use strict";function i(e){return"string"==typeof e||"number"==typeof e}n.array=Array.isArray,n.primitive=i},{}],3:[function(e,r,n){"use strict";function i(e,r,n,i,t){var o=void 0===r?void 0:r.key;return{sel:e,data:r,children:n,text:i,elm:t,key:o}}n.vnode=i,Object.defineProperty(n,"__esModule",{value:!0}),n["default"]=i},{}]},{},[1])(1)});
|
||||
//# sourceMappingURL=h.min.js.map
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
-71
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.snabbdom_attributes=e()}}(function(){return function e(t,r,n){function o(a,u){if(!r[a]){if(!t[a]){var d="function"==typeof require&&require;if(!u&&d)return d(a,!0);if(i)return i(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var f=r[a]={exports:{}};t[a][0].call(f.exports,function(e){var r=t[a][1][e];return o(r?r:e)},f,f.exports,e,t,r,n)}return r[a].exports}for(var i="function"==typeof require&&require,a=0;a<n.length;a++)o(n[a]);return o}({1:[function(e,t,r){"use strict";function n(e,t){var r,n,i,u,d=t.elm,l=e.data.attrs,f=t.data.attrs;if((l||f)&&l!==f){l=l||{},f=f||{};for(r in f)n=f[r],i=l[r],i!==n&&(!n&&a[r]?d.removeAttribute(r):(u=r.split(":"),u.length>1&&o.hasOwnProperty(u[0])?d.setAttributeNS(o[u[0]],r,n):d.setAttribute(r,n)));for(r in l)r in f||d.removeAttribute(r)}}for(var o={xlink:"http://www.w3.org/1999/xlink"},i=["allowfullscreen","async","autofocus","autoplay","checked","compact","controls","declare","default","defaultchecked","defaultmuted","defaultselected","defer","disabled","draggable","enabled","formnovalidate","hidden","indeterminate","inert","ismap","itemscope","loop","multiple","muted","nohref","noresize","noshade","novalidate","nowrap","open","pauseonexit","readonly","required","reversed","scoped","seamless","selected","sortable","spellcheck","translate","truespeed","typemustmatch","visible"],a=Object.create(null),u=0,d=i.length;u<d;u++)a[i[u]]=!0;r.attributesModule={create:n,update:n},Object.defineProperty(r,"__esModule",{value:!0}),r["default"]=r.attributesModule},{}]},{},[1])(1)});
|
||||
//# sourceMappingURL=snabbdom-attributes.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
-29
@@ -1,29 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.snabbdom_class = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function updateClass(oldVnode, vnode) {
|
||||
var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class;
|
||||
if (!oldClass && !klass)
|
||||
return;
|
||||
if (oldClass === klass)
|
||||
return;
|
||||
oldClass = oldClass || {};
|
||||
klass = klass || {};
|
||||
for (name in oldClass) {
|
||||
if (!klass[name]) {
|
||||
elm.classList.remove(name);
|
||||
}
|
||||
}
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
elm.classList[cur ? 'add' : 'remove'](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.classModule = { create: updateClass, update: updateClass };
|
||||
exports.default = exports.classModule;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy8ucmVnaXN0cnkubnBtanMub3JnL2Jyb3dzZXItcGFjay82LjAuMi9ub2RlX21vZHVsZXMvYnJvd3Nlci1wYWNrL19wcmVsdWRlLmpzIiwibW9kdWxlcy9jbGFzcy5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtBQ0FBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIlwidXNlIHN0cmljdFwiO1xuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFwiX19lc01vZHVsZVwiLCB7IHZhbHVlOiB0cnVlIH0pO1xuZnVuY3Rpb24gdXBkYXRlQ2xhc3Mob2xkVm5vZGUsIHZub2RlKSB7XG4gICAgdmFyIGN1ciwgbmFtZSwgZWxtID0gdm5vZGUuZWxtLCBvbGRDbGFzcyA9IG9sZFZub2RlLmRhdGEuY2xhc3MsIGtsYXNzID0gdm5vZGUuZGF0YS5jbGFzcztcbiAgICBpZiAoIW9sZENsYXNzICYmICFrbGFzcylcbiAgICAgICAgcmV0dXJuO1xuICAgIGlmIChvbGRDbGFzcyA9PT0ga2xhc3MpXG4gICAgICAgIHJldHVybjtcbiAgICBvbGRDbGFzcyA9IG9sZENsYXNzIHx8IHt9O1xuICAgIGtsYXNzID0ga2xhc3MgfHwge307XG4gICAgZm9yIChuYW1lIGluIG9sZENsYXNzKSB7XG4gICAgICAgIGlmICgha2xhc3NbbmFtZV0pIHtcbiAgICAgICAgICAgIGVsbS5jbGFzc0xpc3QucmVtb3ZlKG5hbWUpO1xuICAgICAgICB9XG4gICAgfVxuICAgIGZvciAobmFtZSBpbiBrbGFzcykge1xuICAgICAgICBjdXIgPSBrbGFzc1tuYW1lXTtcbiAgICAgICAgaWYgKGN1ciAhPT0gb2xkQ2xhc3NbbmFtZV0pIHtcbiAgICAgICAgICAgIGVsbS5jbGFzc0xpc3RbY3VyID8gJ2FkZCcgOiAncmVtb3ZlJ10obmFtZSk7XG4gICAgICAgIH1cbiAgICB9XG59XG5leHBvcnRzLmNsYXNzTW9kdWxlID0geyBjcmVhdGU6IHVwZGF0ZUNsYXNzLCB1cGRhdGU6IHVwZGF0ZUNsYXNzIH07XG5leHBvcnRzLmRlZmF1bHQgPSBleHBvcnRzLmNsYXNzTW9kdWxlO1xuLy8jIHNvdXJjZU1hcHBpbmdVUkw9Y2xhc3MuanMubWFwIl19
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n;n="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,n.snabbdom_class=e()}}(function(){return function e(n,o,r){function t(i,u){if(!o[i]){if(!n[i]){var s="function"==typeof require&&require;if(!u&&s)return s(i,!0);if(f)return f(i,!0);var d=new Error("Cannot find module '"+i+"'");throw d.code="MODULE_NOT_FOUND",d}var a=o[i]={exports:{}};n[i][0].call(a.exports,function(e){var o=n[i][1][e];return t(o?o:e)},a,a.exports,e,n,o,r)}return o[i].exports}for(var f="function"==typeof require&&require,i=0;i<r.length;i++)t(r[i]);return t}({1:[function(e,n,o){"use strict";function r(e,n){var o,r,t=n.elm,f=e.data["class"],i=n.data["class"];if((f||i)&&f!==i){f=f||{},i=i||{};for(r in f)i[r]||t.classList.remove(r);for(r in i)o=i[r],o!==f[r]&&t.classList[o?"add":"remove"](r)}}o.classModule={create:r,update:r},Object.defineProperty(o,"__esModule",{value:!0}),o["default"]=o.classModule},{}]},{},[1])(1)});
|
||||
//# sourceMappingURL=snabbdom-class.min.js.map
|
||||
File diff suppressed because one or more lines are too long
-42
@@ -1,42 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.snabbdom_dataset = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var CAPS_REGEX = /[A-Z]/g;
|
||||
function updateDataset(oldVnode, vnode) {
|
||||
var elm = vnode.elm, oldDataset = oldVnode.data.dataset, dataset = vnode.data.dataset, key;
|
||||
if (!oldDataset && !dataset)
|
||||
return;
|
||||
if (oldDataset === dataset)
|
||||
return;
|
||||
oldDataset = oldDataset || {};
|
||||
dataset = dataset || {};
|
||||
var d = elm.dataset;
|
||||
for (key in oldDataset) {
|
||||
if (!dataset[key]) {
|
||||
if (d) {
|
||||
if (key in d) {
|
||||
delete d[key];
|
||||
}
|
||||
}
|
||||
else {
|
||||
elm.removeAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key in dataset) {
|
||||
if (oldDataset[key] !== dataset[key]) {
|
||||
if (d) {
|
||||
d[key] = dataset[key];
|
||||
}
|
||||
else {
|
||||
elm.setAttribute('data-' + key.replace(CAPS_REGEX, '-$&').toLowerCase(), dataset[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.datasetModule = { create: updateDataset, update: updateDataset };
|
||||
exports.default = exports.datasetModule;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJtb2R1bGVzL2RhdGFzZXQuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIlwidXNlIHN0cmljdFwiO1xuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFwiX19lc01vZHVsZVwiLCB7IHZhbHVlOiB0cnVlIH0pO1xudmFyIENBUFNfUkVHRVggPSAvW0EtWl0vZztcbmZ1bmN0aW9uIHVwZGF0ZURhdGFzZXQob2xkVm5vZGUsIHZub2RlKSB7XG4gICAgdmFyIGVsbSA9IHZub2RlLmVsbSwgb2xkRGF0YXNldCA9IG9sZFZub2RlLmRhdGEuZGF0YXNldCwgZGF0YXNldCA9IHZub2RlLmRhdGEuZGF0YXNldCwga2V5O1xuICAgIGlmICghb2xkRGF0YXNldCAmJiAhZGF0YXNldClcbiAgICAgICAgcmV0dXJuO1xuICAgIGlmIChvbGREYXRhc2V0ID09PSBkYXRhc2V0KVxuICAgICAgICByZXR1cm47XG4gICAgb2xkRGF0YXNldCA9IG9sZERhdGFzZXQgfHwge307XG4gICAgZGF0YXNldCA9IGRhdGFzZXQgfHwge307XG4gICAgdmFyIGQgPSBlbG0uZGF0YXNldDtcbiAgICBmb3IgKGtleSBpbiBvbGREYXRhc2V0KSB7XG4gICAgICAgIGlmICghZGF0YXNldFtrZXldKSB7XG4gICAgICAgICAgICBpZiAoZCkge1xuICAgICAgICAgICAgICAgIGlmIChrZXkgaW4gZCkge1xuICAgICAgICAgICAgICAgICAgICBkZWxldGUgZFtrZXldO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIGVsbS5yZW1vdmVBdHRyaWJ1dGUoJ2RhdGEtJyArIGtleS5yZXBsYWNlKENBUFNfUkVHRVgsICctJCYnKS50b0xvd2VyQ2FzZSgpKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbiAgICBmb3IgKGtleSBpbiBkYXRhc2V0KSB7XG4gICAgICAgIGlmIChvbGREYXRhc2V0W2tleV0gIT09IGRhdGFzZXRba2V5XSkge1xuICAgICAgICAgICAgaWYgKGQpIHtcbiAgICAgICAgICAgICAgICBkW2tleV0gPSBkYXRhc2V0W2tleV07XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgICAgICBlbG0uc2V0QXR0cmlidXRlKCdkYXRhLScgKyBrZXkucmVwbGFjZShDQVBTX1JFR0VYLCAnLSQmJykudG9Mb3dlckNhc2UoKSwgZGF0YXNldFtrZXldKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbn1cbmV4cG9ydHMuZGF0YXNldE1vZHVsZSA9IHsgY3JlYXRlOiB1cGRhdGVEYXRhc2V0LCB1cGRhdGU6IHVwZGF0ZURhdGFzZXQgfTtcbmV4cG9ydHMuZGVmYXVsdCA9IGV4cG9ydHMuZGF0YXNldE1vZHVsZTtcbi8vIyBzb3VyY2VNYXBwaW5nVVJMPWRhdGFzZXQuanMubWFwIl19
|
||||
-99
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n;n="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,n.snabbdom_eventlisteners=e()}}(function(){return function e(n,t,r){function o(f,u){if(!t[f]){if(!n[f]){var l="function"==typeof require&&require;if(!u&&l)return l(f,!0);if(i)return i(f,!0);var s=new Error("Cannot find module '"+f+"'");throw s.code="MODULE_NOT_FOUND",s}var d=t[f]={exports:{}};n[f][0].call(d.exports,function(e){var t=n[f][1][e];return o(t?t:e)},d,d.exports,e,n,t,r)}return t[f].exports}for(var i="function"==typeof require&&require,f=0;f<r.length;f++)o(r[f]);return o}({1:[function(e,n,t){"use strict";function r(e,n,t){if("function"==typeof e)e.call(n,t,n);else if("object"==typeof e)if("function"==typeof e[0])if(2===e.length)e[0].call(n,e[1],t,n);else{var o=e.slice(1);o.push(t),o.push(n),e[0].apply(n,o)}else for(var i=0;i<e.length;i++)r(e[i])}function o(e,n){var t=e.type,o=n.data.on;o&&o[t]&&r(o[t],n,e)}function i(){return function e(n){o(n,e.vnode)}}function f(e,n){var t,r=e.data.on,o=e.listener,f=e.elm,u=n&&n.data.on,l=n&&n.elm;if(r!==u){if(r&&o)if(u)for(t in r)u[t]||f.removeEventListener(t,o,!1);else for(t in r)f.removeEventListener(t,o,!1);if(u){var s=n.listener=e.listener||i();if(s.vnode=n,r)for(t in u)r[t]||l.addEventListener(t,s,!1);else for(t in u)l.addEventListener(t,s,!1)}}}t.eventListenersModule={create:f,update:f,destroy:f},Object.defineProperty(t,"__esModule",{value:!0}),t["default"]=t.eventListenersModule},{}]},{},[1])(1)});
|
||||
//# sourceMappingURL=snabbdom-eventlisteners.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
-830
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
-30
@@ -1,30 +0,0 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.snabbdom_props = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function updateProps(oldVnode, vnode) {
|
||||
var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props;
|
||||
if (!oldProps && !props)
|
||||
return;
|
||||
if (oldProps === props)
|
||||
return;
|
||||
oldProps = oldProps || {};
|
||||
props = props || {};
|
||||
for (key in oldProps) {
|
||||
if (!props[key]) {
|
||||
delete elm[key];
|
||||
}
|
||||
}
|
||||
for (key in props) {
|
||||
cur = props[key];
|
||||
old = oldProps[key];
|
||||
if (old !== cur && (key !== 'value' || elm[key] !== cur)) {
|
||||
elm[key] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.propsModule = { create: updateProps, update: updateProps };
|
||||
exports.default = exports.propsModule;
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy8ucmVnaXN0cnkubnBtanMub3JnL2Jyb3dzZXItcGFjay82LjAuMi9ub2RlX21vZHVsZXMvYnJvd3Nlci1wYWNrL19wcmVsdWRlLmpzIiwibW9kdWxlcy9wcm9wcy5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtBQ0FBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJmaWxlIjoiZ2VuZXJhdGVkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXNDb250ZW50IjpbIihmdW5jdGlvbiBlKHQsbixyKXtmdW5jdGlvbiBzKG8sdSl7aWYoIW5bb10pe2lmKCF0W29dKXt2YXIgYT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2lmKCF1JiZhKXJldHVybiBhKG8sITApO2lmKGkpcmV0dXJuIGkobywhMCk7dmFyIGY9bmV3IEVycm9yKFwiQ2Fubm90IGZpbmQgbW9kdWxlICdcIitvK1wiJ1wiKTt0aHJvdyBmLmNvZGU9XCJNT0RVTEVfTk9UX0ZPVU5EXCIsZn12YXIgbD1uW29dPXtleHBvcnRzOnt9fTt0W29dWzBdLmNhbGwobC5leHBvcnRzLGZ1bmN0aW9uKGUpe3ZhciBuPXRbb11bMV1bZV07cmV0dXJuIHMobj9uOmUpfSxsLGwuZXhwb3J0cyxlLHQsbixyKX1yZXR1cm4gbltvXS5leHBvcnRzfXZhciBpPXR5cGVvZiByZXF1aXJlPT1cImZ1bmN0aW9uXCImJnJlcXVpcmU7Zm9yKHZhciBvPTA7bzxyLmxlbmd0aDtvKyspcyhyW29dKTtyZXR1cm4gc30pIiwiXCJ1c2Ugc3RyaWN0XCI7XG5PYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgXCJfX2VzTW9kdWxlXCIsIHsgdmFsdWU6IHRydWUgfSk7XG5mdW5jdGlvbiB1cGRhdGVQcm9wcyhvbGRWbm9kZSwgdm5vZGUpIHtcbiAgICB2YXIga2V5LCBjdXIsIG9sZCwgZWxtID0gdm5vZGUuZWxtLCBvbGRQcm9wcyA9IG9sZFZub2RlLmRhdGEucHJvcHMsIHByb3BzID0gdm5vZGUuZGF0YS5wcm9wcztcbiAgICBpZiAoIW9sZFByb3BzICYmICFwcm9wcylcbiAgICAgICAgcmV0dXJuO1xuICAgIGlmIChvbGRQcm9wcyA9PT0gcHJvcHMpXG4gICAgICAgIHJldHVybjtcbiAgICBvbGRQcm9wcyA9IG9sZFByb3BzIHx8IHt9O1xuICAgIHByb3BzID0gcHJvcHMgfHwge307XG4gICAgZm9yIChrZXkgaW4gb2xkUHJvcHMpIHtcbiAgICAgICAgaWYgKCFwcm9wc1trZXldKSB7XG4gICAgICAgICAgICBkZWxldGUgZWxtW2tleV07XG4gICAgICAgIH1cbiAgICB9XG4gICAgZm9yIChrZXkgaW4gcHJvcHMpIHtcbiAgICAgICAgY3VyID0gcHJvcHNba2V5XTtcbiAgICAgICAgb2xkID0gb2xkUHJvcHNba2V5XTtcbiAgICAgICAgaWYgKG9sZCAhPT0gY3VyICYmIChrZXkgIT09ICd2YWx1ZScgfHwgZWxtW2tleV0gIT09IGN1cikpIHtcbiAgICAgICAgICAgIGVsbVtrZXldID0gY3VyO1xuICAgICAgICB9XG4gICAgfVxufVxuZXhwb3J0cy5wcm9wc01vZHVsZSA9IHsgY3JlYXRlOiB1cGRhdGVQcm9wcywgdXBkYXRlOiB1cGRhdGVQcm9wcyB9O1xuZXhwb3J0cy5kZWZhdWx0ID0gZXhwb3J0cy5wcm9wc01vZHVsZTtcbi8vIyBzb3VyY2VNYXBwaW5nVVJMPXByb3BzLmpzLm1hcCJdfQ==
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;o="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,o.snabbdom_props=e()}}(function(){return function e(o,r,n){function t(i,u){if(!r[i]){if(!o[i]){var d="function"==typeof require&&require;if(!u&&d)return d(i,!0);if(f)return f(i,!0);var p=new Error("Cannot find module '"+i+"'");throw p.code="MODULE_NOT_FOUND",p}var a=r[i]={exports:{}};o[i][0].call(a.exports,function(e){var r=o[i][1][e];return t(r?r:e)},a,a.exports,e,o,r,n)}return r[i].exports}for(var f="function"==typeof require&&require,i=0;i<n.length;i++)t(n[i]);return t}({1:[function(e,o,r){"use strict";function n(e,o){var r,n,t,f=o.elm,i=e.data.props,u=o.data.props;if((i||u)&&i!==u){i=i||{},u=u||{};for(r in i)u[r]||delete f[r];for(r in u)n=u[r],t=i[r],t===n||"value"===r&&f[r]===n||(f[r]=n)}}r.propsModule={create:n,update:n},Object.defineProperty(r,"__esModule",{value:!0}),r["default"]=r.propsModule},{}]},{},[1])(1)});
|
||||
//# sourceMappingURL=snabbdom-props.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
-90
File diff suppressed because one or more lines are too long
-2
@@ -1,2 +0,0 @@
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.snabbdom_style=e()}}(function(){return function e(t,n,r){function o(f,d){if(!n[f]){if(!t[f]){var u="function"==typeof require&&require;if(!d&&u)return u(f,!0);if(i)return i(f,!0);var l=new Error("Cannot find module '"+f+"'");throw l.code="MODULE_NOT_FOUND",l}var a=n[f]={exports:{}};t[f][0].call(a.exports,function(e){var n=t[f][1][e];return o(n?n:e)},a,a.exports,e,t,n,r)}return n[f].exports}for(var i="function"==typeof require&&require,f=0;f<r.length;f++)o(r[f]);return o}({1:[function(e,t,n){"use strict";function r(e,t,n){u(function(){e[t]=n})}function o(e,t){var n,o,i=t.elm,f=e.data.style,d=t.data.style;if((f||d)&&f!==d){f=f||{},d=d||{};var u="delayed"in f;for(o in f)d[o]||("-"===o[0]&&"-"===o[1]?i.style.removeProperty(o):i.style[o]="");for(o in d)if(n=d[o],"delayed"===o)for(o in d.delayed)n=d.delayed[o],u&&n===f.delayed[o]||r(i.style,o,n);else"remove"!==o&&n!==f[o]&&("-"===o[0]&&"-"===o[1]?i.style.setProperty(o,n):i.style[o]=n)}}function i(e){var t,n,r=e.elm,o=e.data.style;if(o&&(t=o.destroy))for(n in t)r.style[n]=t[n]}function f(e,t){var n=e.data.style;if(!n||!n.remove)return void t();var r,o,i=e.elm,f=0,d=n.remove,u=0,l=[];for(r in d)l.push(r),i.style[r]=d[r];o=getComputedStyle(i);for(var a=o["transition-property"].split(", ");f<a.length;++f)l.indexOf(a[f])!==-1&&u++;i.addEventListener("transitionend",function(e){e.target===i&&--u,0===u&&t()})}var d="undefined"!=typeof window&&window.requestAnimationFrame||setTimeout,u=function(e){d(function(){d(e)})};n.styleModule={create:o,update:o,destroy:i,remove:f},Object.defineProperty(n,"__esModule",{value:!0}),n["default"]=n.styleModule},{}]},{},[1])(1)});
|
||||
//# sourceMappingURL=snabbdom-style.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
-506
File diff suppressed because one or more lines are too long
Vendored
-2
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
-126
File diff suppressed because one or more lines are too long
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.tovnode=e()}}(function(){return function e(t,n,o){function r(u,d){if(!n[u]){if(!t[u]){var f="function"==typeof require&&require;if(!d&&f)return f(u,!0);if(i)return i(u,!0);var a=new Error("Cannot find module '"+u+"'");throw a.code="MODULE_NOT_FOUND",a}var l=n[u]={exports:{}};t[u][0].call(l.exports,function(e){var n=t[u][1][e];return r(n?n:e)},l,l.exports,e,t,n,o)}return n[u].exports}for(var i="function"==typeof require&&require,u=0;u<o.length;u++)r(o[u]);return r}({1:[function(e,t,n){"use strict";function o(e){return document.createElement(e)}function r(e,t){return document.createElementNS(e,t)}function i(e){return document.createTextNode(e)}function u(e){return document.createComment(e)}function d(e,t,n){e.insertBefore(t,n)}function f(e,t){e.removeChild(t)}function a(e,t){e.appendChild(t)}function l(e){return e.parentNode}function c(e){return e.nextSibling}function s(e){return e.tagName}function m(e,t){e.textContent=t}function v(e){return e.textContent}function p(e){return 1===e.nodeType}function x(e){return 3===e.nodeType}function h(e){return 8===e.nodeType}n.htmlDomApi={createElement:o,createElementNS:r,createTextNode:i,createComment:u,insertBefore:d,removeChild:f,appendChild:a,parentNode:l,nextSibling:c,tagName:s,setTextContent:m,getTextContent:v,isElement:p,isText:x,isComment:h},Object.defineProperty(n,"__esModule",{value:!0}),n["default"]=n.htmlDomApi},{}],2:[function(e,t,n){"use strict";function o(e,t){var n,u=void 0!==t?t:i["default"];if(u.isElement(e)){var d,f=e.id?"#"+e.id:"",a=e.getAttribute("class"),l=a?"."+a.split(" ").join("."):"",c=u.tagName(e).toLowerCase()+f+l,s={},m=[],v=void 0,p=void 0,x=e.attributes,h=e.childNodes;for(v=0,p=x.length;v<p;v++)d=x[v].nodeName,"id"!==d&&"class"!==d&&(s[d]=x[v].nodeValue);for(v=0,p=h.length;v<p;v++)m.push(o(h[v]));return r["default"](c,{attrs:s},m,void 0,e)}return u.isText(e)?(n=u.getTextContent(e),r["default"](void 0,void 0,void 0,n,e)):u.isComment(e)?(n=u.getTextContent(e),r["default"]("!",void 0,void 0,n,void 0)):r["default"]("",{},[],void 0,void 0)}var r=e("./vnode"),i=e("./htmldomapi");n.toVNode=o,Object.defineProperty(n,"__esModule",{value:!0}),n["default"]=o},{"./htmldomapi":1,"./vnode":3}],3:[function(e,t,n){"use strict";function o(e,t,n,o,r){var i=void 0===t?void 0:t.key;return{sel:e,data:t,children:n,text:o,elm:r,key:i}}n.vnode=o,Object.defineProperty(n,"__esModule",{value:!0}),n["default"]=o},{}]},{},[2])(2)});
|
||||
//# sourceMappingURL=tovnode.min.js.map
|
||||
-1
File diff suppressed because one or more lines are too long
@@ -1,19 +0,0 @@
|
||||
This carousel example uses `style transform` and `transition` to rotate a group of SVG triangles.
|
||||
|
||||
Also, the color of each triangle changes when you hover or click/tap it.
|
||||
|
||||
I built the build.js using npm and browserify.
|
||||
|
||||
In my local copy of the snabbdom project root I did these preparations:
|
||||
```
|
||||
npm install --save-dev babelify
|
||||
npm install --save-dev babel-preset-es2015
|
||||
echo '{ "presets": ["es2015"] }' > .babelrc
|
||||
```
|
||||
|
||||
I then built like this:
|
||||
```
|
||||
browserify examples/carousel-svg/script.js -t babelify -o examples/carousel-svg/build.js
|
||||
```
|
||||
|
||||
-- *jk*
|
||||
@@ -1,566 +0,0 @@
|
||||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var VNode = require('./vnode');
|
||||
var is = require('./is');
|
||||
|
||||
function addNS(data, children) {
|
||||
data.ns = 'http://www.w3.org/2000/svg';
|
||||
if (children !== undefined) {
|
||||
for (var i = 0; i < children.length; ++i) {
|
||||
addNS(children[i].data, children[i].children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function h(sel, b, c) {
|
||||
var data = {},
|
||||
children,
|
||||
text,
|
||||
i;
|
||||
if (arguments.length === 3) {
|
||||
data = b;
|
||||
if (is.array(c)) {
|
||||
children = c;
|
||||
} else if (is.primitive(c)) {
|
||||
text = c;
|
||||
}
|
||||
} else if (arguments.length === 2) {
|
||||
if (is.array(b)) {
|
||||
children = b;
|
||||
} else if (is.primitive(b)) {
|
||||
text = b;
|
||||
} else {
|
||||
data = b;
|
||||
}
|
||||
}
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
|
||||
}
|
||||
}
|
||||
if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g') {
|
||||
addNS(data, children);
|
||||
}
|
||||
return VNode(sel, data, children, text, undefined);
|
||||
};
|
||||
|
||||
},{"./is":2,"./vnode":7}],2:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
array: Array.isArray,
|
||||
primitive: function primitive(s) {
|
||||
return typeof s === 'string' || typeof s === 'number';
|
||||
}
|
||||
};
|
||||
|
||||
},{}],3:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare", "default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable", "enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple", "muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly", "required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate", "truespeed", "typemustmatch", "visible"];
|
||||
|
||||
var booleanAttrsDict = {};
|
||||
for (var i = 0, len = booleanAttrs.length; i < len; i++) {
|
||||
booleanAttrsDict[booleanAttrs[i]] = true;
|
||||
}
|
||||
|
||||
function updateAttrs(oldVnode, vnode) {
|
||||
var key,
|
||||
cur,
|
||||
old,
|
||||
elm = vnode.elm,
|
||||
oldAttrs = oldVnode.data.attrs || {},
|
||||
attrs = vnode.data.attrs || {};
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
cur = attrs[key];
|
||||
old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
// TODO: add support to namespaced attributes (setAttributeNS)
|
||||
if (!cur && booleanAttrsDict[key]) elm.removeAttribute(key);else elm.setAttribute(key, cur);
|
||||
}
|
||||
}
|
||||
//remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { create: updateAttrs, update: updateAttrs };
|
||||
|
||||
},{}],4:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var is = require('../is');
|
||||
|
||||
function arrInvoker(arr) {
|
||||
return function () {
|
||||
// Special case when length is two, for performance
|
||||
arr.length === 2 ? arr[0](arr[1]) : arr[0].apply(undefined, arr.slice(1));
|
||||
};
|
||||
}
|
||||
|
||||
function fnInvoker(o) {
|
||||
return function (ev) {
|
||||
o.fn(ev);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode, vnode) {
|
||||
var name,
|
||||
cur,
|
||||
old,
|
||||
elm = vnode.elm,
|
||||
oldOn = oldVnode.data.on || {},
|
||||
on = vnode.data.on;
|
||||
if (!on) return;
|
||||
for (name in on) {
|
||||
cur = on[name];
|
||||
old = oldOn[name];
|
||||
if (old === undefined) {
|
||||
if (is.array(cur)) {
|
||||
elm.addEventListener(name, arrInvoker(cur));
|
||||
} else {
|
||||
cur = { fn: cur };
|
||||
on[name] = cur;
|
||||
elm.addEventListener(name, fnInvoker(cur));
|
||||
}
|
||||
} else if (is.array(old)) {
|
||||
// Deliberately modify old array since it's captured in closure created with `arrInvoker`
|
||||
old.length = cur.length;
|
||||
for (var i = 0; i < old.length; ++i) {
|
||||
old[i] = cur[i];
|
||||
}on[name] = old;
|
||||
} else {
|
||||
old.fn = cur;
|
||||
on[name] = old;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { create: updateEventListeners, update: updateEventListeners };
|
||||
|
||||
},{"../is":2}],5:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var raf = window && window.requestAnimationFrame || setTimeout;
|
||||
var nextFrame = function nextFrame(fn) {
|
||||
raf(function () {
|
||||
raf(fn);
|
||||
});
|
||||
};
|
||||
|
||||
function setNextFrame(obj, prop, val) {
|
||||
nextFrame(function () {
|
||||
obj[prop] = val;
|
||||
});
|
||||
}
|
||||
|
||||
function updateStyle(oldVnode, vnode) {
|
||||
var cur,
|
||||
name,
|
||||
elm = vnode.elm,
|
||||
oldStyle = oldVnode.data.style || {},
|
||||
style = vnode.data.style || {},
|
||||
oldHasDel = 'delayed' in oldStyle;
|
||||
for (name in oldStyle) {
|
||||
if (!style[name]) {
|
||||
elm.style[name] = '';
|
||||
}
|
||||
}
|
||||
for (name in style) {
|
||||
cur = style[name];
|
||||
if (name === 'delayed') {
|
||||
for (name in style.delayed) {
|
||||
cur = style.delayed[name];
|
||||
if (!oldHasDel || cur !== oldStyle.delayed[name]) {
|
||||
setNextFrame(elm.style, name, cur);
|
||||
}
|
||||
}
|
||||
} else if (name !== 'remove' && cur !== oldStyle[name]) {
|
||||
elm.style[name] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDestroyStyle(vnode) {
|
||||
var style,
|
||||
name,
|
||||
elm = vnode.elm,
|
||||
s = vnode.data.style;
|
||||
if (!s || !(style = s.destroy)) return;
|
||||
for (name in style) {
|
||||
elm.style[name] = style[name];
|
||||
}
|
||||
}
|
||||
|
||||
function applyRemoveStyle(vnode, rm) {
|
||||
var s = vnode.data.style;
|
||||
if (!s || !s.remove) {
|
||||
rm();
|
||||
return;
|
||||
}
|
||||
var name,
|
||||
elm = vnode.elm,
|
||||
idx,
|
||||
i = 0,
|
||||
maxDur = 0,
|
||||
compStyle,
|
||||
style = s.remove,
|
||||
amount = 0,
|
||||
applied = [];
|
||||
for (name in style) {
|
||||
applied.push(name);
|
||||
elm.style[name] = style[name];
|
||||
}
|
||||
compStyle = getComputedStyle(elm);
|
||||
var props = compStyle['transition-property'].split(', ');
|
||||
for (; i < props.length; ++i) {
|
||||
if (applied.indexOf(props[i]) !== -1) amount++;
|
||||
}
|
||||
elm.addEventListener('transitionend', function (ev) {
|
||||
if (ev.target === elm) --amount;
|
||||
if (amount === 0) rm();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle };
|
||||
|
||||
},{}],6:[function(require,module,exports){
|
||||
// jshint newcap: false
|
||||
/* global require, module, document, Element */
|
||||
'use strict';
|
||||
|
||||
var VNode = require('./vnode');
|
||||
var is = require('./is');
|
||||
|
||||
function isUndef(s) {
|
||||
return s === undefined;
|
||||
}
|
||||
function isDef(s) {
|
||||
return s !== undefined;
|
||||
}
|
||||
|
||||
function emptyNodeAt(elm) {
|
||||
return VNode(elm.tagName, {}, [], undefined, elm);
|
||||
}
|
||||
|
||||
var emptyNode = VNode('', {}, [], undefined, undefined);
|
||||
|
||||
function sameVnode(vnode1, vnode2) {
|
||||
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
|
||||
}
|
||||
|
||||
function createKeyToOldIdx(children, beginIdx, endIdx) {
|
||||
var i,
|
||||
map = {},
|
||||
key;
|
||||
for (i = beginIdx; i <= endIdx; ++i) {
|
||||
key = children[i].key;
|
||||
if (isDef(key)) map[key] = i;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function createRmCb(childElm, listeners) {
|
||||
return function () {
|
||||
if (--listeners === 0) childElm.parentElement.removeChild(childElm);
|
||||
};
|
||||
}
|
||||
|
||||
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
|
||||
|
||||
function init(modules) {
|
||||
var i,
|
||||
j,
|
||||
cbs = {};
|
||||
for (i = 0; i < hooks.length; ++i) {
|
||||
cbs[hooks[i]] = [];
|
||||
for (j = 0; j < modules.length; ++j) {
|
||||
if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
function createElm(vnode, insertedVnodeQueue) {
|
||||
var i,
|
||||
data = vnode.data;
|
||||
if (isDef(data)) {
|
||||
if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode);
|
||||
if (isDef(i = data.vnode)) vnode = i;
|
||||
}
|
||||
var elm,
|
||||
children = vnode.children,
|
||||
sel = vnode.sel;
|
||||
if (isDef(sel)) {
|
||||
// Parse selector
|
||||
var hashIdx = sel.indexOf('#');
|
||||
var dotIdx = sel.indexOf('.', hashIdx);
|
||||
var hash = hashIdx > 0 ? hashIdx : sel.length;
|
||||
var dot = dotIdx > 0 ? dotIdx : sel.length;
|
||||
var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
|
||||
elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? document.createElementNS(i, tag) : document.createElement(tag);
|
||||
if (hash < dot) elm.id = sel.slice(hash + 1, dot);
|
||||
if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/\./g, ' ');
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
elm.appendChild(createElm(children[i], insertedVnodeQueue));
|
||||
}
|
||||
} else if (is.primitive(vnode.text)) {
|
||||
elm.appendChild(document.createTextNode(vnode.text));
|
||||
}
|
||||
for (i = 0; i < cbs.create.length; ++i) {
|
||||
cbs.create[i](emptyNode, vnode);
|
||||
}i = vnode.data.hook; // Reuse variable
|
||||
if (isDef(i)) {
|
||||
if (i.create) i.create(emptyNode, vnode);
|
||||
if (i.insert) insertedVnodeQueue.push(vnode);
|
||||
}
|
||||
} else {
|
||||
elm = vnode.elm = document.createTextNode(vnode.text);
|
||||
}
|
||||
return vnode.elm;
|
||||
}
|
||||
|
||||
function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
parentElm.insertBefore(createElm(vnodes[startIdx], insertedVnodeQueue), before);
|
||||
}
|
||||
}
|
||||
|
||||
function invokeDestroyHook(vnode) {
|
||||
var i = vnode.data,
|
||||
j;
|
||||
if (isDef(i)) {
|
||||
if (isDef(i = i.hook) && isDef(i = i.destroy)) i(vnode);
|
||||
for (i = 0; i < cbs.destroy.length; ++i) {
|
||||
cbs.destroy[i](vnode);
|
||||
}if (isDef(i = vnode.children)) {
|
||||
for (j = 0; j < vnode.children.length; ++j) {
|
||||
invokeDestroyHook(vnode.children[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
var i,
|
||||
listeners,
|
||||
rm,
|
||||
ch = vnodes[startIdx];
|
||||
if (isDef(ch)) {
|
||||
if (isDef(ch.sel)) {
|
||||
invokeDestroyHook(ch);
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(ch.elm, listeners);
|
||||
for (i = 0; i < cbs.remove.length; ++i) {
|
||||
cbs.remove[i](ch, rm);
|
||||
}if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
|
||||
i(ch, rm);
|
||||
} else {
|
||||
rm();
|
||||
}
|
||||
} else {
|
||||
// Text node
|
||||
parentElm.removeChild(ch.elm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
|
||||
var oldStartIdx = 0,
|
||||
newStartIdx = 0;
|
||||
var oldEndIdx = oldCh.length - 1;
|
||||
var oldStartVnode = oldCh[0];
|
||||
var oldEndVnode = oldCh[oldEndIdx];
|
||||
var newEndIdx = newCh.length - 1;
|
||||
var newStartVnode = newCh[0];
|
||||
var newEndVnode = newCh[newEndIdx];
|
||||
var oldKeyToIdx, idxInOld, elmToMove, before;
|
||||
|
||||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||||
if (isUndef(oldStartVnode)) {
|
||||
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
|
||||
} else if (isUndef(oldEndVnode)) {
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newStartVnode)) {
|
||||
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else if (sameVnode(oldEndVnode, newEndVnode)) {
|
||||
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newEndVnode)) {
|
||||
// Vnode moved right
|
||||
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
|
||||
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
||||
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
|
||||
idxInOld = oldKeyToIdx[newStartVnode.key];
|
||||
if (isUndef(idxInOld)) {
|
||||
// New element
|
||||
parentElm.insertBefore(createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
elmToMove = oldCh[idxInOld];
|
||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
||||
oldCh[idxInOld] = undefined;
|
||||
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
|
||||
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
|
||||
} else if (newStartIdx > newEndIdx) {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
|
||||
var i, hook;
|
||||
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
if (isDef(i = oldVnode.data) && isDef(i = i.vnode)) oldVnode = i;
|
||||
if (isDef(i = vnode.data) && isDef(i = i.vnode)) vnode = i;
|
||||
var elm = vnode.elm = oldVnode.elm,
|
||||
oldCh = oldVnode.children,
|
||||
ch = vnode.children;
|
||||
if (oldVnode === vnode) return;
|
||||
if (isDef(vnode.data)) {
|
||||
for (i = 0; i < cbs.update.length; ++i) {
|
||||
cbs.update[i](oldVnode, vnode);
|
||||
}i = vnode.data.hook;
|
||||
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
|
||||
} else if (isDef(ch)) {
|
||||
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
|
||||
} else if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
elm.textContent = vnode.text;
|
||||
}
|
||||
if (isDef(hook) && isDef(i = hook.postpatch)) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
}
|
||||
|
||||
return function (oldVnode, vnode) {
|
||||
var i;
|
||||
var insertedVnodeQueue = [];
|
||||
for (i = 0; i < cbs.pre.length; ++i) {
|
||||
cbs.pre[i]();
|
||||
}if (oldVnode instanceof Element) {
|
||||
if (oldVnode.parentElement !== null) {
|
||||
createElm(vnode, insertedVnodeQueue);
|
||||
oldVnode.parentElement.replaceChild(vnode.elm, oldVnode);
|
||||
} else {
|
||||
oldVnode = emptyNodeAt(oldVnode);
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
||||
}
|
||||
} else {
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
||||
}
|
||||
for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
||||
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
|
||||
}
|
||||
for (i = 0; i < cbs.post.length; ++i) {
|
||||
cbs.post[i]();
|
||||
}return vnode;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { init: init };
|
||||
|
||||
},{"./is":2,"./vnode":7}],7:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
module.exports = function (sel, data, children, text, elm) {
|
||||
var key = data === undefined ? undefined : data.key;
|
||||
return { sel: sel, data: data, children: children,
|
||||
text: text, elm: elm, key: key };
|
||||
};
|
||||
|
||||
},{}],8:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var snabbdom = require('../../snabbdom.js');
|
||||
var patch = snabbdom.init([require('../../modules/attributes'), require('../../modules/style'), require('../../modules/eventlisteners')]);
|
||||
var h = require('../../h.js');
|
||||
|
||||
var vnode;
|
||||
|
||||
var data = {
|
||||
degRotation: 0
|
||||
};
|
||||
|
||||
function gRotation() {
|
||||
//console.log("gRotation: %s", data.degRotation);
|
||||
return "rotate(" + data.degRotation + "deg)";
|
||||
}
|
||||
|
||||
function triangleClick(id) {
|
||||
console.log("triangleClick: %s", id);
|
||||
render();
|
||||
}
|
||||
|
||||
function handleRotate(degs) {
|
||||
data.degRotation += degs;
|
||||
console.log("handleRotate: %s, %s", degs, data.degRotation);
|
||||
render();
|
||||
}
|
||||
|
||||
function handleReset(degs) {
|
||||
data.degRotation = degs;
|
||||
console.log("handleReset: %s", degs);
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
vnode = patch(vnode, view(data));
|
||||
}
|
||||
|
||||
var hTriangle = function hTriangle(id, degRotation) {
|
||||
return h("polygon#" + id, {
|
||||
attrs: {
|
||||
points: "-50,-88 0,-175 50,-88",
|
||||
transform: "rotate(" + degRotation + ")",
|
||||
"stroke-width": 3
|
||||
},
|
||||
on: { click: [triangleClick, id] }
|
||||
});
|
||||
};
|
||||
|
||||
var view = function view(data) {
|
||||
return h("div.view", [h("h1", "Snabbdom SVG Carousel"), h("svg", { attrs: { width: 380, height: 380, viewBox: [-190, -190, 380, 380] } }, [h("g#carousel", { style: { "-webkit-transform": gRotation(), transform: gRotation() } }, [hTriangle("yellow", 0), hTriangle("green", 60), hTriangle("magenta", 120), hTriangle("red", 180), hTriangle("cyan", 240), hTriangle("blue", 300)])]), h("button", { on: { click: [handleRotate, 60] } }, "Rotate Clockwise"), h("button", { on: { click: [handleRotate, -60] } }, "Rotate Anticlockwise"), h("button", { on: { click: [handleReset, 0] } }, "Reset")]);
|
||||
};
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
var container = document.getElementById("container");
|
||||
vnode = patch(container, view(data));
|
||||
render();
|
||||
});
|
||||
|
||||
},{"../../h.js":1,"../../modules/attributes":3,"../../modules/eventlisteners":4,"../../modules/style":5,"../../snabbdom.js":6}]},{},[8]);
|
||||
@@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta charset="utf-8">
|
||||
<title>Carousel</title>
|
||||
<script type="text/javascript" src="build.js"></script>
|
||||
<style type="text/css">
|
||||
div.view {
|
||||
margin: 10px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
color: #505000;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
g#carousel {
|
||||
-webkit-transition: -webkit-transform 1s ease;
|
||||
transition: transform 1s ease;
|
||||
}
|
||||
polygon {
|
||||
stroke: #808000;
|
||||
transition: fill 0.5s linear;
|
||||
}
|
||||
polygon#yellow {
|
||||
fill: rgba(255,255,0,0.4);
|
||||
}
|
||||
polygon#yellow:hover, polygon#yellow:active {
|
||||
fill: yellow;
|
||||
}
|
||||
polygon#green {
|
||||
fill: rgba(0,128,0,0.4);
|
||||
}
|
||||
polygon#green:hover, polygon#green:active {
|
||||
fill: green;
|
||||
}
|
||||
polygon#magenta {
|
||||
fill: rgba(255,0,255,0.4);
|
||||
}
|
||||
polygon#magenta:hover, polygon#magenta:active {
|
||||
fill: magenta;
|
||||
}
|
||||
polygon#red {
|
||||
fill: rgba(255,0,0,0.4);
|
||||
}
|
||||
polygon#red:hover, polygon#red:active {
|
||||
fill: red;
|
||||
}
|
||||
polygon#cyan {
|
||||
fill: rgba(0,255,255,0.4);
|
||||
}
|
||||
polygon#cyan:hover, polygon#cyan:active {
|
||||
fill: cyan;
|
||||
}
|
||||
polygon#blue {
|
||||
fill: rgba(0,0,255,0.4);
|
||||
}
|
||||
polygon#blue:hover, polygon#blue:active {
|
||||
fill: blue;
|
||||
}
|
||||
button {
|
||||
font-size: 15px;
|
||||
margin: 0 0.7em 0.7em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
var snabbdom = require('../../snabbdom.js');
|
||||
var patch = snabbdom.init([
|
||||
require('../../modules/attributes').default,
|
||||
require('../../modules/style').default,
|
||||
require('../../modules/eventlisteners').default
|
||||
]);
|
||||
var h = require('../../h.js').default;
|
||||
|
||||
var vnode;
|
||||
|
||||
var data = {
|
||||
degRotation: 0
|
||||
};
|
||||
|
||||
function gRotation() {
|
||||
//console.log("gRotation: %s", data.degRotation);
|
||||
return "rotate(" + data.degRotation + "deg)";
|
||||
}
|
||||
|
||||
function triangleClick(id) {
|
||||
console.log("triangleClick: %s", id);
|
||||
render();
|
||||
}
|
||||
|
||||
function handleRotate(degs) {
|
||||
data.degRotation += degs;
|
||||
console.log("handleRotate: %s, %s", degs, data.degRotation);
|
||||
render();
|
||||
}
|
||||
|
||||
function handleReset(degs) {
|
||||
data.degRotation = degs;
|
||||
console.log("handleReset: %s", degs);
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
vnode = patch(vnode, view(data));
|
||||
}
|
||||
|
||||
const hTriangle = (id, degRotation) =>
|
||||
h("polygon#" + id, {
|
||||
attrs: {
|
||||
points: "-50,-88 0,-175 50,-88",
|
||||
transform: "rotate(" + degRotation + ")",
|
||||
"stroke-width": 3
|
||||
},
|
||||
on: {click: [triangleClick, id]}
|
||||
});
|
||||
|
||||
const view = (data) =>
|
||||
h("div.view", [
|
||||
h("h1", "Snabbdom SVG Carousel"),
|
||||
h("svg", {attrs: {width: 380, height: 380, viewBox: [-190, -190, 380, 380]}}, [
|
||||
h("g#carousel",
|
||||
{style: {"-webkit-transform": gRotation(), transform: gRotation()}}, [
|
||||
hTriangle("yellow", 0),
|
||||
hTriangle("green", 60),
|
||||
hTriangle("magenta", 120),
|
||||
hTriangle("red", 180),
|
||||
hTriangle("cyan", 240),
|
||||
hTriangle("blue", 300)
|
||||
])
|
||||
]),
|
||||
h("button", {on: {click: [handleRotate, 60]}}, "Rotate Clockwise"),
|
||||
h("button", {on: {click: [handleRotate, -60]}}, "Rotate Anticlockwise"),
|
||||
h("button", {on: {click: [handleReset, 0]}}, "Reset")
|
||||
]);
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
var container = document.getElementById("container");
|
||||
vnode = patch(container, view(data));
|
||||
render();
|
||||
});
|
||||
@@ -1,708 +0,0 @@
|
||||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
/* jshint esnext: true */
|
||||
'use strict';
|
||||
|
||||
var snabbdom = require('../../snabbdom.js');
|
||||
var patch = snabbdom.init([require('../../modules/class'), require('../../modules/hero'), require('../../modules/style'), require('../../modules/eventlisteners')]);
|
||||
var h = require('../../h.js');
|
||||
|
||||
var vnode;
|
||||
|
||||
var data = {
|
||||
selected: undefined,
|
||||
movies: [{ rank: 1, title: 'This is an', desc: 'Lorem ipsum dolor sit amet, sed pede integer vitae bibendum, accumsan sit, vulputate aenean tempora ipsum. Lorem sed id et metus, eros posuere suspendisse nec nunc justo, fusce augue placerat nibh purus suspendisse. Aliquam aliquam, ut eget. Mollis a eget sed nibh tincidunt nec, mi integer, proin magna lacus iaculis tortor. Aliquam vel arcu arcu, vivamus a urna fames felis vel wisi, cursus tortor nec erat dignissim cras sem, mauris ac venenatis tellus elit.' }, { rank: 2, title: 'example of', desc: 'Consequuntur ipsum nulla, consequat curabitur in magnis risus. Taciti mattis bibendum tellus nibh, at dui neque eget, odio pede ut, sapien pede, ipsum ut. Sagittis dui, sodales sem, praesent ipsum conubia eget lorem lobortis wisi.' }, { rank: 3, title: 'Snabbdom', desc: 'Quam lorem aliquam fusce wisi, urna purus ipsum pharetra sed, at cras sodales enim vestibulum odio cras, luctus integer phasellus.' }, { rank: 4, title: 'doing hero transitions', desc: 'Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.' }, { rank: 5, title: 'using the', desc: 'Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.' }, { rank: 6, title: 'module for hero transitions', desc: 'Sapien laoreet, ligula elit tortor nulla pellentesque, maecenas enim turpis, quae duis venenatis vivamus ultricies, nunc imperdiet sollicitudin ipsum malesuada. Ut sem. Wisi fusce nullam nibh enim. Nisl hymenaeos id sed sed in. Proin leo et, pulvinar nunc pede laoreet.' }, { rank: 7, title: 'click on ar element in', desc: 'Accumsan quia, id nascetur dui et congue erat, id excepteur, primis ratione nec. At nulla et. Suspendisse lobortis, lobortis in tortor fringilla, duis adipiscing vestibulum voluptates sociosqu auctor.' }, { rank: 8, title: 'the list', desc: 'Ante tellus egestas vel hymenaeos, ut viverra nibh ut, ipsum nibh donec donec dolor. Eros ridiculus vel egestas convallis ipsum, commodo ut venenatis nullam porta iaculis, suspendisse ante proin leo, felis risus etiam.' }, { rank: 9, title: 'to witness', desc: 'Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst.' }, { rank: 10, title: 'the effect', desc: 'Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris' }]
|
||||
};
|
||||
|
||||
function select(m) {
|
||||
data.selected = m;
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
vnode = patch(vnode, view(data));
|
||||
}
|
||||
|
||||
var fadeInOutStyle = {
|
||||
opacity: '0', delayed: { opacity: '1' }, remove: { opacity: '0' }
|
||||
};
|
||||
|
||||
var detailView = function detailView(movie) {
|
||||
return h('div.page', { style: fadeInOutStyle }, [h('div.header', [h('div.header-content.detail', {
|
||||
style: { opacity: '1', remove: { opacity: '0' } }
|
||||
}, [h('div.rank', [h('span.header-rank.hero', { hero: { id: 'rank' + movie.rank } }, movie.rank), h('div.rank-circle', {
|
||||
style: { transform: 'scale(0)',
|
||||
delayed: { transform: 'scale(1)' },
|
||||
destroy: { transform: 'scale(0)' } }
|
||||
})]), h('div.hero.header-title', { hero: { id: movie.title } }, movie.title), h('div.spacer'), h('div.close', {
|
||||
on: { click: [select, undefined] },
|
||||
style: { transform: 'scale(0)',
|
||||
delayed: { transform: 'scale(1)' },
|
||||
destroy: { transform: 'scale(0)' } }
|
||||
}, 'x')])]), h('div.page-content', [h('div.desc', {
|
||||
style: { opacity: '0', transform: 'translateX(3em)',
|
||||
delayed: { opacity: '1', transform: 'translate(0)' },
|
||||
remove: { opacity: '0', position: 'absolute', top: '0', left: '0',
|
||||
transform: 'translateX(3em)' }
|
||||
}
|
||||
}, [h('h2', 'Description:'), h('span', movie.desc)])])]);
|
||||
};
|
||||
|
||||
var overviewView = function overviewView(movies) {
|
||||
return h('div.page', { style: fadeInOutStyle }, [h('div.header', [h('div.header-content.overview', {
|
||||
style: fadeInOutStyle
|
||||
}, [h('div.header-title', {
|
||||
style: { transform: 'translateY(-2em)',
|
||||
delayed: { transform: 'translate(0)' },
|
||||
destroy: { transform: 'translateY(-2em)' } }
|
||||
}, 'Top 10 movies'), h('div.spacer')])]), h('div.page-content', [h('div.list', {
|
||||
style: { opacity: '0', delayed: { opacity: '1' },
|
||||
remove: { opacity: '0', position: 'absolute', top: '0', left: '0' } }
|
||||
}, movies.map(function (movie) {
|
||||
return h('div.row', {
|
||||
on: { click: [select, movie] }
|
||||
}, [h('div.hero.rank', [h('span.hero', { hero: { id: 'rank' + movie.rank } }, movie.rank)]), h('div.hero', { hero: { id: movie.title } }, movie.title)]);
|
||||
}))])]);
|
||||
};
|
||||
|
||||
var view = function view(data) {
|
||||
return h('div.page-container', [data.selected ? detailView(data.selected) : overviewView(data.movies)]);
|
||||
};
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var container = document.getElementById('container');
|
||||
vnode = patch(container, view(data));
|
||||
render();
|
||||
});
|
||||
|
||||
},{"../../h.js":2,"../../modules/class":4,"../../modules/eventlisteners":5,"../../modules/hero":6,"../../modules/style":7,"../../snabbdom.js":8}],2:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var VNode = require('./vnode');
|
||||
var is = require('./is');
|
||||
|
||||
function addNS(data, children) {
|
||||
data.ns = 'http://www.w3.org/2000/svg';
|
||||
if (children !== undefined) {
|
||||
for (var i = 0; i < children.length; ++i) {
|
||||
addNS(children[i].data, children[i].children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function h(sel, b, c) {
|
||||
var data = {},
|
||||
children,
|
||||
text,
|
||||
i;
|
||||
if (arguments.length === 3) {
|
||||
data = b;
|
||||
if (is.array(c)) {
|
||||
children = c;
|
||||
} else if (is.primitive(c)) {
|
||||
text = c;
|
||||
}
|
||||
} else if (arguments.length === 2) {
|
||||
if (is.array(b)) {
|
||||
children = b;
|
||||
} else if (is.primitive(b)) {
|
||||
text = b;
|
||||
} else {
|
||||
data = b;
|
||||
}
|
||||
}
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
|
||||
}
|
||||
}
|
||||
if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g') {
|
||||
addNS(data, children);
|
||||
}
|
||||
return VNode(sel, data, children, text, undefined);
|
||||
};
|
||||
|
||||
},{"./is":3,"./vnode":9}],3:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
array: Array.isArray,
|
||||
primitive: function primitive(s) {
|
||||
return typeof s === 'string' || typeof s === 'number';
|
||||
}
|
||||
};
|
||||
|
||||
},{}],4:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
function updateClass(oldVnode, vnode) {
|
||||
var cur,
|
||||
name,
|
||||
elm = vnode.elm,
|
||||
oldClass = oldVnode.data['class'] || {},
|
||||
klass = vnode.data['class'] || {};
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
elm.classList[cur ? 'add' : 'remove'](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { create: updateClass, update: updateClass };
|
||||
|
||||
},{}],5:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var is = require('../is');
|
||||
|
||||
function arrInvoker(arr) {
|
||||
return function () {
|
||||
// Special case when length is two, for performance
|
||||
arr.length === 2 ? arr[0](arr[1]) : arr[0].apply(undefined, arr.slice(1));
|
||||
};
|
||||
}
|
||||
|
||||
function fnInvoker(o) {
|
||||
return function (ev) {
|
||||
o.fn(ev);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode, vnode) {
|
||||
var name,
|
||||
cur,
|
||||
old,
|
||||
elm = vnode.elm,
|
||||
oldOn = oldVnode.data.on || {},
|
||||
on = vnode.data.on;
|
||||
if (!on) return;
|
||||
for (name in on) {
|
||||
cur = on[name];
|
||||
old = oldOn[name];
|
||||
if (old === undefined) {
|
||||
if (is.array(cur)) {
|
||||
elm.addEventListener(name, arrInvoker(cur));
|
||||
} else {
|
||||
cur = { fn: cur };
|
||||
on[name] = cur;
|
||||
elm.addEventListener(name, fnInvoker(cur));
|
||||
}
|
||||
} else if (is.array(old)) {
|
||||
// Deliberately modify old array since it's captured in closure created with `arrInvoker`
|
||||
old.length = cur.length;
|
||||
for (var i = 0; i < old.length; ++i) old[i] = cur[i];
|
||||
on[name] = old;
|
||||
} else {
|
||||
old.fn = cur;
|
||||
on[name] = old;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { create: updateEventListeners, update: updateEventListeners };
|
||||
|
||||
},{"../is":3}],6:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var raf = window && window.requestAnimationFrame || setTimeout;
|
||||
var nextFrame = function nextFrame(fn) {
|
||||
raf(function () {
|
||||
raf(fn);
|
||||
});
|
||||
};
|
||||
|
||||
function setNextFrame(obj, prop, val) {
|
||||
nextFrame(function () {
|
||||
obj[prop] = val;
|
||||
});
|
||||
}
|
||||
|
||||
function getTextNodeRect(textNode) {
|
||||
var rect;
|
||||
if (document.createRange) {
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(textNode);
|
||||
if (range.getBoundingClientRect) {
|
||||
rect = range.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
function calcTransformOrigin(isTextNode, textRect, boundingRect) {
|
||||
if (isTextNode) {
|
||||
if (textRect) {
|
||||
//calculate pixels to center of text from left edge of bounding box
|
||||
var relativeCenterX = textRect.left + textRect.width / 2 - boundingRect.left;
|
||||
var relativeCenterY = textRect.top + textRect.height / 2 - boundingRect.top;
|
||||
return relativeCenterX + 'px ' + relativeCenterY + 'px';
|
||||
}
|
||||
}
|
||||
return '0 0'; //top left
|
||||
}
|
||||
|
||||
function getTextDx(oldTextRect, newTextRect) {
|
||||
if (oldTextRect && newTextRect) {
|
||||
return oldTextRect.left + oldTextRect.width / 2 - (newTextRect.left + newTextRect.width / 2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function getTextDy(oldTextRect, newTextRect) {
|
||||
if (oldTextRect && newTextRect) {
|
||||
return oldTextRect.top + oldTextRect.height / 2 - (newTextRect.top + newTextRect.height / 2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function isTextElement(elm) {
|
||||
return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3;
|
||||
}
|
||||
|
||||
var removed, created;
|
||||
|
||||
function pre(oldVnode, vnode) {
|
||||
removed = {};
|
||||
created = [];
|
||||
}
|
||||
|
||||
function create(oldVnode, vnode) {
|
||||
var hero = vnode.data.hero;
|
||||
if (hero && hero.id) {
|
||||
created.push(hero.id);
|
||||
created.push(vnode);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy(vnode) {
|
||||
var hero = vnode.data.hero;
|
||||
if (hero && hero.id) {
|
||||
var elm = vnode.elm;
|
||||
vnode.isTextNode = isTextElement(elm); //is this a text node?
|
||||
vnode.boundingRect = elm.getBoundingClientRect(); //save the bounding rectangle to a new property on the vnode
|
||||
vnode.textRect = vnode.isTextNode ? getTextNodeRect(elm.childNodes[0]) : null; //save bounding rect of inner text node
|
||||
var computedStyle = window.getComputedStyle(elm, null); //get current styles (includes inherited properties)
|
||||
vnode.savedStyle = JSON.parse(JSON.stringify(computedStyle)); //save a copy of computed style values
|
||||
removed[hero.id] = vnode;
|
||||
}
|
||||
}
|
||||
|
||||
function post() {
|
||||
var i, id, newElm, oldVnode, oldElm, hRatio, wRatio, oldRect, newRect, dx, dy, origTransform, origTransition, newStyle, oldStyle, newComputedStyle, isTextNode, newTextRect, oldTextRect;
|
||||
for (i = 0; i < created.length; i += 2) {
|
||||
id = created[i];
|
||||
newElm = created[i + 1].elm;
|
||||
oldVnode = removed[id];
|
||||
if (oldVnode) {
|
||||
isTextNode = oldVnode.isTextNode && isTextElement(newElm); //Are old & new both text?
|
||||
newStyle = newElm.style;
|
||||
newComputedStyle = window.getComputedStyle(newElm, null); //get full computed style for new element
|
||||
oldElm = oldVnode.elm;
|
||||
oldStyle = oldElm.style;
|
||||
//Overall element bounding boxes
|
||||
newRect = newElm.getBoundingClientRect();
|
||||
oldRect = oldVnode.boundingRect; //previously saved bounding rect
|
||||
//Text node bounding boxes & distances
|
||||
if (isTextNode) {
|
||||
newTextRect = getTextNodeRect(newElm.childNodes[0]);
|
||||
oldTextRect = oldVnode.textRect;
|
||||
dx = getTextDx(oldTextRect, newTextRect);
|
||||
dy = getTextDy(oldTextRect, newTextRect);
|
||||
} else {
|
||||
//Calculate distances between old & new positions
|
||||
dx = oldRect.left - newRect.left;
|
||||
dy = oldRect.top - newRect.top;
|
||||
}
|
||||
hRatio = newRect.height / Math.max(oldRect.height, 1);
|
||||
wRatio = isTextNode ? hRatio : newRect.width / Math.max(oldRect.width, 1); //text scales based on hRatio
|
||||
// Animate new element
|
||||
origTransform = newStyle.transform;
|
||||
origTransition = newStyle.transition;
|
||||
if (newComputedStyle.display === 'inline') //inline elements cannot be transformed
|
||||
newStyle.display = 'inline-block'; //this does not appear to have any negative side effects
|
||||
newStyle.transition = origTransition + 'transform 0s';
|
||||
newStyle.transformOrigin = calcTransformOrigin(isTextNode, newTextRect, newRect);
|
||||
newStyle.opacity = '0';
|
||||
newStyle.transform = origTransform + 'translate(' + dx + 'px, ' + dy + 'px) ' + 'scale(' + 1 / wRatio + ', ' + 1 / hRatio + ')';
|
||||
setNextFrame(newStyle, 'transition', origTransition);
|
||||
setNextFrame(newStyle, 'transform', origTransform);
|
||||
setNextFrame(newStyle, 'opacity', '1');
|
||||
// Animate old element
|
||||
for (var key in oldVnode.savedStyle) {
|
||||
//re-apply saved inherited properties
|
||||
if (parseInt(key) != key) {
|
||||
var ms = key.substring(0, 2) === 'ms';
|
||||
var moz = key.substring(0, 3) === 'moz';
|
||||
var webkit = key.substring(0, 6) === 'webkit';
|
||||
if (!ms && !moz && !webkit) //ignore prefixed style properties
|
||||
oldStyle[key] = oldVnode.savedStyle[key];
|
||||
}
|
||||
}
|
||||
oldStyle.position = 'absolute';
|
||||
oldStyle.top = oldRect.top + 'px'; //start at existing position
|
||||
oldStyle.left = oldRect.left + 'px';
|
||||
oldStyle.width = oldRect.width + 'px'; //Needed for elements who were sized relative to their parents
|
||||
oldStyle.height = oldRect.height + 'px'; //Needed for elements who were sized relative to their parents
|
||||
oldStyle.margin = 0; //Margin on hero element leads to incorrect positioning
|
||||
oldStyle.transformOrigin = calcTransformOrigin(isTextNode, oldTextRect, oldRect);
|
||||
oldStyle.transform = '';
|
||||
oldStyle.opacity = '1';
|
||||
document.body.appendChild(oldElm);
|
||||
setNextFrame(oldStyle, 'transform', 'translate(' + -dx + 'px, ' + -dy + 'px) scale(' + wRatio + ', ' + hRatio + ')'); //scale must be on far right for translate to be correct
|
||||
setNextFrame(oldStyle, 'opacity', '0');
|
||||
oldElm.addEventListener('transitionend', function (ev) {
|
||||
if (ev.propertyName === 'transform') document.body.removeChild(ev.target);
|
||||
});
|
||||
}
|
||||
}
|
||||
removed = created = undefined;
|
||||
}
|
||||
|
||||
module.exports = { pre: pre, create: create, destroy: destroy, post: post };
|
||||
|
||||
},{}],7:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var raf = requestAnimationFrame || setTimeout;
|
||||
var nextFrame = function nextFrame(fn) {
|
||||
raf(function () {
|
||||
raf(fn);
|
||||
});
|
||||
};
|
||||
|
||||
function setNextFrame(obj, prop, val) {
|
||||
nextFrame(function () {
|
||||
obj[prop] = val;
|
||||
});
|
||||
}
|
||||
|
||||
function updateStyle(oldVnode, vnode) {
|
||||
var cur,
|
||||
name,
|
||||
elm = vnode.elm,
|
||||
oldStyle = oldVnode.data.style || {},
|
||||
style = vnode.data.style || {},
|
||||
oldHasDel = ('delayed' in oldStyle);
|
||||
for (name in style) {
|
||||
cur = style[name];
|
||||
if (name === 'delayed') {
|
||||
for (name in style.delayed) {
|
||||
cur = style.delayed[name];
|
||||
if (!oldHasDel || cur !== oldStyle.delayed[name]) {
|
||||
setNextFrame(elm.style, name, cur);
|
||||
}
|
||||
}
|
||||
} else if (name !== 'remove' && cur !== oldStyle[name]) {
|
||||
elm.style[name] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDestroyStyle(vnode) {
|
||||
var style,
|
||||
name,
|
||||
elm = vnode.elm,
|
||||
s = vnode.data.style;
|
||||
if (!s || !(style = s.destroy)) return;
|
||||
for (name in style) {
|
||||
elm.style[name] = style[name];
|
||||
}
|
||||
}
|
||||
|
||||
function applyRemoveStyle(vnode, rm) {
|
||||
var s = vnode.data.style;
|
||||
if (!s || !s.remove) {
|
||||
rm();
|
||||
return;
|
||||
}
|
||||
var name,
|
||||
elm = vnode.elm,
|
||||
idx,
|
||||
i = 0,
|
||||
maxDur = 0,
|
||||
compStyle,
|
||||
style = s.remove,
|
||||
amount = 0,
|
||||
applied = [];
|
||||
for (name in style) {
|
||||
applied.push(name);
|
||||
elm.style[name] = style[name];
|
||||
}
|
||||
compStyle = getComputedStyle(elm);
|
||||
var props = compStyle['transition-property'].split(', ');
|
||||
for (; i < props.length; ++i) {
|
||||
if (applied.indexOf(props[i]) !== -1) amount++;
|
||||
}
|
||||
elm.addEventListener('transitionend', function (ev) {
|
||||
if (ev.target === elm) --amount;
|
||||
if (amount === 0) rm();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle };
|
||||
|
||||
},{}],8:[function(require,module,exports){
|
||||
// jshint newcap: false
|
||||
/* global require, module, document, Element */
|
||||
'use strict';
|
||||
|
||||
var VNode = require('./vnode');
|
||||
var is = require('./is');
|
||||
|
||||
function isUndef(s) {
|
||||
return s === undefined;
|
||||
}
|
||||
function isDef(s) {
|
||||
return s !== undefined;
|
||||
}
|
||||
|
||||
function emptyNodeAt(elm) {
|
||||
return VNode(elm.tagName, {}, [], undefined, elm);
|
||||
}
|
||||
|
||||
var emptyNode = VNode('', {}, [], undefined, undefined);
|
||||
|
||||
function sameVnode(vnode1, vnode2) {
|
||||
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
|
||||
}
|
||||
|
||||
function createKeyToOldIdx(children, beginIdx, endIdx) {
|
||||
var i,
|
||||
map = {},
|
||||
key;
|
||||
for (i = beginIdx; i <= endIdx; ++i) {
|
||||
key = children[i].key;
|
||||
if (isDef(key)) map[key] = i;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function createRmCb(childElm, listeners) {
|
||||
return function () {
|
||||
if (--listeners === 0) childElm.parentElement.removeChild(childElm);
|
||||
};
|
||||
}
|
||||
|
||||
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
|
||||
|
||||
function init(modules) {
|
||||
var i,
|
||||
j,
|
||||
cbs = {};
|
||||
for (i = 0; i < hooks.length; ++i) {
|
||||
cbs[hooks[i]] = [];
|
||||
for (j = 0; j < modules.length; ++j) {
|
||||
if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
function createElm(vnode, insertedVnodeQueue) {
|
||||
var i,
|
||||
data = vnode.data;
|
||||
if (isDef(data)) {
|
||||
if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode);
|
||||
if (isDef(i = data.vnode)) vnode = i;
|
||||
}
|
||||
var elm,
|
||||
children = vnode.children,
|
||||
sel = vnode.sel;
|
||||
if (isDef(sel)) {
|
||||
// Parse selector
|
||||
var hashIdx = sel.indexOf('#');
|
||||
var dotIdx = sel.indexOf('.', hashIdx);
|
||||
var hash = hashIdx > 0 ? hashIdx : sel.length;
|
||||
var dot = dotIdx > 0 ? dotIdx : sel.length;
|
||||
var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
|
||||
elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? document.createElementNS(i, tag) : document.createElement(tag);
|
||||
if (hash < dot) elm.id = sel.slice(hash + 1, dot);
|
||||
if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/\./g, ' ');
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
elm.appendChild(createElm(children[i], insertedVnodeQueue));
|
||||
}
|
||||
} else if (is.primitive(vnode.text)) {
|
||||
elm.appendChild(document.createTextNode(vnode.text));
|
||||
}
|
||||
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
|
||||
i = vnode.data.hook; // Reuse variable
|
||||
if (isDef(i)) {
|
||||
if (i.create) i.create(emptyNode, vnode);
|
||||
if (i.insert) insertedVnodeQueue.push(vnode);
|
||||
}
|
||||
} else {
|
||||
elm = vnode.elm = document.createTextNode(vnode.text);
|
||||
}
|
||||
return vnode.elm;
|
||||
}
|
||||
|
||||
function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
parentElm.insertBefore(createElm(vnodes[startIdx], insertedVnodeQueue), before);
|
||||
}
|
||||
}
|
||||
|
||||
function invokeDestroyHook(vnode) {
|
||||
var i = vnode.data,
|
||||
j;
|
||||
if (isDef(i)) {
|
||||
if (isDef(i = i.hook) && isDef(i = i.destroy)) i(vnode);
|
||||
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
|
||||
if (isDef(i = vnode.children)) {
|
||||
for (j = 0; j < vnode.children.length; ++j) {
|
||||
invokeDestroyHook(vnode.children[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
var i,
|
||||
listeners,
|
||||
rm,
|
||||
ch = vnodes[startIdx];
|
||||
if (isDef(ch)) {
|
||||
if (isDef(ch.sel)) {
|
||||
invokeDestroyHook(ch);
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(ch.elm, listeners);
|
||||
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
|
||||
if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
|
||||
i(ch, rm);
|
||||
} else {
|
||||
rm();
|
||||
}
|
||||
} else {
|
||||
// Text node
|
||||
parentElm.removeChild(ch.elm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
|
||||
var oldStartIdx = 0,
|
||||
newStartIdx = 0;
|
||||
var oldEndIdx = oldCh.length - 1;
|
||||
var oldStartVnode = oldCh[0];
|
||||
var oldEndVnode = oldCh[oldEndIdx];
|
||||
var newEndIdx = newCh.length - 1;
|
||||
var newStartVnode = newCh[0];
|
||||
var newEndVnode = newCh[newEndIdx];
|
||||
var oldKeyToIdx, idxInOld, elmToMove, before;
|
||||
|
||||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||||
if (isUndef(oldStartVnode)) {
|
||||
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
|
||||
} else if (isUndef(oldEndVnode)) {
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newStartVnode)) {
|
||||
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else if (sameVnode(oldEndVnode, newEndVnode)) {
|
||||
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newEndVnode)) {
|
||||
// Vnode moved right
|
||||
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
|
||||
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
||||
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
|
||||
idxInOld = oldKeyToIdx[newStartVnode.key];
|
||||
if (isUndef(idxInOld)) {
|
||||
// New element
|
||||
parentElm.insertBefore(createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
elmToMove = oldCh[idxInOld];
|
||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
||||
oldCh[idxInOld] = undefined;
|
||||
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
|
||||
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
|
||||
} else if (newStartIdx > newEndIdx) {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
|
||||
var i, hook;
|
||||
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
if (isDef(i = oldVnode.data) && isDef(i = i.vnode)) oldVnode = i;
|
||||
if (isDef(i = vnode.data) && isDef(i = i.vnode)) vnode = i;
|
||||
var elm = vnode.elm = oldVnode.elm,
|
||||
oldCh = oldVnode.children,
|
||||
ch = vnode.children;
|
||||
if (oldVnode === vnode) return;
|
||||
if (isDef(vnode.data)) {
|
||||
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
|
||||
i = vnode.data.hook;
|
||||
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
|
||||
} else if (isDef(ch)) {
|
||||
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
|
||||
} else if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
elm.textContent = vnode.text;
|
||||
}
|
||||
if (isDef(hook) && isDef(i = hook.postpatch)) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
}
|
||||
|
||||
return function (oldVnode, vnode) {
|
||||
var i;
|
||||
var insertedVnodeQueue = [];
|
||||
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
|
||||
if (oldVnode instanceof Element) {
|
||||
if (oldVnode.parentElement !== null) {
|
||||
createElm(vnode, insertedVnodeQueue);
|
||||
oldVnode.parentElement.replaceChild(vnode.elm, oldVnode);
|
||||
} else {
|
||||
oldVnode = emptyNodeAt(oldVnode);
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
||||
}
|
||||
} else {
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
||||
}
|
||||
for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
||||
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
|
||||
}
|
||||
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
|
||||
return vnode;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { init: init };
|
||||
|
||||
},{"./is":3,"./vnode":9}],9:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
module.exports = function (sel, data, children, text, elm) {
|
||||
var key = data === undefined ? undefined : data.key;
|
||||
return { sel: sel, data: data, children: children,
|
||||
text: text, elm: elm, key: key };
|
||||
};
|
||||
|
||||
},{}]},{},[1]);
|
||||
@@ -1,166 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<title>Hero animation</title>
|
||||
<script type="text/javascript" src="build.js"></script>
|
||||
<style>
|
||||
{
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
background: #fff;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.page-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
}
|
||||
@media (min-width: 28em),
|
||||
@media (min-height: 38em) {
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #aaaaaa;
|
||||
position: relative;
|
||||
}
|
||||
.page-container {
|
||||
box-shadow: 0 0 1em rgba(0, 0, 0, .5);
|
||||
width: 28em;
|
||||
min-height: 38em;
|
||||
height: 38em;
|
||||
}
|
||||
}
|
||||
.page {
|
||||
background: #fff;
|
||||
transition: opacity 0.4s ease-in-out,
|
||||
transform 0.4s ease-in-out;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.1em;
|
||||
margin: .2em 0;
|
||||
}
|
||||
.header {
|
||||
height: 3.5em;
|
||||
background: #1293ea;
|
||||
overflow: hidden;
|
||||
}
|
||||
.header-content {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: .4em .8em;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
transition: opacity 0.4s ease-in-out,
|
||||
transform 0.4s ease-in-out;
|
||||
}
|
||||
.header h1 {
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.8em;
|
||||
}
|
||||
.header-title {
|
||||
color: #fff;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.8em;
|
||||
transition: transform 0.4s ease-in-out;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.header .rank {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.7em;
|
||||
height: 2.7em;
|
||||
margin-right: .5em;
|
||||
position: relative;
|
||||
}
|
||||
.header .rank-circle {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
width: 2.7em;
|
||||
height: 2.7em;
|
||||
border-radius: 1.35em;
|
||||
margin-right: .5em;
|
||||
transition: transform 0.4s ease-in-out;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.header-rank {
|
||||
z-index: 2;
|
||||
color: #1293ea;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.header .close {
|
||||
line-height: 1.8em;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
border-radius: .9em;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
transition: transform 0.4s ease-in-out;
|
||||
}
|
||||
.hero {
|
||||
transition: transform 0.4s ease-in-out,
|
||||
opacity 0.4s ease-in-out;
|
||||
}
|
||||
.page-content {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: calc(100% - 3.5em);
|
||||
}
|
||||
.list {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
transition: transform 0.4s ease-in-out,
|
||||
opacity 0.4s ease-in-out;
|
||||
}
|
||||
.desc {
|
||||
position: absolute;
|
||||
transition: transform 0.4s ease-in-out,
|
||||
opacity 0.4s ease-in-out;
|
||||
padding: 1em;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.row {
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
padding: 1em;
|
||||
}
|
||||
.row:not(:first-child) {
|
||||
border-top: 1px solid #eeeeee;
|
||||
}
|
||||
.row div {
|
||||
display: inline-block;
|
||||
}
|
||||
.row > div:nth-child(1) {
|
||||
text-align: center;
|
||||
margin-right: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,120 +0,0 @@
|
||||
/* jshint esnext: true */
|
||||
var snabbdom = require('../../snabbdom.js');
|
||||
var patch = snabbdom.init([
|
||||
require('../../modules/class').default,
|
||||
require('../../modules/hero').default,
|
||||
require('../../modules/style').default,
|
||||
require('../../modules/eventlisteners').default,
|
||||
]);
|
||||
var h = require('../../h.js').default;
|
||||
|
||||
var vnode;
|
||||
|
||||
var data = {
|
||||
selected: undefined,
|
||||
movies: [
|
||||
{rank: 1, title: 'This is an', desc: 'Lorem ipsum dolor sit amet, sed pede integer vitae bibendum, accumsan sit, vulputate aenean tempora ipsum. Lorem sed id et metus, eros posuere suspendisse nec nunc justo, fusce augue placerat nibh purus suspendisse. Aliquam aliquam, ut eget. Mollis a eget sed nibh tincidunt nec, mi integer, proin magna lacus iaculis tortor. Aliquam vel arcu arcu, vivamus a urna fames felis vel wisi, cursus tortor nec erat dignissim cras sem, mauris ac venenatis tellus elit.'},
|
||||
{rank: 2, title: 'example of', desc: 'Consequuntur ipsum nulla, consequat curabitur in magnis risus. Taciti mattis bibendum tellus nibh, at dui neque eget, odio pede ut, sapien pede, ipsum ut. Sagittis dui, sodales sem, praesent ipsum conubia eget lorem lobortis wisi.'},
|
||||
{rank: 3, title: 'Snabbdom', desc: 'Quam lorem aliquam fusce wisi, urna purus ipsum pharetra sed, at cras sodales enim vestibulum odio cras, luctus integer phasellus.'},
|
||||
{rank: 4, title: 'doing hero transitions', desc: 'Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.'},
|
||||
{rank: 5, title: 'using the', desc: 'Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris. Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst. Sem nec tristique vel vehicula fringilla, nibh eu et posuere mi rhoncus.'},
|
||||
{rank: 6, title: 'module for hero transitions', desc: 'Sapien laoreet, ligula elit tortor nulla pellentesque, maecenas enim turpis, quae duis venenatis vivamus ultricies, nunc imperdiet sollicitudin ipsum malesuada. Ut sem. Wisi fusce nullam nibh enim. Nisl hymenaeos id sed sed in. Proin leo et, pulvinar nunc pede laoreet.'},
|
||||
{rank: 7, title: 'click on ar element in', desc: 'Accumsan quia, id nascetur dui et congue erat, id excepteur, primis ratione nec. At nulla et. Suspendisse lobortis, lobortis in tortor fringilla, duis adipiscing vestibulum voluptates sociosqu auctor.'},
|
||||
{rank: 8, title: 'the list', desc: 'Ante tellus egestas vel hymenaeos, ut viverra nibh ut, ipsum nibh donec donec dolor. Eros ridiculus vel egestas convallis ipsum, commodo ut venenatis nullam porta iaculis, suspendisse ante proin leo, felis risus etiam.'},
|
||||
{rank: 9, title: 'to witness', desc: 'Metus amet ad, elit porttitor a aliquet commodo lacus, integer neque imperdiet augue laoreet, nonummy turpis lacus sed pulvinar condimentum platea. Wisi eleifend quis, tristique dictum, ac dictumst.'},
|
||||
{rank: 10, title: 'the effect', desc: 'Et orci hac ultrices id in. Diam ultrices luctus egestas, sem aliquam auctor molestie odio laoreet. Pede nam cubilia, diam vestibulum ornare natoque, aenean etiam fusce id, eget dictum blandit et mauris mauris'},
|
||||
]
|
||||
};
|
||||
|
||||
function select(m) {
|
||||
data.selected = m;
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
vnode = patch(vnode, view(data));
|
||||
}
|
||||
|
||||
const fadeInOutStyle = {
|
||||
opacity: '0', delayed: {opacity: '1'}, remove: {opacity: '0'}
|
||||
};
|
||||
|
||||
const detailView = (movie) =>
|
||||
h('div.page', {style: fadeInOutStyle}, [
|
||||
h('div.header', [
|
||||
h('div.header-content.detail', {
|
||||
style: {opacity: '1', remove: {opacity: '0'}},
|
||||
}, [
|
||||
h('div.rank', [
|
||||
h('span.header-rank.hero', {hero: {id: 'rank'+movie.rank}}, movie.rank),
|
||||
h('div.rank-circle', {
|
||||
style: {transform: 'scale(0)',
|
||||
delayed: {transform: 'scale(1)'},
|
||||
destroy: {transform: 'scale(0)'}},
|
||||
}),
|
||||
]),
|
||||
h('div.hero.header-title', {hero: {id: movie.title}}, movie.title),
|
||||
h('div.spacer'),
|
||||
h('div.close', {
|
||||
on: {click: [select, undefined]},
|
||||
style: {transform: 'scale(0)',
|
||||
delayed: {transform: 'scale(1)'},
|
||||
destroy: {transform: 'scale(0)'}},
|
||||
}, 'x'),
|
||||
]),
|
||||
]),
|
||||
h('div.page-content', [
|
||||
h('div.desc', {
|
||||
style: {opacity: '0', transform: 'translateX(3em)',
|
||||
delayed: {opacity: '1', transform: 'translate(0)'},
|
||||
remove: {opacity: '0', position: 'absolute', top: '0', left: '0',
|
||||
transform: 'translateX(3em)'}
|
||||
}
|
||||
}, [
|
||||
h('h2', 'Description:'),
|
||||
h('span', movie.desc),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
const overviewView = (movies) =>
|
||||
h('div.page', {style: fadeInOutStyle}, [
|
||||
h('div.header', [
|
||||
h('div.header-content.overview', {
|
||||
style: fadeInOutStyle,
|
||||
}, [
|
||||
h('div.header-title', {
|
||||
style: {transform: 'translateY(-2em)',
|
||||
delayed: {transform: 'translate(0)'},
|
||||
destroy: {transform: 'translateY(-2em)'}}
|
||||
}, 'Top 10 movies'),
|
||||
h('div.spacer'),
|
||||
]),
|
||||
]),
|
||||
h('div.page-content', [
|
||||
h('div.list', {
|
||||
style: {opacity: '0', delayed: {opacity: '1'},
|
||||
remove: {opacity: '0', position: 'absolute', top: '0', left: '0'}}
|
||||
}, movies.map((movie) =>
|
||||
h('div.row', {
|
||||
on: {click: [select, movie]},
|
||||
}, [
|
||||
h('div.hero.rank', [
|
||||
h('span.hero', {hero: {id: 'rank'+movie.rank}}, movie.rank)
|
||||
]),
|
||||
h('div.hero', {hero: {id: movie.title}}, movie.title)
|
||||
])
|
||||
)),
|
||||
]),
|
||||
]);
|
||||
|
||||
const view = (data) =>
|
||||
h('div.page-container', [
|
||||
data.selected ? detailView(data.selected) : overviewView(data.movies),
|
||||
]);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
var container = document.getElementById('container');
|
||||
vnode = patch(container, view(data));
|
||||
render();
|
||||
});
|
||||
@@ -1,523 +0,0 @@
|
||||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var snabbdom = require('../../snabbdom.js');
|
||||
var patch = snabbdom.init([require('../../modules/class'), require('../../modules/props'), require('../../modules/style'), require('../../modules/eventlisteners')]);
|
||||
var h = require('../../h.js');
|
||||
|
||||
var vnode;
|
||||
|
||||
var nextKey = 11;
|
||||
var margin = 8;
|
||||
var sortBy = 'rank';
|
||||
var totalHeight = 0;
|
||||
var originalData = [{ rank: 1, title: 'The Shawshank Redemption', desc: 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.', elmHeight: 0 }, { rank: 2, title: 'The Godfather', desc: 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', elmHeight: 0 }, { rank: 3, title: 'The Godfather: Part II', desc: 'The early life and career of Vito Corleone in 1920s New York is portrayed while his son, Michael, expands and tightens his grip on his crime syndicate stretching from Lake Tahoe, Nevada to pre-revolution 1958 Cuba.', elmHeight: 0 }, { rank: 4, title: 'The Dark Knight', desc: 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, the caped crusader must come to terms with one of the greatest psychological tests of his ability to fight injustice.', elmHeight: 0 }, { rank: 5, title: 'Pulp Fiction', desc: 'The lives of two mob hit men, a boxer, a gangster\'s wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', elmHeight: 0 }, { rank: 6, title: 'Schindler\'s List', desc: 'In Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.', elmHeight: 0 }, { rank: 7, title: '12 Angry Men', desc: 'A dissenting juror in a murder trial slowly manages to convince the others that the case is not as obviously clear as it seemed in court.', elmHeight: 0 }, { rank: 8, title: 'The Good, the Bad and the Ugly', desc: 'A bounty hunting scam joins two men in an uneasy alliance against a third in a race to find a fortune in gold buried in a remote cemetery.', elmHeight: 0 }, { rank: 9, title: 'The Lord of the Rings: The Return of the King', desc: 'Gandalf and Aragorn lead the World of Men against Sauron\'s army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.', elmHeight: 0 }, { rank: 10, title: 'Fight Club', desc: 'An insomniac office worker looking for a way to change his life crosses paths with a devil-may-care soap maker and they form an underground fight club that evolves into something much, much more...', elmHeight: 0 }];
|
||||
var data = [originalData[0], originalData[1], originalData[2], originalData[3], originalData[4], originalData[5], originalData[6], originalData[7], originalData[8], originalData[9]];
|
||||
|
||||
function changeSort(prop) {
|
||||
sortBy = prop;
|
||||
data.sort(function (a, b) {
|
||||
if (a[prop] > b[prop]) {
|
||||
return 1;
|
||||
}
|
||||
if (a[prop] < b[prop]) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function add() {
|
||||
var n = originalData[Math.floor(Math.random() * 10)];
|
||||
data = [{ rank: nextKey++, title: n.title, desc: n.desc, elmHeight: 0 }].concat(data);
|
||||
render();
|
||||
render();
|
||||
}
|
||||
|
||||
function remove(movie) {
|
||||
data = data.filter(function (m) {
|
||||
return m !== movie;
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function movieView(movie) {
|
||||
return h('div.row', {
|
||||
key: movie.rank,
|
||||
style: { opacity: '0', transform: 'translate(-200px)',
|
||||
delayed: { transform: 'translateY(' + movie.offset + 'px)', opacity: '1' },
|
||||
remove: { opacity: '0', transform: 'translateY(' + movie.offset + 'px) translateX(200px)' } },
|
||||
hook: { insert: function insert(vnode) {
|
||||
movie.elmHeight = vnode.elm.offsetHeight;
|
||||
} } }, [h('div', { style: { fontWeight: 'bold' } }, movie.rank), h('div', movie.title), h('div', movie.desc), h('div.btn.rm-btn', { on: { click: [remove, movie] } }, 'x')]);
|
||||
}
|
||||
|
||||
function render() {
|
||||
data = data.reduce(function (acc, m) {
|
||||
var last = acc[acc.length - 1];
|
||||
m.offset = last ? last.offset + last.elmHeight + margin : margin;
|
||||
return acc.concat(m);
|
||||
}, []);
|
||||
totalHeight = data[data.length - 1].offset + data[data.length - 1].elmHeight;
|
||||
vnode = patch(vnode, view(data));
|
||||
}
|
||||
|
||||
function view(data) {
|
||||
return h('div', [h('h1', 'Top 10 movies'), h('div', [h('a.btn.add', { on: { click: add } }, 'Add'), 'Sort by: ', h('span.btn-group', [h('a.btn.rank', { 'class': { active: sortBy === 'rank' }, on: { click: [changeSort, 'rank'] } }, 'Rank'), h('a.btn.title', { 'class': { active: sortBy === 'title' }, on: { click: [changeSort, 'title'] } }, 'Title'), h('a.btn.desc', { 'class': { active: sortBy === 'desc' }, on: { click: [changeSort, 'desc'] } }, 'Description')])]), h('div.list', { style: { height: totalHeight + 'px' } }, data.map(movieView))]);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var container = document.getElementById('container');
|
||||
vnode = patch(container, view(data));
|
||||
render();
|
||||
});
|
||||
|
||||
},{"../../h.js":2,"../../modules/class":4,"../../modules/eventlisteners":5,"../../modules/props":6,"../../modules/style":7,"../../snabbdom.js":8}],2:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var VNode = require('./vnode');
|
||||
var is = require('./is');
|
||||
|
||||
module.exports = function h(sel, b, c) {
|
||||
var data = {},
|
||||
children,
|
||||
text,
|
||||
i;
|
||||
if (arguments.length === 3) {
|
||||
data = b;
|
||||
if (is.array(c)) {
|
||||
children = c;
|
||||
} else if (is.primitive(c)) {
|
||||
text = c;
|
||||
}
|
||||
} else if (arguments.length === 2) {
|
||||
if (is.array(b)) {
|
||||
children = b;
|
||||
} else if (is.primitive(b)) {
|
||||
text = b;
|
||||
} else {
|
||||
data = b;
|
||||
}
|
||||
}
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
|
||||
}
|
||||
}
|
||||
return VNode(sel, data, children, text, undefined);
|
||||
};
|
||||
|
||||
},{"./is":3,"./vnode":9}],3:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
array: Array.isArray,
|
||||
primitive: function primitive(s) {
|
||||
return typeof s === 'string' || typeof s === 'number';
|
||||
} };
|
||||
|
||||
},{}],4:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
function updateClass(oldVnode, vnode) {
|
||||
var cur,
|
||||
name,
|
||||
elm = vnode.elm,
|
||||
oldClass = oldVnode.data['class'] || {},
|
||||
klass = vnode.data['class'] || {};
|
||||
for (name in klass) {
|
||||
cur = klass[name];
|
||||
if (cur !== oldClass[name]) {
|
||||
elm.classList[cur ? 'add' : 'remove'](name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { create: updateClass, update: updateClass };
|
||||
|
||||
},{}],5:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var is = require('../is');
|
||||
|
||||
function arrInvoker(arr) {
|
||||
return function () {
|
||||
arr[0](arr[1]);
|
||||
};
|
||||
}
|
||||
|
||||
function updateEventListeners(oldVnode, vnode) {
|
||||
var name,
|
||||
cur,
|
||||
old,
|
||||
elm = vnode.elm,
|
||||
oldOn = oldVnode.data.on || {},
|
||||
on = vnode.data.on;
|
||||
if (!on) return;
|
||||
for (name in on) {
|
||||
cur = on[name];
|
||||
old = oldOn[name];
|
||||
if (old === undefined) {
|
||||
elm.addEventListener(name, is.array(cur) ? arrInvoker(cur) : cur);
|
||||
} else if (is.array(old)) {
|
||||
old[0] = cur[0]; // Deliberately modify old array since it's
|
||||
old[1] = cur[1]; // captured in closure created with `arrInvoker`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { create: updateEventListeners, update: updateEventListeners };
|
||||
|
||||
},{"../is":3}],6:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
function updateProps(oldVnode, vnode) {
|
||||
var key,
|
||||
cur,
|
||||
old,
|
||||
elm = vnode.elm,
|
||||
oldProps = oldVnode.data.props || {},
|
||||
props = vnode.data.props || {};
|
||||
for (key in props) {
|
||||
cur = props[key];
|
||||
old = oldProps[key];
|
||||
if (old !== cur) {
|
||||
elm[key] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { create: updateProps, update: updateProps };
|
||||
|
||||
},{}],7:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var raf = requestAnimationFrame || setTimeout;
|
||||
var nextFrame = function nextFrame(fn) {
|
||||
raf(function () {
|
||||
raf(fn);
|
||||
});
|
||||
};
|
||||
|
||||
function setNextFrame(obj, prop, val) {
|
||||
nextFrame(function () {
|
||||
obj[prop] = val;
|
||||
});
|
||||
}
|
||||
|
||||
function updateStyle(oldVnode, vnode) {
|
||||
var cur,
|
||||
name,
|
||||
elm = vnode.elm,
|
||||
oldStyle = oldVnode.data.style || {},
|
||||
style = vnode.data.style || {},
|
||||
oldHasDel = ('delayed' in oldStyle);
|
||||
for (name in style) {
|
||||
cur = style[name];
|
||||
if (name === 'delayed') {
|
||||
for (name in style.delayed) {
|
||||
cur = style.delayed[name];
|
||||
if (!oldHasDel || cur !== oldStyle.delayed[name]) {
|
||||
setNextFrame(elm.style, name, cur);
|
||||
}
|
||||
}
|
||||
} else if (name !== 'remove' && cur !== oldStyle[name]) {
|
||||
elm.style[name] = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyDestroyStyle(vnode) {
|
||||
var style,
|
||||
name,
|
||||
elm = vnode.elm,
|
||||
s = vnode.data.style;
|
||||
if (!s || !(style = s.destroy)) return;
|
||||
for (name in style) {
|
||||
elm.style[name] = style[name];
|
||||
}
|
||||
}
|
||||
|
||||
function applyRemoveStyle(vnode, rm) {
|
||||
var s = vnode.data.style;
|
||||
if (!s || !s.remove) {
|
||||
rm();
|
||||
return;
|
||||
}
|
||||
var name,
|
||||
elm = vnode.elm,
|
||||
idx,
|
||||
i = 0,
|
||||
maxDur = 0,
|
||||
compStyle,
|
||||
style = s.remove,
|
||||
amount = 0;
|
||||
var applied = [];
|
||||
for (name in style) {
|
||||
applied.push(name);
|
||||
elm.style[name] = style[name];
|
||||
}
|
||||
compStyle = getComputedStyle(elm);
|
||||
var props = compStyle['transition-property'].split(', ');
|
||||
for (; i < props.length; ++i) {
|
||||
if (applied.indexOf(props[i]) !== -1) amount++;
|
||||
}
|
||||
elm.addEventListener('transitionend', function (ev) {
|
||||
if (ev.target === elm) --amount;
|
||||
if (amount === 0) rm();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle };
|
||||
|
||||
},{}],8:[function(require,module,exports){
|
||||
// jshint newcap: false
|
||||
'use strict';
|
||||
|
||||
var VNode = require('./vnode');
|
||||
var is = require('./is');
|
||||
|
||||
function isUndef(s) {
|
||||
return s === undefined;
|
||||
}
|
||||
|
||||
function emptyNodeAt(elm) {
|
||||
return VNode(elm.tagName, {}, [], undefined, elm);
|
||||
}
|
||||
|
||||
var emptyNode = VNode('', {}, [], undefined, undefined);
|
||||
|
||||
var insertedVnodeQueue;
|
||||
|
||||
function sameVnode(vnode1, vnode2) {
|
||||
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
|
||||
}
|
||||
|
||||
function createKeyToOldIdx(children, beginIdx, endIdx) {
|
||||
var i,
|
||||
map = {},
|
||||
key;
|
||||
for (i = beginIdx; i <= endIdx; ++i) {
|
||||
key = children[i].key;
|
||||
if (!isUndef(key)) map[key] = i;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function createRmCb(parentElm, childElm, listeners) {
|
||||
return function () {
|
||||
if (--listeners === 0) parentElm.removeChild(childElm);
|
||||
};
|
||||
}
|
||||
|
||||
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
|
||||
|
||||
function init(modules) {
|
||||
var i,
|
||||
j,
|
||||
cbs = {};
|
||||
for (i = 0; i < hooks.length; ++i) {
|
||||
cbs[hooks[i]] = [];
|
||||
for (j = 0; j < modules.length; ++j) {
|
||||
if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
function createElm(vnode) {
|
||||
var i;
|
||||
if (!isUndef(i = vnode.data) && !isUndef(i = i.hook) && !isUndef(i = i.init)) {
|
||||
i(vnode);
|
||||
}
|
||||
if (!isUndef(i = vnode.data) && !isUndef(i = i.vnode)) vnode = i;
|
||||
var elm,
|
||||
children = vnode.children,
|
||||
sel = vnode.sel;
|
||||
if (!isUndef(sel)) {
|
||||
// Parse selector
|
||||
var hashIdx = sel.indexOf('#');
|
||||
var dotIdx = sel.indexOf('.', hashIdx);
|
||||
var hash = hashIdx > 0 ? hashIdx : sel.length;
|
||||
var dot = dotIdx > 0 ? dotIdx : sel.length;
|
||||
var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
|
||||
elm = vnode.elm = document.createElement(tag);
|
||||
if (hash < dot) elm.id = sel.slice(hash + 1, dot);
|
||||
if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/\./g, ' ');
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
elm.appendChild(createElm(children[i]));
|
||||
}
|
||||
} else if (is.primitive(vnode.text)) {
|
||||
elm.appendChild(document.createTextNode(vnode.text));
|
||||
}
|
||||
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
|
||||
i = vnode.data.hook; // Reuse variable
|
||||
if (!isUndef(i)) {
|
||||
if (i.create) i.create(vnode);
|
||||
if (i.insert) insertedVnodeQueue.push(vnode);
|
||||
}
|
||||
} else {
|
||||
elm = vnode.elm = document.createTextNode(vnode.text);
|
||||
}
|
||||
return elm;
|
||||
}
|
||||
|
||||
function addVnodes(parentElm, before, vnodes, startIdx, endIdx) {
|
||||
if (isUndef(before)) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
parentElm.appendChild(createElm(vnodes[startIdx]));
|
||||
}
|
||||
} else {
|
||||
var elm = before.elm;
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
parentElm.insertBefore(createElm(vnodes[startIdx]), elm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function invokeDestroyHook(vnode) {
|
||||
var i = vnode.data.hook,
|
||||
j;
|
||||
if (!isUndef(i) && !isUndef(j = i.destroy)) j(vnode);
|
||||
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
|
||||
if (!isUndef(vnode.children)) {
|
||||
for (j = 0; j < vnode.children.length; ++j) {
|
||||
invokeDestroyHook(vnode.children[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
var i,
|
||||
listeners,
|
||||
rm,
|
||||
ch = vnodes[startIdx];
|
||||
if (!isUndef(ch)) {
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(parentElm, ch.elm, listeners);
|
||||
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
|
||||
invokeDestroyHook(ch);
|
||||
if (ch.data.hook && ch.data.hook.remove) {
|
||||
ch.data.hook.remove(ch, rm);
|
||||
} else {
|
||||
rm();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateChildren(parentElm, oldCh, newCh) {
|
||||
var oldStartIdx = 0,
|
||||
newStartIdx = 0;
|
||||
var oldEndIdx = oldCh.length - 1;
|
||||
var oldStartVnode = oldCh[0];
|
||||
var oldEndVnode = oldCh[oldEndIdx];
|
||||
var newEndIdx = newCh.length - 1;
|
||||
var newStartVnode = newCh[0];
|
||||
var newEndVnode = newCh[newEndIdx];
|
||||
var oldKeyToIdx, idxInOld, elmToMove;
|
||||
|
||||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||||
if (isUndef(oldStartVnode)) {
|
||||
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
|
||||
} else if (isUndef(oldEndVnode)) {
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newStartVnode)) {
|
||||
patchVnode(oldStartVnode, newStartVnode);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else if (sameVnode(oldEndVnode, newEndVnode)) {
|
||||
patchVnode(oldEndVnode, newEndVnode);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newEndVnode)) {
|
||||
// Vnode moved right
|
||||
patchVnode(oldStartVnode, newEndVnode);
|
||||
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode);
|
||||
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
|
||||
idxInOld = oldKeyToIdx[newStartVnode.key];
|
||||
if (isUndef(idxInOld)) {
|
||||
// New element
|
||||
parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
elmToMove = oldCh[idxInOld];
|
||||
patchVnode(elmToMove, newStartVnode);
|
||||
oldCh[idxInOld] = undefined;
|
||||
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldStartIdx > oldEndIdx) addVnodes(parentElm, oldStartVnode, newCh, newStartIdx, newEndIdx);else if (newStartIdx > newEndIdx) removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
|
||||
function patchVnode(oldVnode, vnode) {
|
||||
var i;
|
||||
if (!isUndef(i = vnode.data) && !isUndef(i = i.hook) && !isUndef(i = i.patch)) {
|
||||
i = i(oldVnode, vnode);
|
||||
}
|
||||
if (!isUndef(i = oldVnode.data) && !isUndef(i = i.vnode)) oldVnode = i;
|
||||
if (!isUndef(i = vnode.data) && !isUndef(i = i.vnode)) vnode = i;
|
||||
var elm = vnode.elm = oldVnode.elm,
|
||||
oldCh = oldVnode.children,
|
||||
ch = vnode.children;
|
||||
if (oldVnode === vnode) return;
|
||||
if (!isUndef(vnode.data)) {
|
||||
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
|
||||
i = vnode.data.hook;
|
||||
if (!isUndef(i) && !isUndef(i = i.update)) i(vnode);
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (!isUndef(oldCh) && !isUndef(ch)) {
|
||||
if (oldCh !== ch) updateChildren(elm, oldCh, ch);
|
||||
} else if (!isUndef(ch)) {
|
||||
addVnodes(elm, undefined, ch, 0, ch.length - 1);
|
||||
} else if (!isUndef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
elm.childNodes[0].nodeValue = vnode.text;
|
||||
}
|
||||
return vnode;
|
||||
}
|
||||
|
||||
return function (oldVnode, vnode) {
|
||||
var i;
|
||||
insertedVnodeQueue = [];
|
||||
if (oldVnode instanceof Element) {
|
||||
oldVnode = emptyNodeAt(oldVnode);
|
||||
}
|
||||
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
|
||||
patchVnode(oldVnode, vnode);
|
||||
for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
||||
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
|
||||
}
|
||||
insertedVnodeQueue = undefined;
|
||||
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
|
||||
return vnode;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { init: init };
|
||||
|
||||
},{"./is":3,"./vnode":9}],9:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
module.exports = function (sel, data, children, text, elm) {
|
||||
var key = data === undefined ? undefined : data.key;
|
||||
return { sel: sel, data: data, children: children,
|
||||
text: text, elm: elm, key: key };
|
||||
};
|
||||
|
||||
},{}]},{},[1]);
|
||||
@@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>Reorder animation</title>
|
||||
<script type="text/javascript" src="build.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, .2);
|
||||
padding: .5em .8em;
|
||||
transition: box-shadow .05s ease-in-out;
|
||||
-webkit-transition: box-shadow .05s ease-in-out;
|
||||
}
|
||||
.btn:hover {
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, .2);
|
||||
}
|
||||
.btn:active, .active, .active:hover {
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, .2),
|
||||
inset 0 0 4px rgba(0, 0, 0, .1);
|
||||
}
|
||||
.add {
|
||||
float: right;
|
||||
}
|
||||
#container {
|
||||
max-width: 42em;
|
||||
margin: 0 auto 2em auto;
|
||||
}
|
||||
.list {
|
||||
position: relative;
|
||||
}
|
||||
.row {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
margin: .5em 0;
|
||||
padding: 1em;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, .2);
|
||||
transition: transform .5s ease-in-out, opacity .5s ease-out, left .5s ease-in-out;
|
||||
-webkit-transition: transform .5s ease-in-out, opacity .5s ease-out, left .5s ease-in-out;
|
||||
}
|
||||
.row div {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.row > div:nth-child(1) {
|
||||
width: 5%;
|
||||
}
|
||||
.row > div:nth-child(2) {
|
||||
width: 30%;
|
||||
}
|
||||
.row > div:nth-child(3) {
|
||||
width: 65%;
|
||||
}
|
||||
.rm-btn {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: #C25151;
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
text-align: center;
|
||||
line-height: 1.4em;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,112 +0,0 @@
|
||||
var snabbdom = require('../../snabbdom.js');
|
||||
var patch = snabbdom.init([
|
||||
require('../../modules/class').default,
|
||||
require('../../modules/props').default,
|
||||
require('../../modules/style').default,
|
||||
require('../../modules/eventlisteners').default,
|
||||
]);
|
||||
var h = require('../../h.js').default;
|
||||
|
||||
var vnode;
|
||||
|
||||
var nextKey = 11;
|
||||
var margin = 8;
|
||||
var sortBy = 'rank';
|
||||
var totalHeight = 0;
|
||||
var originalData = [
|
||||
{rank: 1, title: 'The Shawshank Redemption', desc: 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.', elmHeight: 0},
|
||||
{rank: 2, title: 'The Godfather', desc: 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', elmHeight: 0},
|
||||
{rank: 3, title: 'The Godfather: Part II', desc: 'The early life and career of Vito Corleone in 1920s New York is portrayed while his son, Michael, expands and tightens his grip on his crime syndicate stretching from Lake Tahoe, Nevada to pre-revolution 1958 Cuba.', elmHeight: 0},
|
||||
{rank: 4, title: 'The Dark Knight', desc: 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, the caped crusader must come to terms with one of the greatest psychological tests of his ability to fight injustice.', elmHeight: 0},
|
||||
{rank: 5, title: 'Pulp Fiction', desc: 'The lives of two mob hit men, a boxer, a gangster\'s wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', elmHeight: 0},
|
||||
{rank: 6, title: 'Schindler\'s List', desc: 'In Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.', elmHeight: 0},
|
||||
{rank: 7, title: '12 Angry Men', desc: 'A dissenting juror in a murder trial slowly manages to convince the others that the case is not as obviously clear as it seemed in court.', elmHeight: 0},
|
||||
{rank: 8, title: 'The Good, the Bad and the Ugly', desc: 'A bounty hunting scam joins two men in an uneasy alliance against a third in a race to find a fortune in gold buried in a remote cemetery.', elmHeight: 0},
|
||||
{rank: 9, title: 'The Lord of the Rings: The Return of the King', desc: 'Gandalf and Aragorn lead the World of Men against Sauron\'s army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.', elmHeight: 0},
|
||||
{rank: 10, title: 'Fight Club', desc: 'An insomniac office worker looking for a way to change his life crosses paths with a devil-may-care soap maker and they form an underground fight club that evolves into something much, much more...', elmHeight: 0},
|
||||
];
|
||||
var data = [
|
||||
originalData[0],
|
||||
originalData[1],
|
||||
originalData[2],
|
||||
originalData[3],
|
||||
originalData[4],
|
||||
originalData[5],
|
||||
originalData[6],
|
||||
originalData[7],
|
||||
originalData[8],
|
||||
originalData[9],
|
||||
];
|
||||
|
||||
function changeSort(prop) {
|
||||
sortBy = prop;
|
||||
data.sort((a, b) => {
|
||||
if (a[prop] > b[prop]) {
|
||||
return 1;
|
||||
}
|
||||
if (a[prop] < b[prop]) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function add() {
|
||||
var n = originalData[Math.floor(Math.random() * 10)];
|
||||
data = [{rank: nextKey++, title: n.title, desc: n.desc, elmHeight: 0}].concat(data);
|
||||
render();
|
||||
render();
|
||||
}
|
||||
|
||||
function remove(movie) {
|
||||
data = data.filter((m) => { return m !== movie; });
|
||||
render();
|
||||
}
|
||||
|
||||
function movieView(movie) {
|
||||
return h('div.row', {
|
||||
key: movie.rank,
|
||||
style: {opacity: '0', transform: 'translate(-200px)',
|
||||
delayed: {transform: `translateY(${movie.offset}px)`, opacity: '1'},
|
||||
remove: {opacity: '0', transform: `translateY(${movie.offset}px) translateX(200px)`}},
|
||||
hook: {insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight; }},
|
||||
}, [
|
||||
h('div', {style: {fontWeight: 'bold'}}, movie.rank),
|
||||
h('div', movie.title),
|
||||
h('div', movie.desc),
|
||||
h('div.btn.rm-btn', {on: {click: [remove, movie]}}, 'x'),
|
||||
]);
|
||||
}
|
||||
|
||||
function render() {
|
||||
data = data.reduce((acc, m) => {
|
||||
var last = acc[acc.length - 1];
|
||||
m.offset = last ? last.offset + last.elmHeight + margin : margin;
|
||||
return acc.concat(m);
|
||||
}, []);
|
||||
totalHeight = data[data.length - 1].offset + data[data.length - 1].elmHeight;
|
||||
vnode = patch(vnode, view(data));
|
||||
}
|
||||
|
||||
function view(data) {
|
||||
return h('div', [
|
||||
h('h1', 'Top 10 movies'),
|
||||
h('div', [
|
||||
h('a.btn.add', {on: {click: add}}, 'Add'),
|
||||
'Sort by: ',
|
||||
h('span.btn-group', [
|
||||
h('a.btn.rank', {class: {active: sortBy === 'rank'}, on: {click: [changeSort, 'rank']}}, 'Rank'),
|
||||
h('a.btn.title', {class: {active: sortBy === 'title'}, on: {click: [changeSort, 'title']}}, 'Title'),
|
||||
h('a.btn.desc', {class: {active: sortBy === 'desc'}, on: {click: [changeSort, 'desc']}}, 'Description'),
|
||||
]),
|
||||
]),
|
||||
h('div.list', {style: {height: totalHeight+'px'}}, data.map(movieView)),
|
||||
]);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
var container = document.getElementById('container');
|
||||
vnode = patch(container, view(data));
|
||||
render();
|
||||
});
|
||||
@@ -1,376 +0,0 @@
|
||||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var snabbdom = require('../../snabbdom.js');
|
||||
var patch = snabbdom.init([require('../../modules/attributes')]);
|
||||
var h = require('../../h.js');
|
||||
|
||||
var vnode;
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var container = document.getElementById('container');
|
||||
var vnode = h('div', [h('svg', { attrs: { width: 100, height: 100 } }, [h('circle', { attrs: { cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow' } })])]);
|
||||
vnode = patch(container, vnode);
|
||||
});
|
||||
|
||||
},{"../../h.js":2,"../../modules/attributes":4,"../../snabbdom.js":5}],2:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var VNode = require('./vnode');
|
||||
var is = require('./is');
|
||||
|
||||
function addNS(data, children) {
|
||||
data.ns = 'http://www.w3.org/2000/svg';
|
||||
if (children !== undefined) {
|
||||
for (var i = 0; i < children.length; ++i) {
|
||||
addNS(children[i].data, children[i].children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function h(sel, b, c) {
|
||||
var data = {},
|
||||
children,
|
||||
text,
|
||||
i;
|
||||
if (arguments.length === 3) {
|
||||
data = b;
|
||||
if (is.array(c)) {
|
||||
children = c;
|
||||
} else if (is.primitive(c)) {
|
||||
text = c;
|
||||
}
|
||||
} else if (arguments.length === 2) {
|
||||
if (is.array(b)) {
|
||||
children = b;
|
||||
} else if (is.primitive(b)) {
|
||||
text = b;
|
||||
} else {
|
||||
data = b;
|
||||
}
|
||||
}
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
|
||||
}
|
||||
}
|
||||
if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g') {
|
||||
addNS(data, children);
|
||||
}
|
||||
return VNode(sel, data, children, text, undefined);
|
||||
};
|
||||
|
||||
},{"./is":3,"./vnode":6}],3:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
array: Array.isArray,
|
||||
primitive: function primitive(s) {
|
||||
return typeof s === 'string' || typeof s === 'number';
|
||||
} };
|
||||
|
||||
},{}],4:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare", "default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable", "enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple", "muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly", "required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate", "truespeed", "typemustmatch", "visible"];
|
||||
|
||||
var booleanAttrsDict = {};
|
||||
for (var i = 0, len = booleanAttrs.length; i < len; i++) {
|
||||
booleanAttrsDict[booleanAttrs[i]] = true;
|
||||
}
|
||||
|
||||
function updateAttrs(oldVnode, vnode) {
|
||||
var key,
|
||||
cur,
|
||||
old,
|
||||
elm = vnode.elm,
|
||||
oldAttrs = oldVnode.data.attrs || {},
|
||||
attrs = vnode.data.attrs || {};
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
for (key in attrs) {
|
||||
cur = attrs[key];
|
||||
old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
// TODO: add support to namespaced attributes (setAttributeNS)
|
||||
if (!cur && booleanAttrsDict[key]) elm.removeAttribute(key);else elm.setAttribute(key, cur);
|
||||
}
|
||||
}
|
||||
//remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { create: updateAttrs, update: updateAttrs };
|
||||
|
||||
},{}],5:[function(require,module,exports){
|
||||
// jshint newcap: false
|
||||
/* global require, module, document, Element */
|
||||
'use strict';
|
||||
|
||||
var VNode = require('./vnode');
|
||||
var is = require('./is');
|
||||
|
||||
function isUndef(s) {
|
||||
return s === undefined;
|
||||
}
|
||||
function isDef(s) {
|
||||
return s !== undefined;
|
||||
}
|
||||
|
||||
function emptyNodeAt(elm) {
|
||||
return VNode(elm.tagName, {}, [], undefined, elm);
|
||||
}
|
||||
|
||||
var emptyNode = VNode('', {}, [], undefined, undefined);
|
||||
|
||||
function sameVnode(vnode1, vnode2) {
|
||||
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
|
||||
}
|
||||
|
||||
function createKeyToOldIdx(children, beginIdx, endIdx) {
|
||||
var i,
|
||||
map = {},
|
||||
key;
|
||||
for (i = beginIdx; i <= endIdx; ++i) {
|
||||
key = children[i].key;
|
||||
if (isDef(key)) map[key] = i;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function createRmCb(childElm, listeners) {
|
||||
return function () {
|
||||
if (--listeners === 0) childElm.parentElement.removeChild(childElm);
|
||||
};
|
||||
}
|
||||
|
||||
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
|
||||
|
||||
function init(modules) {
|
||||
var i,
|
||||
j,
|
||||
cbs = {};
|
||||
for (i = 0; i < hooks.length; ++i) {
|
||||
cbs[hooks[i]] = [];
|
||||
for (j = 0; j < modules.length; ++j) {
|
||||
if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
function createElm(vnode, insertedVnodeQueue) {
|
||||
var i,
|
||||
data = vnode.data;
|
||||
if (isDef(data)) {
|
||||
if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode);
|
||||
if (isDef(i = data.vnode)) vnode = i;
|
||||
}
|
||||
var elm,
|
||||
children = vnode.children,
|
||||
sel = vnode.sel;
|
||||
if (isDef(sel)) {
|
||||
// Parse selector
|
||||
var hashIdx = sel.indexOf('#');
|
||||
var dotIdx = sel.indexOf('.', hashIdx);
|
||||
var hash = hashIdx > 0 ? hashIdx : sel.length;
|
||||
var dot = dotIdx > 0 ? dotIdx : sel.length;
|
||||
var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
|
||||
elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? document.createElementNS(i, tag) : document.createElement(tag);
|
||||
if (hash < dot) elm.id = sel.slice(hash + 1, dot);
|
||||
if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/\./g, ' ');
|
||||
if (is.array(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
elm.appendChild(createElm(children[i], insertedVnodeQueue));
|
||||
}
|
||||
} else if (is.primitive(vnode.text)) {
|
||||
elm.appendChild(document.createTextNode(vnode.text));
|
||||
}
|
||||
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
|
||||
i = vnode.data.hook; // Reuse variable
|
||||
if (isDef(i)) {
|
||||
if (i.create) i.create(emptyNode, vnode);
|
||||
if (i.insert) insertedVnodeQueue.push(vnode);
|
||||
}
|
||||
} else {
|
||||
elm = vnode.elm = document.createTextNode(vnode.text);
|
||||
}
|
||||
return vnode.elm;
|
||||
}
|
||||
|
||||
function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
parentElm.insertBefore(createElm(vnodes[startIdx], insertedVnodeQueue), before);
|
||||
}
|
||||
}
|
||||
|
||||
function invokeDestroyHook(vnode) {
|
||||
var i = vnode.data,
|
||||
j;
|
||||
if (isDef(i)) {
|
||||
if (isDef(i = i.hook) && isDef(i = i.destroy)) i(vnode);
|
||||
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
|
||||
if (isDef(i = vnode.children)) {
|
||||
for (j = 0; j < vnode.children.length; ++j) {
|
||||
invokeDestroyHook(vnode.children[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
var i,
|
||||
listeners,
|
||||
rm,
|
||||
ch = vnodes[startIdx];
|
||||
if (isDef(ch)) {
|
||||
if (isDef(ch.sel)) {
|
||||
invokeDestroyHook(ch);
|
||||
listeners = cbs.remove.length + 1;
|
||||
rm = createRmCb(ch.elm, listeners);
|
||||
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
|
||||
if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
|
||||
i(ch, rm);
|
||||
} else {
|
||||
rm();
|
||||
}
|
||||
} else {
|
||||
// Text node
|
||||
parentElm.removeChild(ch.elm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
|
||||
var oldStartIdx = 0,
|
||||
newStartIdx = 0;
|
||||
var oldEndIdx = oldCh.length - 1;
|
||||
var oldStartVnode = oldCh[0];
|
||||
var oldEndVnode = oldCh[oldEndIdx];
|
||||
var newEndIdx = newCh.length - 1;
|
||||
var newStartVnode = newCh[0];
|
||||
var newEndVnode = newCh[newEndIdx];
|
||||
var oldKeyToIdx, idxInOld, elmToMove, before;
|
||||
|
||||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||||
if (isUndef(oldStartVnode)) {
|
||||
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
|
||||
} else if (isUndef(oldEndVnode)) {
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newStartVnode)) {
|
||||
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else if (sameVnode(oldEndVnode, newEndVnode)) {
|
||||
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldStartVnode, newEndVnode)) {
|
||||
// Vnode moved right
|
||||
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
|
||||
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
} else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
||||
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
|
||||
idxInOld = oldKeyToIdx[newStartVnode.key];
|
||||
if (isUndef(idxInOld)) {
|
||||
// New element
|
||||
parentElm.insertBefore(createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
} else {
|
||||
elmToMove = oldCh[idxInOld];
|
||||
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
||||
oldCh[idxInOld] = undefined;
|
||||
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
|
||||
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
|
||||
} else if (newStartIdx > newEndIdx) {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
|
||||
var i, hook;
|
||||
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
if (isDef(i = oldVnode.data) && isDef(i = i.vnode)) oldVnode = i;
|
||||
if (isDef(i = vnode.data) && isDef(i = i.vnode)) vnode = i;
|
||||
var elm = vnode.elm = oldVnode.elm,
|
||||
oldCh = oldVnode.children,
|
||||
ch = vnode.children;
|
||||
if (oldVnode === vnode) return;
|
||||
if (isDef(vnode.data)) {
|
||||
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
|
||||
i = vnode.data.hook;
|
||||
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
|
||||
}
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
|
||||
} else if (isDef(ch)) {
|
||||
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
|
||||
} else if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
||||
}
|
||||
} else if (oldVnode.text !== vnode.text) {
|
||||
elm.textContent = vnode.text;
|
||||
}
|
||||
if (isDef(hook) && isDef(i = hook.postpatch)) {
|
||||
i(oldVnode, vnode);
|
||||
}
|
||||
}
|
||||
|
||||
return function (oldVnode, vnode) {
|
||||
var i;
|
||||
var insertedVnodeQueue = [];
|
||||
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
|
||||
if (oldVnode instanceof Element) {
|
||||
if (oldVnode.parentElement !== null) {
|
||||
createElm(vnode, insertedVnodeQueue);
|
||||
oldVnode.parentElement.replaceChild(vnode.elm, oldVnode);
|
||||
} else {
|
||||
oldVnode = emptyNodeAt(oldVnode);
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
||||
}
|
||||
} else {
|
||||
patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
||||
}
|
||||
for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
||||
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
|
||||
}
|
||||
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
|
||||
return vnode;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { init: init };
|
||||
|
||||
},{"./is":3,"./vnode":6}],6:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
module.exports = function (sel, data, children, text, elm) {
|
||||
var key = data === undefined ? undefined : data.key;
|
||||
return { sel: sel, data: data, children: children,
|
||||
text: text, elm: elm, key: key };
|
||||
};
|
||||
|
||||
},{}]},{},[1]);
|
||||
@@ -1,84 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>SVG</title>
|
||||
<script type="text/javascript" src="build.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, .2);
|
||||
padding: .5em .8em;
|
||||
transition: box-shadow .05s ease-in-out;
|
||||
-webkit-transition: box-shadow .05s ease-in-out;
|
||||
}
|
||||
.btn:hover {
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, .2);
|
||||
}
|
||||
.btn:active, .active, .active:hover {
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, .2),
|
||||
inset 0 0 4px rgba(0, 0, 0, .1);
|
||||
}
|
||||
.add {
|
||||
float: right;
|
||||
}
|
||||
#container {
|
||||
max-width: 42em;
|
||||
margin: 0 auto 2em auto;
|
||||
}
|
||||
.list {
|
||||
position: relative;
|
||||
}
|
||||
.row {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
margin: .5em 0;
|
||||
padding: 1em;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 1px rgba(0, 0, 0, .2);
|
||||
transition: transform .5s ease-in-out, opacity .5s ease-out, left .5s ease-in-out;
|
||||
-webkit-transition: transform .5s ease-in-out, opacity .5s ease-out, left .5s ease-in-out;
|
||||
}
|
||||
.row div {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.row > div:nth-child(1) {
|
||||
width: 5%;
|
||||
}
|
||||
.row > div:nth-child(2) {
|
||||
width: 30%;
|
||||
}
|
||||
.row > div:nth-child(3) {
|
||||
width: 65%;
|
||||
}
|
||||
.rm-btn {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: #C25151;
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
text-align: center;
|
||||
line-height: 1.4em;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,17 +0,0 @@
|
||||
var snabbdom = require('../../snabbdom.js');
|
||||
var patch = snabbdom.init([
|
||||
require('../../modules/attributes').default,
|
||||
]);
|
||||
var h = require('../../h.js').default;
|
||||
|
||||
var vnode;
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
var container = document.getElementById('container');
|
||||
var vnode = h('div', [
|
||||
h('svg', {attrs: {width: 100, height: 100}}, [
|
||||
h('circle', {attrs: {cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow'}})
|
||||
])
|
||||
]);
|
||||
vnode = patch(container, vnode);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user