maciejhirsz / kobold

Easy declarative web interfaces.
https://docs.rs/kobold/
Mozilla Public License 2.0
385 stars 7 forks source link

Unsure if its possible to use conditional statement within a view! or if its possible to branch within a loop #60

Closed ltfschoen closed 1 year ago

ltfschoen commented 1 year ago

I've created this for loop here, code snippet:

    for index in 0..data.len() {
        if state.entry[index].editing == true {
            return Branch2::A(view! {
                <DetailEditing {index} {data} {placeholders_file} {state} />
            });
        }
        Branch2::B(view! {
            <DetailView {index} {data} {state} />
        })
    }

It iterates through the number of properties in data variable. for context, data is a vector of tuples Vec<(String, String)>, where each tuple contains the property key and property value that was converted from the Details struct here from default state values or from loading the details.csv file (also see unit test of how that was done here)

Then I want to do the conditional branching from the docs. So to make the code clearer I've created a: *DetailEditing component (which is to only render if state.entry[index].editing == true

The problems that I'm facing is: 1) It gives error:

click to show error ```bash error[E0308]: mismatched types --> examples/invoice/src/main.rs:389:9 | 311 | fn DetailView(index: usize, data: Vec<(String, String)>, state: &Hook) -> impl View + '_ { | -------------- the found opaque type ... 389 | / Branch2::B(view! { 390 | | 391 | | }) | |__________^ expected `()`, found enum `Branch2` | = note: expected unit type `()` found enum `Branch2<_, impl kobold::View + '_>` help: consider using a semicolon here | 391 | }); | + help: you might have meant to return this value | 389 ~ return Branch2::B(view! { 390 | 391 ~ }); | error[E0308]: mismatched types --> examples/invoice/src/main.rs:383:5 | 255 | fn DetailEditing(index: usize, data: Vec<(String, String)>, placeholders_file: Vec, state: &Hook) -> impl View + '_ { | -------------- the expected opaque type ... 328 | fn EntryView<'a>(state: &'a Hook) -> impl View + 'a { | -------------- expected `Branch2` because of return type ... 383 | / for index in 0..data.len() { 384 | | if state.entry[index].editing == true { 385 | | return Branch2::A(view! { 386 | | ... | 391 | | }) 392 | | } | |_____^ expected enum `Branch2`, found `()` | = note: expected enum `Branch2` found unit type `()` help: try wrapping the expression in `kobold::branching::Branch2::B` | 383 ~ kobold::branching::Branch2::B(for index in 0..data.len() { 384 | if state.entry[index].editing == true { ... 391 | }) 392 ~ }) ```

data.len() equals 10, since there are 10 columns of data being loaded from the details.csv file. And so in this case I think I need to use branching with #[component] and Branch9 instead of Branch2, but the max branch I can use is Branch9, and even if I only had 9 columns of data, and used Branch9, I then need to use each of A,B,C,D,E,F,G,H,I for each of the 9 times that I loop through with for index in 0..data.len() { (i.e. Branch9::A, Branch9::B, ..., Branch9::I), so I need to fit 9 branches in 2 blocks, since I only have an if and an else block to put them in... So I could remove the outer loop for index in 0..data.len() { and just manually add a branch for every possibility like the following, but that seems like an extreme measure, and again, I'm not sure whether I need Branch9 or if Branch2 is actually sufficient in this scenerio and I'm just using it wrong. And to fully cater for indexes 0 to 3 I have to use Branch8, since if I use Branch9 I can only cater for an ::I branch state.entry[4].editing == true but not a ::J branch to cater for state.entry[4].editing == false, but I can use ::J to cater for the else at the end.

    if state.entry[0].editing == true {
        return Branch9::A(view! { <DetailEditing index={0} {data} {placeholders_file} {state} /> });
    } else if state.entry[0].editing == false {
        return Branch9::B(view! { <DetailView index={0} {data} {state} /> });
    } else if state.entry[1].editing == true { 
        return Branch9::C(view! { <DetailEditing index={1} {data} {placeholders_file} {state} /> });
    } else if state.entry[1].editing == false {
        return Branch9::D(view! { <DetailView index={1} {data} {state} /> });
    } else if state.entry[2].editing == true { 
        return Branch9::E(view! { <DetailEditing index={2} {data} {placeholders_file} {state} /> });
    } else if state.entry[2].editing == false {
        return Branch9::F(view! { <DetailView index={2} {data} {state} /> });
    } else if state.entry[3].editing == true { 
        return Branch9::G(view! { <DetailEditing index={3} {data} {placeholders_file} {state} /> });
    } else if state.entry[3].editing == false {
        return Branch9::H(view! { <DetailView index={3} {data} {state} /> });
    } else {
        return Branch9::I(view! { <div>{ "branch not configured" }</div> });
    }

