New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Suggestion] Better components inheritance #1736

Open
stsrki opened this Issue Nov 29, 2018 · 19 comments

Comments

Projects
None yet
4 participants
@stsrki

stsrki commented Nov 29, 2018

First let me explain my problem.

I have a Field component inside of MyComponents library. All it contains is just a basic div. For example:
Field.cshtml in MyComponents

@inherits Base.BaseField
<div class="@ClassMapper.Class">
    @ChildContent
</div>

Everything good so far, it renders, and it's all good.

BUT, here is the problem, if I want to extend the Field component in another components library, let's say that this time I want it to contain a Label. I cannot do that. If I wan't custom logic in this new library then I must create a new Field.cshtml and write everything, than I have to delete Field from by base MyComponents library because it will mess with the new component.

This is an implementation of Field.cshtml in MyComponents.Bootstrap library:

@inherits BootstrapBase.BaseBootstrapField
<div class="@ClassMapper.Class">
    @if ( Label != null )
    {
        <FieldLabel>@Label</FieldLabel>
    }
    @ChildContent
</div>

It would be great If for example I could just inherit Field automatically, maybe if both libraries share the same component names that they would combine.

I propose two way how to do it. Maybe :)

First, here is the main Field.cshtml in MyComponents

@inherits Base.BaseField
<div class="@ClassMapper.Class">
    @ChildContent
</div>

Suggestion 1: do it automatically based on the component name
Field.cshtml in MyComponents.Bootstrap

@if ( Label != null )
{
    <FieldLabel>@Label</FieldLabel>
}
@ChildContent

Suggestion 2: explicitly say that it is inherited from Field.cshtml in MyComponents.Bootstrap

@inherits MyComponents.Field
@if ( Label != null )
{
    <FieldLabel>@Label</FieldLabel>
}
@ChildContent

Expected result:

<div class="field">
    <label class="label">Hello label</label>
    Hello field
</div>
@danroth27

This comment has been minimized.

Member

danroth27 commented Nov 29, 2018

This sounds similar to the question raised in #1729.

Instead of using inheritance, why not use composition? I.e. have your Bootstrap Field component use the base Field component?

Also, can you clarify what you mean when you say that the parent & child components (or components with the same name) would combine? How would they combine exactly?

@stsrki

This comment has been minimized.

stsrki commented Nov 29, 2018

I'm not sure what you mean by composition.

Anyway I will try to explain, and if needed maybe I can prepare you a sample project to better illustrate it.

First a project structure

MyComponents
Field.cshtml

MyComponents.Bootstrap
Field.cshtml

MyComponents.Bulma
Field.cshtml

So basically MyComponents.Field has just a basic structure, <div> and ChildContent

@inherits Base.BaseField
<div class="@ClassMapper.Class">
    @ChildContent
</div>

and it is inherited from an abstract class BaseField. Nothing fancy there.

SO, moving on to the Bootstrap and Bulma implementations. Because a user can choose what framework to use, I must have the same component name for all of frameworks. ie. Field.

And that is the problem, because when I create Field.cshtml in MyComponents.Bootstrap and MyComponents.Bulma project I also must remove the Field.cshtml from MyComponents. Otherwise Bootstrap and Bulma will not render.

Bootstrap Field.cshtml

@inherits BootstrapBase.BaseBootstrapField // inherited from BaseField 
<div class="some boostrap class">
    <span>Maybe a span here</span>
    <div class="@ClassMapper.Class">
        @ChildContent
    </div>
</div>

Bulma Field.cshtml

@inherits BulmaBase.BaseBootstrapField // inherited from BaseField 
<p class="some bulma class">    
    <div class="@ClassMapper.Class">
        @ChildContent
    </div>
    <div>Maybe a div here</div>
</p>

As you can see the <div class="@ClassMapper.Class">@ChildContent</div> is the same in both cases.

OK, moving on. Lets say that now I must implement a third framework. And it have entirely different structure.
Material Field.cshtml

@inherits MaterialBase.BaseMaterialField // inherited from BaseField 
<span>Hey look now only I am here</span> 

There can be many different structures, based on the same root component. And they all must share the same name, otherwise it will not make sence when using the library if I need to have different name for every implementation, ie. BootstrapField, BulmaField... I want to avoid that.

@stsrki

This comment has been minimized.

stsrki commented Nov 29, 2018

PS. If none of this make sense or it's impossible to do, maybe there is an easier way.
Allow the Field to exist inside of MyComponent project and inside of any other subproject. But than the Blazor will need to somehow know which one to use.

@DeanRoddey

