Cookies
What we'll learn
- What cookies are
- How we can set them
- How we can detect cookie changes
What's missing
- Cookie paths, lifetimes, and advanced configuration
- Type safety guarantees for non-string types
- Session and persistant cookie types
Caveat
There are many additional options available when setting cookies. This lesson is intended to give you a cursory understanding of how they're written and stored but it is not exhaustive.
The lesson
Cookies are a client storage tool. We can create a cookie with a specific name and assign it a string value. Recall that many numeric (int, float, bool) or complex (structs) can be represented as strings through the user of the characters that make up their numbers, or through serialization.
One important thing to remember is that cookies are sent as part of each web request for your application's domain.
Writing to a cookie
To exemplify how we can write to cookies we'll build a little text to cookie value input box. Typing in it will update a corresponding cookie.
Capturing Input
We'll start with a simple Leptos application component called "App" which contains a text field.
use leptos::*; fn main() { mount_to_body(|cx| { view! { cx, <App /> } }) } #[component] fn App(cx: Scope) -> Element { view! { cx, <div> <input name="my_input" type="text" /> </div> } }
Now let's udpate it with that on key up event handler.
use leptos::*; use web_sys::KeyboardEvent; fn main() { mount_to_body(|cx| { view! { cx, <App /> } }) } #[component] fn App(cx: Scope) -> Element { let write_value_to_cookie = |e:KeyboardEvent|{ leptos::log!("You pressed a key!"); }; view! { cx, <div> <input name="cookie_input" type="text" placeholder="Type text and I'll update a cookie!" on:keyup=write_value_to_cookie /> </div> }}
I used the keyup
event becuase change
events only fire when focus is taken away from the input area (you click elsewhere, press tab, or esc), and keydown
will only fire when a key is pressed but that happens before the value of the input field is updated. If we did that we'd always be one key stoke behind.
Note that we imported the KeyboardEvent from web_sys with:
#![allow(unused)] fn main() { use web_sys::KeyboardEvent; }
And we added that type to the event handler closure.
#![allow(unused)] fn main() { let write_value_to_cookie = |e: KeyboardEvent| { // ... }; }
Now we'll pull the value out of the keyboard event and log it to the console.
#![allow(unused)] fn main() { let write_value_to_cookie = |e: KeyboardEvent| { let input: HtmlInputElement = e.target() .unwrap() .unchecked_into(); leptos::log!("{:?}", input.value()); }; }
e.target()
, returns a result type that we know will have aTargetElement
. We call unwrap to get the value out of theResult
. Then we callunchecked_into()
on theTargetElement
type. Rust will see that we specified the destination type forinput
asHtmlInputElement
. It will use this as the type parameter forunchecked_into()
, casting theTargetElemenet
as aHtmlInputElement
. This is identical to not providing a type forinput
and writing,unchecked_into::<HtmlInputElement>()
, using the turbofish syntax (::<>
).
For the above to work, we need to bring the HtmlInputElement
struct into scope as well with an updated use statement. We can use the destructuring syntax to update our previous statement.
#![allow(unused)] fn main() { use web_sys::{KeyboardEvent, HtmlInputElement}; }
Writing to cookies
The browser's cookie api is a property of the document object. If we want to call things on document, we'll need to use web_sys to get a reference to it. Thankfully Leptos provides a function which allows us to grab the document.
#![allow(unused)] fn main() { let document = document(); }
It's recommended to use this Leptos function over web_sys because Leptos will store the reference in WASM to improve performance.
We'll need to give Rust a bit of help here to identify the type.
#![allow(unused)] fn main() { let doc: HtmlDocument = document().unchecked_into(); }
The struct HtmlDocument
is hidden behind a feature flag. To enable this feature we can add the following to our cargo.toml
.
[dependencies.web-sys]
features = [ "HtmlDocument" ]
We can now extract our cookie data from the the HtmlDocument
. I had a quick look over at the web_sys
documentation to confirm which method exists on the HtmlDocument struct that'll allow me to set a cookies value. It's set_cookie() We can get the cookie value via get_cookie() too.
#![allow(unused)] fn main() { doc.set_cookie("some data"); doc.set_cookie("my-key=first-value"); doc.set_cookie("my-key=second-value"); let cookie = doc.cookie().unwrap(); leptos::log!("{:?}", cookie ); }
The above prints my-key=second-value; some data=
to the console.
What's interesting about set_cookie and the cookie api, is that it will parse the key name and value to make sure the correct cookie is updated.
Reading cookies
As we've seen above, we can read the complete text that makes up the cookie value by calling cookie()
on an HtmlDocument
.
#![allow(unused)] fn main() { let doc: HtmlDocument = document().unchecked_into(); let raw_cookie_data = doc.cookie().unwrap_or_default(); }
We'll need to parse that string into actual key=>value pairs.
We'll first need to split these into individual cookies. We saw that the delimeter was a semicolon and space. We can call split()
on the raw cookie data to break the string up.
Then we call collect() to turn it into an vector.
#![allow(unused)] fn main() { let kvp_strings: Vec<&str> = raw_cookie_data .split("; ") .collect(); }
This provides us with an array of key value pair strings.
#![allow(unused)] fn main() { ["my-key=second-value", "some data="] }
We need to add another step here. We need to split those strings by =
. We'll add a map method call after the split. The map method will apply a function to each item. In this case, we're spliting the strings and returning the result as a Vec<&str>
, a vector of string references.
#![allow(unused)] fn main() { let raw_cookie_data: String = doc .cookie() .unwrap_or_default(); let key_value_pairs: Vec<Vec<&str>> = cookie .split("; ") .map(|kvp_string|{ kvp_string.split('=').collect() }) .collect(); leptos::log!("{:?}", key_value_pairs ); }
We now have a multidimensional array but it's not very usable. We can't check to see if a value is set. Let's turn this multidimensional vector into a hash map.
We need to add a use statement to import the HashMap
type into scope.
We'll update the cookies type to HashMap
with two type parameters for the key type and value type. In this case they're both string slices.
#![allow(unused)] fn main() { use std::collections::HashMap; let raw_cookie_data: String = doc .cookie() .unwrap_or_default(); let cookies: HashMap<&str, &str> = raw_cookie_data .split("; ") .map(|kvp_string|{ kvp_string.split('=').collect() }) .collect(); leptos::log!("{:?}", key_value_pairs ); }
Now we'll turn our attention to the body of the map function which processes kvp_string
We're starting with this:
#![allow(unused)] fn main() { kvp_string.split('=').collect() }
This would split a string into an vector of strings, cut at each '=' character.
HashMaps can be made by providing a vector of tuples with key value pairs. We can use split_at
to provide a tuple.
#![allow(unused)] fn main() { .map(|kvp_string|{ kvp_string.split_at( kvp_string.find("=").unwrap_or_default() ) } }
Running the map with the above body provides the following results:
("my-key", "=second-value"),
("some data", "=")
Look like we need to do another transformation on these rows. We'll add another map which turns these tuples into tuples that have the first character removed from the values.
#![allow(unused)] fn main() { .map(|kvp_tuple|{ (kvp_tuple.0, &kvp_tuple.1[1..]) }) }
We're accessing the first and second elements of the tuple with
.0
and.1
. By using the spread operator on the second value of the tuple (index1
), we're able to tell the compiler, "take from the 1st position onward," which ignores position 0 of the esequence which is the "=" symbol.
It is worth noting that these two values are wrapped in parenthesis and will be returned as
(&str, &str)
type.
We can now use HashMap methods to interact with the cookie data.
#![allow(unused)] fn main() { leptos::log!( "{:?}", cookies.get("my-key").unwrap_or(&"") ); }
Here we log the value stored at "my-key" in the
cookies
HashMap
. This returns aSome(&str)
type. We can unwrap this to make it either the string slice&str
or a reference to an empty string slice, which is also of type&str
The finished code
use leptos::*; use web_sys::{KeyboardEvent, HtmlInputElement, HtmlDocument}; use std::collections::HashMap; fn main() { mount_to_body(|cx| { view! { cx, <App /> } })} #[component] fn App(cx: Scope) -> Element { let write_value_to_cookie = |e: KeyboardEvent| { let input: HtmlInputElement = e.target().unwrap().unchecked_into(); let doc: HtmlDocument = document().unchecked_into(); let cookie_key = "my-cookie"; let cookie_data = vec!(cookie_key, &input.value() ).join("="); doc.set_cookie(&cookie_data); // Parse cookie data and log it out let cookie: String = doc.cookie().unwrap_or_default(); let key_value_pairs: HashMap<&str, &str> = cookie .split("; ") .map(|kvp_string|{ kvp_string.split_at( kvp_string.find("=").unwrap_or_default() ) }) .map(|kvp_tuple|{ (kvp_tuple.0, &kvp_tuple.1[1..]) }) .collect(); leptos::log!("{:?}", key_value_pairs.get(cookie_key).unwrap_or(&"") ); }; view! { cx, <div> <input name="cookie_input" type="text" placeholder="Type text and I'll update a cookie!" on:keyup=write_value_to_cookie /> </div> } }