diff --git a/examples/islands_router/Cargo.toml b/examples/islands_router/Cargo.toml index 31c8f921a..7acfcc573 100644 --- a/examples/islands_router/Cargo.toml +++ b/examples/islands_router/Cargo.toml @@ -14,7 +14,7 @@ leptos = { path = "../../leptos", features = ["tracing", "islands"] } leptos_router = { path = "../../router" } server_fn = { path = "../../server_fn", features = ["serde-lite"] } leptos_axum = { path = "../../integrations/axum", features = [ - "dont-use-islands-router", + "islands-router", ], optional = true } log = "0.4.22" serde = { version = "1.0", features = ["derive"] } @@ -22,7 +22,8 @@ axum = { version = "0.8.1", optional = true } tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5.2", features = ["fs"], optional = true } tokio = { version = "1.39", features = ["full"], optional = true } -wasm-bindgen = "0.2.93" +wasm-bindgen = "0.2.100" +serde_json = "1.0.133" [features] hydrate = ["leptos/hydrate"] @@ -55,11 +56,11 @@ site-root = "target/site" # Defaults to pkg site-pkg-dir = "pkg" # [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css -style-file = "./style.css" +style-file = "style.css" # [Optional] Files in the asset-dir will be copied to the site-root directory assets-dir = "public" # The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. -site-addr = "127.0.0.1:3000" +site-addr = "127.0.0.1:3009" # The port to use for automatic reload monitoring reload-port = 3001 # The browserlist query used for optimizing the CSS. diff --git a/examples/islands_router/mock_data.json b/examples/islands_router/mock_data.json new file mode 100644 index 000000000..2ec2a90c7 --- /dev/null +++ b/examples/islands_router/mock_data.json @@ -0,0 +1,2852 @@ +[ + { + "id": 2, + "first_name": "Pippy", + "last_name": "Yule", + "email": "pyule1@mit.edu" + }, + { + "id": 3, + "first_name": "Rodrick", + "last_name": "Swancock", + "email": "rswancock2@google.co.uk" + }, + { + "id": 8, + "first_name": "Eugenie", + "last_name": "Lanyon", + "email": "elanyon7@nba.com" + }, + { + "id": 9, + "first_name": "Derry", + "last_name": "Bovingdon", + "email": "dbovingdon8@furl.net" + }, + { + "id": 11, + "first_name": "Burnard", + "last_name": "Kuhndel", + "email": "bkuhndela@cmu.edu" + }, + { + "id": 12, + "first_name": "Greer", + "last_name": "Strachan", + "email": "gstrachanb@virginia.edu" + }, + { + "id": 13, + "first_name": "Dorine", + "last_name": "Lougheed", + "email": "dlougheedc@redcross.org" + }, + { + "id": 14, + "first_name": "Jayne", + "last_name": "Geggie", + "email": "jgeggied@youtu.be" + }, + { + "id": 15, + "first_name": "Chelsey", + "last_name": "Botham", + "email": "cbothame@cargocollective.com" + }, + { + "id": 16, + "first_name": "Emmy", + "last_name": "Whittier", + "email": "ewhittierf@delicious.com" + }, + { + "id": 18, + "first_name": "Julio", + "last_name": "Vannini", + "email": "jvanninih@sourceforge.net" + }, + { + "id": 19, + "first_name": "Zorah", + "last_name": "Turbefield", + "email": "zturbefieldi@epa.gov" + }, + { + "id": 20, + "first_name": "Les", + "last_name": "Strutley", + "email": "lstrutleyj@networkadvertising.org" + }, + { + "id": 21, + "first_name": "Dedie", + "last_name": "Roubeix", + "email": "droubeixk@linkedin.com" + }, + { + "id": 22, + "first_name": "Milzie", + "last_name": "Doyley", + "email": "mdoyleyl@youtu.be" + }, + { + "id": 23, + "first_name": "Cleopatra", + "last_name": "Croysdale", + "email": "ccroysdalem@cdc.gov" + }, + { + "id": 24, + "first_name": "Nellie", + "last_name": "Records", + "email": "nrecordsn@rediff.com" + }, + { + "id": 25, + "first_name": "Michelina", + "last_name": "Jentzsch", + "email": "mjentzscho@theguardian.com" + }, + { + "id": 26, + "first_name": "Theodosia", + "last_name": "De Vries", + "email": "tdevriesp@unc.edu" + }, + { + "id": 27, + "first_name": "Maryanna", + "last_name": "Jirieck", + "email": "mjirieckq@meetup.com" + }, + { + "id": 28, + "first_name": "Dreddy", + "last_name": "Labden", + "email": "dlabdenr@feedburner.com" + }, + { + "id": 29, + "first_name": "Glynda", + "last_name": "Geibel", + "email": "ggeibels@yelp.com" + }, + { + "id": 30, + "first_name": "Yulma", + "last_name": "Giroldo", + "email": "ygiroldot@google.co.jp" + }, + { + "id": 31, + "first_name": "Michele", + "last_name": "Jennions", + "email": "mjennionsu@meetup.com" + }, + { + "id": 32, + "first_name": "Hyatt", + "last_name": "Picford", + "email": "hpicfordv@cornell.edu" + }, + { + "id": 33, + "first_name": "Jehanna", + "last_name": "Frunks", + "email": "jfrunksw@slashdot.org" + }, + { + "id": 34, + "first_name": "Gustavo", + "last_name": "Soda", + "email": "gsodax@scientificamerican.com" + }, + { + "id": 35, + "first_name": "Rianon", + "last_name": "Lamey", + "email": "rlameyy@histats.com" + }, + { + "id": 36, + "first_name": "Winston", + "last_name": "Pitcher", + "email": "wpitcherz@sphinn.com" + }, + { + "id": 37, + "first_name": "Schuyler", + "last_name": "Rewcassell", + "email": "srewcassell10@phoca.cz" + }, + { + "id": 38, + "first_name": "Garald", + "last_name": "Thoumas", + "email": "gthoumas11@upenn.edu" + }, + { + "id": 40, + "first_name": "Trudy", + "last_name": "Scarratt", + "email": "tscarratt13@baidu.com" + }, + { + "id": 43, + "first_name": "Ariel", + "last_name": "Brunroth", + "email": "abrunroth16@tinyurl.com" + }, + { + "id": 44, + "first_name": "Fonz", + "last_name": "Duigan", + "email": "fduigan17@issuu.com" + }, + { + "id": 45, + "first_name": "Adeline", + "last_name": "Cashell", + "email": "acashell18@house.gov" + }, + { + "id": 46, + "first_name": "Kurt", + "last_name": "Brittle", + "email": "kbrittle19@mysql.com" + }, + { + "id": 47, + "first_name": "Ginni", + "last_name": "Richardes", + "email": "grichardes1a@phpbb.com" + }, + { + "id": 48, + "first_name": "Christina", + "last_name": "Wheway", + "email": "cwheway1b@wisc.edu" + }, + { + "id": 49, + "first_name": "Erasmus", + "last_name": "Vickors", + "email": "evickors1c@dell.com" + }, + { + "id": 50, + "first_name": "Lillian", + "last_name": "Valentin", + "email": "lvalentin1d@usa.gov" + }, + { + "id": 51, + "first_name": "Rozalie", + "last_name": "Abel", + "email": "rabel1e@walmart.com" + }, + { + "id": 53, + "first_name": "Mendel", + "last_name": "Meaddowcroft", + "email": "mmeaddowcroft1g@csmonitor.com" + }, + { + "id": 55, + "first_name": "Danica", + "last_name": "Kenrack", + "email": "dkenrack1i@nhs.uk" + }, + { + "id": 56, + "first_name": "Reuben", + "last_name": "De Benedictis", + "email": "rdebenedictis1j@cnbc.com" + }, + { + "id": 57, + "first_name": "Larine", + "last_name": "Woffenden", + "email": "lwoffenden1k@goo.ne.jp" + }, + { + "id": 59, + "first_name": "Frank", + "last_name": "Cominello", + "email": "fcominello1m@phpbb.com" + }, + { + "id": 61, + "first_name": "Veda", + "last_name": "Pryn", + "email": "vpryn1o@squidoo.com" + }, + { + "id": 62, + "first_name": "Heddie", + "last_name": "Tinston", + "email": "htinston1p@is.gd" + }, + { + "id": 63, + "first_name": "Lorelle", + "last_name": "Radbone", + "email": "lradbone1q@usgs.gov" + }, + { + "id": 64, + "first_name": "Gustavo", + "last_name": "Jans", + "email": "gjans1r@microsoft.com" + }, + { + "id": 65, + "first_name": "Karita", + "last_name": "Beeching", + "email": "kbeeching1s@skype.com" + }, + { + "id": 66, + "first_name": "Damian", + "last_name": "Bellhanger", + "email": "dbellhanger1t@bbb.org" + }, + { + "id": 67, + "first_name": "Kinna", + "last_name": "Cotherill", + "email": "kcotherill1u@angelfire.com" + }, + { + "id": 68, + "first_name": "Janeva", + "last_name": "Varndall", + "email": "jvarndall1v@vk.com" + }, + { + "id": 70, + "first_name": "Bourke", + "last_name": "Cossum", + "email": "bcossum1x@chron.com" + }, + { + "id": 71, + "first_name": "Berk", + "last_name": "Tomasino", + "email": "btomasino1y@fotki.com" + }, + { + "id": 72, + "first_name": "Shepherd", + "last_name": "Lyness", + "email": "slyness1z@csmonitor.com" + }, + { + "id": 73, + "first_name": "Christoph", + "last_name": "Warrener", + "email": "cwarrener20@gmpg.org" + }, + { + "id": 74, + "first_name": "Artus", + "last_name": "Bantock", + "email": "abantock21@who.int" + }, + { + "id": 75, + "first_name": "Bryana", + "last_name": "Mixer", + "email": "bmixer22@nih.gov" + }, + { + "id": 76, + "first_name": "Blithe", + "last_name": "Brigstock", + "email": "bbrigstock23@goodreads.com" + }, + { + "id": 77, + "first_name": "Krispin", + "last_name": "Gothrup", + "email": "kgothrup24@tuttocitta.it" + }, + { + "id": 78, + "first_name": "Helen-elizabeth", + "last_name": "Hardinge", + "email": "hhardinge25@indiatimes.com" + }, + { + "id": 79, + "first_name": "Zachariah", + "last_name": "Burberye", + "email": "zburberye26@va.gov" + }, + { + "id": 81, + "first_name": "Mozes", + "last_name": "Mityushin", + "email": "mmityushin28@vk.com" + }, + { + "id": 82, + "first_name": "Hyacinthie", + "last_name": "Stirrip", + "email": "hstirrip29@mozilla.com" + }, + { + "id": 83, + "first_name": "Hestia", + "last_name": "Full", + "email": "hfull2a@cornell.edu" + }, + { + "id": 84, + "first_name": "Betty", + "last_name": "Doogan", + "email": "bdoogan2b@paginegialle.it" + }, + { + "id": 86, + "first_name": "Ulrick", + "last_name": "Nowakowska", + "email": "unowakowska2d@newyorker.com" + }, + { + "id": 87, + "first_name": "Susanne", + "last_name": "Bannell", + "email": "sbannell2e@house.gov" + }, + { + "id": 88, + "first_name": "Carlotta", + "last_name": "de Bullion", + "email": "cdebullion2f@wikispaces.com" + }, + { + "id": 89, + "first_name": "Conny", + "last_name": "Rodgerson", + "email": "crodgerson2g@nsw.gov.au" + }, + { + "id": 90, + "first_name": "Anthony", + "last_name": "Stovine", + "email": "astovine2h@trellian.com" + }, + { + "id": 91, + "first_name": "Trula", + "last_name": "Mangenot", + "email": "tmangenot2i@example.com" + }, + { + "id": 92, + "first_name": "Urbain", + "last_name": "Ogglebie", + "email": "uogglebie2j@wix.com" + }, + { + "id": 93, + "first_name": "Robena", + "last_name": "Yve", + "email": "ryve2k@sciencedaily.com" + }, + { + "id": 94, + "first_name": "Axel", + "last_name": "McTrustam", + "email": "amctrustam2l@ucoz.ru" + }, + { + "id": 95, + "first_name": "Link", + "last_name": "Klagges", + "email": "lklagges2m@foxnews.com" + }, + { + "id": 96, + "first_name": "Yoko", + "last_name": "Percifer", + "email": "ypercifer2n@indiegogo.com" + }, + { + "id": 97, + "first_name": "Rheba", + "last_name": "Heaford", + "email": "rheaford2o@ed.gov" + }, + { + "id": 98, + "first_name": "Dorolisa", + "last_name": "Seabert", + "email": "dseabert2p@pen.io" + }, + { + "id": 99, + "first_name": "Asher", + "last_name": "Ffrench", + "email": "affrench2q@miitbeian.gov.cn" + }, + { + "id": 100, + "first_name": "Inga", + "last_name": "Skeen", + "email": "iskeen2r@moonfruit.com" + }, + { + "id": 101, + "first_name": "Dov", + "last_name": "Nevinson", + "email": "dnevinson2s@dedecms.com" + }, + { + "id": 103, + "first_name": "Prudence", + "last_name": "Bysshe", + "email": "pbysshe2u@auda.org.au" + }, + { + "id": 104, + "first_name": "Desdemona", + "last_name": "Belverstone", + "email": "dbelverstone2v@ihg.com" + }, + { + "id": 105, + "first_name": "Adiana", + "last_name": "Arnott", + "email": "aarnott2w@icio.us" + }, + { + "id": 106, + "first_name": "Christoper", + "last_name": "Sutter", + "email": "csutter2x@jimdo.com" + }, + { + "id": 107, + "first_name": "Guinevere", + "last_name": "Morton", + "email": "gmorton2y@drupal.org" + }, + { + "id": 109, + "first_name": "Reynold", + "last_name": "Frigout", + "email": "rfrigout30@digg.com" + }, + { + "id": 110, + "first_name": "Beverlie", + "last_name": "Macek", + "email": "bmacek31@yahoo.com" + }, + { + "id": 112, + "first_name": "Romola", + "last_name": "Penna", + "email": "rpenna33@devhub.com" + }, + { + "id": 114, + "first_name": "Allen", + "last_name": "Lawey", + "email": "alawey35@marketwatch.com" + }, + { + "id": 115, + "first_name": "Yard", + "last_name": "Solon", + "email": "ysolon36@fotki.com" + }, + { + "id": 116, + "first_name": "Hubert", + "last_name": "Life", + "email": "hlife37@w3.org" + }, + { + "id": 117, + "first_name": "Patin", + "last_name": "Prestner", + "email": "pprestner38@flavors.me" + }, + { + "id": 118, + "first_name": "Hedda", + "last_name": "MacKniely", + "email": "hmackniely39@blog.com" + }, + { + "id": 119, + "first_name": "Rube", + "last_name": "Ceyssen", + "email": "rceyssen3a@businesswire.com" + }, + { + "id": 120, + "first_name": "Clementine", + "last_name": "ffrench Beytagh", + "email": "cffrenchbeytagh3b@surveymonkey.com" + }, + { + "id": 122, + "first_name": "Standford", + "last_name": "McGurn", + "email": "smcgurn3d@1688.com" + }, + { + "id": 123, + "first_name": "Nelie", + "last_name": "Grebert", + "email": "ngrebert3e@bluehost.com" + }, + { + "id": 124, + "first_name": "Milly", + "last_name": "Danielsohn", + "email": "mdanielsohn3f@topsy.com" + }, + { + "id": 125, + "first_name": "Debor", + "last_name": "Pighills", + "email": "dpighills3g@google.es" + }, + { + "id": 126, + "first_name": "Gal", + "last_name": "Allebone", + "email": "gallebone3h@amazonaws.com" + }, + { + "id": 127, + "first_name": "Kerwinn", + "last_name": "Gheorghescu", + "email": "kgheorghescu3i@aol.com" + }, + { + "id": 128, + "first_name": "Essa", + "last_name": "Fifield", + "email": "efifield3j@bbb.org" + }, + { + "id": 129, + "first_name": "Philippine", + "last_name": "Daens", + "email": "pdaens3k@boston.com" + }, + { + "id": 130, + "first_name": "Timmy", + "last_name": "Colbeck", + "email": "tcolbeck3l@answers.com" + }, + { + "id": 131, + "first_name": "Raffarty", + "last_name": "Liverock", + "email": "rliverock3m@bandcamp.com" + }, + { + "id": 132, + "first_name": "Valeria", + "last_name": "Marqyes", + "email": "vmarqyes3n@1688.com" + }, + { + "id": 133, + "first_name": "Neille", + "last_name": "Seiler", + "email": "nseiler3o@skyrock.com" + }, + { + "id": 134, + "first_name": "Anet", + "last_name": "Kelle", + "email": "akelle3p@opensource.org" + }, + { + "id": 135, + "first_name": "Barnebas", + "last_name": "Alleway", + "email": "balleway3q@goodreads.com" + }, + { + "id": 136, + "first_name": "Valeria", + "last_name": "Chrippes", + "email": "vchrippes3r@wunderground.com" + }, + { + "id": 137, + "first_name": "Duffy", + "last_name": "Hainge", + "email": "dhainge3s@posterous.com" + }, + { + "id": 138, + "first_name": "Gweneth", + "last_name": "Silberschatz", + "email": "gsilberschatz3t@bing.com" + }, + { + "id": 139, + "first_name": "Fredelia", + "last_name": "Stodd", + "email": "fstodd3u@mit.edu" + }, + { + "id": 140, + "first_name": "Buckie", + "last_name": "MacGinley", + "email": "bmacginley3v@irs.gov" + }, + { + "id": 141, + "first_name": "Ardyth", + "last_name": "Ewbanck", + "email": "aewbanck3w@cisco.com" + }, + { + "id": 142, + "first_name": "Darice", + "last_name": "Martinolli", + "email": "dmartinolli3x@seesaa.net" + }, + { + "id": 143, + "first_name": "Bucky", + "last_name": "Chivrall", + "email": "bchivrall3y@blogger.com" + }, + { + "id": 144, + "first_name": "Freida", + "last_name": "Labrom", + "email": "flabrom3z@github.io" + }, + { + "id": 145, + "first_name": "Huntlee", + "last_name": "Comelini", + "email": "hcomelini40@army.mil" + }, + { + "id": 146, + "first_name": "Lester", + "last_name": "Farrah", + "email": "lfarrah41@princeton.edu" + }, + { + "id": 147, + "first_name": "Chickie", + "last_name": "Lyddon", + "email": "clyddon42@smh.com.au" + }, + { + "id": 148, + "first_name": "Lenette", + "last_name": "McGaffey", + "email": "lmcgaffey43@auda.org.au" + }, + { + "id": 149, + "first_name": "Cleavland", + "last_name": "Balassa", + "email": "cbalassa44@squidoo.com" + }, + { + "id": 150, + "first_name": "Towney", + "last_name": "Wessell", + "email": "twessell45@bloglovin.com" + }, + { + "id": 151, + "first_name": "Marlee", + "last_name": "Sahlstrom", + "email": "msahlstrom46@51.la" + }, + { + "id": 152, + "first_name": "Tadd", + "last_name": "Showalter", + "email": "tshowalter47@irs.gov" + }, + { + "id": 153, + "first_name": "Isabeau", + "last_name": "Smalcombe", + "email": "ismalcombe48@goo.gl" + }, + { + "id": 154, + "first_name": "Aprilette", + "last_name": "Pyett", + "email": "apyett49@taobao.com" + }, + { + "id": 155, + "first_name": "Bendite", + "last_name": "Odney", + "email": "bodney4a@123-reg.co.uk" + }, + { + "id": 156, + "first_name": "Temp", + "last_name": "Scherer", + "email": "tscherer4b@phoca.cz" + }, + { + "id": 157, + "first_name": "Barris", + "last_name": "Ferrarotti", + "email": "bferrarotti4c@bloomberg.com" + }, + { + "id": 158, + "first_name": "Floris", + "last_name": "Loudiane", + "email": "floudiane4d@bbb.org" + }, + { + "id": 159, + "first_name": "Ives", + "last_name": "MacArdle", + "email": "imacardle4e@phoca.cz" + }, + { + "id": 160, + "first_name": "Briano", + "last_name": "Antonsen", + "email": "bantonsen4f@jimdo.com" + }, + { + "id": 161, + "first_name": "Rea", + "last_name": "McCumskay", + "email": "rmccumskay4g@netvibes.com" + }, + { + "id": 162, + "first_name": "Madlen", + "last_name": "Karppi", + "email": "mkarppi4h@prweb.com" + }, + { + "id": 163, + "first_name": "Hillie", + "last_name": "Ollerenshaw", + "email": "hollerenshaw4i@mapquest.com" + }, + { + "id": 164, + "first_name": "Laure", + "last_name": "Giacomazzo", + "email": "lgiacomazzo4j@canalblog.com" + }, + { + "id": 165, + "first_name": "Shanie", + "last_name": "Worsam", + "email": "sworsam4k@google.ca" + }, + { + "id": 166, + "first_name": "Bibbie", + "last_name": "Trosdall", + "email": "btrosdall4l@list-manage.com" + }, + { + "id": 167, + "first_name": "Marcelia", + "last_name": "Symes", + "email": "msymes4m@facebook.com" + }, + { + "id": 168, + "first_name": "Jolene", + "last_name": "Roja", + "email": "jroja4n@mail.ru" + }, + { + "id": 169, + "first_name": "Colas", + "last_name": "Leal", + "email": "cleal4o@ustream.tv" + }, + { + "id": 170, + "first_name": "Oby", + "last_name": "Faichnie", + "email": "ofaichnie4p@goo.gl" + }, + { + "id": 171, + "first_name": "Henry", + "last_name": "Willows", + "email": "hwillows4q@alibaba.com" + }, + { + "id": 172, + "first_name": "Matilda", + "last_name": "Korf", + "email": "mkorf4r@bbb.org" + }, + { + "id": 173, + "first_name": "Hiram", + "last_name": "Balls", + "email": "hballs4s@nba.com" + }, + { + "id": 174, + "first_name": "Terri-jo", + "last_name": "Atrill", + "email": "tatrill4t@so-net.ne.jp" + }, + { + "id": 175, + "first_name": "Tana", + "last_name": "Ciccarello", + "email": "tciccarello4u@cisco.com" + }, + { + "id": 176, + "first_name": "Abbie", + "last_name": "Rohfsen", + "email": "arohfsen4v@japanpost.jp" + }, + { + "id": 177, + "first_name": "Dominga", + "last_name": "Johanssen", + "email": "djohanssen4w@phoca.cz" + }, + { + "id": 178, + "first_name": "Osmond", + "last_name": "Ryland", + "email": "oryland4x@51.la" + }, + { + "id": 179, + "first_name": "Waverly", + "last_name": "Butting", + "email": "wbutting4y@4shared.com" + }, + { + "id": 180, + "first_name": "Colin", + "last_name": "Antosch", + "email": "cantosch4z@unblog.fr" + }, + { + "id": 181, + "first_name": "Filide", + "last_name": "Birks", + "email": "fbirks50@domainmarket.com" + }, + { + "id": 182, + "first_name": "Avery", + "last_name": "Kruschov", + "email": "akruschov51@answers.com" + }, + { + "id": 183, + "first_name": "Doralynne", + "last_name": "Mosten", + "email": "dmosten52@discovery.com" + }, + { + "id": 184, + "first_name": "Dicky", + "last_name": "Muggeridge", + "email": "dmuggeridge53@163.com" + }, + { + "id": 185, + "first_name": "Tucky", + "last_name": "Tennick", + "email": "ttennick54@gnu.org" + }, + { + "id": 186, + "first_name": "Jareb", + "last_name": "Hews", + "email": "jhews55@twitpic.com" + }, + { + "id": 187, + "first_name": "Guthrie", + "last_name": "Castro", + "email": "gcastro56@berkeley.edu" + }, + { + "id": 188, + "first_name": "Rayna", + "last_name": "Howett", + "email": "rhowett57@theguardian.com" + }, + { + "id": 189, + "first_name": "Linnet", + "last_name": "Painter", + "email": "lpainter58@apache.org" + }, + { + "id": 190, + "first_name": "Gisele", + "last_name": "Varcoe", + "email": "gvarcoe59@timesonline.co.uk" + }, + { + "id": 191, + "first_name": "Isabelita", + "last_name": "Klimpke", + "email": "iklimpke5a@technorati.com" + }, + { + "id": 192, + "first_name": "Ofilia", + "last_name": "Kondrachenko", + "email": "okondrachenko5b@github.com" + }, + { + "id": 193, + "first_name": "Margaretta", + "last_name": "Costello", + "email": "mcostello5c@nifty.com" + }, + { + "id": 194, + "first_name": "Elli", + "last_name": "Sudell", + "email": "esudell5d@stumbleupon.com" + }, + { + "id": 195, + "first_name": "Carie", + "last_name": "Preto", + "email": "cpreto5e@prnewswire.com" + }, + { + "id": 196, + "first_name": "Kinny", + "last_name": "Gredden", + "email": "kgredden5f@alexa.com" + }, + { + "id": 197, + "first_name": "Grethel", + "last_name": "Warwicker", + "email": "gwarwicker5g@fda.gov" + }, + { + "id": 198, + "first_name": "Gerti", + "last_name": "Kippling", + "email": "gkippling5h@mayoclinic.com" + }, + { + "id": 199, + "first_name": "Nanice", + "last_name": "Sirrell", + "email": "nsirrell5i@paginegialle.it" + }, + { + "id": 200, + "first_name": "Coraline", + "last_name": "Readie", + "email": "creadie5j@joomla.org" + }, + { + "id": 202, + "first_name": "Heath", + "last_name": "McNeilley", + "email": "hmcneilley5l@soundcloud.com" + }, + { + "id": 203, + "first_name": "Torey", + "last_name": "Lortz", + "email": "tlortz5m@printfriendly.com" + }, + { + "id": 204, + "first_name": "Melodee", + "last_name": "McWhan", + "email": "mmcwhan5n@tinyurl.com" + }, + { + "id": 205, + "first_name": "Selie", + "last_name": "Llywarch", + "email": "sllywarch5o@usda.gov" + }, + { + "id": 206, + "first_name": "Britt", + "last_name": "Shore", + "email": "bshore5p@unesco.org" + }, + { + "id": 207, + "first_name": "Floyd", + "last_name": "Hairyes", + "email": "fhairyes5q@sphinn.com" + }, + { + "id": 208, + "first_name": "Maris", + "last_name": "Fretter", + "email": "mfretter5r@umich.edu" + }, + { + "id": 209, + "first_name": "Andrey", + "last_name": "MacCaughey", + "email": "amaccaughey5s@blinklist.com" + }, + { + "id": 210, + "first_name": "Reena", + "last_name": "Kiledal", + "email": "rkiledal5t@blogs.com" + }, + { + "id": 211, + "first_name": "Adey", + "last_name": "Molohan", + "email": "amolohan5u@yale.edu" + }, + { + "id": 212, + "first_name": "Eddie", + "last_name": "Simner", + "email": "esimner5v@purevolume.com" + }, + { + "id": 213, + "first_name": "Eldon", + "last_name": "Dregan", + "email": "edregan5w@nytimes.com" + }, + { + "id": 214, + "first_name": "Terencio", + "last_name": "Cordell", + "email": "tcordell5x@answers.com" + }, + { + "id": 215, + "first_name": "Barbra", + "last_name": "Matzkaitis", + "email": "bmatzkaitis5y@nyu.edu" + }, + { + "id": 216, + "first_name": "Agathe", + "last_name": "Filler", + "email": "afiller5z@etsy.com" + }, + { + "id": 217, + "first_name": "Jenine", + "last_name": "Adds", + "email": "jadds60@squarespace.com" + }, + { + "id": 218, + "first_name": "Kathy", + "last_name": "Lampbrecht", + "email": "klampbrecht61@t-online.de" + }, + { + "id": 219, + "first_name": "Demetri", + "last_name": "Godfroy", + "email": "dgodfroy62@ibm.com" + }, + { + "id": 220, + "first_name": "Katuscha", + "last_name": "Renon", + "email": "krenon63@friendfeed.com" + }, + { + "id": 221, + "first_name": "Claudian", + "last_name": "Devenport", + "email": "cdevenport64@godaddy.com" + }, + { + "id": 222, + "first_name": "Jenica", + "last_name": "Kornilov", + "email": "jkornilov65@nifty.com" + }, + { + "id": 223, + "first_name": "Grissel", + "last_name": "McMeeking", + "email": "gmcmeeking66@boston.com" + }, + { + "id": 224, + "first_name": "Andy", + "last_name": "Rushforth", + "email": "arushforth67@pcworld.com" + }, + { + "id": 225, + "first_name": "Rana", + "last_name": "Ferrino", + "email": "rferrino68@deviantart.com" + }, + { + "id": 226, + "first_name": "Celie", + "last_name": "Schenkel", + "email": "cschenkel69@cargocollective.com" + }, + { + "id": 227, + "first_name": "Doe", + "last_name": "Chadwyck", + "email": "dchadwyck6a@cargocollective.com" + }, + { + "id": 228, + "first_name": "Amandy", + "last_name": "Marmon", + "email": "amarmon6b@de.vu" + }, + { + "id": 229, + "first_name": "Aliza", + "last_name": "Haggeth", + "email": "ahaggeth6c@ibm.com" + }, + { + "id": 230, + "first_name": "Velma", + "last_name": "Olner", + "email": "volner6d@house.gov" + }, + { + "id": 231, + "first_name": "Bent", + "last_name": "Ayllett", + "email": "bayllett6e@google.com.hk" + }, + { + "id": 232, + "first_name": "Parrnell", + "last_name": "Walker", + "email": "pwalker6f@irs.gov" + }, + { + "id": 233, + "first_name": "Mickie", + "last_name": "Nossent", + "email": "mnossent6g@elegantthemes.com" + }, + { + "id": 234, + "first_name": "Sawyer", + "last_name": "Tranter", + "email": "stranter6h@usa.gov" + }, + { + "id": 235, + "first_name": "Bernetta", + "last_name": "Twine", + "email": "btwine6i@mapy.cz" + }, + { + "id": 236, + "first_name": "Richard", + "last_name": "Kerss", + "email": "rkerss6j@harvard.edu" + }, + { + "id": 237, + "first_name": "Margo", + "last_name": "Danilov", + "email": "mdanilov6k@clickbank.net" + }, + { + "id": 238, + "first_name": "Willem", + "last_name": "Sheen", + "email": "wsheen6l@feedburner.com" + }, + { + "id": 239, + "first_name": "Dukey", + "last_name": "Regnard", + "email": "dregnard6m@opensource.org" + }, + { + "id": 240, + "first_name": "Wynnie", + "last_name": "Manilove", + "email": "wmanilove6n@themeforest.net" + }, + { + "id": 241, + "first_name": "Hartley", + "last_name": "Tungay", + "email": "htungay6o@rambler.ru" + }, + { + "id": 242, + "first_name": "Brena", + "last_name": "Yaneev", + "email": "byaneev6p@java.com" + }, + { + "id": 243, + "first_name": "Fred", + "last_name": "Leaf", + "email": "fleaf6q@usatoday.com" + }, + { + "id": 244, + "first_name": "Morna", + "last_name": "Beardsworth", + "email": "mbeardsworth6r@kickstarter.com" + }, + { + "id": 245, + "first_name": "Griffin", + "last_name": "Kell", + "email": "gkell6s@github.io" + }, + { + "id": 246, + "first_name": "Ozzie", + "last_name": "Picford", + "email": "opicford6t@instagram.com" + }, + { + "id": 247, + "first_name": "Carson", + "last_name": "Andrivot", + "email": "candrivot6u@1und1.de" + }, + { + "id": 248, + "first_name": "Caressa", + "last_name": "Kupis", + "email": "ckupis6v@sakura.ne.jp" + }, + { + "id": 249, + "first_name": "Philly", + "last_name": "Knowlys", + "email": "pknowlys6w@army.mil" + }, + { + "id": 250, + "first_name": "Nady", + "last_name": "Rolling", + "email": "nrolling6x@lulu.com" + }, + { + "id": 251, + "first_name": "Albertina", + "last_name": "Spurdle", + "email": "aspurdle6y@pinterest.com" + }, + { + "id": 252, + "first_name": "Brittne", + "last_name": "Tamlett", + "email": "btamlett6z@princeton.edu" + }, + { + "id": 253, + "first_name": "Biddy", + "last_name": "Station", + "email": "bstation70@altervista.org" + }, + { + "id": 254, + "first_name": "Estelle", + "last_name": "Swaden", + "email": "eswaden71@nih.gov" + }, + { + "id": 255, + "first_name": "Dael", + "last_name": "Noyce", + "email": "dnoyce72@europa.eu" + }, + { + "id": 256, + "first_name": "Verne", + "last_name": "Tomasi", + "email": "vtomasi73@pinterest.com" + }, + { + "id": 257, + "first_name": "Toinette", + "last_name": "Adame", + "email": "tadame74@csmonitor.com" + }, + { + "id": 258, + "first_name": "Teri", + "last_name": "Karolewski", + "email": "tkarolewski75@exblog.jp" + }, + { + "id": 259, + "first_name": "Theo", + "last_name": "Weildish", + "email": "tweildish76@hexun.com" + }, + { + "id": 260, + "first_name": "Eloise", + "last_name": "McLafferty", + "email": "emclafferty77@prnewswire.com" + }, + { + "id": 261, + "first_name": "Pepita", + "last_name": "Fontel", + "email": "pfontel78@chronoengine.com" + }, + { + "id": 262, + "first_name": "Valentine", + "last_name": "Gerry", + "email": "vgerry79@slashdot.org" + }, + { + "id": 263, + "first_name": "Fanni", + "last_name": "Goodbairn", + "email": "fgoodbairn7a@elpais.com" + }, + { + "id": 264, + "first_name": "Esra", + "last_name": "Troppmann", + "email": "etroppmann7b@phoca.cz" + }, + { + "id": 265, + "first_name": "Carlynn", + "last_name": "Vorley", + "email": "cvorley7c@uiuc.edu" + }, + { + "id": 266, + "first_name": "Mellie", + "last_name": "McLeoid", + "email": "mmcleoid7d@mit.edu" + }, + { + "id": 267, + "first_name": "Gaile", + "last_name": "Bucke", + "email": "gbucke7e@alexa.com" + }, + { + "id": 268, + "first_name": "Sherye", + "last_name": "Sheahan", + "email": "ssheahan7f@parallels.com" + }, + { + "id": 269, + "first_name": "Norean", + "last_name": "Desport", + "email": "ndesport7g@taobao.com" + }, + { + "id": 270, + "first_name": "Ugo", + "last_name": "Kernell", + "email": "ukernell7h@csmonitor.com" + }, + { + "id": 271, + "first_name": "Base", + "last_name": "Cullinane", + "email": "bcullinane7i@indiegogo.com" + }, + { + "id": 272, + "first_name": "Alan", + "last_name": "Godmar", + "email": "agodmar7j@fc2.com" + }, + { + "id": 273, + "first_name": "Olly", + "last_name": "Wellstood", + "email": "owellstood7k@friendfeed.com" + }, + { + "id": 274, + "first_name": "Reuben", + "last_name": "Aveyard", + "email": "raveyard7l@lycos.com" + }, + { + "id": 275, + "first_name": "Richmond", + "last_name": "Broadberrie", + "email": "rbroadberrie7m@facebook.com" + }, + { + "id": 276, + "first_name": "Maressa", + "last_name": "Carlett", + "email": "mcarlett7n@bizjournals.com" + }, + { + "id": 277, + "first_name": "Marina", + "last_name": "Sprasen", + "email": "msprasen7o@dropbox.com" + }, + { + "id": 278, + "first_name": "Winnie", + "last_name": "Ostridge", + "email": "wostridge7p@ted.com" + }, + { + "id": 279, + "first_name": "Briney", + "last_name": "Rosenschein", + "email": "brosenschein7q@macromedia.com" + }, + { + "id": 280, + "first_name": "Heidie", + "last_name": "Yeldon", + "email": "hyeldon7r@parallels.com" + }, + { + "id": 281, + "first_name": "Addie", + "last_name": "Coldicott", + "email": "acoldicott7s@eventbrite.com" + }, + { + "id": 282, + "first_name": "Aubrette", + "last_name": "Doswell", + "email": "adoswell7t@imgur.com" + }, + { + "id": 283, + "first_name": "Rouvin", + "last_name": "Kassman", + "email": "rkassman7u@tinyurl.com" + }, + { + "id": 284, + "first_name": "Mitchell", + "last_name": "Pietzke", + "email": "mpietzke7v@wunderground.com" + }, + { + "id": 285, + "first_name": "Eadmund", + "last_name": "Rawstron", + "email": "erawstron7w@yelp.com" + }, + { + "id": 286, + "first_name": "Corri", + "last_name": "Matyasik", + "email": "cmatyasik7x@weibo.com" + }, + { + "id": 287, + "first_name": "Chuck", + "last_name": "Blandamere", + "email": "cblandamere7y@google.cn" + }, + { + "id": 288, + "first_name": "Nari", + "last_name": "Edmondson", + "email": "nedmondson7z@cnbc.com" + }, + { + "id": 289, + "first_name": "Valentine", + "last_name": "Sivyour", + "email": "vsivyour80@reverbnation.com" + }, + { + "id": 290, + "first_name": "Darryl", + "last_name": "Hawket", + "email": "dhawket81@pcworld.com" + }, + { + "id": 291, + "first_name": "Teddie", + "last_name": "Prosek", + "email": "tprosek82@odnoklassniki.ru" + }, + { + "id": 292, + "first_name": "Chloris", + "last_name": "Linder", + "email": "clinder83@instagram.com" + }, + { + "id": 293, + "first_name": "Lonnie", + "last_name": "Glede", + "email": "lglede84@taobao.com" + }, + { + "id": 294, + "first_name": "Denise", + "last_name": "Deakin", + "email": "ddeakin85@si.edu" + }, + { + "id": 295, + "first_name": "Ruprecht", + "last_name": "Sandcroft", + "email": "rsandcroft86@abc.net.au" + }, + { + "id": 296, + "first_name": "Zorah", + "last_name": "Patridge", + "email": "zpatridge87@freewebs.com" + }, + { + "id": 297, + "first_name": "Abigail", + "last_name": "Chatain", + "email": "achatain88@google.cn" + }, + { + "id": 298, + "first_name": "Neala", + "last_name": "Osichev", + "email": "nosichev89@hostgator.com" + }, + { + "id": 299, + "first_name": "Debora", + "last_name": "Crocetto", + "email": "dcrocetto8a@1und1.de" + }, + { + "id": 300, + "first_name": "Dall", + "last_name": "Labeuil", + "email": "dlabeuil8b@ucsd.edu" + }, + { + "id": 301, + "first_name": "Bunny", + "last_name": "McSperrin", + "email": "bmcsperrin8c@hubpages.com" + }, + { + "id": 302, + "first_name": "Marianne", + "last_name": "Sabbin", + "email": "msabbin8d@hc360.com" + }, + { + "id": 303, + "first_name": "Anette", + "last_name": "Wickersley", + "email": "awickersley8e@businesswire.com" + }, + { + "id": 304, + "first_name": "Millisent", + "last_name": "Heinemann", + "email": "mheinemann8f@ameblo.jp" + }, + { + "id": 305, + "first_name": "Kala", + "last_name": "Attock", + "email": "kattock8g@theatlantic.com" + }, + { + "id": 306, + "first_name": "Meriel", + "last_name": "Vasyukhin", + "email": "mvasyukhin8h@linkedin.com" + }, + { + "id": 307, + "first_name": "Julianne", + "last_name": "Baudou", + "email": "jbaudou8i@cbc.ca" + }, + { + "id": 308, + "first_name": "Pryce", + "last_name": "Landal", + "email": "plandal8j@myspace.com" + }, + { + "id": 309, + "first_name": "Nerissa", + "last_name": "Dreghorn", + "email": "ndreghorn8k@usda.gov" + }, + { + "id": 310, + "first_name": "Clyve", + "last_name": "Soldner", + "email": "csoldner8l@bluehost.com" + }, + { + "id": 311, + "first_name": "Hadlee", + "last_name": "Syplus", + "email": "hsyplus8m@auda.org.au" + }, + { + "id": 312, + "first_name": "Prudi", + "last_name": "Merkel", + "email": "pmerkel8n@hao123.com" + }, + { + "id": 313, + "first_name": "Zebulon", + "last_name": "Denisyev", + "email": "zdenisyev8o@parallels.com" + }, + { + "id": 314, + "first_name": "Leopold", + "last_name": "Laddle", + "email": "lladdle8p@si.edu" + }, + { + "id": 315, + "first_name": "Danella", + "last_name": "Aymes", + "email": "daymes8q@earthlink.net" + }, + { + "id": 316, + "first_name": "Murdock", + "last_name": "De Ath", + "email": "mdeath8r@naver.com" + }, + { + "id": 317, + "first_name": "Umeko", + "last_name": "Feavearyear", + "email": "ufeavearyear8s@youtube.com" + }, + { + "id": 318, + "first_name": "Valli", + "last_name": "Neary", + "email": "vneary8t@vimeo.com" + }, + { + "id": 319, + "first_name": "Kendell", + "last_name": "Blaby", + "email": "kblaby8u@tuttocitta.it" + }, + { + "id": 320, + "first_name": "Ahmad", + "last_name": "Tate", + "email": "atate8v@friendfeed.com" + }, + { + "id": 321, + "first_name": "Cullan", + "last_name": "Christofol", + "email": "cchristofol8w@hao123.com" + }, + { + "id": 322, + "first_name": "Nolan", + "last_name": "Betser", + "email": "nbetser8x@github.io" + }, + { + "id": 323, + "first_name": "Vachel", + "last_name": "Burrage", + "email": "vburrage8y@cargocollective.com" + }, + { + "id": 324, + "first_name": "Gigi", + "last_name": "McCaughran", + "email": "gmccaughran8z@fda.gov" + }, + { + "id": 325, + "first_name": "Leanora", + "last_name": "Epple", + "email": "lepple90@stanford.edu" + }, + { + "id": 326, + "first_name": "Kathi", + "last_name": "Yearnes", + "email": "kyearnes91@sfgate.com" + }, + { + "id": 327, + "first_name": "Tadio", + "last_name": "Salleir", + "email": "tsalleir92@a8.net" + }, + { + "id": 328, + "first_name": "Kev", + "last_name": "Mayell", + "email": "kmayell93@weibo.com" + }, + { + "id": 329, + "first_name": "Clarissa", + "last_name": "Hartley", + "email": "chartley94@networksolutions.com" + }, + { + "id": 330, + "first_name": "Cindie", + "last_name": "Skyme", + "email": "cskyme95@etsy.com" + }, + { + "id": 331, + "first_name": "Timoteo", + "last_name": "Wieprecht", + "email": "twieprecht96@dion.ne.jp" + }, + { + "id": 332, + "first_name": "Aloise", + "last_name": "Parres", + "email": "aparres97@infoseek.co.jp" + }, + { + "id": 333, + "first_name": "Ezri", + "last_name": "Jacobsen", + "email": "ejacobsen98@google.de" + }, + { + "id": 334, + "first_name": "Lewie", + "last_name": "Ambroz", + "email": "lambroz99@youku.com" + }, + { + "id": 335, + "first_name": "Kerwin", + "last_name": "Ceney", + "email": "kceney9a@comsenz.com" + }, + { + "id": 336, + "first_name": "Frederich", + "last_name": "Crolly", + "email": "fcrolly9b@shareasale.com" + }, + { + "id": 337, + "first_name": "Sayer", + "last_name": "Matanin", + "email": "smatanin9c@newsvine.com" + }, + { + "id": 338, + "first_name": "Jennifer", + "last_name": "Vasyatkin", + "email": "jvasyatkin9d@chronoengine.com" + }, + { + "id": 339, + "first_name": "Nicky", + "last_name": "Heinsh", + "email": "nheinsh9e@technorati.com" + }, + { + "id": 340, + "first_name": "Neda", + "last_name": "Lanon", + "email": "nlanon9f@toplist.cz" + }, + { + "id": 341, + "first_name": "Elbertine", + "last_name": "Larkcum", + "email": "elarkcum9g@a8.net" + }, + { + "id": 342, + "first_name": "Whitby", + "last_name": "Farrell", + "email": "wfarrell9h@dailymail.co.uk" + }, + { + "id": 343, + "first_name": "Dun", + "last_name": "Mackieson", + "email": "dmackieson9i@weebly.com" + }, + { + "id": 344, + "first_name": "Krishna", + "last_name": "Tacon", + "email": "ktacon9j@w3.org" + }, + { + "id": 345, + "first_name": "Dyna", + "last_name": "Sneezum", + "email": "dsneezum9k@sfgate.com" + }, + { + "id": 346, + "first_name": "Gardner", + "last_name": "Habercham", + "email": "ghabercham9l@goodreads.com" + }, + { + "id": 347, + "first_name": "Kalil", + "last_name": "Reinmar", + "email": "kreinmar9m@google.ru" + }, + { + "id": 348, + "first_name": "Karly", + "last_name": "Cribbins", + "email": "kcribbins9n@ustream.tv" + }, + { + "id": 349, + "first_name": "Jeanne", + "last_name": "Easen", + "email": "jeasen9o@time.com" + }, + { + "id": 350, + "first_name": "Yorgo", + "last_name": "de Courcy", + "email": "ydecourcy9p@reference.com" + }, + { + "id": 351, + "first_name": "Dyanna", + "last_name": "Wordesworth", + "email": "dwordesworth9q@clickbank.net" + }, + { + "id": 352, + "first_name": "Ashien", + "last_name": "Whittles", + "email": "awhittles9r@dell.com" + }, + { + "id": 353, + "first_name": "Alia", + "last_name": "Paradin", + "email": "aparadin9s@ucsd.edu" + }, + { + "id": 354, + "first_name": "Babbie", + "last_name": "Palethorpe", + "email": "bpalethorpe9t@sciencedirect.com" + }, + { + "id": 355, + "first_name": "Mort", + "last_name": "Hargie", + "email": "mhargie9u@nyu.edu" + }, + { + "id": 356, + "first_name": "Lucais", + "last_name": "Writer", + "email": "lwriter9v@domainmarket.com" + }, + { + "id": 357, + "first_name": "Lucho", + "last_name": "Robley", + "email": "lrobley9w@cargocollective.com" + }, + { + "id": 358, + "first_name": "Drucie", + "last_name": "Hapgood", + "email": "dhapgood9x@ft.com" + }, + { + "id": 359, + "first_name": "Arin", + "last_name": "Boddy", + "email": "aboddy9y@cdbaby.com" + }, + { + "id": 360, + "first_name": "Biddy", + "last_name": "Ewles", + "email": "bewles9z@globo.com" + }, + { + "id": 361, + "first_name": "Marlon", + "last_name": "Allder", + "email": "malldera0@t.co" + }, + { + "id": 362, + "first_name": "Jock", + "last_name": "Ing", + "email": "jinga1@ocn.ne.jp" + }, + { + "id": 363, + "first_name": "Franny", + "last_name": "Taverner", + "email": "ftavernera2@ezinearticles.com" + }, + { + "id": 364, + "first_name": "Vanda", + "last_name": "Whiterod", + "email": "vwhiteroda3@usda.gov" + }, + { + "id": 365, + "first_name": "Lezlie", + "last_name": "Godbehere", + "email": "lgodbeherea4@youtube.com" + }, + { + "id": 366, + "first_name": "Rebecka", + "last_name": "Scarsbrook", + "email": "rscarsbrooka5@myspace.com" + }, + { + "id": 367, + "first_name": "Abba", + "last_name": "Mingotti", + "email": "amingottia6@tuttocitta.it" + }, + { + "id": 368, + "first_name": "Miguela", + "last_name": "McNysche", + "email": "mmcnyschea7@t-online.de" + }, + { + "id": 369, + "first_name": "Weider", + "last_name": "Rosenau", + "email": "wrosenaua8@mysql.com" + }, + { + "id": 370, + "first_name": "Antonietta", + "last_name": "Littefair", + "email": "alittefaira9@xing.com" + }, + { + "id": 371, + "first_name": "Heda", + "last_name": "Wheowall", + "email": "hwheowallaa@360.cn" + }, + { + "id": 372, + "first_name": "Nettle", + "last_name": "Semonin", + "email": "nsemoninab@patch.com" + }, + { + "id": 373, + "first_name": "Sheri", + "last_name": "Baudry", + "email": "sbaudryac@google.ru" + }, + { + "id": 374, + "first_name": "Janna", + "last_name": "Bogue", + "email": "jboguead@illinois.edu" + }, + { + "id": 375, + "first_name": "Saundra", + "last_name": "Skaid", + "email": "sskaidae@ycombinator.com" + }, + { + "id": 376, + "first_name": "Xenia", + "last_name": "Cadden", + "email": "xcaddenaf@booking.com" + }, + { + "id": 377, + "first_name": "Claudia", + "last_name": "Spirit", + "email": "cspiritag@linkedin.com" + }, + { + "id": 378, + "first_name": "Willard", + "last_name": "Grimwood", + "email": "wgrimwoodah@oracle.com" + }, + { + "id": 379, + "first_name": "Smith", + "last_name": "Allenby", + "email": "sallenbyai@godaddy.com" + }, + { + "id": 380, + "first_name": "Morlee", + "last_name": "Bernardin", + "email": "mbernardinaj@list-manage.com" + }, + { + "id": 381, + "first_name": "Murvyn", + "last_name": "Becom", + "email": "mbecomak@sourceforge.net" + }, + { + "id": 382, + "first_name": "Teriann", + "last_name": "Flori", + "email": "tflorial@xinhuanet.com" + }, + { + "id": 383, + "first_name": "Liva", + "last_name": "Cabera", + "email": "lcaberaam@t.co" + }, + { + "id": 384, + "first_name": "Andrej", + "last_name": "Hearnden", + "email": "ahearndenan@mysql.com" + }, + { + "id": 385, + "first_name": "Reg", + "last_name": "Vollam", + "email": "rvollamao@networkadvertising.org" + }, + { + "id": 386, + "first_name": "Kimberlyn", + "last_name": "Bedells", + "email": "kbedellsap@bbc.co.uk" + }, + { + "id": 387, + "first_name": "Cam", + "last_name": "Quantrill", + "email": "cquantrillaq@alibaba.com" + }, + { + "id": 388, + "first_name": "Giuditta", + "last_name": "Force", + "email": "gforcear@delicious.com" + }, + { + "id": 389, + "first_name": "Lanie", + "last_name": "MacQueen", + "email": "lmacqueenas@flickr.com" + }, + { + "id": 390, + "first_name": "Marja", + "last_name": "O'Dempsey", + "email": "modempseyat@auda.org.au" + }, + { + "id": 391, + "first_name": "Howey", + "last_name": "St Clair", + "email": "hstclairau@soundcloud.com" + }, + { + "id": 392, + "first_name": "Carolynn", + "last_name": "Khotler", + "email": "ckhotlerav@utexas.edu" + }, + { + "id": 393, + "first_name": "Theodosia", + "last_name": "Ort", + "email": "tortaw@shop-pro.jp" + }, + { + "id": 394, + "first_name": "West", + "last_name": "Matchett", + "email": "wmatchettax@cbc.ca" + }, + { + "id": 395, + "first_name": "Vivianne", + "last_name": "Wheelwright", + "email": "vwheelwrightay@washingtonpost.com" + }, + { + "id": 396, + "first_name": "Pennie", + "last_name": "Beames", + "email": "pbeamesaz@drupal.org" + }, + { + "id": 397, + "first_name": "Nady", + "last_name": "Letch", + "email": "nletchb0@skype.com" + }, + { + "id": 398, + "first_name": "Moselle", + "last_name": "Maytum", + "email": "mmaytumb1@usgs.gov" + }, + { + "id": 399, + "first_name": "Jennilee", + "last_name": "Kid", + "email": "jkidb2@wisc.edu" + }, + { + "id": 400, + "first_name": "Parnell", + "last_name": "Gong", + "email": "pgongb3@oaic.gov.au" + }, + { + "id": 401, + "first_name": "Noll", + "last_name": "Kohtler", + "email": "nkohtlerb4@opensource.org" + }, + { + "id": 402, + "first_name": "Sonya", + "last_name": "Orris", + "email": "sorrisb5@bandcamp.com" + }, + { + "id": 403, + "first_name": "Bronnie", + "last_name": "Guillotin", + "email": "bguillotinb6@geocities.jp" + }, + { + "id": 404, + "first_name": "Inger", + "last_name": "Clipsham", + "email": "iclipshamb7@psu.edu" + }, + { + "id": 405, + "first_name": "Melina", + "last_name": "Grigorio", + "email": "mgrigoriob8@eventbrite.com" + }, + { + "id": 406, + "first_name": "Jermain", + "last_name": "Thraves", + "email": "jthravesb9@biblegateway.com" + }, + { + "id": 407, + "first_name": "Jock", + "last_name": "Payn", + "email": "jpaynba@google.cn" + }, + { + "id": 408, + "first_name": "Nikolia", + "last_name": "Sterre", + "email": "nsterrebb@google.ru" + }, + { + "id": 409, + "first_name": "Rosemarie", + "last_name": "Caurah", + "email": "rcaurahbc@sohu.com" + }, + { + "id": 410, + "first_name": "Conney", + "last_name": "Spawell", + "email": "cspawellbd@ycombinator.com" + }, + { + "id": 411, + "first_name": "Hernando", + "last_name": "Percival", + "email": "hpercivalbe@cisco.com" + }, + { + "id": 412, + "first_name": "Michale", + "last_name": "Stadding", + "email": "mstaddingbf@bandcamp.com" + }, + { + "id": 413, + "first_name": "Zulema", + "last_name": "Danks", + "email": "zdanksbg@ovh.net" + }, + { + "id": 414, + "first_name": "Olia", + "last_name": "Joost", + "email": "ojoostbh@soup.io" + }, + { + "id": 416, + "first_name": "Corbet", + "last_name": "Cliff", + "email": "ccliffbj@oaic.gov.au" + }, + { + "id": 417, + "first_name": "Georgetta", + "last_name": "Tinto", + "email": "gtintobk@craigslist.org" + }, + { + "id": 418, + "first_name": "Priscilla", + "last_name": "Philips", + "email": "pphilipsbl@ox.ac.uk" + }, + { + "id": 419, + "first_name": "David", + "last_name": "Mulcock", + "email": "dmulcockbm@nytimes.com" + }, + { + "id": 420, + "first_name": "Agatha", + "last_name": "Hek", + "email": "ahekbn@homestead.com" + }, + { + "id": 421, + "first_name": "Burty", + "last_name": "Ceschini", + "email": "bceschinibo@jimdo.com" + }, + { + "id": 422, + "first_name": "Ange", + "last_name": "Maeer", + "email": "amaeerbp@feedburner.com" + }, + { + "id": 423, + "first_name": "Dannel", + "last_name": "Sackes", + "email": "dsackesbq@pbs.org" + }, + { + "id": 424, + "first_name": "Lorrie", + "last_name": "Entres", + "email": "lentresbr@ebay.co.uk" + }, + { + "id": 425, + "first_name": "Oswell", + "last_name": "Patrick", + "email": "opatrickbs@answers.com" + }, + { + "id": 426, + "first_name": "Nefen", + "last_name": "Sedgefield", + "email": "nsedgefieldbt@google.com.au" + }, + { + "id": 427, + "first_name": "Crichton", + "last_name": "Giorgione", + "email": "cgiorgionebu@va.gov" + }, + { + "id": 428, + "first_name": "Shaylynn", + "last_name": "Bulstrode", + "email": "sbulstrodebv@ehow.com" + }, + { + "id": 429, + "first_name": "Randolf", + "last_name": "Pickvance", + "email": "rpickvancebw@istockphoto.com" + }, + { + "id": 430, + "first_name": "Diarmid", + "last_name": "Lias", + "email": "dliasbx@alexa.com" + }, + { + "id": 431, + "first_name": "Francis", + "last_name": "Clipson", + "email": "fclipsonby@google.com.au" + }, + { + "id": 432, + "first_name": "Lorene", + "last_name": "Maciejewski", + "email": "lmaciejewskibz@nytimes.com" + }, + { + "id": 433, + "first_name": "Kizzee", + "last_name": "Klammt", + "email": "kklammtc0@vkontakte.ru" + }, + { + "id": 434, + "first_name": "Zolly", + "last_name": "Cattle", + "email": "zcattlec1@va.gov" + }, + { + "id": 435, + "first_name": "Kattie", + "last_name": "Chidwick", + "email": "kchidwickc2@hatena.ne.jp" + }, + { + "id": 436, + "first_name": "Izabel", + "last_name": "Weight", + "email": "iweightc3@ycombinator.com" + }, + { + "id": 437, + "first_name": "Kerby", + "last_name": "Redler", + "email": "kredlerc4@google.es" + }, + { + "id": 438, + "first_name": "Wynn", + "last_name": "Glass", + "email": "wglassc5@yelp.com" + }, + { + "id": 439, + "first_name": "Jemmie", + "last_name": "Scorey", + "email": "jscoreyc6@blogspot.com" + }, + { + "id": 440, + "first_name": "Krispin", + "last_name": "Kirstein", + "email": "kkirsteinc7@elegantthemes.com" + }, + { + "id": 441, + "first_name": "Mil", + "last_name": "Ogdahl", + "email": "mogdahlc8@ft.com" + }, + { + "id": 442, + "first_name": "Van", + "last_name": "Bernholt", + "email": "vbernholtc9@indiegogo.com" + }, + { + "id": 443, + "first_name": "Ilyse", + "last_name": "Boecke", + "email": "iboeckeca@a8.net" + }, + { + "id": 444, + "first_name": "Gabby", + "last_name": "Silcock", + "email": "gsilcockcb@ow.ly" + }, + { + "id": 445, + "first_name": "Ulberto", + "last_name": "Edgeley", + "email": "uedgeleycc@blinklist.com" + }, + { + "id": 446, + "first_name": "Bary", + "last_name": "McGuinley", + "email": "bmcguinleycd@icq.com" + }, + { + "id": 447, + "first_name": "Willie", + "last_name": "Whyard", + "email": "wwhyardce@msn.com" + }, + { + "id": 448, + "first_name": "Borden", + "last_name": "Thrussell", + "email": "bthrussellcf@rediff.com" + }, + { + "id": 449, + "first_name": "Pincus", + "last_name": "McMahon", + "email": "pmcmahoncg@rakuten.co.jp" + }, + { + "id": 450, + "first_name": "Rriocard", + "last_name": "Franke", + "email": "rfrankech@addtoany.com" + }, + { + "id": 451, + "first_name": "Clayborne", + "last_name": "Greensitt", + "email": "cgreensittci@yellowbook.com" + }, + { + "id": 452, + "first_name": "Shayla", + "last_name": "Comber", + "email": "scombercj@sphinn.com" + }, + { + "id": 453, + "first_name": "Ronni", + "last_name": "Errigo", + "email": "rerrigock@tmall.com" + }, + { + "id": 454, + "first_name": "Bunnie", + "last_name": "Fishly", + "email": "bfishlycl@a8.net" + }, + { + "id": 455, + "first_name": "Jilly", + "last_name": "Skelly", + "email": "jskellycm@bbc.co.uk" + }, + { + "id": 456, + "first_name": "Melinde", + "last_name": "Prene", + "email": "mprenecn@smh.com.au" + }, + { + "id": 457, + "first_name": "Alanah", + "last_name": "De Atta", + "email": "adeattaco@gmpg.org" + }, + { + "id": 458, + "first_name": "Tamiko", + "last_name": "Gerrish", + "email": "tgerrishcp@baidu.com" + }, + { + "id": 459, + "first_name": "Winslow", + "last_name": "Waszczyk", + "email": "wwaszczykcq@bbc.co.uk" + }, + { + "id": 460, + "first_name": "Lydon", + "last_name": "Habershaw", + "email": "lhabershawcr@imgur.com" + }, + { + "id": 461, + "first_name": "Dill", + "last_name": "Playle", + "email": "dplaylecs@livejournal.com" + }, + { + "id": 462, + "first_name": "Natassia", + "last_name": "Kendle", + "email": "nkendlect@usa.gov" + }, + { + "id": 463, + "first_name": "Carree", + "last_name": "Bohills", + "email": "cbohillscu@pinterest.com" + }, + { + "id": 464, + "first_name": "Terrel", + "last_name": "Knell", + "email": "tknellcv@webnode.com" + }, + { + "id": 465, + "first_name": "Wilhelmina", + "last_name": "Lumbley", + "email": "wlumbleycw@stanford.edu" + }, + { + "id": 466, + "first_name": "Dori", + "last_name": "Astridge", + "email": "dastridgecx@salon.com" + }, + { + "id": 467, + "first_name": "Cherie", + "last_name": "Houlridge", + "email": "choulridgecy@squarespace.com" + }, + { + "id": 468, + "first_name": "Cord", + "last_name": "Caunt", + "email": "ccauntcz@hatena.ne.jp" + }, + { + "id": 469, + "first_name": "Josie", + "last_name": "MacMeeking", + "email": "jmacmeekingd0@people.com.cn" + }, + { + "id": 470, + "first_name": "Glenine", + "last_name": "Feron", + "email": "gferond1@a8.net" + }, + { + "id": 471, + "first_name": "Eliot", + "last_name": "Doidge", + "email": "edoidged2@intel.com" + }, + { + "id": 472, + "first_name": "Dudley", + "last_name": "Lehrahan", + "email": "dlehrahand3@technorati.com" + }, + { + "id": 473, + "first_name": "Tyler", + "last_name": "Puddan", + "email": "tpuddand4@ft.com" + }, + { + "id": 474, + "first_name": "Egor", + "last_name": "Lindgren", + "email": "elindgrend5@nba.com" + }, + { + "id": 475, + "first_name": "Barnabe", + "last_name": "Rival", + "email": "brivald6@yahoo.co.jp" + }, + { + "id": 476, + "first_name": "Domenico", + "last_name": "De Courtney", + "email": "ddecourtneyd7@themeforest.net" + }, + { + "id": 477, + "first_name": "Matthus", + "last_name": "Bodycote", + "email": "mbodycoted8@usa.gov" + }, + { + "id": 478, + "first_name": "Jamie", + "last_name": "Blyden", + "email": "jblydend9@comcast.net" + }, + { + "id": 479, + "first_name": "Mignon", + "last_name": "Woolnough", + "email": "mwoolnoughda@sciencedaily.com" + }, + { + "id": 480, + "first_name": "Mala", + "last_name": "Devlin", + "email": "mdevlindb@epa.gov" + }, + { + "id": 481, + "first_name": "Tiertza", + "last_name": "Letterick", + "email": "tletterickdc@state.gov" + }, + { + "id": 482, + "first_name": "Rebecka", + "last_name": "Alday", + "email": "raldaydd@flavors.me" + }, + { + "id": 483, + "first_name": "Vinita", + "last_name": "Etter", + "email": "vetterde@wikispaces.com" + }, + { + "id": 484, + "first_name": "Noreen", + "last_name": "Sirmond", + "email": "nsirmonddf@jigsy.com" + }, + { + "id": 485, + "first_name": "Ashley", + "last_name": "McClaughlin", + "email": "amcclaughlindg@bloomberg.com" + }, + { + "id": 486, + "first_name": "Vale", + "last_name": "Le Houx", + "email": "vlehouxdh@nhs.uk" + }, + { + "id": 487, + "first_name": "Donnell", + "last_name": "Treadway", + "email": "dtreadwaydi@mit.edu" + }, + { + "id": 488, + "first_name": "Gwennie", + "last_name": "Gundrey", + "email": "ggundreydj@over-blog.com" + }, + { + "id": 489, + "first_name": "Karel", + "last_name": "Dani", + "email": "kdanidk@163.com" + }, + { + "id": 490, + "first_name": "Merle", + "last_name": "Bonnaire", + "email": "mbonnairedl@uol.com.br" + }, + { + "id": 491, + "first_name": "Annabel", + "last_name": "Nockles", + "email": "anocklesdm@walmart.com" + }, + { + "id": 492, + "first_name": "Urban", + "last_name": "Ivashov", + "email": "uivashovdn@amazon.co.uk" + }, + { + "id": 493, + "first_name": "Hector", + "last_name": "Rothwell", + "email": "hrothwelldo@howstuffworks.com" + }, + { + "id": 494, + "first_name": "Ford", + "last_name": "Brozek", + "email": "fbrozekdp@hibu.com" + }, + { + "id": 495, + "first_name": "Rhody", + "last_name": "Phythean", + "email": "rphytheandq@bbc.co.uk" + }, + { + "id": 496, + "first_name": "Angie", + "last_name": "Durno", + "email": "adurnodr@china.com.cn" + }, + { + "id": 497, + "first_name": "Elisha", + "last_name": "Jerrome", + "email": "ejerromeds@163.com" + }, + { + "id": 498, + "first_name": "Davita", + "last_name": "Dakers", + "email": "ddakersdt@bravesites.com" + }, + { + "id": 499, + "first_name": "Kiersten", + "last_name": "Josey", + "email": "kjoseydu@networksolutions.com" + } +] diff --git a/examples/islands_router/public/routing.js b/examples/islands_router/public/routing.js deleted file mode 100644 index 6948f8230..000000000 --- a/examples/islands_router/public/routing.js +++ /dev/null @@ -1,140 +0,0 @@ -window.addEventListener("click", async (ev) => { - // confirm that this is an that meets our requirements - if ( - ev.defaultPrevented || - ev.button !== 0 || - ev.metaKey || - ev.altKey || - ev.ctrlKey || - ev.shiftKey - ) - return; - - /** @type HTMLAnchorElement | undefined;*/ - const a = ev - .composedPath() - .find(el => el instanceof Node && el.nodeName.toUpperCase() === "A"); - - if (!a) return; - - const svg = a.namespaceURI === "http://www.w3.org/2000/svg"; - const href = svg ? a.href.baseVal : a.href; - const target = svg ? a.target.baseVal : a.target; - if (target || (!href && !a.hasAttribute("state"))) return; - - const rel = (a.getAttribute("rel") || "").split(/\s+/); - if (a.hasAttribute("download") || (rel && rel.includes("external"))) return; - - const url = svg ? new URL(href, document.baseURI) : new URL(href); - if ( - url.origin !== window.location.origin // || - // TODO base - //(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) - ) - return; - - ev.preventDefault(); - - // fetch the new page - const resp = await fetch(url); - const htmlString = await resp.text(); - - // Use DOMParser to parse the HTML string - const parser = new DOMParser(); - // TODO parse from the request stream instead? - const doc = parser.parseFromString(htmlString, 'text/html'); - - // The 'doc' variable now contains the parsed DOM - const transition = async () => { - const oldDocWalker = document.createTreeWalker(document); - const newDocWalker = doc.createTreeWalker(doc); - let oldNode = oldDocWalker.currentNode; - let newNode = newDocWalker.currentNode; - while(oldDocWalker.nextNode() && newDocWalker.nextNode()) { - oldNode = oldDocWalker.currentNode; - newNode = newDocWalker.currentNode; - // if the nodes are different, we need to replace the old with the new - // because of the typed view tree, this should never actually happen - if (oldNode.nodeType !== newNode.nodeType) { - oldNode.replaceWith(newNode); - } - // if it's a text node, just update the text with the new text - else if (oldNode.nodeType === Node.TEXT_NODE) { - oldNode.textContent = newNode.textContent; - } - // if it's an element, replace if it's a different tag, or update attributes - else if (oldNode.nodeType === Node.ELEMENT_NODE) { - /** @type Element */ - const oldEl = oldNode; - /** @type Element */ - const newEl = newNode; - if (oldEl.tagName !== newEl.tagName) { - oldEl.replaceWith(newEl); - } - else { - for(const attr of newEl.attributes) { - oldEl.setAttribute(attr.name, attr.value); - } - } - } - // we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree - // if one of these marker is hit, then there are two options - // 1) it's the same branch, and we just keep walking until the end - // 2) it's a different branch, in which case the old can be replaced with the new wholesale - else if (oldNode.nodeType === Node.COMMENT_NODE) { - const oldText = oldNode.textContent; - const newText = newNode.textContent; - if(oldText.startsWith("bo") && newText !== oldText) { - oldDocWalker.nextNode(); - newDocWalker.nextNode(); - const oldRange = new Range(); - const newRange = new Range(); - let oldBranches = 1; - let newBranches = 1; - while(oldBranches > 0 && newBranches > 0) { - if(oldDocWalker.nextNode() && newDocWalker.nextNode()) { - console.log(oldDocWalker.currentNode, newDocWalker.currentNode); - if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) { - if(oldDocWalker.currentNode.textContent.startsWith("bo")) { - oldBranches += 1; - } else if(oldDocWalker.currentNode.textContent.startsWith("bc")) { - - oldBranches -= 1; - } - } - if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) { - if(newDocWalker.currentNode.textContent.startsWith("bo")) { - newBranches += 1; - } else if(newDocWalker.currentNode.textContent.startsWith("bc")) { - - newBranches -= 1; - } - } - } - } - - try { - oldRange.setStartAfter(oldNode); - oldRange.setEndBefore(oldDocWalker.currentNode); - newRange.setStartAfter(newNode); - newRange.setEndBefore(newDocWalker.currentNode); - const newContents = newRange.extractContents(); - oldRange.deleteContents(); - oldRange.insertNode(newContents); - oldNode.replaceWith(newNode); - oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode); - } catch (e) { - console.error(e); - } - } } - } - }; - // Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition - if (document.startViewTransition) { - await document.startViewTransition(transition); - } else { - await transition() - } - window.history.pushState(undefined, null, url); -}); - diff --git a/examples/islands_router/src/app.rs b/examples/islands_router/src/app.rs index a38cef001..ea72fcf33 100644 --- a/examples/islands_router/src/app.rs +++ b/examples/islands_router/src/app.rs @@ -1,8 +1,13 @@ -use leptos::prelude::*; -use leptos_router::{ - components::{FlatRoutes, Route, Router}, - StaticSegment, +use leptos::{ + either::{Either, EitherOf3}, + prelude::*, }; +use leptos_router::{ + components::{Route, Router, Routes}, + hooks::{use_params_map, use_query_map}, + path, +}; +use serde::{Deserialize, Serialize}; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -12,7 +17,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView { - + @@ -26,34 +31,180 @@ pub fn shell(options: LeptosOptions) -> impl IntoView { #[component] pub fn App() -> impl IntoView { view! { -
-

"My Application"

+

"My Contacts"

-

- -

- - - - + + + + +
} } -#[component] -pub fn PageA() -> impl IntoView { - view! { } +#[server] +pub async fn search(query: String) -> Result, ServerFnError> { + let users = tokio::fs::read_to_string("./mock_data.json").await?; + let data: Vec = serde_json::from_str(&users)?; + let query = query.to_ascii_lowercase(); + Ok(data + .into_iter() + .filter(|user| { + user.first_name.to_ascii_lowercase().contains(&query) + || user.last_name.to_ascii_lowercase().contains(&query) + || user.email.to_ascii_lowercase().contains(&query) + }) + .collect()) +} + +#[server] +pub async fn delete_user(id: u32) -> Result<(), ServerFnError> { + let users = tokio::fs::read_to_string("./mock_data.json").await?; + let mut data: Vec = serde_json::from_str(&users)?; + data.retain(|user| user.id != id); + let new_json = serde_json::to_string(&data)?; + tokio::fs::write("./mock_data.json", &new_json).await?; + Ok(()) +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct User { + id: u32, + first_name: String, + last_name: String, + email: String, } #[component] -pub fn PageB() -> impl IntoView { - view! { } +pub fn Home() -> impl IntoView { + let q = use_query_map(); + let q = move || q.read().get("q"); + let data = Resource::new(q, |q| async move { + if let Some(q) = q { + search(q).await + } else { + Ok(vec![]) + } + }); + let delete_user_action = ServerAction::::new(); + + let view = move || { + Suspend::new(async move { + let users = data.await.unwrap(); + if q().is_none() { + EitherOf3::A(view! { +

"Enter a search to begin viewing contacts."

+ }) + } else if users.is_empty() { + EitherOf3::B(view! { +

"No users found matching that search."

+ }) + } else { + EitherOf3::C(view! { + + + + + + + + + + + +
{user.first_name}{user.last_name}{user.email} + "Details" + + + + + +
+ }) + } + }) + }; + view! { +
+ + "Loading..."

}>{view}
+
+ } +} + +#[component] +pub fn Details() -> impl IntoView { + #[server] + pub async fn get_user(id: u32) -> Result, ServerFnError> { + let users = tokio::fs::read_to_string("./mock_data.json").await?; + let data: Vec = serde_json::from_str(&users)?; + Ok(data.iter().find(|user| user.id == id).cloned()) + } + let params = use_params_map(); + let id = move || { + params + .read() + .get("id") + .and_then(|id| id.parse::().ok()) + }; + let user = Resource::new(id, |id| async move { + match id { + None => Ok(None), + Some(id) => get_user(id).await, + } + }); + + move || { + Suspend::new(async move { + user.await.map(|user| match user { + None => Either::Left(view! { +
+

"Not found."

+

"Sorry — we couldn’t find that user."

+
+ }), + Some(user) => Either::Right(view! { +
+

{user.first_name} " " { user.last_name}

+ +
+ }), + }) + }) + } +} + +#[component] +pub fn About() -> impl IntoView { + view! { +
+