This comment has been minimized.

DeanRoddey commented Dec 1, 2018

Yeh, this is the same issue that I have been complaining about in 1729. I think it will end up being a ubiquitous issue if not addressed.

@wanton7

This comment has been minimized.

wanton7 commented Dec 3, 2018

@stsrki Could you always use Field.cshtml from MyComponentsand make it as dynamic component #723 and have it to select actual implementation BulmaField etc?

@wanton7

This comment has been minimized.

wanton7 commented Dec 3, 2018

@DeanRoddey you can just copy & paste your issue's address from browser's address bar to your comment to link it like this #1729

@stsrki

This comment has been minimized.

stsrki commented Dec 3, 2018

@wanton7 I don't think I can do it as you suggest. My BulmaField is not in the same "MyComponents" project. The base project should not know anything of the implementations. It should provide just the basic interface/layout of component, nothing more. Then the implementation project should do the magic and wire of all the required classes/styles based on the default component layout. And if the default component layout is not enough, than it should override it and add the missing peaces.
Also I'm trying to build a reusable library of components and I need to have ALL of the components in the base project. Otherwise anyone who would like to extend the library should also need implement all the "missing" components.

@DeanRoddey Is on the right track with the issue #1729

PS. I have now published my entire components library and you guys can have a look at it and see what I'm talking about. Blazorise
Look at the Bootstrap and Bulma implementations. Those projects should override the components from root Blazorise project. But I cannot do that because they cannot share the same name.

@wanton7

This comment has been minimized.

wanton7 commented Dec 3, 2018

@stsrki it's up to you how create Type of component it doesn't have to be in the same project/assembly. OpenComponent uses Type and you can create that Type how ever you want.

@stsrki

This comment has been minimized.

stsrki commented Dec 3, 2018

Biggest problem with your approach is that everything would have to be done programmatically. I wan't to use razor, not do everything in code.
Lets say that I go with your route and do it programmatically. Consider this scenario:
In base project I have

public abstract class BaseField
{
    protected virtual RenderFragment CreateDynamicComponent() => builder =>
    {
       // override this in implementation project
    };
}

then in Field.cshtml

@inherits BaseField
@CreateDynamicComponent()

Now in my Bulma project I override BaseField

public abstract class BaseBulmaField : BaseField
{
    protected override RenderFragment CreateDynamicComponent()
    {
        builder.OpenComponent(...);
        ...
        return base.CreateDynamicComponent();
    }
}

Now, how and where do I say to Blazor what implementation it will use? How I tell it to use BaseBulmaField?

Also actual html tree could be fairly complex. Doing it all programmatically would be a huge overhead.

@wanton7

This comment has been minimized.

wanton7 commented Dec 3, 2018

@stsrki It's not ideal but I didn't mean you would do it like that. I tought you could create Field component as proxy component that proxies parameters to actual implementation. So basically that would be only the interface. So your basic implementation wouldn't be in Field.cstml but in something like BasicField.csthml.

@stsrki

This comment has been minimized.

stsrki commented Dec 3, 2018

Yeah but I would still be stuck with the problem that the users could not use Field component like

<Field>...</Field>

Instead they would have to use <BulmaField>...</BulmaField> or <BootstrapField>...</BootstrapField> or <MaterialField>...</MaterialField> or whatever. It's not a solution. The user must use only the Field tag after importing the right implementation from NuGet.

So basically Field.cshtml is an actual BasicField.csthml that you'r proposing.

@wanton7

This comment has been minimized.

wanton7 commented Dec 3, 2018

No they would use <Field> directly because that proxy component that would create that field implementation dynamically. So it would proxy to BasicField, BulmaField, BootstrapField or whatever depending on settings you provide.

@wanton7

This comment has been minimized.

wanton7 commented Dec 3, 2018

Here is a simplified example of what I mean

Field.cshtml

@CreateDynamicComponent()

@functions {
    [Parameter]
    string Text { get; set; }

    private static int _count;

    RenderFragment CreateDynamicComponent() => builder =>
    {
        var count = System.Threading.Interlocked.Increment(ref _count);
        builder.OpenComponent(0, (count & 1) == 0 ? typeof(BasicField) : typeof(BootstrapField));
        builder.AddAttribute(1, nameof(Text), Text);
        builder.CloseComponent();
    };
}

BasicField.cshtml

<span>@Text</span>

@functions {
    [Parameter]
    string Text { get; set; }
}

BootstrapField.cshtml

<span class="label label-default">@Text</span>

@functions {
    [Parameter]
    string Text { get; set; }
}
@stsrki

