adospace / reactorui-maui

MauiReactor is a MVU UI framework built on top of .NET MAUI
MIT License
568 stars 47 forks source link

State not working with custom component #124

Closed Code-DJ closed 1 year ago

Code-DJ commented 1 year ago

This issue is on iOS - not sure about Android.

1.0.137

I have the following simplified custom component, when I use it on a page the State doesn't update:

class MyTestEntryState
{
    public string? Text { get; set; }
    public string? PlaceholderText { get; set; }
}

class MyTestEntry : Component<MyTestEntryState>
{
    public MyTestEntry Text(string? text)
    {
        SetState(state => state.Text = text);
        return this;
    }

    public MyTestEntry PlaceholderText(string? placeholderText)
    {
        SetState(state => state.PlaceholderText = placeholderText);
        return this;
    }

    public override Entry Render() => new Entry()
        .Text(State.Text ?? "")
        .Placeholder(State.PlaceholderText ?? "");
}

Render method on any Component Page:

  1. Enter value in Source, see that the State updates in the "Destination" Entry.
  2. Uncomment the custom component.
  3. Enter value in Source, see that State stops updating - neither "Custom Destination" nor "Destination" updates.
    public override VisualNode Render() => new ContentPage
    {
        new Grid("48, 48, 48", "*")
        {
            // new MyTestEntry()
            //     .PlaceholderText("Custom Destination")
            //     .Text(State.SearchText)
            //     .GridRow(0),

            new Entry()
                .Placeholder("Destination")
                .Text(State.SearchText ?? "")
                .GridRow(1),

            new Entry()
                .Placeholder("Source")
                .Text(State.SearchText ?? "")
                .OnTextChanged((s, e) => SetState(state => state.SearchText = e.NewTextValue))
                .GridRow(2)
        }
    };
adospace commented 1 year ago

Hi, you can't update the state like that (State is not yet ready at that moment). Try this instead:

class MyTestEntryState
{
    public string? Text { get; set; }
    public string? PlaceholderText { get; set; }
}

class MyTestEntry : Component<MyTestEntryState>
{
    string? _text;
    string? _placeholderText;
    public MyTestEntry Text(string? text)
    {
        _text = text;
        return this;
    }

    public MyTestEntry PlaceholderText(string? placeholderText)
    {
        _placeholderText = placeholderText;
        return this;
    }

    protected override OnMountedOrPropsChanged()
    {
       State.Text = _text;
       State.PlaceholderText = _placeholderText;
       base.OnMountedOrPropsChanged();
    }

    public override Entry Render() => new Entry()
        .Text(State.Text ?? "")
        .Placeholder(State.PlaceholderText ?? "");
}

BUT: from what I see you don't need at all a stateful component, like this:

class MyTestEntry : Component
{
    string? _text;
    string? _placeholderText;
    public MyTestEntry Text(string? text)
    {
        _text = text;
        return this;
    }

    public MyTestEntry PlaceholderText(string? placeholderText)
    {
        _placeholderText = placeholderText;
        return this;
    }

    protected override OnMountedOrPropsChanged()
    {
       State.Text = _text;
       State.PlaceholderText = _placeholderText;
       base.OnMountedOrPropsChanged();
    }

    public override Entry Render() => new Entry()
        .Text(_text ?? "")
        .Placeholder(_placeholderText ?? "");
}

or even better, not using a Component, everything could be down to:

public static class Theme
{
    //custom Entry with font, size color etc, reusable 
    public static Entry Entry(string text, string? placeholder = null) 
       => new Entry()
          .Text(_text)
          .Placeholder(_placeholderText ?? "");
}

Why are you using a stateful component?

Code-DJ commented 1 year ago

Will give it a shot. I'm planning to create a Floating Label component.

adospace commented 1 year ago

like this? TestMudEntry

class MainPageState
{
    public string FirstName { get; set; }

    public string LastName { get; set; }
}

