ProductiveRage / Bridge.React

Bindings for Bridge.NET for React - write React applications in C#!
MIT License
74 stars 14 forks source link

How to pass multiple elements and the children to a DIV? #56

Closed kendallb closed 5 years ago

kendallb commented 5 years ago

I am struggling to convert this bit of React javascript to C# rendering?

            <div id="wrapper">
                <div id="content">
                    Some content
                </div>
                {children}
            </div>

My first attempt is this, but it does not work:

            return DOM.Div(new Attributes {
                    Id = "wrapper",
                },
                DOM.Div(new Attributes {
                        Id = "content",
                    },
                    "Some content"),
                Children
            );

I tried to create a new list of ReactElements but I get a compiler error saying it cannot be used as part of a templated argument (so new List() does not work.

[BridgeReact] ReactElement may not be used as a type parameter for an object instantiation as it is an [External] type that will not be available at runtime (for creating sets of components, the ToChildComponentArray extension method may help)

I figure I need to do some kind of union of the DOM element I want to stick in there and the children, but it is not clear to me at all what LINQ or C# code I can use to achieve this?

kendallb commented 5 years ago

Best I can come up with is this, which compiles but seems messy. Is there another way to write this?

            return DOM.Div(new Attributes {
                    Id = "wrapper",
                },
                new[] {
                    DOM.Div(new Attributes {
                            Id = "content",
                        },
                        "Some content")
                }.Union(Children.Cast<ReactElement>())
            );
ProductiveRage commented 5 years ago

Unfortunately, Bridge makes this a bit more awkward because you might expect to use LINQ's "Concat" method here but they have a "Concat" method on the Array type that loses some type information.

You have one workaround.

Another workaround is to use a wrapper div for the children - eg.

return DOM.Div(new Attributes { Id = "wrapper" },
    DOM.Div(new Attributes { Id = "content" },
        "Some content",
        DOM.Div(Children.Cast<ReactElement>())
    )
);

(if you have control over the styling then the extra div needn't be the end of the world, despite it feeling a bit dirty since it's a workaround and doesn't feel like it should be strictly necessary)

Another approach would be to add a couple of extension methods to make this easier - eg.

public static class ReactElementExtensions
{
    public static Union<ReactElement, string>[] Append(this ReactElement source, Union<ReactElement, string> other)
    {
        return new Union<ReactElement, string>[] { source, other };
    }

    public static Union<ReactElement, string>[] Append(this Union<ReactElement, string>[] source, Union<ReactElement, string>[] other)
    {
        return (Union<ReactElement, string>[])source.Concat(other);
    }
}

.. then you rendering would look like this:

return DOM.Div(new Attributes { Id = "wrapper" },
    DOM.Div(new Attributes { Key = 3, Id = "content" }, "Some content!")
        .Append(DOM.Div(new Attributes { Key = 4, Id = "content" }, "Some more content!"))
        .Append(Children)
);

This may be something that could be improved in the bindings library by better supporting arrays of components (such as allowing the return of arrays of components from render methods and potentially exposing the Fragment interface if it's not possible to expand the DOM factory methods and component classes to do this cleanly with C#).

ProductiveRage commented 5 years ago

One more alternative would be to consider adding your own Fragment class as an interim measure, to allow you to write something more like this:

return DOM.Div(new Attributes { Id = "wrapper" },
    new Fragment(
        DOM.Div(new Attributes { Key = 3, Id = "content" }, "Some content!"),
        Children
    )
);

.. by adding this class to your project somewhere:

public sealed class Fragment
{
    private readonly Union<ReactElement, string>[] _elements;
    public Fragment(params Union<ReactElement, string>[] elements)
    {
        _elements = elements;
    }
    public Fragment(Union<ReactElement, string> first, params Union<ReactElement, string>[] other)
    {
        _elements = (Union<ReactElement, string>[])new[] { first }.Concat(other);
    }
    public Fragment(Union<ReactElement, string> first, Union<ReactElement, string> second, params Union<ReactElement, string>[] other)
    {
        _elements = (Union<ReactElement, string>[])new[] { first, second }.Concat(other);
    }
    public Fragment(Union<ReactElement, string> first, Union<ReactElement, string> second, Union<ReactElement, string> third, params Union<ReactElement, string>[] other)
    {
        _elements = (Union<ReactElement, string>[])new[] { first, second, third }.Concat(other);
    }

    public static implicit operator ReactElement(Fragment source)
    {
        return (source == null) ? null : Script.Write<ReactElement>("React.createElement(React.Fragment, null, {0})", source._elements);
    }
}

You may have to add more constructor overloads, depending upon your use cases.

I have no timeline for when the changes to support returning arrays of elements (and/or Fragment) may arrive in this bindings library.

kendallb commented 5 years ago

The core issue is really that the params version of the function only accepts Union<ReactElement, string> and when you build a custom binding, the documentation includes this to do the automatic conversion to ReactElement:

        public static implicit operator ReactElement(
            ReactModal source)
        {
            return Script.Write<ReactElement>("source");
        }

which works fine as long as the target is expecting just ReactElement. For some reason the compiler cannot work out the implicit conversion using that function to Union<ReactElement, string>, but putting in an explicit cast actually works. However I found a better solution was to add this implicit conversion to my binding code so it has both:

        public static implicit operator Union<ReactElement, string>(
            ReactModal source)
        {
            return Script.Write<ReactElement>("source");
        }

This compiles and I believe it will work (have not gotten to testing it yet), but I think that's probably the simplest solution. Components you build entirely in Bridge seem to be fine (anything that derives from the Component classes).