This comment has been minimized.

stsrki commented Dec 3, 2018

Yes, that's what I'm already trying to do now. But I'm stuck with the ChildContent.

Custom.cshtml

@inherits Base.BaseCustom
@if ( Type == null )
{
    <span>
        @Text
        @ChildContent
    </span>
}
else
{
    @CreateDynamicComponent()
}

BaseCustom.cs

public abstract class BaseCustom : BaseComponent
{
    #region Methods

    protected RenderFragment CreateDynamicComponent() => builder =>
    {
        builder.OpenComponent( 0, Type );
        builder.AddAttribute( 1, nameof( Text ), Text );
        builder.AddContent( 2, ChildContent );
        builder.CloseComponent();
    };

    #endregion

    #region Properties

    [Parameter] protected RenderFragment ChildContent { get; set; }

    [Parameter] protected string Text { get; set; }

    // This is just for testing. I will move this to a "registered components" proxy
    [Parameter] protected Type Type { get; set; }

    #endregion
}

CustomBS.cshtml in Bootstrap project

@inherits Base.BaseCustom
<span class="label label-default">
    @Text
    @ChildContent
</span>

Test code

<Custom Text="AA">
    BB
</Custom>
<Custom Text="CC" Type="@(typeof(Blazorise.Bootstrap.CustomBS))">
    DD
</Custom>

Finally it renders

<span>AA BB</span>
<span class="label label-default">CC</span>

It's not rendering DD value. I cannot make it to render ChildContent for CustomBS component.

@wanton7

This comment has been minimized.

wanton7 commented Dec 3, 2018

Something's different in your code compared to mine, tried it and worked just fine.

Field.cshtml

@CreateDynamicComponent()

@functions {
    [Parameter]
    private RenderFragment ChildContent { get; set; }

    private static int _count;

    RenderFragment CreateDynamicComponent() => builder =>
    {
        var count = System.Threading.Interlocked.Increment(ref _count);
        builder.OpenComponent(0, (count & 1) == 0 ? typeof(BasicField) : typeof(BootstrapField));
        builder.AddAttribute(1, nameof(ChildContent), ChildContent);
        builder.CloseComponent();
    };
}

BasicField.cshtml

<span>@ChildContent</span>

@functions {
    [Parameter]
    private RenderFragment ChildContent { get; set; }
}

BootstrapField.cshtml

<span class="label label-default">@ChildContent</span>

@functions {
    [Parameter]
    private RenderFragment ChildContent { get; set; }
}
@stsrki

This comment has been minimized.

stsrki commented Dec 3, 2018

Just wanted to let you know it should have being

builder.AddAttribute( 2, nameof( ChildContent ), ChildContent );

You were faster to comment :)

All in all it's not an ideal solution but it works for now. Now I must see how it will behave with onclick/onchange events, and bind-* attributes.

@wanton7

This comment has been minimized.

wanton7 commented Dec 3, 2018

It seems that you don't have to use .cshtml you can use this instead
Field.cs

public class Field : BlazorComponent
    {
        private static int _count;

        [Parameter]
        private RenderFragment ChildContent { get; set; }

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            var count = System.Threading.Interlocked.Increment(ref _count);
            builder.OpenComponent(0, (count & 1) == 0 ? typeof(BasicField) : typeof(BootstrapField));
            builder.AddAttribute(1, nameof(ChildContent), ChildContent);
            builder.CloseComponent();
        }
    }
@stsrki

This comment has been minimized.

stsrki commented Dec 3, 2018

I can but I still want for a .cshtml to have a basic html until there is a need to implement custom markup for an implementation. eg.

@inherits Base.BaseCustom
@if ( Type == null )
{
    <span>
        @Text
        @ChildContent
    </span>
}
else
{
    @CreateDynamicComponent()
}

PS. I tried the bind-* and a few events on inputs, and it seems good so far

@wanton7

This comment has been minimized.

wanton7 commented Dec 4, 2018

Here is a reusable proxy component base class. Just override ChildComponentType and be done.

public abstract class ProxyComponent : BlazorComponent
{
    private ParameterCollection _parameters;

    public override void SetParameters(ParameterCollection parameters)
    {
        _parameters = parameters;
        base.SetParameters(ParameterCollection.Empty);
    }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        var seq = 0;
        builder.OpenComponent(seq++, ChildComponentType());
        foreach (var parameter in _parameters)
            builder.AddAttribute(seq++, parameter.Name, parameter.Value);

        builder.CloseComponent();
    }

    protected abstract Type ChildComponentType();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment