pepsighan / ruukh-ui

An experimental next-gen frontend framework for the Web in Rust.
MIT License
0 stars 1 forks source link

How to use web-sys fetch api in ruukh? #22

Closed zengsai closed 6 years ago

zengsai commented 6 years ago

I've wrote a demo using ruukh, but don't known how to fetch server data(json). any idear?

zengsai commented 6 years ago
#![feature(proc_macro_hygiene, decl_macro)]

use futures::{future, Future, Poll};
use js_sys::Promise;
use ruukh::prelude::*;
use serde_derive::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::future_to_promise;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};

#[component]
struct MyApp {
    #[state]
    message: String,
}

impl Render for MyApp {
    fn render(&self) -> Markup<Self> {
        html! {
            <h1>"Todo Apps"</h1>
            <p>"message:" {&self.message}</p>
        }
    }
}

/// The lifecycle of a stateful component.
///
/// When you do not require these lifecycle hooks, you may implement them with
/// an auto derive `#[derive(Lifecycle)]` on the component struct.
impl Lifecycle for MyApp {
    /// Invoked when the component is mounted onto the DOM tree.
    fn mounted(&self) {
        self.set_state(|state| state.message = "hello world".into());
        self.fetch_data();
    }

    /// Invoked when the component is removed from the DOM tree.
    fn destroyed(&self) {}
}

// #[wasm_bindgen]
// pub struct FetchTask(Closure<FnMut()>);

