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//![JS-snippet method with wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/reference/js-snippets.html)
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//![The example folder](https://github.com/Madoshakalaka/yew-interop/tree/master/example)
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//!![yew interop demo gif](https://madoshakalaka.github.io/yew-interop/master/static/yew-interop-demo.gif)
44//!
45//![Check out the full online demo](https://madoshakalaka.github.io/yew-interop/master)
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}