"About"

+

"This demo is intended to show off an experimental “islands router” feature, which mimics the smooth transitions and user experience of client-side routing while minimizing the amount of code that actually runs in the browser."

+

"By default, all the content in this application is only rendered on the server. But you can add client-side interactivity via islands like this one:"

+ +
+ } +} + +#[island] +pub fn Counter() -> impl IntoView { + let count = RwSignal::new(0); + view! { + + } } diff --git a/examples/islands_router/style.css b/examples/islands_router/style.css index 9ba7149f9..2a2c7a7b4 100644 --- a/examples/islands_router/style.css +++ b/examples/islands_router/style.css @@ -1,3 +1,52 @@ -.pending { - color: purple; +body { + font-family: system-ui, sans-serif; + background-color: #f6f6fa; +} + +h1, h2, h3, h4, h5, h6 { + font-family: ui-rounded, 'Hiragino Maru Gothic ProN', Quicksand, Comfortaa, Manjari, 'Arial Rounded MT', 'Arial Rounded MT Bold', Calibri, source-sans-pro, sans-serif; + text-align: center; +} + +nav { + padding: 1rem; + text-align: center; +} + +nav a { + margin: 1rem; +} + +form.search { + display: flex; + margin: 2rem auto; + justify-content: center; +} + +td { + min-width: 10rem; + width: 10rem; +} + +table { + min-width: 100%; +} + +.page { + width: 80%; + margin: auto; +} + +td:last-child > * { + display: inline-block; +} + +.note, .note { + text-align: center; +} + +button.counter { + display: block; + font-size: 2rem; + margin: auto; } diff --git a/examples/tailwind_axum/package-lock.json b/examples/tailwind_axum/package-lock.json new file mode 100644 index 000000000..41f579ab1 --- /dev/null +++ b/examples/tailwind_axum/package-lock.json @@ -0,0 +1,22 @@ +{ + "name": "leptos-tailwind", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "leptos-tailwind", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "tailwindcss": "^4.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz", + "integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==", + "dev": true + } + } +} diff --git a/examples/tailwind_axum/src/style.css b/examples/tailwind_axum/src/style.css new file mode 100644 index 000000000..3e0aef66f --- /dev/null +++ b/examples/tailwind_axum/src/style.css @@ -0,0 +1,69 @@ +/*! tailwindcss v4.0.0 | MIT License | https://tailwindcss.com */ +@tailwind base; +@tailwind components; +.relative { + position: relative; +} +.m-auto { + margin: auto; +} +.flex { + display: flex; +} +.min-h-screen { + min-height: 100vh; +} +.transform { + transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y); +} +.flex-col { + flex-direction: column; +} +.flex-row-reverse { + flex-direction: row-reverse; +} +.flex-wrap { + flex-wrap: wrap; +} +.border-b-4 { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 4px; +} +.border-l-2 { + border-left-style: var(--tw-border-style); + border-left-width: 2px; +} +.bg-gradient-to-tl { + --tw-gradient-position: to top left in oklab,; + background-image: linear-gradient(var(--tw-gradient-stops)); +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; + initial-value: rotateX(0); +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; + initial-value: rotateY(0); +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; + initial-value: rotateZ(0); +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; + initial-value: skewX(0); +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; + initial-value: skewY(0); +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml index 894922201..1fa8121ef 100644 --- a/integrations/actix/Cargo.toml +++ b/integrations/actix/Cargo.toml @@ -33,7 +33,7 @@ once_cell = "1" rustdoc-args = ["--generate-link-to-definition"] [features] -dont-use-islands-router = [] +islands-router = [] tracing = ["dep:tracing"] [package.metadata.cargo-all-features] diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 3b1bb0a13..b1aef24f1 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -23,6 +23,7 @@ use hydration_context::SsrSharedContext; use leptos::{ config::LeptosOptions, context::{provide_context, use_context}, + hydration::IslandsRouterNavigation, prelude::expect_context, reactive::{computed::ScopedFuture, owner::Owner}, IntoView, @@ -654,12 +655,27 @@ where IV: IntoView + 'static, { _ = replace_blocks; // TODO - handle_response(method, additional_context, app_fn, |app, chunks| { - Box::pin(async move { - Box::pin(app.to_html_stream_out_of_order().chain(chunks())) - as PinnedStream - }) - }) + handle_response( + method, + additional_context, + app_fn, + |app, chunks, supports_ooo| { + Box::pin(async move { + let app = if cfg!(feature = "islands-router") { + if supports_ooo { + app.to_html_stream_out_of_order_branching() + } else { + app.to_html_stream_in_order_branching() + } + } else if supports_ooo { + app.to_html_stream_out_of_order() + } else { + app.to_html_stream_in_order() + }; + Box::pin(app.chain(chunks())) as PinnedStream + }) + }, + ) } /// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries @@ -685,12 +701,21 @@ pub fn render_app_to_stream_in_order_with_context( where IV: IntoView + 'static, { - handle_response(method, additional_context, app_fn, |app, chunks| { - Box::pin(async move { - Box::pin(app.to_html_stream_in_order().chain(chunks())) - as PinnedStream - }) - }) + handle_response( + method, + additional_context, + app_fn, + |app, chunks, _supports_ooo| { + Box::pin(async move { + let app = if cfg!(feature = "islands-router") { + app.to_html_stream_in_order_branching() + } else { + app.to_html_stream_in_order() + }; + Box::pin(app.chain(chunks())) as PinnedStream + }) + }, + ) } /// Returns an Actix [struct@Route](actix_web::Route) that listens for a `GET` request and tries @@ -722,12 +747,13 @@ where fn async_stream_builder( app: IV, chunks: BoxedFnOnce>, + _supports_ooo: bool, ) -> PinnedFuture> where IV: IntoView + 'static, { Box::pin(async move { - let app = if cfg!(feature = "dont-use-islands-router") { + let app = if cfg!(feature = "islands-router") { app.to_html_stream_in_order_branching() } else { app.to_html_stream_in_order() @@ -767,6 +793,7 @@ fn leptos_corrected_path(req: &HttpRequest) -> String { } } +#[allow(clippy::type_complexity)] fn handle_response( method: Method, additional_context: impl Fn() + 'static + Clone + Send, @@ -774,6 +801,7 @@ fn handle_response( stream_builder: fn( IV, BoxedFnOnce>, + bool, ) -> PinnedFuture>, ) -> Route where @@ -784,6 +812,9 @@ where let add_context = additional_context.clone(); async move { + let is_island_router_navigation = cfg!(feature = "islands-router") + && req.headers().get("Islands-Router").is_some(); + let res_options = ResponseOptions::default(); let (meta_context, meta_output) = ServerMetaContext::new(); @@ -794,6 +825,10 @@ where move || { provide_contexts(req, &meta_context, &res_options); add_context(); + + if is_island_router_navigation { + provide_context(IslandsRouterNavigation); + } } }; @@ -803,6 +838,7 @@ where additional_context, res_options, stream_builder, + !is_island_router_navigation, ) .await; @@ -1093,6 +1129,7 @@ impl StaticRouteGenerator { app_fn.clone(), additional_context, async_stream_builder, + false, ); let sc = owner.shared_context().unwrap(); diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 482acc262..09f387020 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -36,7 +36,7 @@ tokio = { version = "1.43", features = ["net", "rt-multi-thread"] } [features] wasm = [] default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"] -dont-use-islands-router = [] +islands-router = [] tracing = ["dep:tracing"] [package.metadata.docs.rs] diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index 00924a3e8..9ce580131 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -773,12 +773,18 @@ where IV: IntoView + 'static, { _ = replace_blocks; // TODO - handle_response(additional_context, app_fn, |app, chunks| { + handle_response(additional_context, app_fn, |app, chunks, supports_ooo| { Box::pin(async move { - let app = if cfg!(feature = "dont-use-islands-router") { - app.to_html_stream_out_of_order_branching() - } else { + let app = if cfg!(feature = "islands-router") { + if supports_ooo { + app.to_html_stream_out_of_order_branching() + } else { + app.to_html_stream_in_order_branching() + } + } else if supports_ooo { app.to_html_stream_out_of_order() + } else { + app.to_html_stream_in_order() }; Box::pin(app.chain(chunks())) as PinnedStream }) @@ -838,8 +844,8 @@ pub fn render_app_to_stream_in_order_with_context( where IV: IntoView + 'static, { - handle_response(additional_context, app_fn, |app, chunks| { - let app = if cfg!(feature = "dont-use-islands-router") { + handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| { + let app = if cfg!(feature = "islands-router") { app.to_html_stream_in_order_branching() } else { app.to_html_stream_in_order() @@ -856,6 +862,7 @@ fn handle_response( stream_builder: fn( IV, BoxedFnOnce>, + bool, ) -> PinnedFuture>, ) -> impl Fn(Request) -> PinnedFuture> + Clone @@ -879,12 +886,16 @@ fn handle_response_inner( stream_builder: fn( IV, BoxedFnOnce>, + bool, ) -> PinnedFuture>, ) -> PinnedFuture> where IV: IntoView + 'static, { Box::pin(async move { + let is_island_router_navigation = cfg!(feature = "islands-router") + && req.headers().get("Islands-Router").is_some(); + let add_context = additional_context.clone(); let res_options = ResponseOptions::default(); let (meta_context, meta_output) = ServerMetaContext::new(); @@ -906,6 +917,10 @@ where res_options.clone(), ); add_context(); + + if is_island_router_navigation { + provide_context(IslandsRouterNavigation); + } } }; @@ -915,6 +930,7 @@ where additional_context, res_options, stream_builder, + !is_island_router_navigation, ) .await; @@ -1054,9 +1070,9 @@ pub fn render_app_async_stream_with_context( where IV: IntoView + 'static, { - handle_response(additional_context, app_fn, |app, chunks| { + handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| { Box::pin(async move { - let app = if cfg!(feature = "dont-use-islands-router") { + let app = if cfg!(feature = "islands-router") { app.to_html_stream_in_order_branching() } else { app.to_html_stream_in_order() @@ -1127,12 +1143,13 @@ where fn async_stream_builder( app: IV, chunks: BoxedFnOnce>, + _supports_ooo: bool, ) -> PinnedFuture> where IV: IntoView + 'static, { Box::pin(async move { - let app = if cfg!(feature = "dont-use-islands-router") { + let app = if cfg!(feature = "islands-router") { app.to_html_stream_in_order_branching() } else { app.to_html_stream_in_order() @@ -1405,6 +1422,7 @@ impl StaticRouteGenerator { app_fn.clone(), additional_context, async_stream_builder, + false, ); let sc = owner.shared_context().unwrap(); @@ -1837,64 +1855,64 @@ where } } else { router.route( - path, - match listing.mode() { - SsrMode::OutOfOrder => { - let s = render_app_to_stream_with_context( - cx_with_state_and_method.clone(), - app_fn.clone(), - ); - match method { - leptos_router::Method::Get => get(s), - leptos_router::Method::Post => post(s), - leptos_router::Method::Put => put(s), - leptos_router::Method::Delete => delete(s), - leptos_router::Method::Patch => patch(s), + path, + match listing.mode() { + SsrMode::OutOfOrder => { + let s = render_app_to_stream_with_context( + cx_with_state_and_method.clone(), + app_fn.clone(), + ); + match method { + leptos_router::Method::Get => get(s), + leptos_router::Method::Post => post(s), + leptos_router::Method::Put => put(s), + leptos_router::Method::Delete => delete(s), + leptos_router::Method::Patch => patch(s), + } } - } - SsrMode::PartiallyBlocked => { - let s = render_app_to_stream_with_context_and_replace_blocks( - cx_with_state_and_method.clone(), - app_fn.clone(), - true - ); - match method { - leptos_router::Method::Get => get(s), - leptos_router::Method::Post => post(s), - leptos_router::Method::Put => put(s), - leptos_router::Method::Delete => delete(s), - leptos_router::Method::Patch => patch(s), + SsrMode::PartiallyBlocked => { + let s = render_app_to_stream_with_context_and_replace_blocks( + cx_with_state_and_method.clone(), + app_fn.clone(), + true + ); + match method { + leptos_router::Method::Get => get(s), + leptos_router::Method::Post => post(s), + leptos_router::Method::Put => put(s), + leptos_router::Method::Delete => delete(s), + leptos_router::Method::Patch => patch(s), + } } - } - SsrMode::InOrder => { - let s = render_app_to_stream_in_order_with_context( - cx_with_state_and_method.clone(), - app_fn.clone(), - ); - match method { - leptos_router::Method::Get => get(s), - leptos_router::Method::Post => post(s), - leptos_router::Method::Put => put(s), - leptos_router::Method::Delete => delete(s), - leptos_router::Method::Patch => patch(s), + SsrMode::InOrder => { + let s = render_app_to_stream_in_order_with_context( + cx_with_state_and_method.clone(), + app_fn.clone(), + ); + match method { + leptos_router::Method::Get => get(s), + leptos_router::Method::Post => post(s), + leptos_router::Method::Put => put(s), + leptos_router::Method::Delete => delete(s), + leptos_router::Method::Patch => patch(s), + } } - } - SsrMode::Async => { - let s = render_app_async_with_context( - cx_with_state_and_method.clone(), - app_fn.clone(), - ); - match method { - leptos_router::Method::Get => get(s), - leptos_router::Method::Post => post(s), - leptos_router::Method::Put => put(s), - leptos_router::Method::Delete => delete(s), - leptos_router::Method::Patch => patch(s), + SsrMode::Async => { + let s = render_app_async_with_context( + cx_with_state_and_method.clone(), + app_fn.clone(), + ); + match method { + leptos_router::Method::Get => get(s), + leptos_router::Method::Post => post(s), + leptos_router::Method::Put => put(s), + leptos_router::Method::Delete => delete(s), + leptos_router::Method::Patch => patch(s), + } } - } - _ => unreachable!() - }, - ) + _ => unreachable!() + }, + ) }; } } @@ -2028,7 +2046,7 @@ where }, move || shell(options), req, - |app, chunks| { + |app, chunks, _supports_ooo| { Box::pin(async move { let app = app .to_html_stream_in_order() diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 666e3c67d..311f5d530 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::type_complexity)] + use futures::{stream::once, Stream, StreamExt}; use hydration_context::{SharedContext, SsrSharedContext}; use leptos::{ @@ -31,14 +33,20 @@ pub trait ExtendResponse: Sized { stream_builder: fn( IV, BoxedFnOnce>, + bool, ) -> PinnedFuture>, + supports_ooo: bool, ) -> impl Future + Send where IV: IntoView + 'static, { async move { - let (owner, stream) = - build_response(app_fn, additional_context, stream_builder); + let (owner, stream) = build_response( + app_fn, + additional_context, + stream_builder, + supports_ooo, + ); let sc = owner.shared_context().unwrap(); @@ -94,7 +102,11 @@ pub fn build_response( stream_builder: fn( IV, BoxedFnOnce>, + // this argument indicates whether a request wants to support out-of-order streaming + // responses + bool, ) -> PinnedFuture>, + is_islands_router_navigation: bool, ) -> (Owner, PinnedFuture>) where IV: IntoView + 'static, @@ -138,7 +150,7 @@ where // // we also don't actually start hydrating until after the whole stream is complete, // so it's not useful to send those scripts down earlier. - stream_builder(app, chunks) + stream_builder(app, chunks, is_islands_router_navigation) }); stream.await diff --git a/leptos/src/callback.rs b/leptos/src/callback.rs index 6702b21ef..4daf12da9 100644 --- a/leptos/src/callback.rs +++ b/leptos/src/callback.rs @@ -316,7 +316,7 @@ mod tests { #[test] fn callback_matches_same() { let callback1 = Callback::new(|x: i32| x * 2); - let callback2 = callback1.clone(); + let callback2 = callback1; assert!(callback1.matches(&callback2)); } @@ -330,7 +330,7 @@ mod tests { #[test] fn unsync_callback_matches_same() { let callback1 = UnsyncCallback::new(|x: i32| x * 2); - let callback2 = callback1.clone(); + let callback2 = callback1; assert!(callback1.matches(&callback2)); } diff --git a/leptos/src/for_loop.rs b/leptos/src/for_loop.rs index 4c64b3917..d4a775498 100644 --- a/leptos/src/for_loop.rs +++ b/leptos/src/for_loop.rs @@ -121,7 +121,7 @@ where EF: Fn(T) -> N + Send + Clone + 'static, N: IntoView + 'static, KF: Fn(&T) -> K + Send + Clone + 'static, - K: Eq + Hash + 'static, + K: Eq + Hash + ToString + 'static, T: Send + 'static, { // this takes the owner of the For itself @@ -195,7 +195,7 @@ where EF: Fn(ReadSignal, T) -> N + Send + Clone + 'static, N: IntoView + 'static, KF: Fn(&T) -> K + Send + Clone + 'static, - K: Eq + Hash + 'static, + K: Eq + Hash + ToString + 'static, T: Send + 'static, { // this takes the owner of the For itself diff --git a/leptos/src/hydration/island_script.js b/leptos/src/hydration/island_script.js index 12427a156..e0a1887f4 100644 --- a/leptos/src/hydration/island_script.js +++ b/leptos/src/hydration/island_script.js @@ -52,6 +52,8 @@ mod.hydrate(); hydrateIslands(document.body, mod); }); + + window.__hydrateIsland = (el, id) => hydrateIsland(el, id, mod); }) }); }) diff --git a/leptos/src/hydration/islands_routing.js b/leptos/src/hydration/islands_routing.js new file mode 100644 index 000000000..e77d45354 --- /dev/null +++ b/leptos/src/hydration/islands_routing.js @@ -0,0 +1,378 @@ +let NAVIGATION = 0; + +window.addEventListener("click", async (ev) => { + const req = clickToReq(ev); + if(!req) { + return; + } + + ev.preventDefault(); + await navigateToPage(req, true); +}); + +window.addEventListener("popstate", async (ev) => { + const req = new Request(window.location); + ev.preventDefault(); + await navigateToPage(req, true, true); +}); + +window.addEventListener("submit", async (ev) => { + const req = submitToReq(ev); + if(!req) { + return; + } + + ev.preventDefault(); + await navigateToPage(req, true); +}); + +async function navigateToPage( + /** @type Request */ + req, + /** @type bool */ + useViewTransition, + /** @type bool */ + replace +) { + NAVIGATION += 1; + const currentNav = NAVIGATION; + + // add a custom header to indicate that we're on a subsequent navigation + req.headers.append("Islands-Router", "true"); + + // fetch the new page + const resp = await fetch(req); + const redirected = resp.redirected; + const htmlString = await resp.text(); + + if(NAVIGATION === currentNav) { + // The 'doc' variable now contains the parsed DOM + const transition = async () => { + try { + diffPages(htmlString); + for(const island of document.querySelectorAll("leptos-island")) { + if(!island.$$hydrated) { + __hydrateIsland(island, island.dataset.component); + island.$$hydrated = true; + } + } + } catch(e) { + console.error(e); + } + }; + // Not all browsers support startViewTransition; see https://caniuse.com/?search=startViewTransition + if (useViewTransition && document.startViewTransition) { + await document.startViewTransition(transition); + } else { + await transition() + } + + const url = redirected ? resp.url : req.url; + + if(replace) { + window.history.replaceState(undefined, null, url); + } else { + window.history.pushState(undefined, null, url); + } + } +} + +function clickToReq(ev) { + // confirm that this is an that meets our requirements + if ( + ev.defaultPrevented || + ev.button !== 0 || + ev.metaKey || + ev.altKey || + ev.ctrlKey || + ev.shiftKey + ) + return; + + /** @type HTMLAnchorElement | undefined;*/ + const a = ev + .composedPath() + .find(el => el instanceof Node && el.nodeName.toUpperCase() === "A"); + + if (!a) return; + + const svg = a.namespaceURI === "http://www.w3.org/2000/svg"; + const href = svg ? a.href.baseVal : a.href; + const target = svg ? a.target.baseVal : a.target; + if (target || (!href && !a.hasAttribute("state"))) return; + + const rel = (a.getAttribute("rel") || "").split(/\s+/); + if (a.hasAttribute("download") || (rel?.includes("external"))) return; + + const url = svg ? new URL(href, document.baseURI) : new URL(href); + if ( + url.origin !== window.location.origin // || + // TODO base + //(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) + ) + return; + + return new Request(url); +} + +function submitToReq(ev) { + event.preventDefault(); + + const target = ev.target; + /** @type HTMLFormElement */ + let form; + if(target instanceof HTMLFormElement) { + form = target; + } else { + if(!target.form) { + return; + } + form = target.form; + } + + const method = form.method.toUpperCase(); + if(method !== "GET" && method !== "POST") { + return; + } + + const url = new URL(form.action); + let path = url.pathname; + const requestInit = {}; + const data = new FormData(form); + + const params = new URLSearchParams(); + for (const [key, value] of data.entries()) { + params.append(key, value); + } + + requestInit.headers = { + Accept: "text/html" + }; + if(method === "GET") { + path += `?${params.toString()}`; + } + else { + requestInit.method = "POST"; + requestInit.body = params; + } + + return new Request( + path, + requestInit + ); +} + + +function diffPages(htmlString) { + // Use DOMParser to parse the HTML string + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlString, 'text/html'); + + diffRange(document, document, doc, doc); +} + +function diffRange(oldDocument, oldRoot, newDocument, newRoot, oldEnd, newEnd) { + const oldDocWalker = oldDocument.createTreeWalker(oldRoot); + const newDocWalker = newDocument.createTreeWalker(newRoot); + let oldNode = oldDocWalker.currentNode; + let newNode = newDocWalker.currentNode; + + while (oldDocWalker.nextNode() && newDocWalker.nextNode()) { + oldNode = oldDocWalker.currentNode; + newNode = newDocWalker.currentNode; + + if (oldNode == oldEnd || newNode == newEnd) { + break; + } + + // if the nodes are different, we need to replace the old with the new + // because of the typed view tree, this should never actually happen + if (oldNode.nodeType !== newNode.nodeType) { + oldNode.replaceWith(newNode); + } + // if it's a text node, just update the text with the new text + else if (oldNode.nodeType === Node.TEXT_NODE) { + oldNode.textContent = newNode.textContent; + } + // if it's an element, replace if it's a different tag, or update attributes + else if (oldNode.nodeType === Node.ELEMENT_NODE) { + diffElement(oldNode, newNode); + } + // we use comment "branch marker" nodes to distinguish between different branches in the statically-typed view tree + // if one of these marker is hit, then there are two options + // 1) it's the same branch, and we just keep walking until the end + // 2) it's a different branch, in which case the old can be replaced with the new wholesale + else if (oldNode.nodeType === Node.COMMENT_NODE) { + const oldText = oldNode.textContent; + const newText = newNode.textContent; + if(oldText.startsWith("bo-for")) { + replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode); + } + else if (oldText.startsWith("bo-item")) { + // skip, this means we're diffing a new item within a For + } + else if(oldText.startsWith("bo") && newText !== oldText) { + replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode); + } + } + } +} + +function replaceFor(oldDocument, oldDocWalker, newDocument, newDocWalker, oldNode, newNode) { + oldDocWalker.nextNode(); + newDocWalker.nextNode(); + const oldRange = new Range(); + const newRange = new Range(); + let oldBranches = 1; + let newBranches = 1; + + const oldKeys = {}; + const newKeys = {}; + + while(oldBranches > 0) { + const c = oldDocWalker.currentNode; + if(c.nodeType === Node.COMMENT_NODE) { + const t = c.textContent; + if(t.startsWith("bo-for")) { + oldBranches += 1; + } else if(t.startsWith("bc-for")) { + + oldBranches -= 1; + } else if (t.startsWith("bo-item")) { + const k = t.replace("bo-item-", ""); + oldKeys[k] = { open: c, close: null }; + } else if (t.startsWith("bc-item")) { + const k = t.replace("bc-item-", ""); + oldKeys[k].close = c; + } + } + oldDocWalker.nextNode(); + } + while(newBranches > 0) { + const c = newDocWalker.currentNode; + if(c.nodeType === Node.COMMENT_NODE) { + const t = c.textContent; + if(t.startsWith("bo-for")) { + newBranches += 1; + } else if(t.startsWith("bc-for")) { + + newBranches -= 1; + } else if (t.startsWith("bo-item")) { + const k = t.replace("bo-item-", ""); + newKeys[k] = { open: c, close: null }; + } else if (t.startsWith("bc-item")) { + const k = t.replace("bc-item-", ""); + newKeys[k].close = c; + } + } + newDocWalker.nextNode(); + } + + for(const key in oldKeys) { + if(newKeys[key]) { + const oldOne = oldKeys[key]; + const newOne = newKeys[key]; + const oldRange = new Range(); + const newRange = new Range(); + + // then replace the item in the *new* list with the *old* DOM elements + oldRange.setStartAfter(oldOne.open); + oldRange.setEndBefore(oldOne.close); + newRange.setStartAfter(newOne.open); + newRange.setEndBefore(newOne.close); + const oldContents = oldRange.extractContents(); + const newContents = newRange.extractContents(); + + // patch the *old* DOM elements with the new ones + diffRange(oldDocument, oldContents, newDocument, newContents, oldOne.close, newOne.close); + + // then insert the old DOM elements into the new tree + // this means you'll end up with any new attributes or content from the server, + // but with any old DOM state (because they are the old elements) + newRange.insertNode(oldContents); + newOne.open.replaceWith(oldOne.open); + newOne.close.replaceWith(oldOne.close); + } + } + + try { + oldRange.setStartAfter(oldNode); + oldRange.setEndBefore(oldDocWalker.currentNode); + newRange.setStartAfter(newNode); + newRange.setEndAfter(newDocWalker.currentNode); + const newContents = newRange.extractContents(); + oldRange.deleteContents(); + oldRange.insertNode(newContents); + oldNode.replaceWith(newNode); + oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode); + } catch (e) { + console.error(e); + } +} + +function replaceBranch(oldDocWalker, newDocWalker, oldNode, newNode) { + oldDocWalker.nextNode(); + newDocWalker.nextNode(); + const oldRange = new Range(); + const newRange = new Range(); + let oldBranches = 1; + let newBranches = 1; + while(oldBranches > 0) { + if(oldDocWalker.nextNode()) { + if(oldDocWalker.currentNode.nodeType === Node.COMMENT_NODE) { + if(oldDocWalker.currentNode.textContent.startsWith("bo")) { + oldBranches += 1; + } else if(oldDocWalker.currentNode.textContent.startsWith("bc")) { + + oldBranches -= 1; + } + } + } + } + while(newBranches > 0) { + if(newDocWalker.nextNode()) { + if(newDocWalker.currentNode.nodeType === Node.COMMENT_NODE) { + if(newDocWalker.currentNode.textContent.startsWith("bo")) { + newBranches += 1; + } else if(newDocWalker.currentNode.textContent.startsWith("bc")) { + + newBranches -= 1; + } + } + } + } + + try { + oldRange.setStartAfter(oldNode); + oldRange.setEndBefore(oldDocWalker.currentNode); + newRange.setStartAfter(newNode); + newRange.setEndAfter(newDocWalker.currentNode); + const newContents = newRange.extractContents(); + oldRange.deleteContents(); + oldRange.insertNode(newContents); + oldNode.replaceWith(newNode); + oldDocWalker.currentNode.replaceWith(newDocWalker.currentNode); + } catch (e) { + console.error(e); + } +} + +function diffElement(oldNode, newNode) { + /** @type Element */ + const oldEl = oldNode; + /** @type Element */ + const newEl = newNode; + if (oldEl.tagName !== newEl.tagName) { + oldEl.replaceWith(newEl); + + } + else { + for(const attr of newEl.attributes) { + oldEl.setAttribute(attr.name, attr.value); + } + } +} + +for(const island of document.querySelectorAll("leptos-island")) { + island.$$hydrated = true; +} diff --git a/leptos/src/hydration/mod.rs b/leptos/src/hydration/mod.rs index ce87dc63a..9778b395a 100644 --- a/leptos/src/hydration/mod.rs +++ b/leptos/src/hydration/mod.rs @@ -50,6 +50,10 @@ pub fn HydrationScripts( /// Should be `true` to hydrate in `islands` mode. #[prop(optional)] islands: bool, + /// Should be `true` to add the “islands router,” which enables limited client-side routing + /// when running in islands mode. + #[prop(optional)] + islands_router: bool, /// A base url, not including a trailing slash #[prop(optional, into)] root: Option, @@ -98,18 +102,36 @@ pub fn HydrationScripts( include_str!("./hydration_script.js") }; + let islands_router = islands_router + .then_some(include_str!("./islands_routing.js")) + .unwrap_or_default(); + let root = root.unwrap_or_default(); - view! { - - - - } + use_context::().is_none().then(|| { + view! { + + + + } + }) } + +/// If this is provided via context, it means that you are using the islands router and +/// this is a subsequent navigation, made from the client. +/// +/// This should be provided automatically by a server integration if it detects that the +/// header `Islands-Router` is present in the request. +/// +/// This is used to determine how much of the hydration script to include in the page. +/// If it is present, then the contents of the `` component will not be +/// included, as they only need to be sent to the client once. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IslandsRouterNavigation; diff --git a/router/src/flat_router.rs b/router/src/flat_router.rs index 57d553fd7..c2c348d3e 100644 --- a/router/src/flat_router.rs +++ b/router/src/flat_router.rs @@ -10,7 +10,7 @@ use crate::{ use any_spawner::Executor; use either_of::Either; use futures::FutureExt; -use leptos::attr::any_attribute::AnyAttribute; +use leptos::attr::{any_attribute::AnyAttribute, Attribute}; use reactive_graph::{ computed::{ArcMemo, ScopedFuture}, owner::{provide_context, Owner}, @@ -27,7 +27,7 @@ use tachys::{ view::{ add_attr::AddAnyAttr, any_view::{AnyView, AnyViewState, IntoAny}, - Mountable, Position, PositionState, Render, RenderHtml, + MarkBranch, Mountable, Position, PositionState, Render, RenderHtml, }, }; @@ -365,6 +365,112 @@ where } } +#[derive(Debug)] +pub(crate) struct MatchedRoute(pub String, pub AnyView); + +impl Render for MatchedRoute { + type State = ::State; + + fn build(self) -> Self::State { + self.1.build() + } + + fn rebuild(self, state: &mut Self::State) { + self.1.rebuild(state); + } +} + +impl AddAnyAttr for MatchedRoute { + type Output = Self; + + fn add_any_attr( + self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: RenderHtml, + { + let MatchedRoute(id, view) = self; + MatchedRoute(id, view.add_any_attr(attr).into_any()) + } +} + +impl RenderHtml for MatchedRoute { + type AsyncOutput = Self; + type Owned = Self; + const MIN_LENGTH: usize = 0; + + fn dry_resolve(&mut self) { + self.1.dry_resolve(); + } + + async fn resolve(self) -> Self::AsyncOutput { + let MatchedRoute(id, view) = self; + let view = view.resolve().await; + MatchedRoute(id, view) + } + + fn to_html_with_buf( + self, + buf: &mut String, + position: &mut Position, + escape: bool, + mark_branches: bool, + extra_attrs: Vec, + ) { + if mark_branches { + buf.open_branch(&self.0); + } + self.1.to_html_with_buf( + buf, + position, + escape, + mark_branches, + extra_attrs, + ); + if mark_branches { + buf.close_branch(&self.0); + } + } + + fn to_html_async_with_buf( + self, + buf: &mut StreamBuilder, + position: &mut Position, + escape: bool, + mark_branches: bool, + extra_attrs: Vec, + ) where + Self: Sized, + { + if mark_branches { + buf.open_branch(&self.0); + } + self.1.to_html_async_with_buf::( + buf, + position, + escape, + mark_branches, + extra_attrs, + ); + if mark_branches { + buf.close_branch(&self.0); + } + } + + fn hydrate( + self, + cursor: &Cursor, + position: &PositionState, + ) -> Self::State { + self.1.hydrate::(cursor, position) + } + + fn into_owned(self) -> Self::Owned { + self + } +} + impl FlatRoutesView where Loc: LocationProvider + Send, @@ -397,6 +503,7 @@ where let view = match new_match { None => (self.fallback)().into_any(), Some(new_match) => { + let id = new_match.as_matched().to_string(); let (view, _) = new_match.into_view_and_child(); let view = owner .with(|| { @@ -409,6 +516,7 @@ where }) .now_or_never() .expect("async route used in SSR"); + let view = MatchedRoute(id, view); view.into_any() } }; diff --git a/router/src/nested_router.rs b/router/src/nested_router.rs index c04685d70..005b28abb 100644 --- a/router/src/nested_router.rs +++ b/router/src/nested_router.rs @@ -1,4 +1,5 @@ use crate::{ + flat_router::MatchedRoute, hooks::Matched, location::{LocationProvider, Url}, matching::RouteDefs, @@ -642,21 +643,28 @@ where async move { provide_context(params_including_parents); provide_context(url); - provide_context(matched); + provide_context(matched.clone()); view.preload().await; *view_fn.lock().or_poisoned() = Box::new(move || { let view = view.clone(); - owner.with(|| { - Suspend::new(Box::pin(async move { - let view = SendWrapper::new(ScopedFuture::new( - view.choose(), - )); - let view = view.await; - OwnedView::new(view).into_any() - }) - as Pin< - Box + Send>, - >) + owner.with({ + let matched = matched.clone(); + move || { + Suspend::new(Box::pin(async move { + let view = SendWrapper::new( + ScopedFuture::new(view.choose()), + ); + let view = view.await; + let view = + MatchedRoute(matched.0.get(), view); + OwnedView::new(view).into_any() + }) + as Pin< + Box< + dyn Future + Send, + >, + >) + } }) }); trigger diff --git a/tachys/src/view/any_view.rs b/tachys/src/view/any_view.rs index 98d08b074..f1a0fc247 100644 --- a/tachys/src/view/any_view.rs +++ b/tachys/src/view/any_view.rs @@ -67,11 +67,17 @@ pub struct AnyView { #[cfg(feature = "ssr")] dry_resolve: fn(&mut Erased), #[cfg(feature = "hydrate")] - #[cfg(feature = "hydrate")] #[allow(clippy::type_complexity)] hydrate_from_server: fn(Erased, &Cursor, &PositionState) -> AnyViewState, } +impl Debug for AnyView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AnyView") + .field("type_id", &self.type_id) + .finish_non_exhaustive() + } +} /// Retained view state for [`AnyView`]. pub struct AnyViewState { type_id: TypeId, diff --git a/tachys/src/view/keyed.rs b/tachys/src/view/keyed.rs index 184911d13..2dca0d08e 100644 --- a/tachys/src/view/keyed.rs +++ b/tachys/src/view/keyed.rs @@ -1,6 +1,6 @@ use super::{ - add_attr::AddAnyAttr, Mountable, Position, PositionState, Render, - RenderHtml, + add_attr::AddAnyAttr, MarkBranch, Mountable, Position, PositionState, + Render, RenderHtml, }; use crate::{ html::attribute::{any_attribute::AnyAttribute, Attribute}, @@ -66,7 +66,7 @@ where impl Render for Keyed where I: IntoIterator, - K: Eq + Hash + 'static, + K: Eq + Hash + ToString + 'static, KF: Fn(&T) -> K, V: Render, VF: Fn(usize, T) -> (VFS, V), @@ -132,7 +132,7 @@ where impl AddAnyAttr for Keyed where I: IntoIterator + Send + 'static, - K: Eq + Hash + 'static, + K: Eq + Hash + ToString + 'static, KF: Fn(&T) -> K + Send + 'static, V: RenderHtml, V: 'static, @@ -185,7 +185,7 @@ where impl RenderHtml for Keyed where I: IntoIterator + Send + 'static, - K: Eq + Hash + 'static, + K: Eq + Hash + ToString + 'static, KF: Fn(&T) -> K + Send + 'static, V: RenderHtml + 'static, VF: Fn(usize, T) -> (VFS, V) + Send + 'static, @@ -221,8 +221,14 @@ where mark_branches: bool, extra_attrs: Vec, ) { + if mark_branches { + buf.open_branch("for"); + } for (index, item) in self.items.into_iter().enumerate() { let (_, item) = (self.view_fn)(index, item); + if mark_branches { + buf.open_branch("item"); + } item.to_html_with_buf( buf, position, @@ -230,8 +236,14 @@ where mark_branches, extra_attrs.clone(), ); + if mark_branches { + buf.close_branch("item"); + } *position = Position::NextChild; } + if mark_branches { + buf.close_branch("for"); + } buf.push_str(""); } @@ -243,8 +255,19 @@ where mark_branches: bool, extra_attrs: Vec, ) { + if mark_branches { + buf.open_branch("for"); + } for (index, item) in self.items.into_iter().enumerate() { + let branch_name = mark_branches.then(|| { + let key = (self.key_fn)(&item); + let key = key.to_string(); + format!("item-{key}") + }); let (_, item) = (self.view_fn)(index, item); + if mark_branches { + buf.open_branch(branch_name.as_ref().unwrap()); + } item.to_html_async_with_buf::( buf, position, @@ -252,8 +275,14 @@ where mark_branches, extra_attrs.clone(), ); + if mark_branches { + buf.close_branch(branch_name.as_ref().unwrap()); + } *position = Position::NextChild; } + if mark_branches { + buf.close_branch("for"); + } buf.push_sync(""); } diff --git a/tachys/src/view/mod.rs b/tachys/src/view/mod.rs index 3c24bdee9..ddd3cf656 100644 --- a/tachys/src/view/mod.rs +++ b/tachys/src/view/mod.rs @@ -46,7 +46,8 @@ pub trait Render: Sized { fn rebuild(self, state: &mut Self::State); } -pub(crate) trait MarkBranch { +#[doc(hidden)] +pub trait MarkBranch { fn open_branch(&mut self, branch_id: &str); fn close_branch(&mut self, branch_id: &str);