#[wasm_bindgen]
extern "C" {
    fn setTimeout(closure: &Closure<FnMut()>, time: u32);

    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

impl MyApp {
    fn fetch_data(&self) -> Promise {
        let setter = self.state_setter();
        let mut opts = RequestInit::new();
        opts.method("GET");
        opts.mode(RequestMode::Cors);

        let request = Request::new_with_str_and_init(
            "https://api.github.com/repos/rustwasm/wasm-bindgen/branches/master",
            &opts,
        )
        .unwrap();

        request
            .headers()
            .set("Accept", "application/vnd.github.v3+json")
            .unwrap();

        let window = web_sys::window().unwrap();
        let request_promise = window.fetch_with_request(&request);

        let closure: Closure<dyn FnMut(JsValue)> = Closure::wrap(Box::new(move |b| {
            handler(b);
        }));

        let promise = request_promise.then(&closure);
        closure.forget();

        promise

        // let future = JsFuture::from(request_promise)
        //     .and_then(|resp_value| {
        //         // `resp_value` is a `Response` object.
        //         assert!(resp_value.is_instance_of::<Response>());
        //         let resp: Response = resp_value.dyn_into().unwrap();
        //         resp.json()
        //     })
        //     .and_then(|json_value: Promise| {
        //         // Convert this other `Promise` into a rust `Future`.
        //         JsFuture::from(json_value)
        //     })
        //     .and_then(|json| {
        //         // state.set_state(|state| state.message = string);
        //         // Use serde to parse the JSON into a struct.
        //         let branch_info: Branch = json.into_serde().unwrap();

        //         log(&format!("{:?}", branch_info));

        //         // Send the `Branch` struct back to JS as an `Object`.
        //         future::ok(JsValue::NULL)
        //     });

        // // Convert this Rust `Future` back into a JS `Promise`.
        // future_to_promise(future)
    }
}

pub fn handler(b: JsValue) {
    let resp: Response = b.dyn_into().unwrap();
    log(&resp.status_text());
    log("......");
}

#[wasm_bindgen]
pub fn run() {
    App::<MyApp>::new().mount("app");
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Branch {
    pub name: String,
    pub commit: Commit,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Commit {
    pub sha: String,
    pub commit: CommitDetails,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CommitDetails {
    pub author: Signature,
    pub committer: Signature,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Signature {
    pub name: String,
    pub email: String,
}

I've tried this way, it works, handle get called.

but still no way to update MyApp.messge using the result json from promise.

zengsai commented 6 years ago
#![feature(proc_macro_hygiene, decl_macro)]

use futures::{future, Future};
use js_sys::Promise;
use ruukh::prelude::*;
use serde_derive::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::future_to_promise;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};

#[component]
struct MyApp {
    #[state]
    message: String,
    #[state]
    seconds: i32,
}

impl Render for MyApp {
    fn render(&self) -> Markup<Self> {
        html! {
            <h1>"Todo Apps"</h1>
            <p>"message:" {&self.message}</p>
        }
    }
}

/// The lifecycle of a stateful component.
///
/// When you do not require these lifecycle hooks, you may implement them with
/// an auto derive `#[derive(Lifecycle)]` on the component struct.
impl Lifecycle for MyApp {
    fn mounted(&self) {
        self.fetch_data();
    }

    /// Invoked when the component is removed from the DOM tree.
    fn destroyed(&self) {}
}

// #[wasm_bindgen]
// pub struct FetchTask(Closure<FnMut()>);

#[wasm_bindgen]
extern "C" {
    fn setTimeout(closure: &Closure<FnMut()>, time: u32);

    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

impl MyApp {
    fn fetch_data(&self) -> Promise {
        let setter = self.state_setter();
        let mut opts = RequestInit::new();
        opts.method("GET");
        opts.mode(RequestMode::Cors);

        let request = Request::new_with_str_and_init(
            "https://api.github.com/repos/rustwasm/wasm-bindgen/branches/master",
            &opts,
        )
        .unwrap();

        request
            .headers()
            .set("Accept", "application/vnd.github.v3+json")
            .unwrap();

        let window = web_sys::window().unwrap();
        let request_promise = window.fetch_with_request(&request);

        let future = JsFuture::from(request_promise)
            .and_then(|resp_value| {
                // `resp_value` is a `Response` object.
                assert!(resp_value.is_instance_of::<Response>());
                let resp: Response = resp_value.dyn_into().unwrap();
                resp.json()
            })
            .and_then(|json_value: Promise| {
                // Convert this other `Promise` into a rust `Future`.
                JsFuture::from(json_value)
            })
            .and_then(|json| {
                // Use serde to parse the JSON into a struct.
                let branch_info: Branch = json.into_serde().unwrap();

                let s = setter;
                s.set_state(|state| state.message = format!("{:?}", branch_info));

                log(&format!("{:?}", branch_info));

                // Send the `Branch` struct back to JS as an `Object`.
                future::ok(JsValue::NULL)
            });

        // Convert this Rust `Future` back into a JS `Promise`.
        future_to_promise(future)
    }
}

#[wasm_bindgen]
pub fn run() {
    App::<MyApp>::new().mount("app");
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Branch {
    pub name: String,
    pub commit: Commit,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Commit {
    pub sha: String,
    pub commit: CommitDetails,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CommitDetails {
    pub author: Signature,
    pub committer: Signature,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Signature {
    pub name: String,
    pub email: String,
}

Finally, It works!

zengsai commented 6 years ago

I'd like to make it a fetch api for Ruukh.

pepsighan commented 6 years ago

@zengsai Yeah, you need to get a state_setter before you can mutate the state within a closure.

I'd like to make it a fetch api for Ruukh.

Sorry, I did not understand what you mean. It seems you are successful in using a fetch api within Ruukh.

zengsai commented 6 years ago

I mean I already write this fetch api as a service mod in my project. like FetchService in Yew project, my code like this:

/// impl Lifecycle for Deities {
///     fn created(&self) {
///         self.set_state(|state| {
///             state.input = "Hello".into();
///         })
///     }
///     fn mounted(&self) {
///         let url = "http://0.0.0.0:3000/login";
///
///         let params = LoginParam {
///             shop_id: 123,
///             password: "hello".into(),
///         };
///
///         let state_for_data = self.state_setter();
///
///         let _ = post(url, params, move |data| {
///             state_for_data.set_state(|state| {
///                 LoginInfo::from_resp(&data)
///                     .map(|info| state.message = info.token)
///                     .unwrap_or_else(|error| {
///                         state.error = error.message();
///                     });
///             })
///         })
///         .map_err(|e| {
///             self.set_state(|state| state.error = e.message());
///         });
///     }
/// }
pub fn post<H>(url: &str, params: impl Params, handler: H) -> Result<Promise, Error>
where
    H: FnMut(String) + 'static,
{
    let mut opts = RequestInit::new();
    opts.method("POST").mode(RequestMode::Cors);

    params
        .fill_body(&mut opts)
        .context(ErrorKind::Initailization)?;

    let request = Request::new_with_str_and_init(url, &opts)
        .map_err(|_| Error::from(ErrorKind::Initailization))?;
    request
        .headers()
        .set("Content-Type", "application/json")
        .map_err(|_| Error::from(ErrorKind::Initailization))?;

    let window = web_sys::window().ok_or(Error::from(ErrorKind::Initailization))?;;

    fetch(&window, &request, handler)
}

pub trait Params: Serialize {
    fn fill_body<'b>(&self, opts: &'b mut RequestInit) -> serde_json::Result<&'b mut RequestInit> {
        let s = serde_json::to_string(self)?;
        opts.body(Some(&JsValue::from_str(&s)));
        Ok(opts)
    }
}

impl<T> Params for T where T: Serialize {}

fn fetch<H>(window: &Window, request: &Request, mut handler: H) -> Result<Promise, Error>
where
    H: FnMut(String) + 'static,
{
    use std::any::Any;
    let request_promise = window.fetch_with_request(&request);

    let future = JsFuture::from(request_promise)
        .and_then(|resp_value| {
            // `resp_value` is a `Response` object.
            assert!(resp_value.is_instance_of::<Response>());
            let resp: Response = resp_value.dyn_into().unwrap();

            if resp.ok() {
                resp.text()
            } else {
                Ok(Promise::reject(&JsValue::from(resp.status_text())))
            }
        })
        .and_then(|js_value: Promise| {
            // Convert this other `Promise` into a rust `Future`.
            JsFuture::from(js_value)
        })
        .or_else(move |error| {
            // AbortError   The request was aborted (using AbortController.abort()).
            // TypeError    Since Firefox 43, fetch() will throw a TypeError if the URL has credentials, such as http://user:password@example.com.
            // CustomError  String type from this model.

            let message = error.as_string().unwrap_or("网络通信错误".into());
            log(&format!(
                "FetchError: {:?} {}",
                error.get_type_id(),
                message
            ));

            let error = format!(r#"{{"error": {{ "message": "{}"}}}}"#, message);
            future::ok(JsValue::from_str(error.as_str()))
        })
        .and_then(move |text| {
            let resp_text = text
                .as_string()
                .unwrap_or(r#"{{"error": {{ "message": "返回数据不是文本"}}}}"#.into());

            log(&format!("FetchSuccess: {}", resp_text));
            handler(resp_text);
            future::ok(JsValue::null())
        });

    // Convert this Rust `Future` back into a JS `Promise`.
    Ok(crate::future_to_promise(future))
}

But it not ready to push it, right now it only handle json response, I need more time to make it a better and more generic service.

And I don't known if this kind of thing (it belong to data layer) is proper for this project, but we MUST have it in real project.

pepsighan commented 6 years ago

Are you trying to say that you would like to have a Service-like thing within Ruukh? If that is the question then I think it would be better in a third-party library as it is pretty much framework agnostic.

If that was not your question, please write it in simple words what you are trying to achieve with Ruukh in a new issue (probably because it is a different issue).

Also, this issue has already been answered by you, so closing it.

#[derive(Debug, Serialize, Deserialize)]
pub struct Signature {
    pub name: String,
    pub email: String,
}

Finally, It works!

zengsai commented 6 years ago

Yes, I mean "I would like to have a Service-like thing within Ruukh", a fetching Api for real project to communicate with backend server. @csharad

I can read English will, but not good at writing English, so ..., pardon me!

pepsighan commented 6 years ago

@zengsai That's fine :+1: .

But it still stands that a Service API is to be provided in a separate crate as the scope of this library is only framework specific things.