yew_interop/lib.rs
1// for publishing docs
2#![cfg_attr(documenting, feature(doc_cfg))]
3
4//!<div align="center"> <h1>Yew Interop</h1> <p> <strong>Load JavaScript and CSS asynchronously in Yew</strong> </p> <img alt="Crates.io" src="https://img.shields.io/crates/v/yew-interop">
5// use the latest demo after release
6//!<a href="https://madoshakalaka.github.io/yew-interop/v0.2.1"><img alt="demo badge released" src="https://img.shields.io/badge/demo-v0.2.1-brightgreen"/></a>
7//!<a href="https://madoshakalaka.github.io/yew-interop/master"><img alt="demo badge master" src="https://img.shields.io/badge/demo-master-brightgreen"/></a>
8//!<a href="https://docs.rs/yew-interop"><img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-released-brightgreen"/></a>
9//!<a href="https://madoshakalaka.github.io/yew-interop/docsrs/yew_interop/"><img alt="docs master" src="https://img.shields.io/badge/docs-master-brightgreen"/></a>
10//!<a href="#"><img alt="minimal rustc" src="https://img.shields.io/badge/rustc-1.56%2B-lightgrey"/></a>
11//!</div>
12//!
13//!## Load On Demand
14//!
15//!With `yew-interop`, each resource is requested on demand when a consuming component requests it.
16//!
17//!
18//!If you include your libraries using the
19//
20//!or insert them as `<script/>` or `<link/>` directly in the `index.html`,
21//!the resources will load for every request,
22//!even if there is no consuming component.
23//!This can cause congestion and wasted data.
24//!
25//!
26//!## Load Once, Use Everywhere
27//!
28//!Each resource is strictly requested once.
29//!If a resource has been requested in one component,
30//!other consuming components won't trigger a reload.
31//!Other requests to the same resource will either wait for the load to complete,
32//!or return ready immediately if the resource is loaded.
33//!
34//!
35//!## Demo
36//!
37//
38//!has a demo website built with`yew-interop`
39//!
40//!The gif below shows the first two use cases,
41//!loading speed is throttled for demo purposes.
42//!
43//!
44//!
45//
46//!
47//!# Install
48//!The master branch has the the lastest in-development code.
49//!
50//!```toml
51//!yew-interop = {git="https://github.com/Madoshakalaka/yew-interop.git", branch="master", features=["yew-stable"]}
52//!```
53//!
54//!The `yew-stable` feature works with the latest release of yew on crates.io, currently 0.19.
55//!If you are using yew-next (yew's master branch), change the `yew-stable` feature to `yew-next`.
56//!
57//!Or you can install the latest version published on crates.io, which uses yew 0.19.
58//!
59// bump this after release when 0.x or major version changed
60//!```toml
61//!yew-interop = "0.2"
62//!```
63//!
64//!Note the `yew-next`/`yew-stable` features only exist in the master branch
65//!since published crates can't have git dependencies.
66//!
67//!# API
68//!
69//!## Asynchronously Load CSS or Javascript Libraries
70//!
71//!If your javascript library exposes functions or objects you want to use in Rust,
72//!then `yew_interop::declare_resources!` is the right choice.
73//!
74//!First you want to create a separate module `interop` and declare your dependencies there.
75//!
76#![doc = "```rust"]
77//!// alternatively, create a separate file `interop.rs`,
78//!// and add `mod interop;` to `main.rs` to have tidier code.
79//!mod interop{
80//! use yew_interop::declare_resources;
81//!
82//! declare_resources!(
83//! library_a
84//! "https://my-cdn.com/library-a.min.js"
85//! library_b
86//! "https://my-cdn.com/library-b.min.js"
87//! "https://my-cdn.com/library-b.min.css"
88//! library_c
89//! "/static/library-c.min.js"
90//! "/static/library-c.min.css"
91//! );
92//!}
93#![doc = "```"]
94//!This macro expands into a `<ResourceProvider/>` component.
95//!you want to wrap the root of your application in the provider:
96//!
97#![doc = "```rust"]
98//!# mod interop{
99//!# use yew_interop::declare_resources;
100//!#
101//!# declare_resources!(
102//!# library_a
103//!# "https://my-cdn.com/library-a.min.js"
104//!# library_b
105//!# "https://my-cdn.com/library-b.min.js"
106//!# "https://my-cdn.com/library-b.min.css"
107//!# );
108//!# }
109//!use yew::prelude::*;
110//!use interop::ResourceProvider;
111//!
112//!#[function_component(App)]
113//!pub fn app() -> Html {
114//! html! {
115//! <ResourceProvider>
116//!# <></>
117//! // the rest of your app
118//! </ResourceProvider>
119//! }
120//!}
121#![doc = "```"]
122//!The macro will also expand into hooks by prepending your resource names with "_use__", in this case,
123//!the macro will expand into `pub fn use_library_a() -> bool` and `pub fn use_library_b() -> bool`
124//!
125//!At your consuming component, you can use these hooks to asynchronously wait for libraries to be loaded:
126//!
127#![doc = "```rust"]
128//!# mod interop{
129//!# use yew_interop::declare_resources;
130//!#
131//!# declare_resources!(
132//!# library_a
133//!# "https://my-cdn.com/library-a.min.js"
134//!# library_b
135//!# "https://my-cdn.com/library-b.min.js"
136//!# "https://my-cdn.com/library-b.min.css"
137//!# );
138//!# }
139//!
140//!use yew::prelude::*;
141//!use interop::use_library_a;
142//!
143//!#[function_component(Consumer)]
144//!pub fn consumer() -> Html {
145//! let library_a_ready = use_library_a(); // <-- generated hook
146//!
147//! html! {
148//! if library_a_ready{
149//! // use library a here
150//!# {html!{}}
151//! }else{
152//! <p>{"please wait..."}</p>
153//! }
154//! }
155//!}
156#![doc = "```"]
157//!>For javascript libraries,
158//!you will also need to write some stubs using `wasm-bindgen` and `js-sys` before using the library in Rust.
159//!The wasm-bindgen book has [a good chapter](https://rustwasm.github.io/wasm-bindgen/examples/import-js.html) on that.
160//!You can also check out our demo website and have a look [how it's done there](https://github.com/Madoshakalaka/yew-interop/blob/master/example/src/interop.rs)
161//!
162//!## Explicit Resource Type
163//!
164//!The `declare_resources!` macro needs to know whether a url is JavaScript or CSS.
165//!When you provide a string literal as in the examples above,
166//!the macro derives the information from the suffix of the last path segment.
167//!It expects .js or .css and is smart enough to exclude the query params or the fragment.
168//!
169//!When the path segment doesn't end with .js or .css,
170//!or when you provide other expressions like a macro call or an identifier,
171//!you need to manually specify the URL type by prepending the custom keyword js/css
172//!before the url.
173//!
174//!`declare_resources!` will accept any expression with a return type that implements `Into<Cow<'static, str>>`,
175//!so `&'static str`, `String`, `Cow<'static, str>` are all fine.
176//!
177//!here's a more complex example:
178//!
179#![doc = "```rust"]
180//!use yew_interop::declare_resources;
181//!
182//!const MY_LIB_JS: &str = "https://cdn.com/my_lib.js";
183//!
184//!declare_resources!(
185//! my_lib
186//! js MY_LIB_JS
187//! "https://cdn.com/my_lic_b.css" // <-- when a string literal is provided, script type is determined from the suffix
188//! "/static/snippet.js"
189//! js concat!("https://a.com/", "b.js")
190//! my_lib_b
191//! css "/somehow/ends/with/.js" // <-- explicit type css overrides the suffix
192//! my_lib_c
193//! js String::from("https://a.com/test.js")
194//! );
195//!
196#![doc = "```"]
197//!## Side Effect Javascript
198//!
199//!Here, side effect scripts refers to the JavaScript that run something onload,
200//!as opposed to a library that exposes functions and classes.
201//!
202//!If your javascript is a side effect script,
203//!you want to enable the `script` feature.
204//!
205//!
206//!```toml
207//!# change yew-stable to yew-next if you use yew's master branch
208//!yew-interop = {git="https://github.com/Madoshakalaka/yew-interop.git", features=["yew-stable", "script"]}
209//!```
210//!or
211// bump this after release when 0.x or major version changed
212//!```toml
213//!yew-interop = {version = "0.2", features = ["script"]}
214//!```
215//!
216//!You will need to prepend the identifier of a script with an exclamation mark (!).
217//!And only one script url for each identifier, here's an example:
218//!
219#![cfg_attr(feature = "script", doc = "```rust")]
220#![cfg_attr(
221 feature = "script",
222 doc = r##"use yew_interop::declare_resources;
223
224declare_resources!(
225 lib // <- normal library
226 "https://cdn.com/lib.js"
227 "/static/lib.css"
228 ! my_script // <- exclamation mark for side effect scripts
229 "https://cdn.com/script.js"
230);
231```
232"##
233)]
234//!You never need to specify the resource type explicitly,
235//!since only JavaScript is allowed.
236//!
237//!Same as previous examples, this will expand into a `use_<identifier>` hook.
238//!What's different is that instead of a bool,
239//!the hook returns an `Option<Script>`,
240//!it is `None` when the script is loading.
241//!
242//!To run the script, you will need to render a `<ScriptEffect/>` component and pass
243//!the script object to the component.
244//!This allows you to freely control whether and when the script should be run.
245//!The `<ScriptEffect/>` component is a [portal](https://yew.rs/docs/next/advanced-topics/portals)
246//!to the `<head/>` element of the document,
247//!so it won't render anything in its place,
248//!it will only run the script on render.
249#![cfg_attr(feature = "script", doc = "```rust")]
250#![cfg_attr(
251 feature = "script",
252 doc = r##"mod interop{
253 use yew_interop::declare_resources;
254 declare_resources!(
255 ! my_script
256 "https://cdn.com/script.js"
257 );
258}
259
260use yew::prelude::*;
261use yew_interop::ScriptEffect;
262use interop::use_my_script; // <-- generated hook
263
264/// this example simply runs the script on every re-render, if the script is ready.
265#[function_component(MyComp)]
266pub fn my_comp() -> Html {
267 let script = use_my_script(); // <-- returns Option<Script>
268
269 // ...snip
270
271 html! {
272 if let Some(script) = script{
273 <ScriptEffect {script}/>
274 }else{
275 <p>{"Please wait..."}</p>
276 }
277 }
278}
279```
280"##
281)]
282
283//!If your script depends on other components being rendered,
284//!such as the fourth example [in the demo](https://madoshakalaka.github.io/yew-interop/master/),
285//!where the script adds onclick handlers to the rendered elements,
286//!you will need to guarantee the script is rendered after all the dependees.
287//!
288//!Yew renders descendents in a breadth-first order from bottom to top,
289//!which is not the most intuitive rendering order.
290//!
291//!One way to guarantee the correct rendering order is to
292//!place the `<ScriptEffect/>` component as a **sibling on top of the deepest dependees**,
293//!
294//!For example, let's say your script depends on two components `<ComponentA/>` and `<ComponentB/>`.
295//!
296//!The case below shows a correct placement where A and B has the same depth,
297//!
298#![cfg_attr(feature = "script", doc = "```rust")]
299#![cfg_attr(
300 feature = "script",
301 doc = r##"# use yew_interop::declare_resources;
302#
303# declare_resources!(
304# ! my_script
305# "https://cdn.com/script.js"
306# );
307#
308# use yew::prelude::*;
309# use yew_interop::ScriptEffect;
310#
311# #[function_component(ComponentA)]
312# pub fn component_a() -> Html {
313# html! {}
314# }
315# #[function_component(ComponentB)]
316# pub fn component_b() -> Html {
317# html! {}
318# }
319# #[function_component(MyComp)]
320# pub fn my_comp() -> Html {
321# let script = use_my_script().unwrap();
322#
323 html!{
324 <>
325 <ScriptEffect {script}/>
326 <ComponentA/>
327 <ComponentB/>
328 // <ScriptEffect {script}/> !!! do not place here, otherwise it would render first
329 </>
330 }
331# }
332```
333"##
334)]
335//!the rendering order here is B -> A -> ScriptEffect.
336//!
337//!Here's trickier one, where B is deeper, so we place our component on top of B:
338//!
339#![cfg_attr(feature = "script", doc = "```rust")]
340#![cfg_attr(
341 feature = "script",
342 doc = r##"# use yew_interop::declare_resources;
343#
344# declare_resources!(
345# ! my_script
346# "https://cdn.com/script.js"
347# );
348#
349# use yew::prelude::*;
350# use yew_interop::ScriptEffect;
351#
352# #[function_component(ComponentA)]
353# pub fn component_a() -> Html {
354# html! {}
355# }
356# #[function_component(ComponentB)]
357# pub fn component_b() -> Html {
358# html! {}
359# }
360# #[function_component(ComponentC)]
361# pub fn component_c() -> Html {
362# html! {
363# <div></div>
364# }
365# }
366#[derive(Properties, PartialEq)]
367pub struct ContainerProps {
368 children: Children
369}
370
371#[function_component(Container)]
372pub fn container(props: &ContainerProps) -> Html {
373 // --snip--
374 html! {
375 {for props.children.iter()}
376 }
377}
378
379# #[function_component(MyComp)]
380# pub fn my_comp() -> Html {
381# let script = use_my_script().unwrap();
382#
383html!{
384 <>
385 <ComponentA/>
386 <Container>
387 <ScriptEffect {script}/>
388 <ComponentB/>
389 </Container>
390 <ComponentC/>
391 </>
392}
393# }
394```
395"##
396)]
397//!The rendering order is C -> Container -> A -> B -> ScriptEffect.
398//!
399//!# Contributing
400//!
401//!Your pull request is welcome!
402//!There is extensive testing in CI.
403//!Be sure to check out our [development guide](https://github.com/Madoshakalaka/yew-interop/blob/master/CONTRIBUTING.md).
404//!
405#[doc(hidden)]
406#[cfg(feature = "script")]
407pub mod script;
408
409#[cfg(feature = "script")]
410#[doc(inline)]
411pub use script::ScriptEffect;
412
413#[cfg(feature = "script")]
414#[doc(inline)]
415pub use script::ScriptEffectProps;
416use std::borrow::Cow;
417
418use std::rc::Rc;
419
420use yew::Reducible;
421
422#[doc(hidden)]
423pub use yew_interop_core::LinkType;
424pub use yew_interop_macro::declare_resources;
425
426#[doc(hidden)]
427#[derive(PartialEq, Debug, Clone)]
428pub struct Link {
429 pub r#type: LinkType,
430 pub src: Cow<'static, str>,
431}
432
433#[doc(hidden)]
434pub enum LinkGroupStatusAction {
435 PleaseStart(Vec<Link>),
436 Completed,
437}
438
439#[doc(hidden)]
440#[derive(PartialEq, Clone)]
441pub enum LinkGroupStatus {
442 NotRequested,
443 Started { links: Vec<Link> },
444 Completed { links: Vec<Link> },
445}
446
447impl Default for LinkGroupStatus {
448 fn default() -> Self {
449 Self::NotRequested
450 }
451}
452
453impl Reducible for LinkGroupStatus {
454 type Action = LinkGroupStatusAction;
455
456 fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
457 match action {
458 LinkGroupStatusAction::PleaseStart(links) => match *self {
459 LinkGroupStatus::NotRequested => Rc::new(Self::Started { links }),
460 _ => self,
461 },
462 LinkGroupStatusAction::Completed => match &*self {
463 LinkGroupStatus::NotRequested => {
464 unreachable!("resource not requested but received completed message")
465 }
466 LinkGroupStatus::Completed { .. } => unreachable!(
467 "resource is already completed but received more completed message"
468 ),
469 LinkGroupStatus::Started { links } => Rc::new(Self::Completed {
470 links: links.clone(),
471 }),
472 },
473 }
474 }
475}