Whilst that compiles, it only renders one of the input fields instead of all of them.

So then I reverted my code to the way it was before doing all this: But that only works when I change if state.entry[index].editing == true { and let editing = class!("editing" if state.entry[index].editing); to have hard-coded values of index like 0, but then it triggers out of bounds errors as expected since I'm not sure how to move the if statement inside the view!

if state.entry[index].editing == true {
    Branch2::A(view! {
        <div>
            {
                for (0..data.len()).map(move |index|
                    view! {
                        <div.edit>
                            { data[index].1.clone() }
                            <input.edit
                                value={ data[index].1.clone() }
                                type="text"
                                placeholder={ format!("<Enter {:#?}>", placeholders_file[index]) }
                                data_index={ index.to_string() }
                                onchange={
                                    state.bind(move |state, e: Event<InputElement>| {
                                        if let Some(data_index) = e.target().get_attribute("data_index") {
                                            let index: usize = data_index.parse::<usize>().unwrap();
                                            state.details.table.rows[0][index] = Text::Owned(e.target().value().into());
                                            state.entry[index].editing = false;
                                        }
                                    })
                                }
                                onmouseover={
                                    |e: MouseEvent<InputElement>| e.target().focus()
                                }
                                onkeypress={
                                    state.bind(move |state, e: KeyboardEvent<InputElement>| {
                                        if e.key() == "Enter" && e.target().value() != "" {
                                            state.update(index, e.target().value());

                                            Then::Render
                                        } else {
                                            Then::Stop
                                        }
                                    })
                                }
                                onkeypress={
                                    state.bind(move |state, e: KeyboardEvent<InputElement>| {
                                        if e.key() == "Enter" && e.target().value() != "" {
                                            state.update(index, e.target().value());

                                            Then::Render
                                        } else {
                                            Then::Stop
                                        }
                                    })
                                }
                                onblur={
                                    state.bind(move |state, e: Event<InputElement>| {
                                        if e.target().value() != "" {
                                            state.update(index, e.target().value())
                                        }
                                    })
                                }
                            />
                        </div>
                    }
                )
            }
        </div>
    })
} else {
    let editing = class!("editing" if state.entry[index].editing);

    Branch2::B(view! {
        <div>
            {
                for (0..data.len()).map(move |index|
                    view! {
                        <div .details.{editing}>
                            <div .view>
                                <label
                                    ondblclick={
                                        state.bind(move |s, _| {
                                            // s.editing = Cell { index, 0 };
                                            s.edit_entry(index);
                                            // s.entry[index].editing = true;
                                        })
                                    }
                                >
                                    { data[index].1.clone() }
                                </label>
                            </div>
                        </div>
                    }
                )
            }
        </div>
    })
}

Sorry for the convoluted post but any tips on whether it's possible to have an if statement inside for (0..data.len()).map(move |index|, and whether it's bad practice to have a loop and the condition outside a Branch like:

for index in 0..data.len() {
  if state.entry[index].editing == true {
    Branch2::A(view! {

I'm going to revert back to using Cell and Head in the interim.

maciejhirsz commented 1 year ago

Your loop doesn't work since loops aren't iterators.

Note that for in { for (0..data.len()).map(...) } is not a for loop, it's a keyword that turns an iterator into a view.

If you need to branch inside an iterator, do it inside the map closure:

for (0..data.len()).map(move |index| {
    if condition {     
        Branch2::A(view! {})
    } else {
        Branch2::B(view! {})
    }
})

Also couple notes:

Use iterators

Idiomatic way to iterate over entries in a vec or slice is:

/// loop:
for entry in data.iter() { ... }

/// mapping iterator:
data.iter().map(|entry| { ... })

If you need the entry and index use enumerate:

/// loop:
for (index, entry) in data.iter().enumerate() { ... }

/// mapping iterator:
data.iter().enumerate().map(|(index, entry)| { ... })

Not only is this cleaner and less bug prone, it can also potentially compile to faster code since it guarantees that bounds checks are unnecessary.

Don't do unnecessary clones

I know people do these a lot when they learn the language, but after using the language for a long time it hurts my soul when I see an unnecessary clone.

// this makes a new `String` which allocates and copies content:
data[index].1.clone()
// instead this just grabs a `&String` reference:
&data[index].1
// even better, this grabs a `&str` reference, which has less pointer indirection:
data[index].1.as_str()

// and of course if you switch to using iterator over data it would be:
entry.1.as_str()

You don't need to turn integers to strings in Kobold

This is related to the above note, Kobold automatically stringifies all attributes that aren't known to it, so:

// this:
data_index={ index.to_string() }
// is just a very inefficient way of doing this:
data_index={ index }

Note: using an undefined attribute like this will become a compile error soon, though I now realize I need to add support for data attributes.

Instead of passing data via attributes, just capture them in closures

Instead of this:

<input.edit
    // ...
    data_index={ index.to_string() }
    onchange={
        state.bind(move |state, e: Event<InputElement>| {
            if let Some(data_index) = e.target().get_attribute("data_index") {
                let index: usize = data_index.parse::<usize>().unwrap();
                state.details.table.rows[0][index] = Text::Owned(e.target().value().into());
                state.entry[index].editing = false;
            }
        })
    }
    // ...

Just do this:

<input.edit
    // ...
    onchange={
        // The `move` keyword in front of a closure means that captured values
        // are moved (or copied if they implement `Copy`, which `usize` does)
        // inside the closure.
        //
        // Event handlers need to live for a `'static` lifetime, or in simpler
        // terms they need to "own" their content. If you didn't put the `move`
        // here the `index` inside would be borrowed (`&usize` instead of `usize`),
        // and the code would fail to compile.
        state.bind(move |state, e: Event<InputElement>| {
            // Since there is no `index` passed in as a parameter in this closure,
            // it's moved (copied) inside it from parent scope.
            state.details.table.rows[0][index] = Text::Owned(e.target().value().into());
            state.entry[index].editing = false;
        })
    }
    // ...

Edit:

If you are curious about performance of closures and how this all works internally on re-renders in Kobold: this onchange closure will be two "words" in size (one for bound state, one for index). In wasm32 that just means 8 bytes (2 * 32 bits). The actual JavaScript callback is created once on the initial render. Subsequent renders just move the new closure into the memory allocated for the previous closure:

https://github.com/maciejhirsz/kobold/blob/7679026592aae45f08a5ecee0a7bb4d3aab70aab/crates/kobold/src/event.rs#L110-L112

There is no diffing as this doesn't interact with JS/DOM side at all, and the difference between having to copy 4 bytes (just the state reference) and 8 bytes (state + index) is so small it's immaterial.

There is going to be a small performance difference between bound closures and ones that don't capture anything, namely this handler:

onmouseover={|e: MouseEvent<InputElement>| e.target().focus()}

Is zero-sized (it's a plain/unbound closure that doesn't capture anything). Internally it's still put into a Box, but Rust is smart enough to know that it doesn't need to allocate memory for something that takes no memory to represent to begin with, so the initial render is going to be faster. The memory use isn't a problem, it's just that creating allocations (be it a Box<T>, a Vec<T>, or a String) is relatively "expensive".

If you look at the update here again:

https://github.com/maciejhirsz/kobold/blob/7679026592aae45f08a5ecee0a7bb4d3aab70aab/crates/kobold/src/event.rs#L110-L112

The self takes no memory at all (is 0-bytes) == there is nothing to move == this compiles to nothing at all.

ltfschoen commented 1 year ago

thanks it compiled successfully when i changed it to:

let editing = class!("editing" if state.entry[0].editing);

view! {
    <div>
    {
        for (0..data.len()).map(move |index|
            if state.entry[0].editing == true {
                Branch2::A(view! {
                    <div.edit>
                      ...
                    </div>
                })
            } else {
                Branch2::B(view! {
                    <div .details.{editing}>
                        ...
                    </div>
                })
            }
        )
    }
    </div>
}

i tried your other recommendations in this commit https://github.com/ltfschoen/kobold/commit/41deda62736586a9d5312916df90f0b9fc4c95ad but got the following errors. i tried to resolve them but unfortunately even after re-reading chapters 1-4 of the rust book i'm still not able to grasp how to resolve it.

error[E0597]: `details` does not live long enough
   --> examples/invoice/src/main.rs:432:37
    |
413 |   #[component]
    |              - `details` dropped here while still borrowed
414 |   fn EntryView<'a>(state: &'a Hook<State>) -> impl View + 'a {
    |                -- lifetime `'a` defined here
...
432 |       let mut data = get_details_data(&details);
    |                                       ^^^^^^^^ borrowed value does not live long enough
...
472 | /     view! {
473 | |         <div>
474 | |         {
475 | |             for (0..(&data).len()).map(move |index|
...   |
550 | |         </div>
551 | |     }
    | |_____- opaque type requires that `details` is borrowed for `'a`

error[E0502]: cannot borrow `details` as mutable because it is also borrowed as immutable
   --> examples/invoice/src/main.rs:457:6
    |
414 |   fn EntryView<'a>(state: &'a Hook<State>) -> impl View + 'a {
    |                -- lifetime `'a` defined here
...
432 |       let mut data = get_details_data(&details);
    |                                       -------- immutable borrow occurs here
...
457 |       &details.apply(&dynamic_struct);
    |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
...
472 | /     view! {
473 | |         <div>
474 | |         {
475 | |             for (0..(&data).len()).map(move |index|
...   |
550 | |         </div>
551 | |     }
    | |_____- opaque type requires that `details` is borrowed for `'a`

error[E0597]: `details` does not live long enough
   --> examples/invoice/src/main.rs:461:29
    |
413 |   #[component]
    |              - `details` dropped here while still borrowed
414 |   fn EntryView<'a>(state: &'a Hook<State>) -> impl View + 'a {
    |                -- lifetime `'a` defined here
...
461 |       data = get_details_data(&details);
    |                               ^^^^^^^^ borrowed value does not live long enough
...
472 | /     view! {
473 | |         <div>
474 | |         {
475 | |             for (0..(&data).len()).map(move |index|
...   |
550 | |         </div>
551 | |     }
    | |_____- opaque type requires that `details` is borrowed for `'a`

error[E0382]: borrow of moved value: `data`
   --> examples/invoice/src/main.rs:475:21
    |
432 |     let mut data = get_details_data(&details);
    |         -------- move occurs because `data` has type `Vec<(&str, &str)>`, which does not implement the `Copy` trait
...
464 |     let (valid_labels, values): &(Vec<&str>, Vec<&str>) = &data.into_iter().unzip();
    |                                                                 ----------- `data` moved due to this method call
...
475 |             for (0..(&data).len()).map(move |index|
    |                     ^^^^^^^ value borrowed here after move
    |
note: `into_iter` takes ownership of the receiver `self`, which moves `data`
   --> /Users/luke/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/iter/traits/collect.rs:262:18
    |
262 |     fn into_iter(self) -> Self::IntoIter;
    |                  ^^^^
help: you can `clone` the value and consume it, but this might not be your desired behavior
    |
464 |     let (valid_labels, values): &(Vec<&str>, Vec<&str>) = &data.clone().into_iter().unzip();
    |                                                                 ++++++++
maciejhirsz commented 1 year ago

I'll have another look in the morning, this looks like you're trying to copy-paste JavaScript into Rust and change the syntax, and solving the wrong kind of problems with the wrong kind of solutions because JavaScript has a GC and you can freely copy references all over the place, while in Rust you can't.

I've no idea what the purpose of get_details_data is, as in why are you even pulling extra dependencies to iterate over fields of a struct instead of just making something you can iterate over to begin with?

You are also doing a lot of work inside a view that should be probably delegated to state. The compiler rightfully complains about you trying to borrow stuff out of a function scope outside of it's scope, basically this:

fn foo() -> &str {
    // This is a heap allocated string that this function owns
    let a: String = "foo".into();

    // Here we try to return a `&str` reference to `a`, but
    // `a` is being dropped (deallocated, deleted, gonezo)
    // at the end of this function, and so this doesn't compile.
    //
    // If it did compile, this would be a dangling pointer leading
    // to a lovely thing we call Undefined Behavior.
    a.as_str()
}

But this works:

fn foo(a: &String) -> &str {
    // this is just a simple reborrow
    a.as_str()
}

Or just this:

fn foo() -> String {
    let a: String = "foo".into();

    // _move_ `a` outside of `foo`, so now the caller owns it
    // and it's not going to be dropped at the end of the block
    a
}

If your component (or any function) gets something like &Hook<State> as a parameter, it can borrow from it and use borrowed values in its return type. You can't borrow from things you create inside a function scope, but you can return owned values you create.

There is one exception though, and that's stuff with a 'static lifetime, which includes all string literals. This code compiles fine:

fn foo() -> &'static str {
    "foo"
}

The string foo in this case is present in the binary blob of the program (be it Wasm or native), it "lives" for the entire life cycle of the program, and so it can be returned as a reference just fine without violating the borrow checker.

I recommend watching some videos on ownership to begin with, this one really worked for me back in the day: https://www.youtube.com/watch?v=TCUBSbJENO4. You might think you understand it, but it's only after you really internalize the rules that you stop fighting the compiler so much.

maciejhirsz commented 1 year ago

Cleaning up, we moved this to Discord already.