Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consistent history access via gloo::history #166

Closed
futursolo opened this issue Nov 21, 2021 · 7 comments · Fixed by #178
Closed

Consistent history access via gloo::history #166

futursolo opened this issue Nov 21, 2021 · 7 comments · Fixed by #178
Labels
enhancement New feature or request

Comments

@futursolo
Copy link
Collaborator

Summary

Short overview of the proposal.

Motivation

Why are we doing this? What problems does it solve?

There's a JavaScript package history which provides a consistent API to access Session History in both browser and non-browser environment.

This is currently implemented in yew-router, but I think it might be a better fit for gloo. This issue proposes to upstream the history implementation to gloo.

Detailed Explanation

There're 2 traits that will be introduced:

  • History (API similar to window.history)
  • Location (API similar to window.location)

There're a couple flavours of History and Location:

  • BrowserHistory and BrowserLocation (history managed via window.history.pushState)
  • HashHistory and HashLocation (history managed via window.history.pushState, but path is stored in location.hash)
  • MemoryHistory and MemoryLocation (used in environment where Web API is not available)
  • AnyHistory and AnyLocation (a enum that helps to access history and location when it's variant is not known.)
pub trait History {
    type Location: Location<History = Self> + 'static;

    /// Returns the number of elements in [`History`].
    fn len(&self) -> usize;

    /// Returns true if the current [`History`] is empty.
    fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// Moves back 1 page in [`History`].
    fn back(&self) {
        self.go(-1);
    }

    /// Moves forward 1 page in [`History`].
    fn forward(&self) {
        self.go(1);
    }

    /// Loads a specific page in [`History`] with a `delta` relative to current page.
    ///
    /// See: <https://developer.mozilla.org/en-US/docs/Web/API/History/go>
    fn go(&self, delta: isize);

    /// Pushes a route with [`None`] being the state.
    fn push(&self, route: impl Into<String>);

    /// Replaces the current history entry with provided route and [`None`] state.
    fn replace(&self, route: impl Into<String>);

    /// Pushes a route entry with state.
    ///
    /// The implementation of state serialization differs between [`History`] types.
    ///
    /// For [`BrowserHistory`], it uses [`serde_wasm_bindgen`] where as other types uses
    /// [`Any`](std::any::Any).
    fn push_with_state<T>(&self, route: impl Into<String>, state: T) -> HistoryResult<()>
    where
        T: Serialize + 'static;

    /// Replaces the current history entry with provided route and state.
    ///
    /// The implementation of state serialization differs between [`History`] types.
    ///
    /// For [`BrowserHistory`], it uses [`serde_wasm_bindgen`] where as other types uses
    /// [`Any`](std::any::Any).
    fn replace_with_state<T>(&self, route: impl Into<String>, state: T) -> HistoryResult<()>
    where
        T: Serialize + 'static;

    /// Same as `.push()` but affix the queries to the end of the route.
    fn push_with_query<Q>(&self, route: impl Into<String>, query: Q) -> HistoryResult<()>
    where
        Q: Serialize;

    /// Same as `.replace()` but affix the queries to the end of the route.
    fn replace_with_query<Q>(&self, route: impl Into<String>, query: Q) -> HistoryResult<()>
    where
        Q: Serialize;

    /// Same as `.push_with_state()` but affix the queries to the end of the route.
    fn push_with_query_and_state<Q, T>(
        &self,
        route: impl Into<String>,
        query: Q,
        state: T,
    ) -> HistoryResult<()>
    where
        Q: Serialize,
        T: Serialize + 'static;

    /// Same as `.replace_with_state()` but affix the queries to the end of the route.
    fn replace_with_query_and_state<Q, T>(
        &self,
        route: impl Into<String>,
        query: Q,
        state: T,
    ) -> HistoryResult<()>
    where
        Q: Serialize,
        T: Serialize + 'static;

    /// Creates a Listener that will be notified when current state changes.
    ///
    /// This method returns a [`HistoryListener`] that will automatically unregister the callback
    /// when dropped.
    fn listen<CB>(&self, callback: CB) -> HistoryListener
    where
        CB: Fn() + 'static;

    /// Returns the associated [`Location`] of the current history.
    fn location(&self) -> Self::Location;

    fn into_any_history(self) -> AnyHistory;

    /// Returns the State.
    ///
    /// The implementation differs between [`History`] type.
    ///
    /// For [`BrowserHistory`], it uses [`serde_wasm_bindgen`] where as other types uses
    /// `downcast_ref()` on [`Any`](std::any::Any).
    fn state<T>(&self) -> HistoryResult<T>
    where
        T: DeserializeOwned + 'static;
}

pub trait Location {
    type History: History<Location = Self> + 'static;

    /// Returns the `pathname` on the [`Location`] struct.
    fn pathname(&self) -> String;

    /// Returns the queries of current URL in [`String`]
    fn search(&self) -> String;

    /// Returns the queries of current URL parsed as `T`.
    fn query<T>(&self) -> HistoryResult<T>
    where
        T: DeserializeOwned;

    /// Returns the hash fragment of current URL.
    fn hash(&self) -> String;
}

Full Implementation: https://github.com/yewstack/yew/blob/master/packages/yew-router/src/history.rs

Drawbacks, Rationale, and Alternatives

Does this design have drawbacks? Are there alternative approaches? Why is this
design the best of all designs available?

This is a design that is familiar to anyone that have used browser's native history management API and is also used by reacr-router.

What prior art exists? There are many good sources of inspiration: Ember, React,
Angular, Vue, Knockout, jQuery, Closure, Elm, Emscripten, ClojureScript,
Polymer, etc..

https://github.com/remix-run/history

Unresolved Questions

What is not clear yet? What do we expect to clarify through implementation
and/or usage experience?

@ranile ranile added the enhancement New feature or request label Nov 21, 2021
@ranile
Copy link
Collaborator

ranile commented Nov 21, 2021

We should add this. This would also be beneficial for anyone writing a router for any framework/library.

Where should this be added? Should this have it's own crate or be part of gloo_utils?

@futursolo
Copy link
Collaborator Author

I think this should be in it's own crate. For the following reasons:

  1. It's not small (maybe 1000+ lines for the completed implementation?).
  2. It's not common that you will be using this directly if you are using a framework (developers should use the API provided by the router they use).
  3. This has a slightly different API than web_sys::History and web_sys::Location and is geared towards SPA which strips any API that would redirect / causes the page to reload.

@ranile
Copy link
Collaborator

ranile commented Nov 21, 2021

I see. Let's make a new gloo-history crate. Would you like to PR the implementation?

@futursolo
Copy link
Collaborator Author

Yes, I will file a PR.

However, I am a little busy at the moment. I will put together a pull request when I have time.
(The Hash and Memory variant are not implemented in yew-router and I will also implement them before filing the PR.)

@ranile
Copy link
Collaborator

ranile commented Nov 21, 2021

If you want, you can file a PR only with what's currently implemented. That will get the initial setup going and will subsequent PRs to be smaller

@futursolo
Copy link
Collaborator Author

Sounds like a plan.

Currently, BrowserHistory has some edge cases where it cannot correctly shadow the history API and causes the page to reload (e.g: history.go(0);).

I will fix these edge cases properly before filing the PR and will limit initial PR to BrowserHistory and BrowserLocation.

@ranile
Copy link
Collaborator

ranile commented Nov 21, 2021

Great, I'm looking forward to it.

Btw, you can file the PR as draft and and work on it to fix the edge cases

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants