dioxus-field 0.1.0

A form-library-agnostic field convention for Dioxus
Documentation
use std::{cell::RefCell, rc::Rc};

use dioxus_core::{Callback, VNode, VirtualDom};
use dioxus_field::{
    Binding, BindingPropTrio, ChangeOrigin, FieldContext, provide_field_context, use_binding,
};
use dioxus_hooks::use_signal;
use dioxus_signals::{ReadSignal, Signal};

#[test]
fn from_signal_preserves_two_way_access_and_identity() {
    fn app() -> dioxus_core::Element {
        let signal = use_signal(|| 1);
        let first: Binding<i32> = signal.into();
        let second: Binding<i32> = signal.into();

        assert_eq!((first.read)(), 1);
        assert_eq!(first, second);

        first.write(2, ChangeOrigin::Programmatic);
        assert_eq!(signal(), 2);

        VNode::empty()
    }

    VirtualDom::new(app).rebuild_in_place();
}

#[test]
fn from_read_signal_and_callback_forwards_writes() {
    fn app(writes: Rc<RefCell<Vec<i32>>>) -> dioxus_core::Element {
        let signal = use_signal(|| 3);
        let write = Callback::new(move |value| writes.borrow_mut().push(value));
        let binding: Binding<i32> = (ReadSignal::from(signal), write).into();

        assert_eq!((binding.read)(), 3);
        binding.write(4, ChangeOrigin::User);

        VNode::empty()
    }

    let writes = Rc::new(RefCell::new(Vec::new()));
    VirtualDom::new_with_props(app, Rc::clone(&writes)).rebuild_in_place();

    assert_eq!(*writes.borrow(), [4]);
}

#[test]
fn from_plain_value_creates_an_uncontrolled_binding() {
    fn app() -> dioxus_core::Element {
        let binding: Binding<i32> = 5.into();

        assert_eq!((binding.read)(), 5);
        binding.write(6, ChangeOrigin::User);
        assert_eq!((binding.read)(), 6);

        VNode::empty()
    }

    VirtualDom::new(app).rebuild_in_place();
}

#[test]
fn trio_decomposition_marks_changes_as_user_changes_and_forwards_commit() {
    #[derive(Default)]
    struct Probe {
        writes: RefCell<Vec<(i32, ChangeOrigin)>>,
        commits: RefCell<usize>,
    }

    #[allow(
        clippy::needless_pass_by_value,
        reason = "VirtualDom entrypoints receive their root properties by value"
    )]
    fn app(probe: Rc<Probe>) -> dioxus_core::Element {
        let signal = use_signal(|| 7);
        let write_probe = Rc::clone(&probe);
        let commit_probe = Rc::clone(&probe);
        let binding = Binding::new(
            ReadSignal::from(signal),
            Callback::new(move |write| write_probe.writes.borrow_mut().push(write)),
            Callback::new(move |()| *commit_probe.commits.borrow_mut() += 1),
        );
        binding.write(9, ChangeOrigin::Programmatic);
        let BindingPropTrio {
            value,
            on_change,
            on_commit,
        } = binding.into_trio();

        assert_eq!(value(), 7);
        on_change.call(8);
        on_commit.call(());

        VNode::empty()
    }

    let probe = Rc::new(Probe::default());
    VirtualDom::new_with_props(app, Rc::clone(&probe)).rebuild_in_place();

    assert_eq!(
        *probe.writes.borrow(),
        [(9, ChangeOrigin::Programmatic), (8, ChangeOrigin::User)]
    );
    assert_eq!(*probe.commits.borrow(), 1);
}

#[test]
fn binding_resolution_prefers_explicit_then_context_then_internal_state() {
    fn app() -> dioxus_core::Element {
        let context_signal = use_signal(|| 10);
        let explicit_signal = use_signal(|| 20);
        let context: Binding<i32> = context_signal.into();
        let explicit: Binding<i32> = explicit_signal.into();
        provide_field_context(context.clone());

        let resolved_explicit = use_binding(Some(explicit.clone()), 30);
        assert_eq!(resolved_explicit, explicit);

        let resolved_context = use_binding(None, 30);
        assert_eq!(resolved_context, context);

        VNode::empty()
    }

    fn internal_app() -> dioxus_core::Element {
        let resolved: Binding<i32> = use_binding(None, 30);
        assert_eq!((resolved.read)(), 30);
        resolved.write(31, ChangeOrigin::User);
        assert_eq!((resolved.read)(), 31);

        VNode::empty()
    }

    VirtualDom::new(app).rebuild_in_place();
    VirtualDom::new(internal_app).rebuild_in_place();
}

#[test]
#[should_panic(expected = "Field Context contains a binding for")]
fn wrong_context_value_type_is_a_loud_error() {
    #[allow(
        clippy::needless_pass_by_value,
        reason = "VirtualDom entrypoints receive their root properties by value"
    )]
    fn app(context: Rc<RefCell<Option<FieldContext>>>) -> dioxus_core::Element {
        let signal = use_signal(|| String::from("text"));
        let binding: Binding<String> = signal.into();
        context.borrow_mut().replace(FieldContext::new(binding));
        VNode::empty()
    }

    let context = Rc::new(RefCell::new(None));
    VirtualDom::new_with_props(app, Rc::clone(&context)).rebuild_in_place();
    let _: Binding<i32> = context
        .borrow()
        .as_ref()
        .expect("probe should contain field context")
        .resolve();
}

#[test]
fn identity_equality_never_equates_independent_bindings() {
    fn app() -> dioxus_core::Element {
        let first_signal: Signal<i32> = use_signal(|| 1);
        let second_signal: Signal<i32> = use_signal(|| 1);
        let first: Binding<i32> = first_signal.into();
        let first_clone = first.clone();
        let second: Binding<i32> = second_signal.into();

        assert_eq!(first, first_clone);
        assert_ne!(first, second);

        VNode::empty()
    }

    VirtualDom::new(app).rebuild_in_place();
}