class MainPage : Component<MainPageState>
{
    public override VisualNode Render()
    {
        return new ContentPage
        {
            new VStack(spacing: 10)
            {
                new MudEntry()
                    .Label("First Name")
                    .OnTextChanged(text => SetState(s => s.FirstName = text)),

                new MudEntry()
                    .Label("Last Name")
                    .OnTextChanged(text => SetState(s => s.LastName = text)),
            }
            .HFill()
            .VCenter()
            .Margin(50)
        };
    }
}

class MudEntryState
{
    public bool Focused { get; set; }

    public bool IsEmpty { get; set; } = true;
}

class MudEntry : Component<MudEntryState>
{
    private MauiControls.Entry _entryRef;
    private Action<string> _textChangedAction;
    private string _label;

    public MudEntry OnTextChanged(Action<string> textChangedAction)
    {
        _textChangedAction = textChangedAction;
        return this;
    }

    public MudEntry Label(string label)
    {
        _label = label;
        return this;
    }

    public override VisualNode Render()
    {
        return new Grid("Auto", "*")
        {
            new Entry(entryRef => _entryRef = entryRef)
                .OnAfterTextChanged(OnTextChanged)
                .VCenter()
                .OnFocused(()=>SetState(s => s.Focused = true))
                .OnUnfocused(()=>SetState(s => s.Focused = false)),

            new Label(_label)                
                .OnTapped(()=>_entryRef?.Focus())
                .Margin(5,0)
                .HStart()
                .VCenter()
                .TranslationY(State.Focused || !State.IsEmpty ? -20 : 0)
                .ScaleX(State.Focused || !State.IsEmpty ? 0.8 : 1.0)
                .AnchorX(0)
                .TextColor(!State.Focused || State.IsEmpty ? Colors.Gray : Colors.Red)
                .WithAnimation(duration: 200),
        }
        .VCenter();
    }

    private void OnTextChanged(string text)
    {
        SetState(s => s.IsEmpty = string.IsNullOrWhiteSpace(text));
        _textChangedAction?.Invoke(text);
    }
}
Code-DJ commented 1 year ago

Hi awesome! thank you. I guess my mistake was I was setting everything in State and applying Text, Padding, HeightRequest etc. all from State. Also, I was expecting only the component to break but it was affecting the page using the component as well.

Thanks again!

adospace commented 1 year ago

just to further improve my answer this is a perfect case for the Inline Component feature:

class MainPageState
{
    public string FirstName { get; set; }

    public string LastName { get; set; }
}

class MainPage : Component<MainPageState>
{
    public override VisualNode Render()
    {
        return new ContentPage
        {
            new VStack(spacing: 10)
            {
                RenderMudEntry("First Name", text => SetState(s => s.FirstName = text)),

                RenderMudEntry("Last Name", text => SetState(s => s.LastName = text)),
            }
            .HFill()
            .VCenter()
            .Margin(50)
        };
    }

    static VisualNode RenderMudEntry(string label, Action<string> textChangedAction)
        => Component.Render(context =>
        {
            MauiControls.Entry _entryRef = null;
            var state = context.UseState<(bool IsFocused, bool IsFilled)>();

            return new Grid("Auto", "*")
            {
                new Entry(entryRef => _entryRef = entryRef)
                    .OnAfterTextChanged(text =>
                    {
                        state.Set(s => (s.IsFocused, IsFilled: !string.IsNullOrWhiteSpace(text)));
                        textChangedAction?.Invoke(text);
                    })
                    .VCenter()
                    .OnFocused(()=>state.Set(s => (IsFocused: true, s.IsFilled)))
                    .OnUnfocused(()=>state.Set(s => (IsFocused: false, s.IsFilled))),

                new Label(label)
                    .OnTapped(() =>_entryRef?.Focus())
                    .Margin(5,0)
                    .HStart()
                    .VCenter()
                    .TranslationY(state.Value.IsFocused || state.Value.IsFilled ? -20 : 0)
                    .ScaleX(state.Value.IsFocused || state.Value.IsFilled ? 0.8 : 1.0)
                    .AnchorX(0)
                    .TextColor(!state.Value.IsFocused || !state.Value.IsFilled ? Colors.Gray : Colors.Red)
                    .WithAnimation(duration: 200),
            }
            .VCenter();
        });
}